From 42ad068fab3dc01fe439096a686461cc928e1c9a Mon Sep 17 00:00:00 2001 From: Gabriel Nordeborn Date: Thu, 29 Dec 2022 14:07:21 +0100 Subject: [PATCH 1/7] basic snippet support in completions --- analysis/src/Cfg.ml | 1 + analysis/src/Cli.ml | 12 +++- analysis/src/CompletionBackEnd.ml | 29 +++++++-- analysis/src/Protocol.ml | 63 ++++++++++++++----- analysis/src/SharedTypes.ml | 26 +++++++- .../CompletionFunctionArguments.res.txt | 4 +- server/src/server.ts | 9 ++- 7 files changed, 116 insertions(+), 28 deletions(-) create mode 100644 analysis/src/Cfg.ml diff --git a/analysis/src/Cfg.ml b/analysis/src/Cfg.ml new file mode 100644 index 000000000..28b92aaf7 --- /dev/null +++ b/analysis/src/Cfg.ml @@ -0,0 +1 @@ +let supportsSnippets = ref false diff --git a/analysis/src/Cli.ml b/analysis/src/Cli.ml index c0887eee4..407e9cde2 100644 --- a/analysis/src/Cli.ml +++ b/analysis/src/Cli.ml @@ -3,7 +3,7 @@ let help = **Private CLI For rescript-vscode usage only** API examples: - ./rescript-editor-analysis.exe completion src/MyFile.res 0 4 currentContent.res + ./rescript-editor-analysis.exe completion src/MyFile.res 0 4 currentContent.res true ./rescript-editor-analysis.exe definition src/MyFile.res 9 3 ./rescript-editor-analysis.exe typeDefinition src/MyFile.res 9 3 ./rescript-editor-analysis.exe documentSymbol src/Foo.res @@ -86,7 +86,11 @@ Options: let main () = match Array.to_list Sys.argv with - | [_; "completion"; path; line; col; currentFile] -> + | [_; "completion"; path; line; col; currentFile; supportsSnippets] -> + (Cfg.supportsSnippets := + match supportsSnippets with + | "true" -> true + | _ -> false); Commands.completion ~debug:false ~path ~pos:(int_of_string line, int_of_string col) ~currentFile @@ -143,7 +147,9 @@ let main () = (Json.escape (CreateInterface.command ~path ~cmiFile)) | [_; "format"; path] -> Printf.printf "\"%s\"" (Json.escape (Commands.format ~path)) - | [_; "test"; path] -> Commands.test ~path + | [_; "test"; path] -> + Cfg.supportsSnippets := true; + Commands.test ~path | args when List.mem "-h" args || List.mem "--help" args -> prerr_endline help | _ -> prerr_endline help; diff --git a/analysis/src/CompletionBackEnd.ml b/analysis/src/CompletionBackEnd.ml index 641d0f453..9df423043 100644 --- a/analysis/src/CompletionBackEnd.ml +++ b/analysis/src/CompletionBackEnd.ml @@ -1144,12 +1144,29 @@ let mkItem ~name ~kind ~detail ~deprecated ~docstring = documentation = (if docContent = "" then None else Some {kind = "markdown"; value = docContent}); + sortText = None; + insertText = None; + insertTextFormat = None; } -let completionToItem {Completion.name; deprecated; docstring; kind} = - mkItem ~name - ~kind:(Completion.kindToInt kind) - ~deprecated ~detail:(detail name kind) ~docstring +let completionToItem + { + Completion.name; + deprecated; + docstring; + kind; + sortText; + insertText; + insertTextFormat; + } = + let item = + mkItem ~name + ~kind:(Completion.kindToInt kind) + ~deprecated ~detail:(detail name kind) ~docstring + in + if !Cfg.supportsSnippets then + {item with sortText; insertText; insertTextFormat} + else item let completionsGetTypeEnv = function | {Completion.kind = Value typ; env} :: _ -> Some (typ, env) @@ -1554,9 +1571,9 @@ let completeTypedValue ~env ~envWhereCompletionStarted ~full ~prefix Completion.create ~name:"None" ~kind:(Label (t |> Shared.typeToString)) ~env; - Completion.create ~name:"Some(_)" + Completion.createWithSnippet ~name:"Some(_)" ~kind:(Label (t |> Shared.typeToString)) - ~env; + ~env ~insertText:"Some(${1:_})" (); ] |> filterItems ~prefix | _ -> [] diff --git a/analysis/src/Protocol.ml b/analysis/src/Protocol.ml index 68cd7661c..3dc07b564 100644 --- a/analysis/src/Protocol.ml +++ b/analysis/src/Protocol.ml @@ -33,11 +33,22 @@ type signatureHelp = { activeParameter: int option; } +(* https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#insertTextFormat *) +type insertTextFormat = PlainText | Snippet + +let insertTextFormatToInt f = + match f with + | PlainText -> 1 + | Snippet -> 2 + type completionItem = { label: string; kind: int; tags: int list; detail: string; + sortText: string option; + insertTextFormat: insertTextFormat option; + insertText: string option; documentation: markupContent option; } @@ -86,21 +97,45 @@ let stringifyRange r = let stringifyMarkupContent (m : markupContent) = Printf.sprintf {|{"kind": "%s", "value": "%s"}|} m.kind (Json.escape m.value) +(** None values are not emitted in the output. *) +let stringifyObject properties = + {|{ +|} + ^ (properties + |> List.filter_map (fun (key, value) -> + match value with + | None -> None + | Some v -> Some (Printf.sprintf {| "%s": %s|} key v)) + |> String.concat ",\n") + ^ "\n }" + +let wrapInQuotes s = "\"" ^ s ^ "\"" + +let optWrapInQuotes s = + match s with + | None -> None + | Some s -> Some (wrapInQuotes s) + let stringifyCompletionItem c = - Printf.sprintf - {|{ - "label": "%s", - "kind": %i, - "tags": %s, - "detail": "%s", - "documentation": %s - }|} - (Json.escape c.label) c.kind - (c.tags |> List.map string_of_int |> array) - (Json.escape c.detail) - (match c.documentation with - | None -> null - | Some doc -> stringifyMarkupContent doc) + stringifyObject + [ + ("label", Some (wrapInQuotes (Json.escape c.label))); + ("kind", Some (string_of_int c.kind)); + ("tags", Some (c.tags |> List.map string_of_int |> array)); + ("detail", Some (wrapInQuotes (Json.escape c.detail))); + ( "documentation", + Some + (match c.documentation with + | None -> null + | Some doc -> stringifyMarkupContent doc) ); + ("sortText", optWrapInQuotes c.sortText); + ("insertText", optWrapInQuotes c.insertText); + ( "insertTextFormat", + match c.insertTextFormat with + | None -> None + | Some insertTextFormat -> + Some (Printf.sprintf "%i" (insertTextFormatToInt insertTextFormat)) ); + ] let stringifyHover value = Printf.sprintf {|{"contents": %s}|} diff --git a/analysis/src/SharedTypes.ml b/analysis/src/SharedTypes.ml index 888b5adfc..333e5d427 100644 --- a/analysis/src/SharedTypes.ml +++ b/analysis/src/SharedTypes.ml @@ -292,6 +292,9 @@ module Completion = struct type t = { name: string; + sortText: string option; + insertText: string option; + insertTextFormat: Protocol.insertTextFormat option; env: QueryEnv.t; deprecated: string option; docstring: string list; @@ -299,7 +302,28 @@ module Completion = struct } let create ~name ~kind ~env = - {name; env; deprecated = None; docstring = []; kind} + { + name; + env; + deprecated = None; + docstring = []; + kind; + sortText = None; + insertText = None; + insertTextFormat = None; + } + + let createWithSnippet ~name ?insertText ~kind ~env ?sortText () = + { + name; + env; + deprecated = None; + docstring = []; + kind; + sortText; + insertText; + insertTextFormat = Some Protocol.Snippet; + } (* https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_completion *) (* https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#completionItemKind *) diff --git a/analysis/tests/src/expected/CompletionFunctionArguments.res.txt b/analysis/tests/src/expected/CompletionFunctionArguments.res.txt index 07b8b0604..58e291bb0 100644 --- a/analysis/tests/src/expected/CompletionFunctionArguments.res.txt +++ b/analysis/tests/src/expected/CompletionFunctionArguments.res.txt @@ -137,7 +137,9 @@ Completable: Cargument Value[someFnTakingVariant]($0=S) "kind": 4, "tags": [], "detail": "someVariant", - "documentation": null + "documentation": null, + "insertText": "Some(${1:_})", + "insertTextFormat": 2 }] Complete src/CompletionFunctionArguments.res 60:44 diff --git a/server/src/server.ts b/server/src/server.ts index e381851a2..ef2e24438 100644 --- a/server/src/server.ts +++ b/server/src/server.ts @@ -680,6 +680,7 @@ function completion(msg: p.RequestMessage) { params.position.line, params.position.character, tmpname, + "true", ], msg ); @@ -946,7 +947,7 @@ function createInterface(msg: p.RequestMessage): p.Message { jsonrpc: c.jsonrpcVersion, id: msg.id, result: { - uri: utils.pathToURI(resiPath) + uri: utils.pathToURI(resiPath), }, }; return response; @@ -1013,7 +1014,7 @@ function openCompiledFile(msg: p.RequestMessage): p.Message { id: msg.id, result: { uri: utils.pathToURI(compiledFilePath.result), - } + }, }; return response; @@ -1111,7 +1112,9 @@ function onMessage(msg: p.Message) { codeActionProvider: true, renameProvider: { prepareProvider: true }, documentSymbolProvider: true, - completionProvider: { triggerCharacters: [".", ">", "@", "~", '"', "="] }, + completionProvider: { + triggerCharacters: [".", ">", "@", "~", '"', "="], + }, semanticTokensProvider: { legend: { tokenTypes: [ From f5cfbf654dc45497c4a78bd57931b4d8fbfe8875 Mon Sep 17 00:00:00 2001 From: Gabriel Nordeborn Date: Thu, 29 Dec 2022 14:19:07 +0100 Subject: [PATCH 2/7] add snippets to constructor args when completing --- analysis/src/CompletionBackEnd.ml | 29 ++++++++++++------- .../CompletionFunctionArguments.res.txt | 20 +++++++++---- 2 files changed, 34 insertions(+), 15 deletions(-) diff --git a/analysis/src/CompletionBackEnd.ml b/analysis/src/CompletionBackEnd.ml index 9df423043..39e345b11 100644 --- a/analysis/src/CompletionBackEnd.ml +++ b/analysis/src/CompletionBackEnd.ml @@ -1528,6 +1528,15 @@ let filterItems items ~prefix = |> List.filter (fun (item : Completion.t) -> Utils.startsWith item.name prefix) +let printConstructorArgs argsLen ~asSnippet = + let args = ref [] in + for argNum = 1 to argsLen do + args := + !args @ [(if asSnippet then Printf.sprintf "${%i:_}" argNum else "_")] + done; + if List.length !args > 0 then "(" ^ (!args |> String.concat ", ") ^ ")" + else "" + let completeTypedValue ~env ~envWhereCompletionStarted ~full ~prefix ~expandOption = let namesUsed = Hashtbl.create 10 in @@ -1549,22 +1558,22 @@ let completeTypedValue ~env ~envWhereCompletionStarted ~full ~prefix | Some (Tvariant {env; constructors; variantDecl; variantName}) -> constructors |> List.map (fun (constructor : Constructor.t) -> - Completion.create + Completion.createWithSnippet ~name: (constructor.cname.txt - ^ - if constructor.args |> List.length > 0 then - "(" - ^ (constructor.args - |> List.map (fun _ -> "_") - |> String.concat ", ") - ^ ")" - else "") + ^ printConstructorArgs + (List.length constructor.args) + ~asSnippet:false) + ~insertText: + (constructor.cname.txt + ^ printConstructorArgs + (List.length constructor.args) + ~asSnippet:true) ~kind: (Constructor ( constructor, variantDecl |> Shared.declToString variantName )) - ~env) + ~env ()) |> filterItems ~prefix | Some (Toption (env, t)) -> [ diff --git a/analysis/tests/src/expected/CompletionFunctionArguments.res.txt b/analysis/tests/src/expected/CompletionFunctionArguments.res.txt index 58e291bb0..8f704e73a 100644 --- a/analysis/tests/src/expected/CompletionFunctionArguments.res.txt +++ b/analysis/tests/src/expected/CompletionFunctionArguments.res.txt @@ -95,19 +95,25 @@ Completable: Cargument Value[someFnTakingVariant](~config) "kind": 4, "tags": [], "detail": "One\n\ntype someVariant = One | Two | Three(int, string)", - "documentation": null + "documentation": null, + "insertText": "One", + "insertTextFormat": 2 }, { "label": "Two", "kind": 4, "tags": [], "detail": "Two\n\ntype someVariant = One | Two | Three(int, string)", - "documentation": null + "documentation": null, + "insertText": "Two", + "insertTextFormat": 2 }, { "label": "Three(_, _)", "kind": 4, "tags": [], "detail": "Three(int, string)\n\ntype someVariant = One | Two | Three(int, string)", - "documentation": null + "documentation": null, + "insertText": "Three(${1:_}, ${2:_})", + "insertTextFormat": 2 }] Complete src/CompletionFunctionArguments.res 54:40 @@ -119,7 +125,9 @@ Completable: Cargument Value[someFnTakingVariant](~config=O) "kind": 4, "tags": [], "detail": "One\n\ntype someVariant = One | Two | Three(int, string)", - "documentation": null + "documentation": null, + "insertText": "One", + "insertTextFormat": 2 }, { "label": "OIncludeMeInCompletions", "kind": 9, @@ -151,7 +159,9 @@ Completable: Cargument Value[someFnTakingVariant](~configOpt2=O) "kind": 4, "tags": [], "detail": "One\n\ntype someVariant = One | Two | Three(int, string)", - "documentation": null + "documentation": null, + "insertText": "One", + "insertTextFormat": 2 }, { "label": "OIncludeMeInCompletions", "kind": 9, From af97f37b20d7fffd5bd7d2befcb848c7a7a8a52b Mon Sep 17 00:00:00 2001 From: Gabriel Nordeborn Date: Thu, 29 Dec 2022 14:29:56 +0100 Subject: [PATCH 3/7] simple completion for tuples --- analysis/src/CompletionBackEnd.ml | 10 +++++++++- analysis/src/SharedTypes.ml | 2 +- .../tests/src/CompletionFunctionArguments.res | 7 +++++++ .../CompletionFunctionArguments.res.txt | 20 +++++++++++++++++++ 4 files changed, 37 insertions(+), 2 deletions(-) diff --git a/analysis/src/CompletionBackEnd.ml b/analysis/src/CompletionBackEnd.ml index 39e345b11..daaa056f6 100644 --- a/analysis/src/CompletionBackEnd.ml +++ b/analysis/src/CompletionBackEnd.ml @@ -1518,7 +1518,7 @@ let rec extractType ~env ~package (t : Types.type_expr) = (Tvariant {env; constructors; variantName = name.txt; variantDecl = decl}) | _ -> None) - | Ttuple expressions -> Some (Tuple (env, expressions)) + | Ttuple expressions -> Some (Tuple (env, expressions, t)) | _ -> None let filterItems items ~prefix = @@ -1585,6 +1585,14 @@ let completeTypedValue ~env ~envWhereCompletionStarted ~full ~prefix ~env ~insertText:"Some(${1:_})" (); ] |> filterItems ~prefix + | Some (Tuple (env, exprs, typ)) -> + let numExprs = List.length exprs in + [ + Completion.createWithSnippet + ~name:(printConstructorArgs numExprs ~asSnippet:false) + ~insertText:(printConstructorArgs numExprs ~asSnippet:true) + ~kind:(Value typ) ~env (); + ] | _ -> [] in (* Include all values and modules in completion if there's a prefix, not otherwise *) diff --git a/analysis/src/SharedTypes.ml b/analysis/src/SharedTypes.ml index 333e5d427..b68eb57ee 100644 --- a/analysis/src/SharedTypes.ml +++ b/analysis/src/SharedTypes.ml @@ -569,7 +569,7 @@ module Completable = struct (** An extracted type from a type expr *) type extractedType = - | Tuple of QueryEnv.t * Types.type_expr list + | Tuple of QueryEnv.t * Types.type_expr list * Types.type_expr | Toption of QueryEnv.t * Types.type_expr | Tbool of QueryEnv.t | Tvariant of { diff --git a/analysis/tests/src/CompletionFunctionArguments.res b/analysis/tests/src/CompletionFunctionArguments.res index f002a444d..b6961351c 100644 --- a/analysis/tests/src/CompletionFunctionArguments.res +++ b/analysis/tests/src/CompletionFunctionArguments.res @@ -69,3 +69,10 @@ let someFnTakingVariant = ( // let _ = 1->someOtherFn(1, t) // ^com + +let fnTakingTuple = (arg: (int, int, float)) => { + ignore(arg) +} + +// let _ = fnTakingTuple() +// ^com diff --git a/analysis/tests/src/expected/CompletionFunctionArguments.res.txt b/analysis/tests/src/expected/CompletionFunctionArguments.res.txt index 8f704e73a..1d265ed4a 100644 --- a/analysis/tests/src/expected/CompletionFunctionArguments.res.txt +++ b/analysis/tests/src/expected/CompletionFunctionArguments.res.txt @@ -84,6 +84,12 @@ Completable: Cargument Value[someOtherFn]($0=f) "tags": [], "detail": "bool", "documentation": null + }, { + "label": "fnTakingTuple", + "kind": 12, + "tags": [], + "detail": "((int, int, float)) => unit", + "documentation": null }] Complete src/CompletionFunctionArguments.res 51:39 @@ -223,3 +229,17 @@ Completable: Cargument Value[someOtherFn]($2=t) "documentation": null }] +Complete src/CompletionFunctionArguments.res 76:25 +posCursor:[76:25] posNoWhite:[76:24] Found expr:[76:11->76:26] +Pexp_apply ...[76:11->76:24] (...[76:25->76:26]) +Completable: Cargument Value[fnTakingTuple]($0) +[{ + "label": "(_, _, _)", + "kind": 12, + "tags": [], + "detail": "(int, int, float)", + "documentation": null, + "insertText": "(${1:_}, ${2:_}, ${3:_})", + "insertTextFormat": 2 + }] + From 92dad36d36fa1f151e00f526ecb4a62f3c490310 Mon Sep 17 00:00:00 2001 From: Gabriel Nordeborn Date: Thu, 29 Dec 2022 14:32:38 +0100 Subject: [PATCH 4/7] undo unecessary formatting --- server/src/server.ts | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/server/src/server.ts b/server/src/server.ts index ef2e24438..dfa55bd60 100644 --- a/server/src/server.ts +++ b/server/src/server.ts @@ -947,7 +947,7 @@ function createInterface(msg: p.RequestMessage): p.Message { jsonrpc: c.jsonrpcVersion, id: msg.id, result: { - uri: utils.pathToURI(resiPath), + uri: utils.pathToURI(resiPath) }, }; return response; @@ -1014,7 +1014,7 @@ function openCompiledFile(msg: p.RequestMessage): p.Message { id: msg.id, result: { uri: utils.pathToURI(compiledFilePath.result), - }, + } }; return response; @@ -1112,9 +1112,7 @@ function onMessage(msg: p.Message) { codeActionProvider: true, renameProvider: { prepareProvider: true }, documentSymbolProvider: true, - completionProvider: { - triggerCharacters: [".", ">", "@", "~", '"', "="], - }, + completionProvider: { triggerCharacters: [".", ">", "@", "~", '"', "="] }, semanticTokensProvider: { legend: { tokenTypes: [ From 2da13134c30f90b06a0593910ca8589f2f8778b5 Mon Sep 17 00:00:00 2001 From: Gabriel Nordeborn Date: Thu, 29 Dec 2022 18:47:06 +0100 Subject: [PATCH 5/7] propagate snippet support status from client capabilities --- server/src/server.ts | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/server/src/server.ts b/server/src/server.ts index dfa55bd60..696247abf 100644 --- a/server/src/server.ts +++ b/server/src/server.ts @@ -46,6 +46,7 @@ interface extensionConfiguration { // work in one client, like VSCode, but perhaps not in others, like vim. export interface extensionClientCapabilities { supportsMarkdownLinks?: boolean | null; + supportsSnippetSyntax?: boolean | null; } let extensionClientCapabilities: extensionClientCapabilities = {}; @@ -680,7 +681,7 @@ function completion(msg: p.RequestMessage) { params.position.line, params.position.character, tmpname, - "true", + Boolean(extensionClientCapabilities.supportsSnippetSyntax), ], msg ); @@ -947,7 +948,7 @@ function createInterface(msg: p.RequestMessage): p.Message { jsonrpc: c.jsonrpcVersion, id: msg.id, result: { - uri: utils.pathToURI(resiPath) + uri: utils.pathToURI(resiPath), }, }; return response; @@ -1014,7 +1015,7 @@ function openCompiledFile(msg: p.RequestMessage): p.Message { id: msg.id, result: { uri: utils.pathToURI(compiledFilePath.result), - } + }, }; return response; @@ -1096,6 +1097,11 @@ function onMessage(msg: p.Message) { extensionClientCapabilities = extensionClientCapabilitiesFromClient; } + extensionClientCapabilities.supportsSnippetSyntax = Boolean( + initParams.capabilities.textDocument?.completion?.completionItem + ?.snippetSupport + ); + // send the list of features we support let result: p.InitializeResult = { // This tells the client: "hey, we support the following operations". @@ -1112,7 +1118,9 @@ function onMessage(msg: p.Message) { codeActionProvider: true, renameProvider: { prepareProvider: true }, documentSymbolProvider: true, - completionProvider: { triggerCharacters: [".", ">", "@", "~", '"', "="] }, + completionProvider: { + triggerCharacters: [".", ">", "@", "~", '"', "="], + }, semanticTokensProvider: { legend: { tokenTypes: [ From e9a2acbcca679e1761ef6e19e0fd6d45920e4bb9 Mon Sep 17 00:00:00 2001 From: Gabriel Nordeborn Date: Thu, 29 Dec 2022 18:50:16 +0100 Subject: [PATCH 6/7] regen test after rebase --- analysis/tests/src/expected/CompletionJsxProps.res.txt | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/analysis/tests/src/expected/CompletionJsxProps.res.txt b/analysis/tests/src/expected/CompletionJsxProps.res.txt index c0c4f5133..7a6dec0df 100644 --- a/analysis/tests/src/expected/CompletionJsxProps.res.txt +++ b/analysis/tests/src/expected/CompletionJsxProps.res.txt @@ -37,12 +37,16 @@ Completable: CjsxPropValue [CompletionSupport, TestComponent] test=T "kind": 4, "tags": [], "detail": "Two\n\ntype testVariant = One | Two | Three(int)", - "documentation": null + "documentation": null, + "insertText": "Two", + "insertTextFormat": 2 }, { "label": "Three(_)", "kind": 4, "tags": [], "detail": "Three(int)\n\ntype testVariant = One | Two | Three(int)", - "documentation": null + "documentation": null, + "insertText": "Three(${1:_})", + "insertTextFormat": 2 }] From 7b911225f38040fcc7d6f9a7bd3c8be06774ea58 Mon Sep 17 00:00:00 2001 From: Gabriel Nordeborn Date: Thu, 29 Dec 2022 18:51:16 +0100 Subject: [PATCH 7/7] changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 49b4d835a..2528a1e55 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,7 @@ - Add autocomplete for function argument values (booleans, variants and options. More values coming), both labelled and unlabelled. https://github.com/rescript-lang/rescript-vscode/pull/665 - Add autocomplete for JSX prop values. https://github.com/rescript-lang/rescript-vscode/pull/667 +- Add snippet support in completion items. https://github.com/rescript-lang/rescript-vscode/pull/668 #### :nail_care: Polish