diff --git a/compiler/src/diagnostics/comments.re b/compiler/src/diagnostics/comments.re index ff9bccfb67..3ce62ee485 100644 --- a/compiler/src/diagnostics/comments.re +++ b/compiler/src/diagnostics/comments.re @@ -87,9 +87,12 @@ module type OrderedComments = { }; module MakeOrderedComments = - (Raw: { - let comments: list(Typedtree.comment); - }) + ( + Raw: { + let comments: list(Typedtree.comment); + let extract_attributes: bool; + }, + ) : OrderedComments => { module IntMap = Map.Make(Int); @@ -117,9 +120,15 @@ module MakeOrderedComments = let data = (comment, None, []); (cmt_loc.loc_start.pos_lnum, cmt_loc.loc_end.pos_lnum, data); | Doc({cmt_source, cmt_content, cmt_loc}) => - let (description, attributes) = - Attribute.extract(cmt_source, cmt_content, cmt_loc); - let data = (comment, description, attributes); + let data = + if (Raw.extract_attributes) { + let (description, attributes) = + Attribute.extract(cmt_source, cmt_content, cmt_loc); + (comment, description, attributes); + } else { + (comment, None, []); + }; + (cmt_loc.loc_start.pos_lnum, cmt_loc.loc_end.pos_lnum, data); }; comments.by_start_lnum = @@ -137,10 +146,12 @@ module MakeOrderedComments = let iter = fn => IntMap.iter(fn, comments.by_start_lnum); }; -let to_ordered = (comments): (module OrderedComments) => +let to_ordered = + (~extract_attributes=true, comments): (module OrderedComments) => (module MakeOrderedComments({ let comments = comments; + let extract_attributes = extract_attributes; })); let start_line = (comment: Typedtree.comment) => { diff --git a/compiler/src/language_server/code_action.re b/compiler/src/language_server/code_action.re index 94244979a1..0a602824d6 100644 --- a/compiler/src/language_server/code_action.re +++ b/compiler/src/language_server/code_action.re @@ -84,6 +84,29 @@ let named_arg_label = (range, uri, arg_label) => { }; }; +let add_graindoc = (range, uri, message) => { + ResponseResult.{ + title: "Add Graindoc", + kind: "add-graindoc", + edit: { + document_changes: [ + { + text_document: { + uri, + version: None, + }, + edits: [ + { + range, + new_text: message, + }, + ], + }, + ], + }, + }; +}; + let send_code_actions = (id: Protocol.message_id, code_actions: list(ResponseResult.code_action)) => { Protocol.response(~id, ResponseResult.to_yojson(Some(code_actions))); @@ -214,6 +237,90 @@ let rec process_add_or_remove_braces = (uri, results: list(Sourcetree.node)) => ); }; +let get_end_of_last_line = (loc: Location.t) => { + { + ...loc, + loc_start: { + ...loc.loc_start, + pos_cnum: Int.max_int, + pos_lnum: loc.loc_start.pos_lnum - 1, + }, + loc_end: { + ...loc.loc_start, + pos_cnum: Int.max_int, + pos_lnum: loc.loc_start.pos_lnum - 1, + }, + }; +}; +let template_graindoc = fn => { + let output = Buffer.create(128); + Buffer.add_string(output, "\n/**\n"); + Buffer.add_string(output, " *\n"); // Blank spot for description entry + Buffer.add_string(output, " *\n"); // Spacing + fn(output); + Buffer.add_string(output, " * @example\n"); + Buffer.add_string(output, " *\n"); // Spacing + Buffer.add_string(output, " * @since\n"); + Buffer.add_string(output, " */"); + Buffer.contents(output); +}; +let rec process_graindoc = + ( + uri, + results: list(Sourcetree.node), + comments: list(Typedtree.comment), + ) => { + switch (results) { + | [LetBind({pat, expr, loc}), ..._] => + let ordered = Comments.to_ordered(~extract_attributes=false, comments); + let comment = + Comments.Doc.ending_on(~lnum=loc.loc_start.pos_lnum - 1, ordered); + switch (comment) { + | Some((Doc(_), _, _)) => None + | _ => + let loc = get_end_of_last_line(loc); + let message = + template_graindoc(output => { + switch (expr.exp_type.desc) { + | TTyArrow(args, ret_typ, _) => + Buffer.add_string(output, " *\n"); // Spacing + List.iteri( + (index, (label, _)) => { + let param_name = + switch (label) { + | Types.Labeled({txt}) + | Default({txt}) => txt + | Unlabeled => string_of_int(index) + }; + Buffer.add_string( + output, + Printf.sprintf(" * @param %s:\n", param_name), + ); + }, + args, + ); + Buffer.add_string(output, " * @returns \n"); + | _ => () + } + }); + Some(add_graindoc(Utils.loc_to_range(loc), uri, message)); + }; + | [Module({loc}), ...rest] => + let ordered = Comments.to_ordered(~extract_attributes=false, comments); + let comment = + Comments.Doc.ending_on(~lnum=loc.loc_start.pos_lnum - 1, ordered); + switch (comment) { + | Some((Doc(_), _, _)) => None + | _ => + let loc = get_end_of_last_line(loc); + let message = template_graindoc(_ => ()); + Some(add_graindoc(Utils.loc_to_range(loc), uri, message)); + }; + | [_, ...rest] => process_graindoc(uri, rest, comments) + | _ => None + }; +}; + let process = ( ~id: Protocol.message_id, @@ -233,6 +340,11 @@ let process = process_explicit_type_annotation(params.text_document.uri, results), process_named_arg_label(params.text_document.uri, results), process_add_or_remove_braces(params.text_document.uri, results), + process_graindoc( + params.text_document.uri, + results, + program.comments, + ), ], ); diff --git a/compiler/src/language_server/sourcetree.re b/compiler/src/language_server/sourcetree.re index c2069e7589..946e363181 100644 --- a/compiler/src/language_server/sourcetree.re +++ b/compiler/src/language_server/sourcetree.re @@ -178,6 +178,13 @@ module type Sourcetree = { | Include({ path: Path.t, loc: Location.t, + }) + | LetBind({ + rec_flag: Typedtree.rec_flag, + mut_flag: Typedtree.mut_flag, + pat: Typedtree.pattern, + expr: Typedtree.expression, + loc: Location.t, }); type sourcetree = t(node); @@ -276,6 +283,13 @@ module Sourcetree: Sourcetree = { | Include({ path: Path.t, loc: Location.t, + }) + | LetBind({ + rec_flag: Typedtree.rec_flag, + mut_flag: Typedtree.mut_flag, + pat: Typedtree.pattern, + expr: Typedtree.expression, + loc: Location.t, }); type sourcetree = t(node); @@ -561,6 +575,27 @@ module Sourcetree: Sourcetree = { ), ...segments^, ] + | TTopLet(rec_flag, mut_flag, binds) => + segments := + List.fold_left( + (segments, {vb_pat, vb_expr, vb_loc}) => { + [ + ( + loc_to_interval(vb_loc), + LetBind({ + rec_flag, + mut_flag, + pat: vb_pat, + expr: vb_expr, + loc: vb_loc, + }), + ), + ...segments, + ] + }, + segments^, + binds, + ) | _ => () }; }; diff --git a/compiler/test/suites/grainlsp.re b/compiler/test/suites/grainlsp.re index 0e9e387f40..bc7e85db7a 100644 --- a/compiler/test/suites/grainlsp.re +++ b/compiler/test/suites/grainlsp.re @@ -219,6 +219,25 @@ let abc = { x: 1 } ), ), ]), + `Assoc([ + ("title", `String("Add Graindoc")), + ("kind", `String("add-graindoc")), + ( + "edit", + lsp_text_document_edit( + "file:///a.gr", + [ + ( + lsp_range( + (1, 4611686018427387871), + (1, 4611686018427387871), + ), + "\n/**\n *\n *\n * @example\n *\n * @since\n */", + ), + ], + ), + ), + ]), ]), ); @@ -252,6 +271,25 @@ let f = val => { ), ), ]), + `Assoc([ + ("title", `String("Add Graindoc")), + ("kind", `String("add-graindoc")), + ( + "edit", + lsp_text_document_edit( + "file:///a.gr", + [ + ( + lsp_range( + (1, 4611686018427387871), + (1, 4611686018427387871), + ), + "\n/**\n *\n *\n *\n * @param val:\n * @returns \n * @example\n *\n * @since\n */", + ), + ], + ), + ), + ]), ]), ); @@ -270,7 +308,27 @@ let abc: T = { x: 1 } ("context", `Assoc([("diagnostics", `List([]))])), ]), ), - `Null, + `List([ + `Assoc([ + ("title", `String("Add Graindoc")), + ("kind", `String("add-graindoc")), + ( + "edit", + lsp_text_document_edit( + "file:///a.gr", + [ + ( + lsp_range( + (1, 4611686018427387871), + (1, 4611686018427387871), + ), + "\n/**\n *\n *\n * @example\n *\n * @since\n */", + ), + ], + ), + ), + ]), + ]), ); assertLspOutput( @@ -417,6 +475,25 @@ let f = (x) => { ), ), ]), + `Assoc([ + ("title", `String("Add Graindoc")), + ("kind", `String("add-graindoc")), + ( + "edit", + lsp_text_document_edit( + "file:///a.gr", + [ + ( + lsp_range( + (0, 4611686018427387894), + (0, 4611686018427387894), + ), + "\n/**\n *\n *\n *\n * @param x:\n * @returns \n * @example\n *\n * @since\n */", + ), + ], + ), + ), + ]), ]), ); @@ -437,7 +514,27 @@ let f = (x) => { ("context", `Assoc([("diagnostics", `List([]))])), ]), ), - `Null, + `List([ + `Assoc([ + ("title", `String("Add Graindoc")), + ("kind", `String("add-graindoc")), + ( + "edit", + lsp_text_document_edit( + "file:///a.gr", + [ + ( + lsp_range( + (0, 4611686018427387894), + (0, 4611686018427387894), + ), + "\n/**\n *\n *\n *\n * @param x:\n * @returns \n * @example\n *\n * @since\n */", + ), + ], + ), + ), + ]), + ]), ); assertLspOutput( @@ -469,6 +566,25 @@ let f = (x) => print(x) ), ), ]), + `Assoc([ + ("title", `String("Add Graindoc")), + ("kind", `String("add-graindoc")), + ( + "edit", + lsp_text_document_edit( + "file:///a.gr", + [ + ( + lsp_range( + (0, 4611686018427387894), + (0, 4611686018427387894), + ), + "\n/**\n *\n *\n *\n * @param x:\n * @returns \n * @example\n *\n * @since\n */", + ), + ], + ), + ), + ]), ]), ); @@ -501,9 +617,158 @@ let f = () => () => print(1) ), ), ]), + `Assoc([ + ("title", `String("Add Graindoc")), + ("kind", `String("add-graindoc")), + ( + "edit", + lsp_text_document_edit( + "file:///a.gr", + [ + ( + lsp_range( + (0, 4611686018427387894), + (0, 4611686018427387894), + ), + "\n/**\n *\n *\n *\n * @returns \n * @example\n *\n * @since\n */", + ), + ], + ), + ), + ]), ]), ); + assertLspOutput( + "code_action_add_graindoc_module", + "file:///a.gr", + {|module Main +module Test { + let f = 0 +}|}, + lsp_input( + "textDocument/codeAction", + `Assoc([ + ("textDocument", `Assoc([("uri", `String("file:///a.gr"))])), + ("range", lsp_range((1, 10), (1, 11))), + ("context", `Assoc([("diagnostics", `List([]))])), + ]), + ), + `List([ + `Assoc([ + ("title", `String("Add Graindoc")), + ("kind", `String("add-graindoc")), + ( + "edit", + lsp_text_document_edit( + "file:///a.gr", + [ + ( + lsp_range( + (0, 4611686018427387891), + (0, 4611686018427387891), + ), + "\n/**\n *\n *\n * @example\n *\n * @since\n */", + ), + ], + ), + ), + ]), + ]), + ); + + assertLspOutput( + "code_action_add_graindoc_function", + "file:///a.gr", + {|module A +let f = (x0, (x1, x2), x3, (x4, x5)) => 1 +|}, + lsp_input( + "textDocument/codeAction", + `Assoc([ + ("textDocument", `Assoc([("uri", `String("file:///a.gr"))])), + ("range", lsp_range((1, 6), (1, 7))), + ("context", `Assoc([("diagnostics", `List([]))])), + ]), + ), + `List([ + `Assoc([ + ("title", `String("Add Graindoc")), + ("kind", `String("add-graindoc")), + ( + "edit", + lsp_text_document_edit( + "file:///a.gr", + [ + ( + lsp_range( + (0, 4611686018427387894), + (0, 4611686018427387894), + ), + "\n/**\n *\n *\n *\n * @param x0:\n * @param 1:\n * @param x3:\n * @param 3:\n * @returns \n * @example\n *\n * @since\n */", + ), + ], + ), + ), + ]), + ]), + ); + + assertLspOutput( + "code_action_add_graindoc_value", + "file:///a.gr", + {|module A +let f = 0 +|}, + lsp_input( + "textDocument/codeAction", + `Assoc([ + ("textDocument", `Assoc([("uri", `String("file:///a.gr"))])), + ("range", lsp_range((1, 6), (1, 7))), + ("context", `Assoc([("diagnostics", `List([]))])), + ]), + ), + `List([ + `Assoc([ + ("title", `String("Add Graindoc")), + ("kind", `String("add-graindoc")), + ( + "edit", + lsp_text_document_edit( + "file:///a.gr", + [ + ( + lsp_range( + (0, 4611686018427387894), + (0, 4611686018427387894), + ), + "\n/**\n *\n *\n * @example\n *\n * @since\n */", + ), + ], + ), + ), + ]), + ]), + ); + + assertLspOutput( + "code_action_add_graindoc_existing", + "file:///a.gr", + {|module A +/** Existing graindoc */ +let f = 0 +|}, + lsp_input( + "textDocument/codeAction", + `Assoc([ + ("textDocument", `Assoc([("uri", `String("file:///a.gr"))])), + ("range", lsp_range((2, 6), (2, 7))), + ("context", `Assoc([("diagnostics", `List([]))])), + ]), + ), + `Null, + ); + assertLspOutput( "hover_pattern", "file:///a.gr",