From 1e3a9d1f108c98b1f95c0edf4067109f450d800e Mon Sep 17 00:00:00 2001 From: Spotandjake Date: Fri, 6 Feb 2026 10:18:44 -0500 Subject: [PATCH 1/3] feat(lsp): Initial graindoc code action implementation --- compiler/src/diagnostics/comments.re | 25 +++++-- compiler/src/language_server/code_action.re | 81 +++++++++++++++++++++ compiler/src/language_server/sourcetree.re | 38 ++++++++++ 3 files changed, 137 insertions(+), 7 deletions(-) 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..130d555310 100644 --- a/compiler/src/language_server/code_action.re +++ b/compiler/src/language_server/code_action.re @@ -84,6 +84,58 @@ let named_arg_label = (range, uri, arg_label) => { }; }; +let add_graindoc = (range, uri, expr_type: Types.type_expr) => { + let output = Buffer.create(128); + Buffer.add_string(output, "/**\n"); + Buffer.add_string(output, " *\n"); // Blank spot for description entry + switch (expr_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"); + | _ => () + }; + Buffer.add_string(output, " *\n"); // Spacing + Buffer.add_string(output, " * @example\n"); + Buffer.add_string(output, " *\n"); // Spacing + Buffer.add_string(output, " * @since\n"); + Buffer.add_string(output, " */\n"); + ResponseResult.{ + title: "Add Graindoc", + kind: "add-graindoc", + edit: { + document_changes: [ + { + text_document: { + uri, + version: None, + }, + edits: [ + { + range, + new_text: Buffer.contents(output), + }, + ], + }, + ], + }, + }; +}; + let send_code_actions = (id: Protocol.message_id, code_actions: list(ResponseResult.code_action)) => { Protocol.response(~id, ResponseResult.to_yojson(Some(code_actions))); @@ -213,6 +265,30 @@ let rec process_add_or_remove_braces = (uri, results: list(Sourcetree.node)) => } ); }; +let rec process_graindoc = + ( + uri, + results: list(Sourcetree.node), + comments: list(Typedtree.comment), + ) => { + switch (results) { + | [LetBind({pat, expr, loc}), ..._] => + let loc = { + ...loc, + loc_end: loc.loc_start, + }; + // TODO: Better place after and + 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 + | _ => Some(add_graindoc(Utils.loc_to_range(loc), uri, expr.exp_type)) + }; + | [_, ...rest] => process_graindoc(uri, rest, comments) + | _ => None + }; +}; let process = ( @@ -233,6 +309,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..3e4c260341 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,30 @@ 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, + loc_start: stmt.ttop_loc.loc_start, + }, + }), + ), + ...segments, + ] + }, + segments^, + binds, + ) | _ => () }; }; From 2f1b1928160b581f8010de1efc90ae45aa774976 Mon Sep 17 00:00:00 2001 From: Spotandjake Date: Fri, 6 Feb 2026 10:46:40 -0500 Subject: [PATCH 2/3] feat(lsp): Better place comments and support modules --- compiler/src/language_server/code_action.re | 103 +++++++++++++------- compiler/src/language_server/sourcetree.re | 5 +- 2 files changed, 68 insertions(+), 40 deletions(-) diff --git a/compiler/src/language_server/code_action.re b/compiler/src/language_server/code_action.re index 130d555310..0a602824d6 100644 --- a/compiler/src/language_server/code_action.re +++ b/compiler/src/language_server/code_action.re @@ -84,36 +84,7 @@ let named_arg_label = (range, uri, arg_label) => { }; }; -let add_graindoc = (range, uri, expr_type: Types.type_expr) => { - let output = Buffer.create(128); - Buffer.add_string(output, "/**\n"); - Buffer.add_string(output, " *\n"); // Blank spot for description entry - switch (expr_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"); - | _ => () - }; - Buffer.add_string(output, " *\n"); // Spacing - Buffer.add_string(output, " * @example\n"); - Buffer.add_string(output, " *\n"); // Spacing - Buffer.add_string(output, " * @since\n"); - Buffer.add_string(output, " */\n"); +let add_graindoc = (range, uri, message) => { ResponseResult.{ title: "Add Graindoc", kind: "add-graindoc", @@ -127,7 +98,7 @@ let add_graindoc = (range, uri, expr_type: Types.type_expr) => { edits: [ { range, - new_text: Buffer.contents(output), + new_text: message, }, ], }, @@ -265,6 +236,34 @@ 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, @@ -273,17 +272,49 @@ let rec process_graindoc = ) => { switch (results) { | [LetBind({pat, expr, loc}), ..._] => - let loc = { - ...loc, - loc_end: loc.loc_start, + 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)); }; - // TODO: Better place after and + | [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 - | _ => Some(add_graindoc(Utils.loc_to_range(loc), uri, expr.exp_type)) + | _ => + 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 diff --git a/compiler/src/language_server/sourcetree.re b/compiler/src/language_server/sourcetree.re index 3e4c260341..946e363181 100644 --- a/compiler/src/language_server/sourcetree.re +++ b/compiler/src/language_server/sourcetree.re @@ -587,10 +587,7 @@ module Sourcetree: Sourcetree = { mut_flag, pat: vb_pat, expr: vb_expr, - loc: { - ...vb_loc, - loc_start: stmt.ttop_loc.loc_start, - }, + loc: vb_loc, }), ), ...segments, From 8be204465eb29288824f47caa70025947ee7f162 Mon Sep 17 00:00:00 2001 From: Spotandjake Date: Tue, 10 Feb 2026 18:45:35 -0500 Subject: [PATCH 3/3] feat: Add tests --- compiler/test/suites/grainlsp.re | 269 ++++++++++++++++++++++++++++++- 1 file changed, 267 insertions(+), 2 deletions(-) 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",