diff --git a/CHANGELOG.md b/CHANGELOG.md index f968d0863..19f723e83 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,7 @@ - Add support for create interface file for JSX V4 https://github.com/rescript-lang/rescript-vscode/pull/580 - Expand one level of type definition on hover. Dig into record/variant body. https://github.com/rescript-lang/rescript-vscode/pull/584 - Add clickable links to type definitions in hovers. https://github.com/rescript-lang/rescript-vscode/pull/585 +- Add experimental signature help for function calls. https://github.com/rescript-lang/rescript-vscode/pull/547 #### :bug: Bug Fix diff --git a/README.md b/README.md index 7b6c9eaf2..66c007084 100644 --- a/README.md +++ b/README.md @@ -68,6 +68,7 @@ The only 2 themes we don't (and can't) support, due to their lack of coloring, a - Find references. - Rename. - Inlay Hints. +- Signature help. - Code lenses. - Snippets to ease a few syntaxes: - `external` features such as `@bs.module` and `@bs.val` @@ -85,6 +86,7 @@ ext install chenglou92.rescript-vscode The plugin activates on `.res` and `.resi` files. If you've already got Reason-Language-Server installed, it's possible that the latter took precedence over this one. Make sure you're using this plugin ("ReScript syntax") rather than Reason-Language-Server ("BuckleScript syntax"). ### Pre-release channel + There is a pre-release channel available. It is intended for testing new and therefore possibly unstable features. You can activate it by clicking on the "Switch to Pre-Release Version" button on the `rescript-vscode` extension page in VSCode. From this point on, pre-release versions will always have an odd version minor (1.5.x, 1.7.x, 2.1.x, etc.) while stable releases have even version minor numbers (1.4.x, 1.6.x, 2.0.0, etc.). Even if the pre-release channel seems too experimental to you, we still suggest you to give it a try and submit any issues that you run into. In the long run it will give us a better editor experience overall. @@ -92,7 +94,7 @@ Even if the pre-release channel seems too experimental to you, we still suggest ## 📦 Commands | Command | Description | -|------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| ---------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | ReScript: Create an interface file for this implementation file | Creates an interface file (`.resi`) for the current `.res` file, automatically filling in all types and values in the current file. | | ReScript: Open the compiled JS file for this implementation file | Opens the compiled JS file for the current ReScript file. | | ReScript: Switch implementation/interface | Switches between the implementation and interface file. If you're in a `.res` file, the command will open the corresponding `.resi` file (if it exists), and if you're in a `.resi` file the command will open the corresponding `.res` file. This can also be triggered with the keybinding `Alt+O`. | @@ -104,10 +106,11 @@ You'll find all ReScript specific settings under the scope `rescript.settings`. | Setting | Description | | ------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| Prompt to Start Build | If there's no ReScript build running already in the opened project, the extension will prompt you and ask if you want to start a build automatically. You can turn off this automatic prompt via the setting `rescript.settings.askToStartBuild`. | +| Prompt to Start Build | If there's no ReScript build running already in the opened project, the extension will prompt you and ask if you want to start a build automatically. You can turn off this automatic prompt via the setting `rescript.settings.askToStartBuild`. | | ReScript Binary Path | The extension will look for the existence of a `/node_modules/.bin/rescript` file and use its directory as the `binaryPath`. If it does not find it at the project root (which is where the nearest `bsconfig.json` resides), it goes up folders in the filesystem recursively until it either finds it (often the case in monorepos) or hits the top level. To override this lookup process, the path can be configured explicitly using the setting `rescript.settings.binaryPath` | -| Inlay Hints (experimental) | This allows an editor to place annotations inline with text to display type hints. Enable using `rescript.settings.inlayHints.enable: true` | -| Code Lens (experimental) | This tells the editor to add code lenses to function definitions, showing its full type above the definition. Enable using `rescript.settings.codeLens: true` | +| Inlay Hints (experimental) | This allows an editor to place annotations inline with text to display type hints. Enable using `rescript.settings.inlayHints.enable: true` | +| Code Lens (experimental) | This tells the editor to add code lenses to function definitions, showing its full type above the definition. Enable using `rescript.settings.codeLens: true` | +| Signature Help (experimental) | This tells the editor to show signature help when you're writing function calls. Enable using `rescript.settings.signatureHelp.enable: true` | | Autostarting the Code Analyzer | The Code Analyzer needs to be started manually by default. However, you can configure the extension to start the Code Analyzer automatically via the setting `rescript.settings.autoRunCodeAnalysis`. | **Default settings:** diff --git a/analysis/src/Cli.ml b/analysis/src/Cli.ml index cf657cec4..c0887eee4 100644 --- a/analysis/src/Cli.ml +++ b/analysis/src/Cli.ml @@ -75,6 +75,10 @@ Options: ./rescript-editor-analysis.exe codeLens src/MyFile.res + signatureHelp: get signature help if available for position at line 10 column 2 in src/MyFile.res + + ./rescript-editor-analysis.exe signatureHelp src/MyFile.res 10 2 + test: run tests specified by special comments in file src/MyFile.res ./rescript-editor-analysis.exe test src/src/MyFile.res @@ -103,6 +107,10 @@ let main () = (match supportsMarkdownLinks with | "true" -> true | _ -> false) + | [_; "signatureHelp"; path; line; col; currentFile] -> + Commands.signatureHelp ~path + ~pos:(int_of_string line, int_of_string col) + ~currentFile ~debug:false | [_; "inlayHint"; path; line_start; line_end; maxLength] -> Commands.inlayhint ~path ~pos:(int_of_string line_start, int_of_string line_end) diff --git a/analysis/src/Commands.ml b/analysis/src/Commands.ml index 2ad4731f2..cfece2bc2 100644 --- a/analysis/src/Commands.ml +++ b/analysis/src/Commands.ml @@ -79,6 +79,15 @@ let hover ~path ~pos ~currentFile ~debug ~supportsMarkdownLinks = in print_endline result +let signatureHelp ~path ~pos ~currentFile ~debug = + let result = + match SignatureHelp.signatureHelp ~path ~pos ~currentFile ~debug with + | None -> + {Protocol.signatures = []; activeSignature = None; activeParameter = None} + | Some res -> res + in + print_endline (Protocol.stringifySignatureHelp result) + let codeAction ~path ~pos ~currentFile ~debug = Xform.extractCodeActions ~path ~pos ~currentFile ~debug |> CodeActions.stringifyCodeActions |> print_endline @@ -335,6 +344,13 @@ let test ~path = hover ~supportsMarkdownLinks:true ~path ~pos:(line, col) ~currentFile ~debug:true; Sys.remove currentFile + | "she" -> + print_endline + ("Signature help " ^ path ^ " " ^ string_of_int line ^ ":" + ^ string_of_int col); + let currentFile = createCurrentFile () in + signatureHelp ~path ~pos:(line, col) ~currentFile ~debug:true; + Sys.remove currentFile | "int" -> print_endline ("Create Interface " ^ path); let cmiFile = diff --git a/analysis/src/CompletionFrontEnd.ml b/analysis/src/CompletionFrontEnd.ml index 5c55baf62..bce92250b 100644 --- a/analysis/src/CompletionFrontEnd.ml +++ b/analysis/src/CompletionFrontEnd.ml @@ -106,16 +106,6 @@ let extractJsxProps ~(compName : Longident.t Location.loc) ~args = in args |> processProps ~acc:[] -type labelled = { - name: string; - opt: bool; - posStart: int * int; - posEnd: int * int; -} - -type label = labelled option -type arg = {label: label; exp: Parsetree.expression} - let findNamedArgCompletable ~(args : arg list) ~endPos ~posBeforeCursor ~(contextPath : Completable.contextPath) ~posAfterFunExpr = let allNames = @@ -145,37 +135,6 @@ let findNamedArgCompletable ~(args : arg list) ~endPos ~posBeforeCursor in loop args -let extractExpApplyArgs ~args = - let rec processArgs ~acc args = - match args with - | (((Asttypes.Labelled s | Optional s) as label), (e : Parsetree.expression)) - :: rest -> ( - let namedArgLoc = - e.pexp_attributes - |> List.find_opt (fun ({Asttypes.txt}, _) -> txt = "ns.namedArgLoc") - in - match namedArgLoc with - | Some ({loc}, _) -> - let labelled = - { - name = s; - opt = - (match label with - | Optional _ -> true - | _ -> false); - posStart = Loc.start loc; - posEnd = Loc.end_ loc; - } - in - processArgs ~acc:({label = Some labelled; exp = e} :: acc) rest - | None -> processArgs ~acc rest) - | (Asttypes.Nolabel, (e : Parsetree.expression)) :: rest -> - if e.pexp_loc.loc_ghost then processArgs ~acc rest - else processArgs ~acc:({label = None; exp = e} :: acc) rest - | [] -> List.rev acc - in - args |> processArgs ~acc:[] - let rec exprToContextPath (e : Parsetree.expression) = match e.pexp_desc with | Pexp_constant (Pconst_string _) -> Some Completable.CPString @@ -204,7 +163,7 @@ let completionWithParser1 ~currentFile ~debug ~offset ~path ~posCursor ~text = let line, col = posCursor in (line, max 0 col - offset + offsetNoWhite) in - let posBeforeCursor = (fst posCursor, max 0 (snd posCursor - 1)) in + let posBeforeCursor = Pos.posBeforeCursor posCursor in let charBeforeCursor, blankAfterCursor = match Pos.positionToOffset text posCursor with | Some offset when offset > 0 -> ( diff --git a/analysis/src/Pos.ml b/analysis/src/Pos.ml index a493946fc..081c575ea 100644 --- a/analysis/src/Pos.ml +++ b/analysis/src/Pos.ml @@ -24,3 +24,5 @@ let positionToOffset text (line, character) = | Some bol -> if bol + character <= String.length text then Some (bol + character) else None + +let posBeforeCursor pos = (fst pos, max 0 (snd pos - 1)) diff --git a/analysis/src/Protocol.ml b/analysis/src/Protocol.ml index a1440aa85..5a40a7d0c 100644 --- a/analysis/src/Protocol.ml +++ b/analysis/src/Protocol.ml @@ -16,6 +16,23 @@ type inlayHint = { paddingRight: bool; } +(* https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#parameterInformation *) +type parameterInformation = {label: int * int; documentation: markupContent} + +(* https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#signatureInformation *) +type signatureInformation = { + label: string; + parameters: parameterInformation list; + documentation: markupContent option; +} + +(* https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#signatureHelp *) +type signatureHelp = { + signatures: signatureInformation list; + activeSignature: int option; + activeParameter: int option; +} + type completionItem = { label: string; kind: int; @@ -170,6 +187,45 @@ let stringifyCodeLens (codeLens : codeLens) = | None -> "" | Some command -> stringifyCommand command) +let stringifyParameterInformation (parameterInformation : parameterInformation) + = + Printf.sprintf {|{"label": %s, "documentation": %s}|} + (let line, chr = parameterInformation.label in + "[" ^ string_of_int line ^ ", " ^ string_of_int chr ^ "]") + (stringifyMarkupContent parameterInformation.documentation) + +let stringifySignatureInformation (signatureInformation : signatureInformation) + = + Printf.sprintf + {|{ + "label": "%s", + "parameters": %s%s + }|} + (Json.escape signatureInformation.label) + (signatureInformation.parameters + |> List.map stringifyParameterInformation + |> array) + (match signatureInformation.documentation with + | None -> "" + | Some docs -> + Printf.sprintf ",\n \"documentation\": %s" + (stringifyMarkupContent docs)) + +let stringifySignatureHelp (signatureHelp : signatureHelp) = + Printf.sprintf + {|{ + "signatures": %s, + "activeSignature": %s, + "activeParameter": %s +}|} + (signatureHelp.signatures |> List.map stringifySignatureInformation |> array) + (match signatureHelp.activeSignature with + | None -> null + | Some activeSignature -> activeSignature |> string_of_int) + (match signatureHelp.activeParameter with + | None -> null + | Some activeParameter -> activeParameter |> string_of_int) + (* https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#diagnostic *) let stringifyDiagnostic d = Printf.sprintf diff --git a/analysis/src/SignatureHelp.ml b/analysis/src/SignatureHelp.ml new file mode 100644 index 000000000..fdefee2a1 --- /dev/null +++ b/analysis/src/SignatureHelp.ml @@ -0,0 +1,284 @@ +open SharedTypes +type cursorAtArg = Unlabelled of int | Labelled of string + +let findFunctionType ~currentFile ~debug ~path ~pos = + let completables = + let textOpt = Files.readFile currentFile in + match textOpt with + | None | Some "" -> None + | Some text -> ( + (* Leverage the completion functionality to pull out the type of the identifier doing the function application. + This lets us leverage all of the smart work done in completions to find the correct type in many cases even + for files not saved yet. *) + match + CompletionFrontEnd.completionWithParser ~debug ~path ~posCursor:pos + ~currentFile ~text + with + | None -> None + | Some (completable, scope) -> ( + match Cmt.fullFromPath ~path with + | None -> None + | Some {file; package} -> + let env = QueryEnv.fromFile file in + Some + ( completable + |> CompletionBackEnd.processCompletable ~debug ~package ~pos + ~scope ~env ~forHover:true, + env, + package, + file ))) + in + match completables with + | Some ({kind = Value type_expr; name; docstring} :: _, env, package, file) -> + let args, _ = + CompletionBackEnd.extractFunctionType type_expr ~env ~package + in + Some (args, name, docstring, type_expr, package, env, file) + | _ -> None + +(* Extracts all parameters from a parsed function signature *) +let extractParameters ~signature ~label = + match signature with + | [ + { + Parsetree.psig_desc = + Psig_value {pval_type = {ptyp_desc = Ptyp_arrow _} as expr}; + }; + ] -> + let rec extractParams expr params = + match expr with + | { + (* Gotcha: functions with multiple arugments are modelled as a series of single argument functions. *) + Parsetree.ptyp_desc = + Ptyp_arrow (argumentLabel, argumentTypeExpr, nextFunctionExpr); + ptyp_loc; + } -> + let startOffset = + ptyp_loc |> Loc.start |> Pos.positionToOffset label |> Option.get + in + let endOffset = + argumentTypeExpr.ptyp_loc |> Loc.end_ |> Pos.positionToOffset label + |> Option.get + in + (* The AST locations does not account for "=?" of optional arguments, so add that to the offset here if needed. *) + let endOffset = + match argumentLabel with + | Asttypes.Optional _ -> endOffset + 2 + | _ -> endOffset + in + extractParams nextFunctionExpr + (params @ [(argumentLabel, startOffset, endOffset)]) + | _ -> params + in + extractParams expr [] + | _ -> [] + +(* Finds what parameter is active, if any *) +let findActiveParameter ~argAtCursor ~args = + match argAtCursor with + | None -> ( + (* If a function only has one, unlabelled argument, we can safely assume that's active whenever we're in the signature help for that function, + even if we technically didn't find anything at the cursor (which we don't for empty expressions). *) + match args with + | [(Asttypes.Nolabel, _)] -> Some 0 + | _ -> None) + | Some (Unlabelled unlabelledArgumentIndex) -> + let index = ref 0 in + args + |> List.find_map (fun (label, _) -> + match label with + | Asttypes.Nolabel when !index = unlabelledArgumentIndex -> + Some !index + | _ -> + index := !index + 1; + None) + | Some (Labelled name) -> + let index = ref 0 in + args + |> List.find_map (fun (label, _) -> + match label with + | (Asttypes.Labelled labelName | Optional labelName) + when labelName = name -> + Some !index + | _ -> + index := !index + 1; + None) + +let shouldPrintMainTypeStr typ ~env ~package = + match typ |> Shared.digConstructor with + | Some path -> ( + match References.digConstructor ~env ~package path with + | Some (_, {item = {kind = Record _}}) -> false + | _ -> true) + | _ -> false + +(* Produces the doc string shown below the signature help for each parameter. *) +let docsForLabel typeExpr ~file ~package ~supportsMarkdownLinks = + let env = QueryEnv.fromFile file in + let types = Hover.findRelevantTypesFromType ~file ~package typeExpr in + let typeString = + if shouldPrintMainTypeStr typeExpr ~env ~package then + Markdown.codeBlock (typeExpr |> Shared.typeToString) + else "" + in + let typeNames = types |> List.map (fun {Hover.name} -> name) in + let typeDefinitions = + types + |> List.map (fun {Hover.decl; name; env; loc; path} -> + let linkToTypeDefinitionStr = + if supportsMarkdownLinks then + Markdown.goToDefinitionText ~env ~pos:loc.Warnings.loc_start + else "" + in + (* Since printing the whole name via its path can get quite long, and + we're short on space for the signature help, we'll only print the + fully "qualified" type name if we must (ie if several types we're + displaying have the same name). *) + let multipleTypesHaveThisName = + typeNames + |> List.filter (fun typeName -> typeName = name) + |> List.length > 1 + in + let typeName = + if multipleTypesHaveThisName then + path |> SharedTypes.pathIdentToString + else name + in + Markdown.codeBlock + (Shared.declToString ~printNameAsIs:true typeName decl) + ^ linkToTypeDefinitionStr) + in + typeString :: typeDefinitions |> String.concat "\n" + +let signatureHelp ~path ~pos ~currentFile ~debug = + let posBeforeCursor = Pos.posBeforeCursor pos in + let supportsMarkdownLinks = true in + let foundFunctionApplicationExpr = ref None in + let setFound r = foundFunctionApplicationExpr := Some r in + let expr (iterator : Ast_iterator.iterator) (expr : Parsetree.expression) = + (match expr with + (* Look for applying idents, like someIdent(...) *) + | { + pexp_desc = Pexp_apply (({pexp_desc = Pexp_ident _} as exp), args); + pexp_loc; + } + when pexp_loc + |> CursorPosition.classifyLoc ~pos:posBeforeCursor + == HasCursor -> + let extractedArgs = extractExpApplyArgs ~args in + let argAtCursor = + let unlabelledArgCount = ref 0 in + extractedArgs + |> List.find_map (fun arg -> + match arg.label with + | None -> + let currentUnlabelledArgCount = !unlabelledArgCount in + unlabelledArgCount := currentUnlabelledArgCount + 1; + (* An argument without a label is just the expression, so we can use that. *) + if arg.exp.pexp_loc |> Loc.hasPos ~pos:posBeforeCursor then + Some (Unlabelled currentUnlabelledArgCount) + else None + | Some {name; posStart; posEnd} -> ( + (* Check for the label identifier itself having the cursor *) + match + pos |> CursorPosition.classifyPositions ~posStart ~posEnd + with + | HasCursor -> Some (Labelled name) + | NoCursor | EmptyLoc -> ( + (* If we're not in the label, check the exp. Either the exp + exists and has the cursor. Or the exp is a parser recovery + node, in which case we assume that the parser recovery + indicates that the cursor was here. *) + match + ( arg.exp.pexp_desc, + arg.exp.pexp_loc + |> CursorPosition.classifyLoc ~pos:posBeforeCursor ) + with + | Pexp_extension ({txt = "rescript.exprhole"}, _), _ + | _, HasCursor -> + Some (Labelled name) + | _ -> None))) + in + setFound (argAtCursor, exp, extractedArgs) + | _ -> ()); + Ast_iterator.default_iterator.expr iterator expr + in + let iterator = {Ast_iterator.default_iterator with expr} in + let parser = Res_driver.parsingEngine.parseImplementation ~forPrinter:false in + let {Res_driver.parsetree = structure} = parser ~filename:currentFile in + iterator.structure iterator structure |> ignore; + match !foundFunctionApplicationExpr with + | Some (argAtCursor, exp, _extractedArgs) -> ( + (* Not looking for the cursor position after this, but rather the target function expression's loc. *) + let pos = exp.pexp_loc |> Loc.end_ in + match findFunctionType ~currentFile ~debug ~path ~pos with + | Some (args, name, docstring, type_expr, package, _env, file) -> + if debug then + Printf.printf "argAtCursor: %s\n" + (match argAtCursor with + | None -> "none" + | Some (Labelled name) -> "~" ^ name + | Some (Unlabelled index) -> "unlabelled<" ^ string_of_int index ^ ">"); + + (* The LS protocol wants us to send both the full type signature (label) that the end user sees as the signature help, and all parameters in that label + in the form of a list of start/end character offsets. We leverage the parser to figure the offsets out by parsing the label, and extract the + offsets from the parser. *) + + (* Put together a label here that both makes sense to show to the end user in the signature help, but also can be passed to the parser. *) + let label = "let " ^ name ^ ": " ^ Shared.typeToString type_expr in + let {Res_driver.parsetree = signature} = + Res_driver.parseInterfaceFromSource ~forPrinter:false + ~displayFilename:"" ~source:label + in + + let parameters = extractParameters ~signature ~label in + if debug then + Printf.printf "extracted params: \n%s\n" + (parameters + |> List.map (fun (_, start, end_) -> + String.sub label start (end_ - start)) + |> list); + + (* Figure out the active parameter *) + let activeParameter = findActiveParameter ~argAtCursor ~args in + Some + { + Protocol.signatures = + [ + { + label; + parameters = + parameters + |> List.map (fun (argLabel, start, end_) -> + { + Protocol.label = (start, end_); + documentation = + (match + args + |> List.find_opt (fun (lbl, _) -> + lbl = argLabel) + with + | None -> + {Protocol.kind = "markdown"; value = "Nope"} + | Some (_, labelTypExpr) -> + { + Protocol.kind = "markdown"; + value = + docsForLabel ~supportsMarkdownLinks ~file + ~package labelTypExpr; + }); + }); + documentation = + (match List.nth_opt docstring 0 with + | None -> None + | Some docs -> Some {Protocol.kind = "markdown"; value = docs}); + }; + ]; + activeSignature = Some 0; + activeParameter = + (match activeParameter with + | None -> Some (-1) + | activeParameter -> activeParameter); + } + | _ -> None) + | _ -> None diff --git a/analysis/src/Uri.ml b/analysis/src/Uri.ml index 8fbd935c5..4b1d67f54 100644 --- a/analysis/src/Uri.ml +++ b/analysis/src/Uri.ml @@ -23,7 +23,6 @@ let toTopLevelLoc {path} = let toString {uri} = if !stripPath then Filename.basename uri else uri - (* Light weight, hopefully-enough-for-the-purpose fn to encode URI components. Built to handle the reserved characters listed in https://en.wikipedia.org/wiki/Percent-encoding. Note that this function is not diff --git a/analysis/tests/src/SignatureHelp.res b/analysis/tests/src/SignatureHelp.res new file mode 100644 index 000000000..6d2e81b9b --- /dev/null +++ b/analysis/tests/src/SignatureHelp.res @@ -0,0 +1,52 @@ +type someVariant = One | Two | Three + +/** Does stuff. */ +let someFunc = (one: int, ~two: option=?, ~three: unit => unit, ~four: someVariant, ()) => { + ignore(one) + ignore(two) + ignore(three()) + ignore(four) +} + +let otherFunc = (first: string, second: int, third: float) => { + ignore(first) + ignore(second) + ignore(third) +} + +// let _ = someFunc( +// ^she + +// let _ = someFunc(1 +// ^she + +// let _ = someFunc(123, ~two +// ^she + +// let _ = someFunc(123, ~two="123" +// ^she + +// let _ = someFunc(123, ~two="123", ~four +// ^she + +// let _ = someFunc(123, ~two="123", ~four=O +// ^she + +// let _ = otherFunc( +// ^she + +// let _ = otherFunc("123" +// ^she + +// let _ = otherFunc("123", 123, 123.0) +// ^she + +// let _ = Completion.Lib.foo(~age +// ^she + +let iAmSoSpecial = (iJustHaveOneArg: string) => { + ignore(iJustHaveOneArg) +} + +// let _ = iAmSoSpecial( +// ^she diff --git a/analysis/tests/src/expected/SignatureHelp.res.txt b/analysis/tests/src/expected/SignatureHelp.res.txt new file mode 100644 index 000000000..897a350a3 --- /dev/null +++ b/analysis/tests/src/expected/SignatureHelp.res.txt @@ -0,0 +1,199 @@ +Signature help src/SignatureHelp.res 16:20 +posCursor:[16:19] posNoWhite:[16:18] Found expr:[16:11->16:20] +Pexp_apply ...[16:11->16:19] (...[46:0->16:20]) +posCursor:[16:19] posNoWhite:[16:18] Found expr:[16:11->16:19] +Pexp_ident someFunc:[16:11->16:19] +argAtCursor: none +extracted params: +[( + int, ~two: string=?, ~three: unit => unit, ~four: someVariant, unit] +{ + "signatures": [{ + "label": "let someFunc: (\n int,\n ~two: string=?,\n ~three: unit => unit,\n ~four: someVariant,\n unit,\n) => unit", + "parameters": [{"label": [14, 21], "documentation": {"kind": "markdown", "value": "```rescript\nint\n```"}}, {"label": [25, 39], "documentation": {"kind": "markdown", "value": "```rescript\noption\n```"}}, {"label": [43, 63], "documentation": {"kind": "markdown", "value": ""}}, {"label": [67, 85], "documentation": {"kind": "markdown", "value": "```rescript\nsomeVariant\n```\n```rescript\ntype someVariant = One | Two | Three\n```\nGo to: [Type definition](command:rescript-vscode.go_to_location?%5B%22SignatureHelp.res%22%2C0%2C0%5D)"}}, {"label": [89, 93], "documentation": {"kind": "markdown", "value": "```rescript\nint\n```"}}], + "documentation": {"kind": "markdown", "value": " Does stuff. "} + }], + "activeSignature": 0, + "activeParameter": -1 +} + +Signature help src/SignatureHelp.res 19:21 +posCursor:[19:19] posNoWhite:[19:18] Found expr:[19:11->19:21] +Pexp_apply ...[19:11->19:19] (...[19:20->19:21]) +posCursor:[19:19] posNoWhite:[19:18] Found expr:[19:11->19:19] +Pexp_ident someFunc:[19:11->19:19] +argAtCursor: unlabelled<0> +extracted params: +[( + int, ~two: string=?, ~three: unit => unit, ~four: someVariant, unit] +{ + "signatures": [{ + "label": "let someFunc: (\n int,\n ~two: string=?,\n ~three: unit => unit,\n ~four: someVariant,\n unit,\n) => unit", + "parameters": [{"label": [14, 21], "documentation": {"kind": "markdown", "value": "```rescript\nint\n```"}}, {"label": [25, 39], "documentation": {"kind": "markdown", "value": "```rescript\noption\n```"}}, {"label": [43, 63], "documentation": {"kind": "markdown", "value": ""}}, {"label": [67, 85], "documentation": {"kind": "markdown", "value": "```rescript\nsomeVariant\n```\n```rescript\ntype someVariant = One | Two | Three\n```\nGo to: [Type definition](command:rescript-vscode.go_to_location?%5B%22SignatureHelp.res%22%2C0%2C0%5D)"}}, {"label": [89, 93], "documentation": {"kind": "markdown", "value": "```rescript\nint\n```"}}], + "documentation": {"kind": "markdown", "value": " Does stuff. "} + }], + "activeSignature": 0, + "activeParameter": 0 +} + +Signature help src/SignatureHelp.res 22:29 +posCursor:[22:19] posNoWhite:[22:18] Found expr:[22:11->22:29] +Pexp_apply ...[22:11->22:19] (...[22:20->22:23], ~two22:26->22:29=...[22:26->22:29]) +posCursor:[22:19] posNoWhite:[22:18] Found expr:[22:11->22:19] +Pexp_ident someFunc:[22:11->22:19] +argAtCursor: ~two +extracted params: +[( + int, ~two: string=?, ~three: unit => unit, ~four: someVariant, unit] +{ + "signatures": [{ + "label": "let someFunc: (\n int,\n ~two: string=?,\n ~three: unit => unit,\n ~four: someVariant,\n unit,\n) => unit", + "parameters": [{"label": [14, 21], "documentation": {"kind": "markdown", "value": "```rescript\nint\n```"}}, {"label": [25, 39], "documentation": {"kind": "markdown", "value": "```rescript\noption\n```"}}, {"label": [43, 63], "documentation": {"kind": "markdown", "value": ""}}, {"label": [67, 85], "documentation": {"kind": "markdown", "value": "```rescript\nsomeVariant\n```\n```rescript\ntype someVariant = One | Two | Three\n```\nGo to: [Type definition](command:rescript-vscode.go_to_location?%5B%22SignatureHelp.res%22%2C0%2C0%5D)"}}, {"label": [89, 93], "documentation": {"kind": "markdown", "value": "```rescript\nint\n```"}}], + "documentation": {"kind": "markdown", "value": " Does stuff. "} + }], + "activeSignature": 0, + "activeParameter": 1 +} + +Signature help src/SignatureHelp.res 25:33 +posCursor:[25:19] posNoWhite:[25:18] Found expr:[25:11->25:35] +Pexp_apply ...[25:11->25:19] (...[25:20->25:23], ~two25:26->25:29=...[25:30->25:35]) +posCursor:[25:19] posNoWhite:[25:18] Found expr:[25:11->25:19] +Pexp_ident someFunc:[25:11->25:19] +argAtCursor: ~two +extracted params: +[( + int, ~two: string=?, ~three: unit => unit, ~four: someVariant, unit] +{ + "signatures": [{ + "label": "let someFunc: (\n int,\n ~two: string=?,\n ~three: unit => unit,\n ~four: someVariant,\n unit,\n) => unit", + "parameters": [{"label": [14, 21], "documentation": {"kind": "markdown", "value": "```rescript\nint\n```"}}, {"label": [25, 39], "documentation": {"kind": "markdown", "value": "```rescript\noption\n```"}}, {"label": [43, 63], "documentation": {"kind": "markdown", "value": ""}}, {"label": [67, 85], "documentation": {"kind": "markdown", "value": "```rescript\nsomeVariant\n```\n```rescript\ntype someVariant = One | Two | Three\n```\nGo to: [Type definition](command:rescript-vscode.go_to_location?%5B%22SignatureHelp.res%22%2C0%2C0%5D)"}}, {"label": [89, 93], "documentation": {"kind": "markdown", "value": "```rescript\nint\n```"}}], + "documentation": {"kind": "markdown", "value": " Does stuff. "} + }], + "activeSignature": 0, + "activeParameter": 1 +} + +Signature help src/SignatureHelp.res 28:38 +posCursor:[28:19] posNoWhite:[28:18] Found expr:[28:11->28:42] +Pexp_apply ...[28:11->28:19] (...[28:20->28:23], ~two28:26->28:29=...[28:30->28:35], ~four28:38->28:42=...[28:38->28:42]) +posCursor:[28:19] posNoWhite:[28:18] Found expr:[28:11->28:19] +Pexp_ident someFunc:[28:11->28:19] +argAtCursor: ~four +extracted params: +[( + int, ~two: string=?, ~three: unit => unit, ~four: someVariant, unit] +{ + "signatures": [{ + "label": "let someFunc: (\n int,\n ~two: string=?,\n ~three: unit => unit,\n ~four: someVariant,\n unit,\n) => unit", + "parameters": [{"label": [14, 21], "documentation": {"kind": "markdown", "value": "```rescript\nint\n```"}}, {"label": [25, 39], "documentation": {"kind": "markdown", "value": "```rescript\noption\n```"}}, {"label": [43, 63], "documentation": {"kind": "markdown", "value": ""}}, {"label": [67, 85], "documentation": {"kind": "markdown", "value": "```rescript\nsomeVariant\n```\n```rescript\ntype someVariant = One | Two | Three\n```\nGo to: [Type definition](command:rescript-vscode.go_to_location?%5B%22SignatureHelp.res%22%2C0%2C0%5D)"}}, {"label": [89, 93], "documentation": {"kind": "markdown", "value": "```rescript\nint\n```"}}], + "documentation": {"kind": "markdown", "value": " Does stuff. "} + }], + "activeSignature": 0, + "activeParameter": 3 +} + +Signature help src/SignatureHelp.res 31:42 +posCursor:[31:19] posNoWhite:[31:18] Found expr:[31:11->31:44] +Pexp_apply ...[31:11->31:19] (...[31:20->31:23], ~two31:26->31:29=...[31:30->31:35], ~four31:38->31:42=...[31:43->31:44]) +posCursor:[31:19] posNoWhite:[31:18] Found expr:[31:11->31:19] +Pexp_ident someFunc:[31:11->31:19] +argAtCursor: ~four +extracted params: +[( + int, ~two: string=?, ~three: unit => unit, ~four: someVariant, unit] +{ + "signatures": [{ + "label": "let someFunc: (\n int,\n ~two: string=?,\n ~three: unit => unit,\n ~four: someVariant,\n unit,\n) => unit", + "parameters": [{"label": [14, 21], "documentation": {"kind": "markdown", "value": "```rescript\nint\n```"}}, {"label": [25, 39], "documentation": {"kind": "markdown", "value": "```rescript\noption\n```"}}, {"label": [43, 63], "documentation": {"kind": "markdown", "value": ""}}, {"label": [67, 85], "documentation": {"kind": "markdown", "value": "```rescript\nsomeVariant\n```\n```rescript\ntype someVariant = One | Two | Three\n```\nGo to: [Type definition](command:rescript-vscode.go_to_location?%5B%22SignatureHelp.res%22%2C0%2C0%5D)"}}, {"label": [89, 93], "documentation": {"kind": "markdown", "value": "```rescript\nint\n```"}}], + "documentation": {"kind": "markdown", "value": " Does stuff. "} + }], + "activeSignature": 0, + "activeParameter": 3 +} + +Signature help src/SignatureHelp.res 34:21 +posCursor:[34:20] posNoWhite:[34:19] Found expr:[34:11->34:21] +Pexp_apply ...[34:11->34:20] (...[46:0->34:21]) +posCursor:[34:20] posNoWhite:[34:19] Found expr:[34:11->34:20] +Pexp_ident otherFunc:[34:11->34:20] +argAtCursor: none +extracted params: +[(string, int, float] +{ + "signatures": [{ + "label": "let otherFunc: (string, int, float) => unit", + "parameters": [{"label": [15, 22], "documentation": {"kind": "markdown", "value": "```rescript\nstring\n```"}}, {"label": [24, 27], "documentation": {"kind": "markdown", "value": "```rescript\nstring\n```"}}, {"label": [29, 34], "documentation": {"kind": "markdown", "value": "```rescript\nstring\n```"}}] + }], + "activeSignature": 0, + "activeParameter": -1 +} + +Signature help src/SignatureHelp.res 37:24 +posCursor:[37:20] posNoWhite:[37:19] Found expr:[37:11->37:26] +Pexp_apply ...[37:11->37:20] (...[37:21->37:26]) +posCursor:[37:20] posNoWhite:[37:19] Found expr:[37:11->37:20] +Pexp_ident otherFunc:[37:11->37:20] +argAtCursor: unlabelled<0> +extracted params: +[(string, int, float] +{ + "signatures": [{ + "label": "let otherFunc: (string, int, float) => unit", + "parameters": [{"label": [15, 22], "documentation": {"kind": "markdown", "value": "```rescript\nstring\n```"}}, {"label": [24, 27], "documentation": {"kind": "markdown", "value": "```rescript\nstring\n```"}}, {"label": [29, 34], "documentation": {"kind": "markdown", "value": "```rescript\nstring\n```"}}] + }], + "activeSignature": 0, + "activeParameter": 0 +} + +Signature help src/SignatureHelp.res 40:35 +posCursor:[40:20] posNoWhite:[40:19] Found expr:[40:11->40:39] +Pexp_apply ...[40:11->40:20] (...[40:21->40:26], ...[40:28->40:31], ...[40:33->40:38]) +posCursor:[40:20] posNoWhite:[40:19] Found expr:[40:11->40:20] +Pexp_ident otherFunc:[40:11->40:20] +argAtCursor: unlabelled<2> +extracted params: +[(string, int, float] +{ + "signatures": [{ + "label": "let otherFunc: (string, int, float) => unit", + "parameters": [{"label": [15, 22], "documentation": {"kind": "markdown", "value": "```rescript\nstring\n```"}}, {"label": [24, 27], "documentation": {"kind": "markdown", "value": "```rescript\nstring\n```"}}, {"label": [29, 34], "documentation": {"kind": "markdown", "value": "```rescript\nstring\n```"}}] + }], + "activeSignature": 0, + "activeParameter": 2 +} + +Signature help src/SignatureHelp.res 43:33 +posCursor:[43:29] posNoWhite:[43:28] Found expr:[43:11->43:34] +Pexp_apply ...[43:11->43:29] (~age43:31->43:34=...[43:31->43:34]) +posCursor:[43:29] posNoWhite:[43:28] Found expr:[43:11->43:29] +Pexp_ident Completion.Lib.foo:[43:11->43:29] +argAtCursor: ~age +extracted params: +[(~age: int, ~name: string] +{ + "signatures": [{ + "label": "let foo: (~age: int, ~name: string) => string", + "parameters": [{"label": [9, 19], "documentation": {"kind": "markdown", "value": "```rescript\nint\n```"}}, {"label": [21, 34], "documentation": {"kind": "markdown", "value": "```rescript\nstring\n```"}}] + }], + "activeSignature": 0, + "activeParameter": 0 +} + +Signature help src/SignatureHelp.res 50:24 +posCursor:[50:23] posNoWhite:[50:22] Found expr:[50:11->50:24] +Pexp_apply ...[50:11->50:23] (...[53:0->50:24]) +posCursor:[50:23] posNoWhite:[50:22] Found expr:[50:11->50:23] +Pexp_ident iAmSoSpecial:[50:11->50:23] +argAtCursor: none +extracted params: +[string] +{ + "signatures": [{ + "label": "let iAmSoSpecial: string => unit", + "parameters": [{"label": [18, 24], "documentation": {"kind": "markdown", "value": "```rescript\nstring\n```"}}] + }], + "activeSignature": 0, + "activeParameter": 0 +} + diff --git a/analysis/vendor/res_outcome_printer/res_driver.ml b/analysis/vendor/res_outcome_printer/res_driver.ml index f30a071cf..c7c722d9c 100644 --- a/analysis/vendor/res_outcome_printer/res_driver.ml +++ b/analysis/vendor/res_outcome_printer/res_driver.ml @@ -41,6 +41,10 @@ let setup ~filename ~forPrinter () = let mode = if forPrinter then Res_parser.Default else ParseForTypeChecker in Res_parser.make ~mode src filename +let setupFromSource ~displayFilename ~source ~forPrinter () = + let mode = if forPrinter then Res_parser.Default else ParseForTypeChecker in + Res_parser.make ~mode source displayFilename + let parsingEngine = { parseImplementation = @@ -82,6 +86,40 @@ let parsingEngine = Res_diagnostics.printReport diagnostics source); } +let parseImplementationFromSource ~forPrinter ~displayFilename ~source = + let engine = setupFromSource ~displayFilename ~source ~forPrinter () in + let structure = Res_core.parseImplementation engine in + let invalid, diagnostics = + match engine.diagnostics with + | [] as diagnostics -> (false, diagnostics) + | _ as diagnostics -> (true, diagnostics) + in + { + filename = engine.scanner.filename; + source = engine.scanner.src; + parsetree = structure; + diagnostics; + invalid; + comments = List.rev engine.comments; + } + +let parseInterfaceFromSource ~forPrinter ~displayFilename ~source = + let engine = setupFromSource ~displayFilename ~source ~forPrinter () in + let signature = Res_core.parseSpecification engine in + let invalid, diagnostics = + match engine.diagnostics with + | [] as diagnostics -> (false, diagnostics) + | _ as diagnostics -> (true, diagnostics) + in + { + filename = engine.scanner.filename; + source = engine.scanner.src; + parsetree = signature; + diagnostics; + invalid; + comments = List.rev engine.comments; + } + let printEngine = { printImplementation = diff --git a/analysis/vendor/res_outcome_printer/res_driver.mli b/analysis/vendor/res_outcome_printer/res_driver.mli index 9ea21e37c..8211487ef 100644 --- a/analysis/vendor/res_outcome_printer/res_driver.mli +++ b/analysis/vendor/res_outcome_printer/res_driver.mli @@ -19,6 +19,18 @@ type 'diagnostics parsingEngine = { stringOfDiagnostics: source:string -> filename:string -> 'diagnostics -> unit; } +val parseImplementationFromSource : + forPrinter:bool -> + displayFilename:string -> + source:string -> + (Parsetree.structure, Res_diagnostics.t list) parseResult + +val parseInterfaceFromSource : + forPrinter:bool -> + displayFilename:string -> + source:string -> + (Parsetree.signature, Res_diagnostics.t list) parseResult + type printEngine = { printImplementation: width:int -> diff --git a/client/src/extension.ts b/client/src/extension.ts index 88048f5af..f8acdb1ba 100644 --- a/client/src/extension.ts +++ b/client/src/extension.ts @@ -272,14 +272,6 @@ export function activate(context: ExtensionContext) { // language client, and because of that requires a full restart. context.subscriptions.push( workspace.onDidChangeConfiguration(({ affectsConfiguration }) => { - // Send a general message that configuration has updated. Clients - // interested can then pull the new configuration as they see fit. - client - .sendNotification("workspace/didChangeConfiguration") - .catch((err) => { - window.showErrorMessage(String(err)); - }); - // Put any configuration that, when changed, requires a full restart of // the server here. That will typically be any configuration that affects // the capabilities declared by the server, since those cannot be updated @@ -287,9 +279,18 @@ export function activate(context: ExtensionContext) { // initializing. if ( affectsConfiguration("rescript.settings.inlayHints") || - affectsConfiguration("rescript.settings.codeLens") + affectsConfiguration("rescript.settings.codeLens") || + affectsConfiguration("rescript.settings.signatureHelp") ) { commands.executeCommand("rescript-vscode.restart_language_server"); + } else { + // Send a general message that configuration has updated. Clients + // interested can then pull the new configuration as they see fit. + client + .sendNotification("workspace/didChangeConfiguration") + .catch((err) => { + window.showErrorMessage(String(err)); + }); } }) ); diff --git a/package.json b/package.json index 02d239506..3a4144a4a 100644 --- a/package.json +++ b/package.json @@ -158,6 +158,11 @@ "default": false, "description": "Enable (experimental) code lens for function definitions." }, + "rescript.settings.signatureHelp.enable": { + "type": "boolean", + "default": false, + "description": "Enable (experimental) signature help for function calls." + }, "rescript.settings.binaryPath": { "type": ["string", "null"], "default": null, diff --git a/server/src/server.ts b/server/src/server.ts index f384e41fa..92c79d670 100644 --- a/server/src/server.ts +++ b/server/src/server.ts @@ -13,6 +13,7 @@ import { InitializeParams, InlayHintParams, CodeLensParams, + SignatureHelpParams, } from "vscode-languageserver-protocol"; import * as utils from "./utils"; import * as codeActions from "./codeActions"; @@ -33,6 +34,9 @@ interface extensionConfiguration { }; codeLens: boolean; binaryPath: string | null; + signatureHelp: { + enable: boolean; + }; } // This holds client capabilities specific to our extension, and not necessarily @@ -54,6 +58,9 @@ let extensionConfiguration: extensionConfiguration = { }, codeLens: false, binaryPath: null, + signatureHelp: { + enable: false, + }, }; // Below here is some state that's not important exactly how long it lives. let hasPromptedAboutBuiltInFormatter = false; @@ -500,6 +507,27 @@ function sendCodeLensRefresh() { send(request); } +function signatureHelp(msg: p.RequestMessage) { + let params = msg.params as p.SignatureHelpParams; + let filePath = fileURLToPath(params.textDocument.uri); + let code = getOpenedFileContent(params.textDocument.uri); + let tmpname = utils.createFileInTempDir(); + fs.writeFileSync(tmpname, code, { encoding: "utf-8" }); + let response = utils.runAnalysisCommand( + filePath, + [ + "signatureHelp", + filePath, + params.position.line, + params.position.character, + tmpname, + ], + msg + ); + fs.unlink(tmpname, () => null); + return response; +} + function definition(msg: p.RequestMessage) { // https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_definition let params = msg.params as p.DefinitionParams; @@ -1130,6 +1158,12 @@ function onMessage(msg: p.Message) { workDoneProgress: false, } : undefined, + signatureHelpProvider: extensionConfiguration.signatureHelp?.enable + ? { + triggerCharacters: ["("], + retriggerCharacters: ["=", ","], + } + : undefined, }, }; let response: p.ResponseMessage = { @@ -1221,6 +1255,12 @@ function onMessage(msg: p.Message) { if (extName === c.resExt) { send(codeLens(msg)); } + } else if (msg.method === p.SignatureHelpRequest.method) { + let params = msg.params as SignatureHelpParams; + let extName = path.extname(params.textDocument.uri); + if (extName === c.resExt) { + send(signatureHelp(msg)); + } } else { let response: p.ResponseMessage = { jsonrpc: c.jsonrpcVersion,