From 779e12a7d6f101e9cf92d8fb273c32c44bb32205 Mon Sep 17 00:00:00 2001 From: Gabriel Nordeborn Date: Mon, 25 Jul 2022 20:10:52 +0200 Subject: [PATCH 01/19] wire up server capability + command for signature help --- server/src/server.ts | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/server/src/server.ts b/server/src/server.ts index 76c78e0b7..bc586b452 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"; @@ -446,6 +447,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; @@ -1041,6 +1063,10 @@ function onMessage(msg: p.Message) { workDoneProgress: false, } : undefined, + signatureHelpProvider: { + triggerCharacters: ["("], + retriggerCharacters: ["=", ","], + }, }, }; let response: p.ResponseMessage = { @@ -1132,6 +1158,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, From 6a51a0de71d13b3245a49cc4f0dc39402397abc0 Mon Sep 17 00:00:00 2001 From: Gabriel Nordeborn Date: Mon, 25 Jul 2022 20:29:14 +0200 Subject: [PATCH 02/19] set up analysis bin command, and needed things from the LS protocol --- analysis/src/Cli.ml | 8 +++++ analysis/src/Commands.ml | 21 +++++++++++-- analysis/src/Protocol.ml | 66 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 92 insertions(+), 3 deletions(-) diff --git a/analysis/src/Cli.ml b/analysis/src/Cli.ml index 90b82b1c8..b86e9025a 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 @@ -99,6 +103,10 @@ let main () = Commands.hover ~path ~pos:(int_of_string line, int_of_string col) ~currentFile ~debug:false + | [_; "signatureHelp"; path; line; col; currentFile] -> + Commands.hover ~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 ad9ad4a89..92391f4c9 100644 --- a/analysis/src/Commands.ml +++ b/analysis/src/Commands.ml @@ -88,6 +88,11 @@ let hover ~path ~pos ~currentFile ~debug = in print_endline result +let signatureHelp ~path ~pos ~currentFile ~debug = + print_endline + (Protocol.stringifySignatureHelp + {signatures = []; activeSignature = None; activeParameter = None}) + let codeAction ~path ~pos ~currentFile ~debug = Xform.extractCodeActions ~path ~pos ~currentFile ~debug |> CodeActions.stringifyCodeActions |> print_endline @@ -340,6 +345,13 @@ let test ~path = let currentFile = createCurrentFile () in hover ~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 = @@ -390,11 +402,14 @@ let test ~path = (Protocol.stringifyRange range) indent indent newText))) | "dia" -> diagnosticSyntax ~path - | "hin" -> ( + | "hin" -> let line_start = 0 in let line_end = 6 in - print_endline ("Inlay Hint " ^ path ^ " " ^ string_of_int line_start ^ ":" ^ string_of_int line_end); - inlayhint ~path ~pos:(line_start, line_end) ~maxLength:"25" ~debug:false) + print_endline + ("Inlay Hint " ^ path ^ " " ^ string_of_int line_start ^ ":" + ^ string_of_int line_end); + inlayhint ~path ~pos:(line_start, line_end) ~maxLength:"25" + ~debug:false | "cle" -> print_endline ("Code Lens " ^ path); codeLens ~path ~debug:false diff --git a/analysis/src/Protocol.ml b/analysis/src/Protocol.ml index a1440aa85..3ef8c8394 100644 --- a/analysis/src/Protocol.ml +++ b/analysis/src/Protocol.ml @@ -16,6 +16,24 @@ type inlayHint = { paddingRight: bool; } +(* https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#parameterInformation *) +type parameterInformation = {label: string; documentation: string option} + +(* https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#signatureInformation *) +type signatureInformation = { + label: string; + documentation: string option; + parameters: parameterInformation list option; + activeParameter: int 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 +188,54 @@ let stringifyCodeLens (codeLens : codeLens) = | None -> "" | Some command -> stringifyCommand command) +let stringifyParameterInformation (parameterInformation : parameterInformation) + = + Printf.sprintf + {|{ + "label": %s, + "documentation": %s + }|} + (Json.escape parameterInformation.label) + (match parameterInformation.documentation with + | None -> null + | Some documentation -> Json.escape documentation) + +let stringifySignatureInformation (signatureInformation : signatureInformation) + = + Printf.sprintf + {|{ + "label": %s, + "documentation": %s, + "parameters": %s, + "activeParameter": %s + }|} + (Json.escape signatureInformation.label) + (match signatureInformation.documentation with + | None -> null + | Some documentation -> Json.escape documentation) + (match signatureInformation.parameters with + | None -> [] |> array + | Some parameters -> + parameters |> List.map stringifyParameterInformation |> array) + (match signatureInformation.activeParameter with + | None -> null + | Some activeParameter -> activeParameter |> string_of_int) + +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 From b3113691d8a9db096758a706721b885d228eebe7 Mon Sep 17 00:00:00 2001 From: Gabriel Nordeborn Date: Mon, 25 Jul 2022 20:29:25 +0200 Subject: [PATCH 03/19] set up empty signature help test --- analysis/tests/src/SignatureHelp.res | 11 +++++++++++ analysis/tests/src/expected/Dce.res.txt | 2 +- analysis/tests/src/expected/Debug.res.txt | 3 ++- analysis/tests/src/expected/SignatureHelp.res.txt | 7 +++++++ 4 files changed, 21 insertions(+), 2 deletions(-) create mode 100644 analysis/tests/src/SignatureHelp.res create mode 100644 analysis/tests/src/expected/SignatureHelp.res.txt diff --git a/analysis/tests/src/SignatureHelp.res b/analysis/tests/src/SignatureHelp.res new file mode 100644 index 000000000..ea9da4ec9 --- /dev/null +++ b/analysis/tests/src/SignatureHelp.res @@ -0,0 +1,11 @@ +type someVariant = One | Two | Three + +let someFunc = (one: int, ~two: option=?, ~three: int, ~four: someVariant, ()) => { + ignore(one) + ignore(two) + ignore(three) + ignore(four) +} + +// someFunc( +// ^she diff --git a/analysis/tests/src/expected/Dce.res.txt b/analysis/tests/src/expected/Dce.res.txt index 9e47ef48c..31c4156d5 100644 --- a/analysis/tests/src/expected/Dce.res.txt +++ b/analysis/tests/src/expected/Dce.res.txt @@ -1,3 +1,3 @@ DCE src/Dce.res -issues:249 +issues:254 diff --git a/analysis/tests/src/expected/Debug.res.txt b/analysis/tests/src/expected/Debug.res.txt index a95597eae..e98cb1548 100644 --- a/analysis/tests/src/expected/Debug.res.txt +++ b/analysis/tests/src/expected/Debug.res.txt @@ -4,7 +4,7 @@ Dependencies: @rescript/react Source directories: ./node_modules/@rescript/react/src ./node_modules/@rescript/react/src/legacy Source files: ./node_modules/@rescript/react/src/React.res ./node_modules/@rescript/react/src/ReactDOM.res ./node_modules/@rescript/react/src/ReactDOMServer.res ./node_modules/@rescript/react/src/ReactDOMStyle.res ./node_modules/@rescript/react/src/ReactEvent.res ./node_modules/@rescript/react/src/ReactEvent.resi ./node_modules/@rescript/react/src/ReactTestUtils.res ./node_modules/@rescript/react/src/ReactTestUtils.resi ./node_modules/@rescript/react/src/RescriptReactErrorBoundary.res ./node_modules/@rescript/react/src/RescriptReactErrorBoundary.resi ./node_modules/@rescript/react/src/RescriptReactRouter.res ./node_modules/@rescript/react/src/RescriptReactRouter.resi ./node_modules/@rescript/react/src/legacy/ReactDOMRe.res ./node_modules/@rescript/react/src/legacy/ReasonReact.res Source directories: ./src ./src/expected -Source files: ./src/Auto.res ./src/CodeLens.res ./src/CompletePrioritize1.res ./src/CompletePrioritize2.res ./src/Completion.res ./src/Component.res ./src/Component.resi ./src/CreateInterface.res ./src/Cross.res ./src/Dce.res ./src/Debug.res ./src/Definition.res ./src/DefinitionWithInterface.res ./src/DefinitionWithInterface.resi ./src/Div.res ./src/DocumentSymbol.res ./src/Fragment.res ./src/Highlight.res ./src/Hover.res ./src/InlayHint.res ./src/Jsx.res ./src/Jsx.resi ./src/LongIdentTest.res ./src/Object.res ./src/Patterns.res ./src/RecModules.res ./src/RecordCompletion.res ./src/RecoveryOnProp.res ./src/References.res ./src/ReferencesWithInterface.res ./src/ReferencesWithInterface.resi ./src/Rename.res ./src/RenameWithInterface.res ./src/RenameWithInterface.resi ./src/TableclothMap.ml ./src/TableclothMap.mli ./src/TypeDefinition.res ./src/Xform.res +Source files: ./src/Auto.res ./src/CodeLens.res ./src/CompletePrioritize1.res ./src/CompletePrioritize2.res ./src/Completion.res ./src/Component.res ./src/Component.resi ./src/CreateInterface.res ./src/Cross.res ./src/Dce.res ./src/Debug.res ./src/Definition.res ./src/DefinitionWithInterface.res ./src/DefinitionWithInterface.resi ./src/Div.res ./src/DocumentSymbol.res ./src/Fragment.res ./src/Highlight.res ./src/Hover.res ./src/InlayHint.res ./src/Jsx.res ./src/Jsx.resi ./src/LongIdentTest.res ./src/Object.res ./src/Patterns.res ./src/RecModules.res ./src/RecordCompletion.res ./src/RecoveryOnProp.res ./src/References.res ./src/ReferencesWithInterface.res ./src/ReferencesWithInterface.resi ./src/Rename.res ./src/RenameWithInterface.res ./src/RenameWithInterface.resi ./src/SignatureHelp.res ./src/TableclothMap.ml ./src/TableclothMap.mli ./src/TypeDefinition.res ./src/Xform.res Impl cmt:./lib/bs/src/Auto.cmt res:./src/Auto.res Impl cmt:./lib/bs/src/CodeLens.cmt res:./src/CodeLens.res Impl cmt:./lib/bs/src/CompletePrioritize1.cmt res:./src/CompletePrioritize1.res @@ -34,6 +34,7 @@ Impl cmt:./lib/bs/src/References.cmt res:./src/References.res IntfAndImpl cmti:./lib/bs/src/ReferencesWithInterface.cmti resi:./src/ReferencesWithInterface.resi cmt:./lib/bs/src/ReferencesWithInterface.cmt res:./src/ReferencesWithInterface.res Impl cmt:./lib/bs/src/Rename.cmt res:./src/Rename.res IntfAndImpl cmti:./lib/bs/src/RenameWithInterface.cmti resi:./src/RenameWithInterface.resi cmt:./lib/bs/src/RenameWithInterface.cmt res:./src/RenameWithInterface.res +Impl cmt:./lib/bs/src/SignatureHelp.cmt res:./src/SignatureHelp.res IntfAndImpl cmti:./lib/bs/src/TableclothMap.cmti resi:./src/TableclothMap.mli cmt:./lib/bs/src/TableclothMap.cmt res:./src/TableclothMap.ml Impl cmt:./lib/bs/src/TypeDefinition.cmt res:./src/TypeDefinition.res Impl cmt:./lib/bs/src/Xform.cmt res:./src/Xform.res diff --git a/analysis/tests/src/expected/SignatureHelp.res.txt b/analysis/tests/src/expected/SignatureHelp.res.txt new file mode 100644 index 000000000..6269d1d98 --- /dev/null +++ b/analysis/tests/src/expected/SignatureHelp.res.txt @@ -0,0 +1,7 @@ +Signature help src/SignatureHelp.res 9:12 +{ + "signatures": [], + "activeSignature": null, + "activeParameter": null + } + From 6f19fbbe39a565eabc1d6205199da44c26f83f53 Mon Sep 17 00:00:00 2001 From: Gabriel Nordeborn Date: Mon, 25 Jul 2022 20:33:41 +0200 Subject: [PATCH 04/19] another test to help distinguish --- analysis/tests/src/SignatureHelp.res | 3 +++ analysis/tests/src/expected/SignatureHelp.res.txt | 7 +++++++ 2 files changed, 10 insertions(+) diff --git a/analysis/tests/src/SignatureHelp.res b/analysis/tests/src/SignatureHelp.res index ea9da4ec9..993fd8200 100644 --- a/analysis/tests/src/SignatureHelp.res +++ b/analysis/tests/src/SignatureHelp.res @@ -9,3 +9,6 @@ let someFunc = (one: int, ~two: option=?, ~three: int, ~four: someVarian // someFunc( // ^she + +// someFunc +// ^she diff --git a/analysis/tests/src/expected/SignatureHelp.res.txt b/analysis/tests/src/expected/SignatureHelp.res.txt index 6269d1d98..d0cdab08f 100644 --- a/analysis/tests/src/expected/SignatureHelp.res.txt +++ b/analysis/tests/src/expected/SignatureHelp.res.txt @@ -5,3 +5,10 @@ Signature help src/SignatureHelp.res 9:12 "activeParameter": null } +Signature help src/SignatureHelp.res 12:4 +{ + "signatures": [], + "activeSignature": null, + "activeParameter": null + } + From 9c26acd930559c7f6f6ddbca7f1fa7be3514098d Mon Sep 17 00:00:00 2001 From: Gabriel Nordeborn Date: Sat, 6 Aug 2022 20:21:54 +0200 Subject: [PATCH 05/19] implement signature help for function applications --- analysis/src/Cli.ml | 2 +- analysis/src/Commands.ml | 10 +- analysis/src/Loc.ml | 2 + analysis/src/Protocol.ml | 47 ++-- analysis/src/SharedTypes.ml | 7 +- analysis/src/SignatureHelp.ml | 207 ++++++++++++++++ analysis/tests/src/SignatureHelp.res | 50 +++- analysis/tests/src/expected/Dce.res.txt | 2 +- .../tests/src/expected/SignatureHelp.res.txt | 230 +++++++++++++++++- 9 files changed, 504 insertions(+), 53 deletions(-) create mode 100644 analysis/src/SignatureHelp.ml diff --git a/analysis/src/Cli.ml b/analysis/src/Cli.ml index b86e9025a..dcd37c675 100644 --- a/analysis/src/Cli.ml +++ b/analysis/src/Cli.ml @@ -104,7 +104,7 @@ let main () = ~pos:(int_of_string line, int_of_string col) ~currentFile ~debug:false | [_; "signatureHelp"; path; line; col; currentFile] -> - Commands.hover ~path + Commands.signatureHelp ~path ~pos:(int_of_string line, int_of_string col) ~currentFile ~debug:false | [_; "inlayHint"; path; line_start; line_end; maxLength] -> diff --git a/analysis/src/Commands.ml b/analysis/src/Commands.ml index 92391f4c9..4a3c1cde0 100644 --- a/analysis/src/Commands.ml +++ b/analysis/src/Commands.ml @@ -89,9 +89,13 @@ let hover ~path ~pos ~currentFile ~debug = print_endline result let signatureHelp ~path ~pos ~currentFile ~debug = - print_endline - (Protocol.stringifySignatureHelp - {signatures = []; activeSignature = None; activeParameter = None}) + 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 diff --git a/analysis/src/Loc.ml b/analysis/src/Loc.ml index 8e0c72bd6..202ff6041 100644 --- a/analysis/src/Loc.ml +++ b/analysis/src/Loc.ml @@ -8,3 +8,5 @@ let toString (loc : t) = (if loc.loc_ghost then "__ghost__" else "") ^ (loc |> range |> Range.toString) let hasPos ~pos loc = start loc <= pos && pos < end_ loc + +let hasPosInclusive ~pos loc = start loc <= pos && pos <= end_ loc diff --git a/analysis/src/Protocol.ml b/analysis/src/Protocol.ml index 3ef8c8394..abb513728 100644 --- a/analysis/src/Protocol.ml +++ b/analysis/src/Protocol.ml @@ -17,14 +17,12 @@ type inlayHint = { } (* https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#parameterInformation *) -type parameterInformation = {label: string; documentation: string option} +type parameterInformation = {label: int * int} (* https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#signatureInformation *) type signatureInformation = { label: string; - documentation: string option; - parameters: parameterInformation list option; - activeParameter: int option; + parameters: parameterInformation list; } (* https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#signatureHelp *) @@ -190,44 +188,29 @@ let stringifyCodeLens (codeLens : codeLens) = let stringifyParameterInformation (parameterInformation : parameterInformation) = - Printf.sprintf - {|{ - "label": %s, - "documentation": %s - }|} - (Json.escape parameterInformation.label) - (match parameterInformation.documentation with - | None -> null - | Some documentation -> Json.escape documentation) + Printf.sprintf {|{"label": %s}|} + (let line, chr = parameterInformation.label in + "[" ^ string_of_int line ^ ", " ^ string_of_int chr ^ "]") let stringifySignatureInformation (signatureInformation : signatureInformation) = Printf.sprintf {|{ - "label": %s, - "documentation": %s, - "parameters": %s, - "activeParameter": %s - }|} + "label": "%s", + "parameters": %s + }|} (Json.escape signatureInformation.label) - (match signatureInformation.documentation with - | None -> null - | Some documentation -> Json.escape documentation) - (match signatureInformation.parameters with - | None -> [] |> array - | Some parameters -> - parameters |> List.map stringifyParameterInformation |> array) - (match signatureInformation.activeParameter with - | None -> null - | Some activeParameter -> activeParameter |> string_of_int) + (signatureInformation.parameters + |> List.map stringifyParameterInformation + |> array) let stringifySignatureHelp (signatureHelp : signatureHelp) = Printf.sprintf {|{ - "signatures": %s, - "activeSignature": %s, - "activeParameter": %s - }|} + "signatures": %s, + "activeSignature": %s, + "activeParameter": %s +}|} (signatureHelp.signatures |> List.map stringifySignatureInformation |> array) (match signatureHelp.activeSignature with | None -> null diff --git a/analysis/src/SharedTypes.ml b/analysis/src/SharedTypes.ml index 314f7670b..bb77ee7a2 100644 --- a/analysis/src/SharedTypes.ml +++ b/analysis/src/SharedTypes.ml @@ -1,3 +1,8 @@ +let str s = if s = "" then "\"\"" else s +let list l = "[" ^ (l |> List.map str |> String.concat ", ") ^ "]" + +let ident i = i |> List.map str |> String.concat "." + type modulePath = | File of Uri.t * string | NotVisible @@ -435,8 +440,6 @@ module Completable = struct (** E.g. (["M", "Comp"], "id", ["id1", "id2"]) for List.map str |> String.concat ", ") ^ "]" in let completionContextToString = function | Value -> "Value" | Type -> "Type" diff --git a/analysis/src/SignatureHelp.ml b/analysis/src/SignatureHelp.ml new file mode 100644 index 000000000..42d2a6ee9 --- /dev/null +++ b/analysis/src/SignatureHelp.ml @@ -0,0 +1,207 @@ +(* This returns a string for a label + type expression that is identical to what that label + type expression would look like inside of a function type. + Example: let someFn = (~first: int, ~second: float) => unit. Running this on the label "second" would return "~second: float". *) +let getPrintedAsLabel label type_expr = + let stringified = + Shared.typeToString + (* Set up a synthetical, dummy function with a single argument (the label we're looking for) and a return value we know exactly what it will be, + so we can slice it off to extract the label annotation we're after. *) + Types. + { + level = 0; + id = 0; + desc = + Tarrow + ( label, + type_expr, + {level = 0; id = 0; desc = Tvar (Some "a")}, + Cunknown ); + } + ~lineWidth:400 + in + let extracted = + match label with + (* No label means function is printed without parens. Account for that. + The final thing subtracted number accounts for the dummy ` => 'a` that automatically comes from printing the label as a function. *) + | Nolabel -> String.sub stringified 0 (String.length stringified - 6) + | Labelled _ | Optional _ -> + (* Labelled arguments are printed with parens. *) + String.sub stringified 1 (String.length stringified - 8) + in + extracted + +type cursorAtArg = Unlabelled of int | Labelled of string + +let signatureHelp ~path ~pos ~currentFile ~debug = + let posBeforeCursor = (fst pos, max 0 (snd pos - 1)) 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 + | { + pexp_desc = Pexp_apply (({pexp_desc = Pexp_ident ident} as exp), args); + pexp_loc; + } + when pexp_loc |> Loc.hasPosInclusive ~pos:posBeforeCursor -> + (* TODO: Move extractExpApplyArgs to a shared module that doesn't look like it's only for completion. *) + let extractedArgs = CompletionFrontEnd.extractExpApplyArgs ~args in + let argAtCursor = + let unlabelledArgCount = ref 0 in + extractedArgs + |> List.find_map (fun arg -> + match arg.CompletionFrontEnd.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.hasPosInclusive ~pos:posBeforeCursor + then Some (Unlabelled currentUnlabelledArgCount) + else None + | Some {name; posStart; posEnd} -> + (* Check for the label identifier itself having the cursor *) + let cursorIsWithinLabelIdPos = + posStart <= pos && pos <= posEnd + in + (* Check for the expr assigned to the label having the cursor *) + let cursorIsWithinArgExprPos = + arg.exp.pexp_loc |> Loc.hasPosInclusive ~pos:posBeforeCursor + in + (* This most likely means that parsing failed and recovered. Happens for empty assignments for example. *) + let argExprIsParserRecovery = + arg.exp.pexp_loc |> Loc.end_ = (Location.none |> Loc.end_) + in + if + cursorIsWithinLabelIdPos || cursorIsWithinArgExprPos + || argExprIsParserRecovery + then Some (Labelled name) + else None) + in + setFound (argAtCursor, exp, ident) + | _ -> ()); + 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, ident) -> ( + let pos = exp.pexp_loc |> Loc.end_ in + let completions, env, package = + let textOpt = Files.readFile currentFile in + match textOpt with + | None | Some "" -> ([], None, 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. *) + (* TODO: This should probably eventually be factored out to its own thing, like "find type for pos", that feels less like abusing completions. *) + match + CompletionFrontEnd.completionWithParser ~debug ~path ~posCursor:pos + ~currentFile ~text + with + | None -> ([], None, None) + | Some (completable, scope) -> ( + match Cmt.fullFromPath ~path with + | None -> ([], None, None) + | Some {file; package} -> + let env = SharedTypes.QueryEnv.fromFile file in + ( completable + |> CompletionBackEnd.processCompletable ~debug ~package ~pos + ~scope ~env ~forHover:true, + Some env, + Some package ))) + in + match (completions, env, package) with + | {kind = Value type_expr} :: _, Some env, Some package -> + let args, _ = + CompletionBackEnd.extractFunctionType type_expr ~env ~package + in + 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 return a string "label" that contains what we want to put inside of the signature help hint itself. + It then also wants a list of arguments by character start/end offsets that denotes every paramter of the main type. + That list then helps us say which parameter is currently highlighted via the `activeParamter` index. + So, we figure those out here. *) + + (* The offset belows accounts for `let = ` which is what we're starting with. + We're prepending `let` because it looks a bit better than just having the plain type definition with no identifier. *) + let name = Utils.flattenLongIdent ident.txt |> SharedTypes.ident in + let currentOffset = ref (4 + String.length name + 4) in + let label = + "let " ^ name ^ " = " ^ Shared.typeToString type_expr ~lineWidth:400 + in + + (* Calculate all parameter character offsets. *) + let argsCount = List.length args in + let parameters = + args + |> List.mapi (fun index (label, type_expr) -> + let separatorLength = + (* comma + space, unless last arg *) + if index = argsCount - 1 then 0 else 2 + in + let stringified = getPrintedAsLabel label type_expr in + let offset = String.length stringified in + let start = !currentOffset in + let end_ = start + offset in + let parameterCharacterOffset = (start, end_) in + currentOffset := !currentOffset + offset + separatorLength; + parameterCharacterOffset) + in + if debug then + Printf.printf "extracted params: %s\n" + (parameters + |> List.map (fun (start, end_) -> + String.sub label start (end_ - start)) + |> SharedTypes.list); + + (* Figure out the active parameter *) + let activeParameter = + match argAtCursor with + | None -> 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) + in + Some + { + Protocol.signatures = + [ + { + label; + parameters = + parameters + |> List.map (fun params -> {Protocol.label = params}); + }; + ]; + activeSignature = Some 0; + activeParameter = + (match activeParameter with + | None -> Some (-1) + | activeParameter -> activeParameter); + } + | _ -> None) + | _ -> None diff --git a/analysis/tests/src/SignatureHelp.res b/analysis/tests/src/SignatureHelp.res index 993fd8200..da7de56b7 100644 --- a/analysis/tests/src/SignatureHelp.res +++ b/analysis/tests/src/SignatureHelp.res @@ -7,8 +7,50 @@ let someFunc = (one: int, ~two: option=?, ~three: int, ~four: someVarian ignore(four) } -// someFunc( -// ^she +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= +// ^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", +// ^she + +// let _ = otherFunc("123", 123 +// ^she + +// let _ = otherFunc("123", 123, +// ^she + +// let _ = otherFunc("123", 123, 123.0) +// ^she -// someFunc -// ^she +// let _ = Completion.Lib.foo(~age +// ^she diff --git a/analysis/tests/src/expected/Dce.res.txt b/analysis/tests/src/expected/Dce.res.txt index 31c4156d5..6f182fe75 100644 --- a/analysis/tests/src/expected/Dce.res.txt +++ b/analysis/tests/src/expected/Dce.res.txt @@ -1,3 +1,3 @@ DCE src/Dce.res -issues:254 +issues:255 diff --git a/analysis/tests/src/expected/SignatureHelp.res.txt b/analysis/tests/src/expected/SignatureHelp.res.txt index d0cdab08f..7e72dc91a 100644 --- a/analysis/tests/src/expected/SignatureHelp.res.txt +++ b/analysis/tests/src/expected/SignatureHelp.res.txt @@ -1,14 +1,224 @@ -Signature help src/SignatureHelp.res 9:12 +Signature help src/SignatureHelp.res 15:20 +posCursor:[15:19] posNoWhite:[15:18] Found expr:[15:11->15:20] +Pexp_apply ...[15:11->15:19] (...[57:0->15:20]) +posCursor:[15:19] posNoWhite:[15:18] Found expr:[15:11->15:19] +Pexp_ident someFunc:[15:11->15:19] +argAtCursor: none +extracted params: [int, ~two: string=?, ~three: int, ~four: someVariant, unit] { - "signatures": [], - "activeSignature": null, - "activeParameter": null - } + "signatures": [{ + "label": "let someFunc = (int, ~two: string=?, ~three: int, ~four: someVariant, unit) => unit", + "parameters": [{"label": [16, 19]}, {"label": [21, 35]}, {"label": [37, 48]}, {"label": [50, 68]}, {"label": [70, 74]}] + }], + "activeSignature": 0, + "activeParameter": -1 +} -Signature help src/SignatureHelp.res 12:4 +Signature help src/SignatureHelp.res 18:21 +posCursor:[18:19] posNoWhite:[18:18] Found expr:[18:11->18:21] +Pexp_apply ...[18:11->18:19] (...[18:20->18:21]) +posCursor:[18:19] posNoWhite:[18:18] Found expr:[18:11->18:19] +Pexp_ident someFunc:[18:11->18:19] +argAtCursor: unlabelled<0> +extracted params: [int, ~two: string=?, ~three: int, ~four: someVariant, unit] { - "signatures": [], - "activeSignature": null, - "activeParameter": null - } + "signatures": [{ + "label": "let someFunc = (int, ~two: string=?, ~three: int, ~four: someVariant, unit) => unit", + "parameters": [{"label": [16, 19]}, {"label": [21, 35]}, {"label": [37, 48]}, {"label": [50, 68]}, {"label": [70, 74]}] + }], + "activeSignature": 0, + "activeParameter": 0 +} + +Signature help src/SignatureHelp.res 21:29 +posCursor:[21:19] posNoWhite:[21:18] Found expr:[21:11->21:29] +Pexp_apply ...[21:11->21:19] (...[21:20->21:23], ~two21:26->21:29=...[21:26->21:29]) +posCursor:[21:19] posNoWhite:[21:18] Found expr:[21:11->21:19] +Pexp_ident someFunc:[21:11->21:19] +argAtCursor: ~two +extracted params: [int, ~two: string=?, ~three: int, ~four: someVariant, unit] +{ + "signatures": [{ + "label": "let someFunc = (int, ~two: string=?, ~three: int, ~four: someVariant, unit) => unit", + "parameters": [{"label": [16, 19]}, {"label": [21, 35]}, {"label": [37, 48]}, {"label": [50, 68]}, {"label": [70, 74]}] + }], + "activeSignature": 0, + "activeParameter": 1 +} + +Signature help src/SignatureHelp.res 24:31 +posCursor:[24:19] posNoWhite:[24:18] Found expr:[24:11->24:30] +Pexp_apply ...[24:11->24:19] (...[24:20->24:23], ~two24:26->24:29=...__ghost__[0:-1->0:-1]) +posCursor:[24:19] posNoWhite:[24:18] Found expr:[24:11->24:19] +Pexp_ident someFunc:[24:11->24:19] +argAtCursor: ~two +extracted params: [int, ~two: string=?, ~three: int, ~four: someVariant, unit] +{ + "signatures": [{ + "label": "let someFunc = (int, ~two: string=?, ~three: int, ~four: someVariant, unit) => unit", + "parameters": [{"label": [16, 19]}, {"label": [21, 35]}, {"label": [37, 48]}, {"label": [50, 68]}, {"label": [70, 74]}] + }], + "activeSignature": 0, + "activeParameter": 1 +} + +Signature help src/SignatureHelp.res 27:33 +posCursor:[27:19] posNoWhite:[27:18] Found expr:[27:11->27:35] +Pexp_apply ...[27:11->27:19] (...[27:20->27:23], ~two27:26->27:29=...[27:30->27:35]) +posCursor:[27:19] posNoWhite:[27:18] Found expr:[27:11->27:19] +Pexp_ident someFunc:[27:11->27:19] +argAtCursor: ~two +extracted params: [int, ~two: string=?, ~three: int, ~four: someVariant, unit] +{ + "signatures": [{ + "label": "let someFunc = (int, ~two: string=?, ~three: int, ~four: someVariant, unit) => unit", + "parameters": [{"label": [16, 19]}, {"label": [21, 35]}, {"label": [37, 48]}, {"label": [50, 68]}, {"label": [70, 74]}] + }], + "activeSignature": 0, + "activeParameter": 1 +} + +Signature help src/SignatureHelp.res 30:38 +posCursor:[30:19] posNoWhite:[30:18] Found expr:[30:11->30:42] +Pexp_apply ...[30:11->30:19] (...[30:20->30:23], ~two30:26->30:29=...[30:30->30:35], ~four30:38->30:42=...[30:38->30:42]) +posCursor:[30:19] posNoWhite:[30:18] Found expr:[30:11->30:19] +Pexp_ident someFunc:[30:11->30:19] +argAtCursor: ~four +extracted params: [int, ~two: string=?, ~three: int, ~four: someVariant, unit] +{ + "signatures": [{ + "label": "let someFunc = (int, ~two: string=?, ~three: int, ~four: someVariant, unit) => unit", + "parameters": [{"label": [16, 19]}, {"label": [21, 35]}, {"label": [37, 48]}, {"label": [50, 68]}, {"label": [70, 74]}] + }], + "activeSignature": 0, + "activeParameter": 3 +} + +Signature help src/SignatureHelp.res 33:42 +posCursor:[33:19] posNoWhite:[33:18] Found expr:[33:11->33:44] +Pexp_apply ...[33:11->33:19] (...[33:20->33:23], ~two33:26->33:29=...[33:30->33:35], ~four33:38->33:42=...[33:43->33:44]) +posCursor:[33:19] posNoWhite:[33:18] Found expr:[33:11->33:19] +Pexp_ident someFunc:[33:11->33:19] +argAtCursor: ~four +extracted params: [int, ~two: string=?, ~three: int, ~four: someVariant, unit] +{ + "signatures": [{ + "label": "let someFunc = (int, ~two: string=?, ~three: int, ~four: someVariant, unit) => unit", + "parameters": [{"label": [16, 19]}, {"label": [21, 35]}, {"label": [37, 48]}, {"label": [50, 68]}, {"label": [70, 74]}] + }], + "activeSignature": 0, + "activeParameter": 3 +} + +Signature help src/SignatureHelp.res 36:21 +posCursor:[36:20] posNoWhite:[36:19] Found expr:[36:11->36:21] +Pexp_apply ...[36:11->36:20] (...[57:0->36:21]) +posCursor:[36:20] posNoWhite:[36:19] Found expr:[36:11->36:20] +Pexp_ident otherFunc:[36:11->36:20] +argAtCursor: none +extracted params: [string, int, float] +{ + "signatures": [{ + "label": "let otherFunc = (string, int, float) => unit", + "parameters": [{"label": [17, 23]}, {"label": [25, 28]}, {"label": [30, 35]}] + }], + "activeSignature": 0, + "activeParameter": -1 +} + +Signature help src/SignatureHelp.res 39:24 +posCursor:[39:20] posNoWhite:[39:19] Found expr:[39:11->39:26] +Pexp_apply ...[39:11->39:20] (...[39:21->39:26]) +posCursor:[39:20] posNoWhite:[39:19] Found expr:[39:11->39:20] +Pexp_ident otherFunc:[39:11->39:20] +argAtCursor: unlabelled<0> +extracted params: [string, int, float] +{ + "signatures": [{ + "label": "let otherFunc = (string, int, float) => unit", + "parameters": [{"label": [17, 23]}, {"label": [25, 28]}, {"label": [30, 35]}] + }], + "activeSignature": 0, + "activeParameter": 0 +} + +Signature help src/SignatureHelp.res 42:28 +posCursor:[42:20] posNoWhite:[42:19] Found expr:[42:11->42:27] +Pexp_apply ...[42:11->42:20] (...[42:21->42:26]) +posCursor:[42:20] posNoWhite:[42:19] Found expr:[42:11->42:20] +Pexp_ident otherFunc:[42:11->42:20] +argAtCursor: none +extracted params: [string, int, float] +{ + "signatures": [{ + "label": "let otherFunc = (string, int, float) => unit", + "parameters": [{"label": [17, 23]}, {"label": [25, 28]}, {"label": [30, 35]}] + }], + "activeSignature": 0, + "activeParameter": -1 +} + +Signature help src/SignatureHelp.res 45:30 +posCursor:[45:20] posNoWhite:[45:19] Found expr:[45:11->45:31] +Pexp_apply ...[45:11->45:20] (...[45:21->45:26], ...[45:28->45:31]) +posCursor:[45:20] posNoWhite:[45:19] Found expr:[45:11->45:20] +Pexp_ident otherFunc:[45:11->45:20] +argAtCursor: unlabelled<1> +extracted params: [string, int, float] +{ + "signatures": [{ + "label": "let otherFunc = (string, int, float) => unit", + "parameters": [{"label": [17, 23]}, {"label": [25, 28]}, {"label": [30, 35]}] + }], + "activeSignature": 0, + "activeParameter": 1 +} + +Signature help src/SignatureHelp.res 48:33 +posCursor:[48:20] posNoWhite:[48:19] Found expr:[48:11->48:32] +Pexp_apply ...[48:11->48:20] (...[48:21->48:26], ...[48:28->48:31]) +posCursor:[48:20] posNoWhite:[48:19] Found expr:[48:11->48:20] +Pexp_ident otherFunc:[48:11->48:20] +argAtCursor: none +extracted params: [string, int, float] +{ + "signatures": [{ + "label": "let otherFunc = (string, int, float) => unit", + "parameters": [{"label": [17, 23]}, {"label": [25, 28]}, {"label": [30, 35]}] + }], + "activeSignature": 0, + "activeParameter": -1 +} + +Signature help src/SignatureHelp.res 51:35 +posCursor:[51:20] posNoWhite:[51:19] Found expr:[51:11->51:39] +Pexp_apply ...[51:11->51:20] (...[51:21->51:26], ...[51:28->51:31], ...[51:33->51:38]) +posCursor:[51:20] posNoWhite:[51:19] Found expr:[51:11->51:20] +Pexp_ident otherFunc:[51:11->51:20] +argAtCursor: unlabelled<2> +extracted params: [string, int, float] +{ + "signatures": [{ + "label": "let otherFunc = (string, int, float) => unit", + "parameters": [{"label": [17, 23]}, {"label": [25, 28]}, {"label": [30, 35]}] + }], + "activeSignature": 0, + "activeParameter": 2 +} + +Signature help src/SignatureHelp.res 54:33 +posCursor:[54:29] posNoWhite:[54:28] Found expr:[54:11->54:34] +Pexp_apply ...[54:11->54:29] (~age54:31->54:34=...[54:31->54:34]) +posCursor:[54:29] posNoWhite:[54:28] Found expr:[54:11->54:29] +Pexp_ident Completion.Lib.foo:[54:11->54:29] +argAtCursor: ~age +extracted params: [~age: int, ~name: string] +{ + "signatures": [{ + "label": "let Completion.Lib.foo = (~age: int, ~name: string) => string", + "parameters": [{"label": [26, 35]}, {"label": [37, 50]}] + }], + "activeSignature": 0, + "activeParameter": 0 +} From 7a5769c86c2e4f5bce9fbaacf1861ba4359290d5 Mon Sep 17 00:00:00 2001 From: Gabriel Nordeborn Date: Sat, 6 Aug 2022 20:22:47 +0200 Subject: [PATCH 06/19] changelog + readme --- CHANGELOG.md | 1 + README.md | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 91e63f573..6436be91d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ - Inlay Hints (experimetal). `rescript.settings.inlayHints.enable: true`. Turned off by default. - Code Lenses for functions (experimetal). `rescript.settings.codeLens: true`. Turned off by default. - Markdown code blocks tagged as `rescript` now get basic syntax highlighting. +- Signature help for function applications. ## v1.4.2 diff --git a/README.md b/README.md index 04ea02b79..ded211314 100644 --- a/README.md +++ b/README.md @@ -69,7 +69,8 @@ The only 2 themes we don't (and can't) support, due to their lack of coloring, a - Autocomplete. - Find references. - Rename. -- Inlay Hints +- Inlay Hints. +- Signature help. - Snippets to ease a few syntaxes: - `external` features such as `@bs.module` and `@bs.val` - `try`, `for`, etc. From 07e2807a033e2b0c559a475809d895d677194cbd Mon Sep 17 00:00:00 2001 From: Gabriel Nordeborn Date: Sun, 7 Aug 2022 20:54:36 +0200 Subject: [PATCH 07/19] leverage the parser to figure out the parameter offsets --- analysis/src/Files.ml | 6 + analysis/src/SignatureHelp.ml | 119 +++++++++--------- analysis/tests/src/SignatureHelp.res | 4 +- .../tests/src/expected/SignatureHelp.res.txt | 105 +++++++++------- 4 files changed, 129 insertions(+), 105 deletions(-) diff --git a/analysis/src/Files.ml b/analysis/src/Files.ml index 9ab016da5..055a8f4b2 100644 --- a/analysis/src/Files.ml +++ b/analysis/src/Files.ml @@ -102,3 +102,9 @@ let classifySourceFile path = if Filename.check_suffix path ".res" && exists path then Res else if Filename.check_suffix path ".resi" && exists path then Resi else Other + +let writeTempFile ~prefix ~suffix contents = + let tempFile, outChannel = Filename.open_temp_file prefix suffix in + Printf.fprintf outChannel "%s" contents; + close_out outChannel; + tempFile diff --git a/analysis/src/SignatureHelp.ml b/analysis/src/SignatureHelp.ml index 42d2a6ee9..74723c346 100644 --- a/analysis/src/SignatureHelp.ml +++ b/analysis/src/SignatureHelp.ml @@ -1,34 +1,3 @@ -(* This returns a string for a label + type expression that is identical to what that label + type expression would look like inside of a function type. - Example: let someFn = (~first: int, ~second: float) => unit. Running this on the label "second" would return "~second: float". *) -let getPrintedAsLabel label type_expr = - let stringified = - Shared.typeToString - (* Set up a synthetical, dummy function with a single argument (the label we're looking for) and a return value we know exactly what it will be, - so we can slice it off to extract the label annotation we're after. *) - Types. - { - level = 0; - id = 0; - desc = - Tarrow - ( label, - type_expr, - {level = 0; id = 0; desc = Tvar (Some "a")}, - Cunknown ); - } - ~lineWidth:400 - in - let extracted = - match label with - (* No label means function is printed without parens. Account for that. - The final thing subtracted number accounts for the dummy ` => 'a` that automatically comes from printing the label as a function. *) - | Nolabel -> String.sub stringified 0 (String.length stringified - 6) - | Labelled _ | Optional _ -> - (* Labelled arguments are printed with parens. *) - String.sub stringified 1 (String.length stringified - 8) - in - extracted - type cursorAtArg = Unlabelled of int | Labelled of string let signatureHelp ~path ~pos ~currentFile ~debug = @@ -38,7 +7,7 @@ let signatureHelp ~path ~pos ~currentFile ~debug = let expr (iterator : Ast_iterator.iterator) (expr : Parsetree.expression) = (match expr with | { - pexp_desc = Pexp_apply (({pexp_desc = Pexp_ident ident} as exp), args); + pexp_desc = Pexp_apply (({pexp_desc = Pexp_ident _} as exp), args); pexp_loc; } when pexp_loc |> Loc.hasPosInclusive ~pos:posBeforeCursor -> @@ -75,7 +44,7 @@ let signatureHelp ~path ~pos ~currentFile ~debug = then Some (Labelled name) else None) in - setFound (argAtCursor, exp, ident) + setFound (argAtCursor, exp) | _ -> ()); Ast_iterator.default_iterator.expr iterator expr in @@ -84,7 +53,8 @@ let signatureHelp ~path ~pos ~currentFile ~debug = let {Res_driver.parsetree = structure} = parser ~filename:currentFile in iterator.structure iterator structure |> ignore; match !foundFunctionApplicationExpr with - | Some (argAtCursor, exp, ident) -> ( + | Some (argAtCursor, exp) -> ( + (* Not looking for the cursor position after this, but rather the target function expression's loc. *) let pos = exp.pexp_loc |> Loc.end_ in let completions, env, package = let textOpt = Files.readFile currentFile in @@ -112,7 +82,7 @@ let signatureHelp ~path ~pos ~currentFile ~debug = Some package ))) in match (completions, env, package) with - | {kind = Value type_expr} :: _, Some env, Some package -> + | {kind = Value type_expr; name} :: _, Some env, Some package -> let args, _ = CompletionBackEnd.extractFunctionType type_expr ~env ~package in @@ -123,38 +93,65 @@ let signatureHelp ~path ~pos ~currentFile ~debug = | Some (Labelled name) -> "~" ^ name | Some (Unlabelled index) -> "unlabelled<" ^ string_of_int index ^ ">"); - (* The LS protocol wants us to return a string "label" that contains what we want to put inside of the signature help hint itself. - It then also wants a list of arguments by character start/end offsets that denotes every paramter of the main type. - That list then helps us say which parameter is currently highlighted via the `activeParamter` index. - So, we figure those out here. *) + (* 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'll leverage the parser to figure the offsets out by parsing the label, and extract the + offsets from the parser. *) - (* The offset belows accounts for `let = ` which is what we're starting with. - We're prepending `let` because it looks a bit better than just having the plain type definition with no identifier. *) - let name = Utils.flattenLongIdent ident.txt |> SharedTypes.ident in - let currentOffset = ref (4 + String.length name + 4) in - let label = - "let " ^ name ^ " = " ^ Shared.typeToString type_expr ~lineWidth:400 + (* 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 + (* TODO: Refactor the parser to support reading string contents directly in addition to taking a file path. + For now, create a temp file, pass it to the parser, and then immediately delete it. *) + let fileWithTypeSignature = + Files.writeTempFile ~prefix:"typ_sig" ~suffix:".resi" label + in + let parser = Res_driver.parsingEngine.parseInterface ~forPrinter:false in + let {Res_driver.parsetree = signature} = + parser ~filename:fileWithTypeSignature in + Sys.remove fileWithTypeSignature; - (* Calculate all parameter character offsets. *) - let argsCount = List.length args in let parameters = - args - |> List.mapi (fun index (label, type_expr) -> - let separatorLength = - (* comma + space, unless last arg *) - if index = argsCount - 1 then 0 else 2 - in - let stringified = getPrintedAsLabel label type_expr in - let offset = String.length stringified in - let start = !currentOffset in - let end_ = start + offset in - let parameterCharacterOffset = (start, end_) in - currentOffset := !currentOffset + offset + separatorLength; - parameterCharacterOffset) + 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 + |> CompletionFrontEnd.positionToOffset label + |> Option.get + in + let endOffset = + argumentTypeExpr.ptyp_loc |> Loc.end_ + |> CompletionFrontEnd.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 @ [(startOffset, endOffset)]) + | _ -> params + in + extractParams expr [] + | _ -> [] in + if debug then - Printf.printf "extracted params: %s\n" + Printf.printf "extracted params: \n%s\n" (parameters |> List.map (fun (start, end_) -> String.sub label start (end_ - start)) diff --git a/analysis/tests/src/SignatureHelp.res b/analysis/tests/src/SignatureHelp.res index da7de56b7..af10773a6 100644 --- a/analysis/tests/src/SignatureHelp.res +++ b/analysis/tests/src/SignatureHelp.res @@ -1,9 +1,9 @@ type someVariant = One | Two | Three -let someFunc = (one: int, ~two: option=?, ~three: int, ~four: someVariant, ()) => { +let someFunc = (one: int, ~two: option=?, ~three: unit => unit, ~four: someVariant, ()) => { ignore(one) ignore(two) - ignore(three) + ignore(three()) ignore(four) } diff --git a/analysis/tests/src/expected/SignatureHelp.res.txt b/analysis/tests/src/expected/SignatureHelp.res.txt index 7e72dc91a..074e42935 100644 --- a/analysis/tests/src/expected/SignatureHelp.res.txt +++ b/analysis/tests/src/expected/SignatureHelp.res.txt @@ -4,11 +4,13 @@ Pexp_apply ...[15:11->15:19] (...[57:0->15:20]) posCursor:[15:19] posNoWhite:[15:18] Found expr:[15:11->15:19] Pexp_ident someFunc:[15:11->15:19] argAtCursor: none -extracted params: [int, ~two: string=?, ~three: int, ~four: someVariant, unit] +extracted params: +[( + int, ~two: string=?, ~three: unit => unit, ~four: someVariant, unit] { "signatures": [{ - "label": "let someFunc = (int, ~two: string=?, ~three: int, ~four: someVariant, unit) => unit", - "parameters": [{"label": [16, 19]}, {"label": [21, 35]}, {"label": [37, 48]}, {"label": [50, 68]}, {"label": [70, 74]}] + "label": "let someFunc: (\n int,\n ~two: string=?,\n ~three: unit => unit,\n ~four: someVariant,\n unit,\n) => unit", + "parameters": [{"label": [14, 21]}, {"label": [25, 39]}, {"label": [43, 63]}, {"label": [67, 85]}, {"label": [89, 93]}] }], "activeSignature": 0, "activeParameter": -1 @@ -20,11 +22,13 @@ Pexp_apply ...[18:11->18:19] (...[18:20->18:21]) posCursor:[18:19] posNoWhite:[18:18] Found expr:[18:11->18:19] Pexp_ident someFunc:[18:11->18:19] argAtCursor: unlabelled<0> -extracted params: [int, ~two: string=?, ~three: int, ~four: someVariant, unit] +extracted params: +[( + int, ~two: string=?, ~three: unit => unit, ~four: someVariant, unit] { "signatures": [{ - "label": "let someFunc = (int, ~two: string=?, ~three: int, ~four: someVariant, unit) => unit", - "parameters": [{"label": [16, 19]}, {"label": [21, 35]}, {"label": [37, 48]}, {"label": [50, 68]}, {"label": [70, 74]}] + "label": "let someFunc: (\n int,\n ~two: string=?,\n ~three: unit => unit,\n ~four: someVariant,\n unit,\n) => unit", + "parameters": [{"label": [14, 21]}, {"label": [25, 39]}, {"label": [43, 63]}, {"label": [67, 85]}, {"label": [89, 93]}] }], "activeSignature": 0, "activeParameter": 0 @@ -36,11 +40,13 @@ Pexp_apply ...[21:11->21:19] (...[21:20->21:23], ~two21:26->21:29=...[21:26->21: posCursor:[21:19] posNoWhite:[21:18] Found expr:[21:11->21:19] Pexp_ident someFunc:[21:11->21:19] argAtCursor: ~two -extracted params: [int, ~two: string=?, ~three: int, ~four: someVariant, unit] +extracted params: +[( + int, ~two: string=?, ~three: unit => unit, ~four: someVariant, unit] { "signatures": [{ - "label": "let someFunc = (int, ~two: string=?, ~three: int, ~four: someVariant, unit) => unit", - "parameters": [{"label": [16, 19]}, {"label": [21, 35]}, {"label": [37, 48]}, {"label": [50, 68]}, {"label": [70, 74]}] + "label": "let someFunc: (\n int,\n ~two: string=?,\n ~three: unit => unit,\n ~four: someVariant,\n unit,\n) => unit", + "parameters": [{"label": [14, 21]}, {"label": [25, 39]}, {"label": [43, 63]}, {"label": [67, 85]}, {"label": [89, 93]}] }], "activeSignature": 0, "activeParameter": 1 @@ -52,11 +58,13 @@ Pexp_apply ...[24:11->24:19] (...[24:20->24:23], ~two24:26->24:29=...__ghost__[0 posCursor:[24:19] posNoWhite:[24:18] Found expr:[24:11->24:19] Pexp_ident someFunc:[24:11->24:19] argAtCursor: ~two -extracted params: [int, ~two: string=?, ~three: int, ~four: someVariant, unit] +extracted params: +[( + int, ~two: string=?, ~three: unit => unit, ~four: someVariant, unit] { "signatures": [{ - "label": "let someFunc = (int, ~two: string=?, ~three: int, ~four: someVariant, unit) => unit", - "parameters": [{"label": [16, 19]}, {"label": [21, 35]}, {"label": [37, 48]}, {"label": [50, 68]}, {"label": [70, 74]}] + "label": "let someFunc: (\n int,\n ~two: string=?,\n ~three: unit => unit,\n ~four: someVariant,\n unit,\n) => unit", + "parameters": [{"label": [14, 21]}, {"label": [25, 39]}, {"label": [43, 63]}, {"label": [67, 85]}, {"label": [89, 93]}] }], "activeSignature": 0, "activeParameter": 1 @@ -68,11 +76,13 @@ Pexp_apply ...[27:11->27:19] (...[27:20->27:23], ~two27:26->27:29=...[27:30->27: posCursor:[27:19] posNoWhite:[27:18] Found expr:[27:11->27:19] Pexp_ident someFunc:[27:11->27:19] argAtCursor: ~two -extracted params: [int, ~two: string=?, ~three: int, ~four: someVariant, unit] +extracted params: +[( + int, ~two: string=?, ~three: unit => unit, ~four: someVariant, unit] { "signatures": [{ - "label": "let someFunc = (int, ~two: string=?, ~three: int, ~four: someVariant, unit) => unit", - "parameters": [{"label": [16, 19]}, {"label": [21, 35]}, {"label": [37, 48]}, {"label": [50, 68]}, {"label": [70, 74]}] + "label": "let someFunc: (\n int,\n ~two: string=?,\n ~three: unit => unit,\n ~four: someVariant,\n unit,\n) => unit", + "parameters": [{"label": [14, 21]}, {"label": [25, 39]}, {"label": [43, 63]}, {"label": [67, 85]}, {"label": [89, 93]}] }], "activeSignature": 0, "activeParameter": 1 @@ -84,11 +94,13 @@ Pexp_apply ...[30:11->30:19] (...[30:20->30:23], ~two30:26->30:29=...[30:30->30: posCursor:[30:19] posNoWhite:[30:18] Found expr:[30:11->30:19] Pexp_ident someFunc:[30:11->30:19] argAtCursor: ~four -extracted params: [int, ~two: string=?, ~three: int, ~four: someVariant, unit] +extracted params: +[( + int, ~two: string=?, ~three: unit => unit, ~four: someVariant, unit] { "signatures": [{ - "label": "let someFunc = (int, ~two: string=?, ~three: int, ~four: someVariant, unit) => unit", - "parameters": [{"label": [16, 19]}, {"label": [21, 35]}, {"label": [37, 48]}, {"label": [50, 68]}, {"label": [70, 74]}] + "label": "let someFunc: (\n int,\n ~two: string=?,\n ~three: unit => unit,\n ~four: someVariant,\n unit,\n) => unit", + "parameters": [{"label": [14, 21]}, {"label": [25, 39]}, {"label": [43, 63]}, {"label": [67, 85]}, {"label": [89, 93]}] }], "activeSignature": 0, "activeParameter": 3 @@ -100,11 +112,13 @@ Pexp_apply ...[33:11->33:19] (...[33:20->33:23], ~two33:26->33:29=...[33:30->33: posCursor:[33:19] posNoWhite:[33:18] Found expr:[33:11->33:19] Pexp_ident someFunc:[33:11->33:19] argAtCursor: ~four -extracted params: [int, ~two: string=?, ~three: int, ~four: someVariant, unit] +extracted params: +[( + int, ~two: string=?, ~three: unit => unit, ~four: someVariant, unit] { "signatures": [{ - "label": "let someFunc = (int, ~two: string=?, ~three: int, ~four: someVariant, unit) => unit", - "parameters": [{"label": [16, 19]}, {"label": [21, 35]}, {"label": [37, 48]}, {"label": [50, 68]}, {"label": [70, 74]}] + "label": "let someFunc: (\n int,\n ~two: string=?,\n ~three: unit => unit,\n ~four: someVariant,\n unit,\n) => unit", + "parameters": [{"label": [14, 21]}, {"label": [25, 39]}, {"label": [43, 63]}, {"label": [67, 85]}, {"label": [89, 93]}] }], "activeSignature": 0, "activeParameter": 3 @@ -116,11 +130,12 @@ Pexp_apply ...[36:11->36:20] (...[57:0->36:21]) posCursor:[36:20] posNoWhite:[36:19] Found expr:[36:11->36:20] Pexp_ident otherFunc:[36:11->36:20] argAtCursor: none -extracted params: [string, int, float] +extracted params: +[(string, int, float] { "signatures": [{ - "label": "let otherFunc = (string, int, float) => unit", - "parameters": [{"label": [17, 23]}, {"label": [25, 28]}, {"label": [30, 35]}] + "label": "let otherFunc: (string, int, float) => unit", + "parameters": [{"label": [15, 22]}, {"label": [24, 27]}, {"label": [29, 34]}] }], "activeSignature": 0, "activeParameter": -1 @@ -132,11 +147,12 @@ Pexp_apply ...[39:11->39:20] (...[39:21->39:26]) posCursor:[39:20] posNoWhite:[39:19] Found expr:[39:11->39:20] Pexp_ident otherFunc:[39:11->39:20] argAtCursor: unlabelled<0> -extracted params: [string, int, float] +extracted params: +[(string, int, float] { "signatures": [{ - "label": "let otherFunc = (string, int, float) => unit", - "parameters": [{"label": [17, 23]}, {"label": [25, 28]}, {"label": [30, 35]}] + "label": "let otherFunc: (string, int, float) => unit", + "parameters": [{"label": [15, 22]}, {"label": [24, 27]}, {"label": [29, 34]}] }], "activeSignature": 0, "activeParameter": 0 @@ -148,11 +164,12 @@ Pexp_apply ...[42:11->42:20] (...[42:21->42:26]) posCursor:[42:20] posNoWhite:[42:19] Found expr:[42:11->42:20] Pexp_ident otherFunc:[42:11->42:20] argAtCursor: none -extracted params: [string, int, float] +extracted params: +[(string, int, float] { "signatures": [{ - "label": "let otherFunc = (string, int, float) => unit", - "parameters": [{"label": [17, 23]}, {"label": [25, 28]}, {"label": [30, 35]}] + "label": "let otherFunc: (string, int, float) => unit", + "parameters": [{"label": [15, 22]}, {"label": [24, 27]}, {"label": [29, 34]}] }], "activeSignature": 0, "activeParameter": -1 @@ -164,11 +181,12 @@ Pexp_apply ...[45:11->45:20] (...[45:21->45:26], ...[45:28->45:31]) posCursor:[45:20] posNoWhite:[45:19] Found expr:[45:11->45:20] Pexp_ident otherFunc:[45:11->45:20] argAtCursor: unlabelled<1> -extracted params: [string, int, float] +extracted params: +[(string, int, float] { "signatures": [{ - "label": "let otherFunc = (string, int, float) => unit", - "parameters": [{"label": [17, 23]}, {"label": [25, 28]}, {"label": [30, 35]}] + "label": "let otherFunc: (string, int, float) => unit", + "parameters": [{"label": [15, 22]}, {"label": [24, 27]}, {"label": [29, 34]}] }], "activeSignature": 0, "activeParameter": 1 @@ -180,11 +198,12 @@ Pexp_apply ...[48:11->48:20] (...[48:21->48:26], ...[48:28->48:31]) posCursor:[48:20] posNoWhite:[48:19] Found expr:[48:11->48:20] Pexp_ident otherFunc:[48:11->48:20] argAtCursor: none -extracted params: [string, int, float] +extracted params: +[(string, int, float] { "signatures": [{ - "label": "let otherFunc = (string, int, float) => unit", - "parameters": [{"label": [17, 23]}, {"label": [25, 28]}, {"label": [30, 35]}] + "label": "let otherFunc: (string, int, float) => unit", + "parameters": [{"label": [15, 22]}, {"label": [24, 27]}, {"label": [29, 34]}] }], "activeSignature": 0, "activeParameter": -1 @@ -196,11 +215,12 @@ Pexp_apply ...[51:11->51:20] (...[51:21->51:26], ...[51:28->51:31], ...[51:33->5 posCursor:[51:20] posNoWhite:[51:19] Found expr:[51:11->51:20] Pexp_ident otherFunc:[51:11->51:20] argAtCursor: unlabelled<2> -extracted params: [string, int, float] +extracted params: +[(string, int, float] { "signatures": [{ - "label": "let otherFunc = (string, int, float) => unit", - "parameters": [{"label": [17, 23]}, {"label": [25, 28]}, {"label": [30, 35]}] + "label": "let otherFunc: (string, int, float) => unit", + "parameters": [{"label": [15, 22]}, {"label": [24, 27]}, {"label": [29, 34]}] }], "activeSignature": 0, "activeParameter": 2 @@ -212,11 +232,12 @@ Pexp_apply ...[54:11->54:29] (~age54:31->54:34=...[54:31->54:34]) posCursor:[54:29] posNoWhite:[54:28] Found expr:[54:11->54:29] Pexp_ident Completion.Lib.foo:[54:11->54:29] argAtCursor: ~age -extracted params: [~age: int, ~name: string] +extracted params: +[(~age: int, ~name: string] { "signatures": [{ - "label": "let Completion.Lib.foo = (~age: int, ~name: string) => string", - "parameters": [{"label": [26, 35]}, {"label": [37, 50]}] + "label": "let foo: (~age: int, ~name: string) => string", + "parameters": [{"label": [9, 19]}, {"label": [21, 34]}] }], "activeSignature": 0, "activeParameter": 0 From d9702904aff08fc42fd1615c03a63c7c375c85dc Mon Sep 17 00:00:00 2001 From: Gabriel Nordeborn Date: Sun, 7 Aug 2022 21:14:02 +0200 Subject: [PATCH 08/19] include docs for signature if available --- analysis/src/Protocol.ml | 8 +- analysis/src/SignatureHelp.ml | 6 +- analysis/tests/src/SignatureHelp.res | 1 + .../tests/src/expected/SignatureHelp.res.txt | 161 +++++++++--------- 4 files changed, 97 insertions(+), 79 deletions(-) diff --git a/analysis/src/Protocol.ml b/analysis/src/Protocol.ml index abb513728..ce3e57075 100644 --- a/analysis/src/Protocol.ml +++ b/analysis/src/Protocol.ml @@ -23,6 +23,7 @@ type parameterInformation = {label: int * int} type signatureInformation = { label: string; parameters: parameterInformation list; + documentation: markupContent option; } (* https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#signatureHelp *) @@ -197,12 +198,17 @@ let stringifySignatureInformation (signatureInformation : signatureInformation) Printf.sprintf {|{ "label": "%s", - "parameters": %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 diff --git a/analysis/src/SignatureHelp.ml b/analysis/src/SignatureHelp.ml index 74723c346..40759c7e5 100644 --- a/analysis/src/SignatureHelp.ml +++ b/analysis/src/SignatureHelp.ml @@ -82,7 +82,7 @@ let signatureHelp ~path ~pos ~currentFile ~debug = Some package ))) in match (completions, env, package) with - | {kind = Value type_expr; name} :: _, Some env, Some package -> + | {kind = Value type_expr; name; docstring} :: _, Some env, Some package -> let args, _ = CompletionBackEnd.extractFunctionType type_expr ~env ~package in @@ -192,6 +192,10 @@ let signatureHelp ~path ~pos ~currentFile ~debug = parameters = parameters |> List.map (fun params -> {Protocol.label = params}); + documentation = + (match List.nth_opt docstring 0 with + | None -> None + | Some docs -> Some {Protocol.kind = "markdown"; value = docs}); }; ]; activeSignature = Some 0; diff --git a/analysis/tests/src/SignatureHelp.res b/analysis/tests/src/SignatureHelp.res index af10773a6..62fcd89cf 100644 --- a/analysis/tests/src/SignatureHelp.res +++ b/analysis/tests/src/SignatureHelp.res @@ -1,5 +1,6 @@ type someVariant = One | Two | Three +/** Does stuff. */ let someFunc = (one: int, ~two: option=?, ~three: unit => unit, ~four: someVariant, ()) => { ignore(one) ignore(two) diff --git a/analysis/tests/src/expected/SignatureHelp.res.txt b/analysis/tests/src/expected/SignatureHelp.res.txt index 074e42935..0ee73e260 100644 --- a/analysis/tests/src/expected/SignatureHelp.res.txt +++ b/analysis/tests/src/expected/SignatureHelp.res.txt @@ -1,8 +1,8 @@ -Signature help src/SignatureHelp.res 15:20 -posCursor:[15:19] posNoWhite:[15:18] Found expr:[15:11->15:20] -Pexp_apply ...[15:11->15:19] (...[57:0->15:20]) -posCursor:[15:19] posNoWhite:[15:18] Found expr:[15:11->15:19] -Pexp_ident someFunc:[15:11->15:19] +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] (...[58: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: [( @@ -10,17 +10,18 @@ extracted params: { "signatures": [{ "label": "let someFunc: (\n int,\n ~two: string=?,\n ~three: unit => unit,\n ~four: someVariant,\n unit,\n) => unit", - "parameters": [{"label": [14, 21]}, {"label": [25, 39]}, {"label": [43, 63]}, {"label": [67, 85]}, {"label": [89, 93]}] + "parameters": [{"label": [14, 21]}, {"label": [25, 39]}, {"label": [43, 63]}, {"label": [67, 85]}, {"label": [89, 93]}], + "documentation": {"kind": "markdown", "value": " Does stuff. "} }], "activeSignature": 0, "activeParameter": -1 } -Signature help src/SignatureHelp.res 18:21 -posCursor:[18:19] posNoWhite:[18:18] Found expr:[18:11->18:21] -Pexp_apply ...[18:11->18:19] (...[18:20->18:21]) -posCursor:[18:19] posNoWhite:[18:18] Found expr:[18:11->18:19] -Pexp_ident someFunc:[18:11->18:19] +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: [( @@ -28,17 +29,18 @@ extracted params: { "signatures": [{ "label": "let someFunc: (\n int,\n ~two: string=?,\n ~three: unit => unit,\n ~four: someVariant,\n unit,\n) => unit", - "parameters": [{"label": [14, 21]}, {"label": [25, 39]}, {"label": [43, 63]}, {"label": [67, 85]}, {"label": [89, 93]}] + "parameters": [{"label": [14, 21]}, {"label": [25, 39]}, {"label": [43, 63]}, {"label": [67, 85]}, {"label": [89, 93]}], + "documentation": {"kind": "markdown", "value": " Does stuff. "} }], "activeSignature": 0, "activeParameter": 0 } -Signature help src/SignatureHelp.res 21:29 -posCursor:[21:19] posNoWhite:[21:18] Found expr:[21:11->21:29] -Pexp_apply ...[21:11->21:19] (...[21:20->21:23], ~two21:26->21:29=...[21:26->21:29]) -posCursor:[21:19] posNoWhite:[21:18] Found expr:[21:11->21:19] -Pexp_ident someFunc:[21:11->21:19] +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: [( @@ -46,17 +48,18 @@ extracted params: { "signatures": [{ "label": "let someFunc: (\n int,\n ~two: string=?,\n ~three: unit => unit,\n ~four: someVariant,\n unit,\n) => unit", - "parameters": [{"label": [14, 21]}, {"label": [25, 39]}, {"label": [43, 63]}, {"label": [67, 85]}, {"label": [89, 93]}] + "parameters": [{"label": [14, 21]}, {"label": [25, 39]}, {"label": [43, 63]}, {"label": [67, 85]}, {"label": [89, 93]}], + "documentation": {"kind": "markdown", "value": " Does stuff. "} }], "activeSignature": 0, "activeParameter": 1 } -Signature help src/SignatureHelp.res 24:31 -posCursor:[24:19] posNoWhite:[24:18] Found expr:[24:11->24:30] -Pexp_apply ...[24:11->24:19] (...[24:20->24:23], ~two24:26->24:29=...__ghost__[0:-1->0:-1]) -posCursor:[24:19] posNoWhite:[24:18] Found expr:[24:11->24:19] -Pexp_ident someFunc:[24:11->24:19] +Signature help src/SignatureHelp.res 25:31 +posCursor:[25:19] posNoWhite:[25:18] Found expr:[25:11->25:30] +Pexp_apply ...[25:11->25:19] (...[25:20->25:23], ~two25:26->25:29=...__ghost__[0:-1->0:-1]) +posCursor:[25:19] posNoWhite:[25:18] Found expr:[25:11->25:19] +Pexp_ident someFunc:[25:11->25:19] argAtCursor: ~two extracted params: [( @@ -64,17 +67,18 @@ extracted params: { "signatures": [{ "label": "let someFunc: (\n int,\n ~two: string=?,\n ~three: unit => unit,\n ~four: someVariant,\n unit,\n) => unit", - "parameters": [{"label": [14, 21]}, {"label": [25, 39]}, {"label": [43, 63]}, {"label": [67, 85]}, {"label": [89, 93]}] + "parameters": [{"label": [14, 21]}, {"label": [25, 39]}, {"label": [43, 63]}, {"label": [67, 85]}, {"label": [89, 93]}], + "documentation": {"kind": "markdown", "value": " Does stuff. "} }], "activeSignature": 0, "activeParameter": 1 } -Signature help src/SignatureHelp.res 27:33 -posCursor:[27:19] posNoWhite:[27:18] Found expr:[27:11->27:35] -Pexp_apply ...[27:11->27:19] (...[27:20->27:23], ~two27:26->27:29=...[27:30->27:35]) -posCursor:[27:19] posNoWhite:[27:18] Found expr:[27:11->27:19] -Pexp_ident someFunc:[27:11->27:19] +Signature help src/SignatureHelp.res 28:33 +posCursor:[28:19] posNoWhite:[28:18] Found expr:[28:11->28:35] +Pexp_apply ...[28:11->28:19] (...[28:20->28:23], ~two28:26->28:29=...[28:30->28:35]) +posCursor:[28:19] posNoWhite:[28:18] Found expr:[28:11->28:19] +Pexp_ident someFunc:[28:11->28:19] argAtCursor: ~two extracted params: [( @@ -82,17 +86,18 @@ extracted params: { "signatures": [{ "label": "let someFunc: (\n int,\n ~two: string=?,\n ~three: unit => unit,\n ~four: someVariant,\n unit,\n) => unit", - "parameters": [{"label": [14, 21]}, {"label": [25, 39]}, {"label": [43, 63]}, {"label": [67, 85]}, {"label": [89, 93]}] + "parameters": [{"label": [14, 21]}, {"label": [25, 39]}, {"label": [43, 63]}, {"label": [67, 85]}, {"label": [89, 93]}], + "documentation": {"kind": "markdown", "value": " Does stuff. "} }], "activeSignature": 0, "activeParameter": 1 } -Signature help src/SignatureHelp.res 30:38 -posCursor:[30:19] posNoWhite:[30:18] Found expr:[30:11->30:42] -Pexp_apply ...[30:11->30:19] (...[30:20->30:23], ~two30:26->30:29=...[30:30->30:35], ~four30:38->30:42=...[30:38->30:42]) -posCursor:[30:19] posNoWhite:[30:18] Found expr:[30:11->30:19] -Pexp_ident someFunc:[30:11->30:19] +Signature help src/SignatureHelp.res 31:38 +posCursor:[31:19] posNoWhite:[31:18] Found expr:[31:11->31:42] +Pexp_apply ...[31:11->31:19] (...[31:20->31:23], ~two31:26->31:29=...[31:30->31:35], ~four31:38->31:42=...[31:38->31:42]) +posCursor:[31:19] posNoWhite:[31:18] Found expr:[31:11->31:19] +Pexp_ident someFunc:[31:11->31:19] argAtCursor: ~four extracted params: [( @@ -100,17 +105,18 @@ extracted params: { "signatures": [{ "label": "let someFunc: (\n int,\n ~two: string=?,\n ~three: unit => unit,\n ~four: someVariant,\n unit,\n) => unit", - "parameters": [{"label": [14, 21]}, {"label": [25, 39]}, {"label": [43, 63]}, {"label": [67, 85]}, {"label": [89, 93]}] + "parameters": [{"label": [14, 21]}, {"label": [25, 39]}, {"label": [43, 63]}, {"label": [67, 85]}, {"label": [89, 93]}], + "documentation": {"kind": "markdown", "value": " Does stuff. "} }], "activeSignature": 0, "activeParameter": 3 } -Signature help src/SignatureHelp.res 33:42 -posCursor:[33:19] posNoWhite:[33:18] Found expr:[33:11->33:44] -Pexp_apply ...[33:11->33:19] (...[33:20->33:23], ~two33:26->33:29=...[33:30->33:35], ~four33:38->33:42=...[33:43->33:44]) -posCursor:[33:19] posNoWhite:[33:18] Found expr:[33:11->33:19] -Pexp_ident someFunc:[33:11->33:19] +Signature help src/SignatureHelp.res 34:42 +posCursor:[34:19] posNoWhite:[34:18] Found expr:[34:11->34:44] +Pexp_apply ...[34:11->34:19] (...[34:20->34:23], ~two34:26->34:29=...[34:30->34:35], ~four34:38->34:42=...[34:43->34:44]) +posCursor:[34:19] posNoWhite:[34:18] Found expr:[34:11->34:19] +Pexp_ident someFunc:[34:11->34:19] argAtCursor: ~four extracted params: [( @@ -118,17 +124,18 @@ extracted params: { "signatures": [{ "label": "let someFunc: (\n int,\n ~two: string=?,\n ~three: unit => unit,\n ~four: someVariant,\n unit,\n) => unit", - "parameters": [{"label": [14, 21]}, {"label": [25, 39]}, {"label": [43, 63]}, {"label": [67, 85]}, {"label": [89, 93]}] + "parameters": [{"label": [14, 21]}, {"label": [25, 39]}, {"label": [43, 63]}, {"label": [67, 85]}, {"label": [89, 93]}], + "documentation": {"kind": "markdown", "value": " Does stuff. "} }], "activeSignature": 0, "activeParameter": 3 } -Signature help src/SignatureHelp.res 36:21 -posCursor:[36:20] posNoWhite:[36:19] Found expr:[36:11->36:21] -Pexp_apply ...[36:11->36:20] (...[57:0->36:21]) -posCursor:[36:20] posNoWhite:[36:19] Found expr:[36:11->36:20] -Pexp_ident otherFunc:[36:11->36:20] +Signature help src/SignatureHelp.res 37:21 +posCursor:[37:20] posNoWhite:[37:19] Found expr:[37:11->37:21] +Pexp_apply ...[37:11->37:20] (...[58:0->37:21]) +posCursor:[37:20] posNoWhite:[37:19] Found expr:[37:11->37:20] +Pexp_ident otherFunc:[37:11->37:20] argAtCursor: none extracted params: [(string, int, float] @@ -141,11 +148,11 @@ extracted params: "activeParameter": -1 } -Signature help src/SignatureHelp.res 39:24 -posCursor:[39:20] posNoWhite:[39:19] Found expr:[39:11->39:26] -Pexp_apply ...[39:11->39:20] (...[39:21->39:26]) -posCursor:[39:20] posNoWhite:[39:19] Found expr:[39:11->39:20] -Pexp_ident otherFunc:[39:11->39:20] +Signature help src/SignatureHelp.res 40:24 +posCursor:[40:20] posNoWhite:[40:19] Found expr:[40:11->40:26] +Pexp_apply ...[40:11->40:20] (...[40:21->40:26]) +posCursor:[40:20] posNoWhite:[40:19] Found expr:[40:11->40:20] +Pexp_ident otherFunc:[40:11->40:20] argAtCursor: unlabelled<0> extracted params: [(string, int, float] @@ -158,11 +165,11 @@ extracted params: "activeParameter": 0 } -Signature help src/SignatureHelp.res 42:28 -posCursor:[42:20] posNoWhite:[42:19] Found expr:[42:11->42:27] -Pexp_apply ...[42:11->42:20] (...[42:21->42:26]) -posCursor:[42:20] posNoWhite:[42:19] Found expr:[42:11->42:20] -Pexp_ident otherFunc:[42:11->42:20] +Signature help src/SignatureHelp.res 43:28 +posCursor:[43:20] posNoWhite:[43:19] Found expr:[43:11->43:27] +Pexp_apply ...[43:11->43:20] (...[43:21->43:26]) +posCursor:[43:20] posNoWhite:[43:19] Found expr:[43:11->43:20] +Pexp_ident otherFunc:[43:11->43:20] argAtCursor: none extracted params: [(string, int, float] @@ -175,11 +182,11 @@ extracted params: "activeParameter": -1 } -Signature help src/SignatureHelp.res 45:30 -posCursor:[45:20] posNoWhite:[45:19] Found expr:[45:11->45:31] -Pexp_apply ...[45:11->45:20] (...[45:21->45:26], ...[45:28->45:31]) -posCursor:[45:20] posNoWhite:[45:19] Found expr:[45:11->45:20] -Pexp_ident otherFunc:[45:11->45:20] +Signature help src/SignatureHelp.res 46:30 +posCursor:[46:20] posNoWhite:[46:19] Found expr:[46:11->46:31] +Pexp_apply ...[46:11->46:20] (...[46:21->46:26], ...[46:28->46:31]) +posCursor:[46:20] posNoWhite:[46:19] Found expr:[46:11->46:20] +Pexp_ident otherFunc:[46:11->46:20] argAtCursor: unlabelled<1> extracted params: [(string, int, float] @@ -192,11 +199,11 @@ extracted params: "activeParameter": 1 } -Signature help src/SignatureHelp.res 48:33 -posCursor:[48:20] posNoWhite:[48:19] Found expr:[48:11->48:32] -Pexp_apply ...[48:11->48:20] (...[48:21->48:26], ...[48:28->48:31]) -posCursor:[48:20] posNoWhite:[48:19] Found expr:[48:11->48:20] -Pexp_ident otherFunc:[48:11->48:20] +Signature help src/SignatureHelp.res 49:33 +posCursor:[49:20] posNoWhite:[49:19] Found expr:[49:11->49:32] +Pexp_apply ...[49:11->49:20] (...[49:21->49:26], ...[49:28->49:31]) +posCursor:[49:20] posNoWhite:[49:19] Found expr:[49:11->49:20] +Pexp_ident otherFunc:[49:11->49:20] argAtCursor: none extracted params: [(string, int, float] @@ -209,11 +216,11 @@ extracted params: "activeParameter": -1 } -Signature help src/SignatureHelp.res 51:35 -posCursor:[51:20] posNoWhite:[51:19] Found expr:[51:11->51:39] -Pexp_apply ...[51:11->51:20] (...[51:21->51:26], ...[51:28->51:31], ...[51:33->51:38]) -posCursor:[51:20] posNoWhite:[51:19] Found expr:[51:11->51:20] -Pexp_ident otherFunc:[51:11->51:20] +Signature help src/SignatureHelp.res 52:35 +posCursor:[52:20] posNoWhite:[52:19] Found expr:[52:11->52:39] +Pexp_apply ...[52:11->52:20] (...[52:21->52:26], ...[52:28->52:31], ...[52:33->52:38]) +posCursor:[52:20] posNoWhite:[52:19] Found expr:[52:11->52:20] +Pexp_ident otherFunc:[52:11->52:20] argAtCursor: unlabelled<2> extracted params: [(string, int, float] @@ -226,11 +233,11 @@ extracted params: "activeParameter": 2 } -Signature help src/SignatureHelp.res 54:33 -posCursor:[54:29] posNoWhite:[54:28] Found expr:[54:11->54:34] -Pexp_apply ...[54:11->54:29] (~age54:31->54:34=...[54:31->54:34]) -posCursor:[54:29] posNoWhite:[54:28] Found expr:[54:11->54:29] -Pexp_ident Completion.Lib.foo:[54:11->54:29] +Signature help src/SignatureHelp.res 55:33 +posCursor:[55:29] posNoWhite:[55:28] Found expr:[55:11->55:34] +Pexp_apply ...[55:11->55:29] (~age55:31->55:34=...[55:31->55:34]) +posCursor:[55:29] posNoWhite:[55:28] Found expr:[55:11->55:29] +Pexp_ident Completion.Lib.foo:[55:11->55:29] argAtCursor: ~age extracted params: [(~age: int, ~name: string] From ac24e9390e0a962abaa3565885f6323c3ba90c18 Mon Sep 17 00:00:00 2001 From: Gabriel Nordeborn Date: Sun, 7 Aug 2022 21:30:50 +0200 Subject: [PATCH 09/19] if a function has only one (unlabelled) argument, we can always highlight that as active, no matter what is picked up at the cursor --- analysis/src/SignatureHelp.ml | 11 ++++++++--- analysis/tests/src/SignatureHelp.res | 7 +++++++ .../tests/src/expected/SignatureHelp.res.txt | 17 +++++++++++++++++ 3 files changed, 32 insertions(+), 3 deletions(-) diff --git a/analysis/src/SignatureHelp.ml b/analysis/src/SignatureHelp.ml index 40759c7e5..c7cb2d3ab 100644 --- a/analysis/src/SignatureHelp.ml +++ b/analysis/src/SignatureHelp.ml @@ -44,7 +44,7 @@ let signatureHelp ~path ~pos ~currentFile ~debug = then Some (Labelled name) else None) in - setFound (argAtCursor, exp) + setFound (argAtCursor, exp, extractedArgs) | _ -> ()); Ast_iterator.default_iterator.expr iterator expr in @@ -53,7 +53,7 @@ let signatureHelp ~path ~pos ~currentFile ~debug = let {Res_driver.parsetree = structure} = parser ~filename:currentFile in iterator.structure iterator structure |> ignore; match !foundFunctionApplicationExpr with - | Some (argAtCursor, exp) -> ( + | 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 let completions, env, package = @@ -160,7 +160,12 @@ let signatureHelp ~path ~pos ~currentFile ~debug = (* Figure out the active parameter *) let activeParameter = match argAtCursor with - | None -> None + | 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 + | [(Nolabel, _)] -> Some 0 + | _ -> None) | Some (Unlabelled unlabelledArgumentIndex) -> let index = ref 0 in args diff --git a/analysis/tests/src/SignatureHelp.res b/analysis/tests/src/SignatureHelp.res index 62fcd89cf..fac0d1926 100644 --- a/analysis/tests/src/SignatureHelp.res +++ b/analysis/tests/src/SignatureHelp.res @@ -55,3 +55,10 @@ let otherFunc = (first: string, second: int, third: float) => { // 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 index 0ee73e260..deafaca07 100644 --- a/analysis/tests/src/expected/SignatureHelp.res.txt +++ b/analysis/tests/src/expected/SignatureHelp.res.txt @@ -250,3 +250,20 @@ extracted params: "activeParameter": 0 } +Signature help src/SignatureHelp.res 62:24 +posCursor:[62:23] posNoWhite:[62:22] Found expr:[62:11->62:24] +Pexp_apply ...[62:11->62:23] (...[65:0->62:24]) +posCursor:[62:23] posNoWhite:[62:22] Found expr:[62:11->62:23] +Pexp_ident iAmSoSpecial:[62:11->62:23] +argAtCursor: none +extracted params: +[string] +{ + "signatures": [{ + "label": "let iAmSoSpecial: string => unit", + "parameters": [{"label": [18, 24]}] + }], + "activeSignature": 0, + "activeParameter": 0 +} + From 03cd58cc89bb16f7981a52cdd54f2606cacc4c6f Mon Sep 17 00:00:00 2001 From: Gabriel Nordeborn Date: Sun, 18 Sep 2022 20:03:47 +0200 Subject: [PATCH 10/19] add debug utilities --- analysis/src/Commands.ml | 7 + analysis/src/CompletionFrontEnd.ml | 28 +-- analysis/src/DumpAst.ml | 315 +++++++++++++++++++++++++++++ analysis/src/Pos.ml | 20 ++ analysis/src/SharedTypes.ml | 67 +++++- analysis/src/Utils.ml | 76 +++++++ 6 files changed, 488 insertions(+), 25 deletions(-) create mode 100644 analysis/src/DumpAst.ml diff --git a/analysis/src/Commands.ml b/analysis/src/Commands.ml index f0ad3efd2..d7871446e 100644 --- a/analysis/src/Commands.ml +++ b/analysis/src/Commands.ml @@ -408,6 +408,13 @@ let test ~path = | "cle" -> print_endline ("Code Lens " ^ path); codeLens ~path ~debug:false + | "ast" -> + print_endline + ("Dump AST " ^ path ^ " " ^ string_of_int line ^ ":" + ^ string_of_int col); + let currentFile = createCurrentFile () in + DumpAst.dump ~pos:(line, col) ~currentFile; + Sys.remove currentFile | _ -> ()); print_newline ()) in diff --git a/analysis/src/CompletionFrontEnd.ml b/analysis/src/CompletionFrontEnd.ml index 2bb5ab2de..5c55baf62 100644 --- a/analysis/src/CompletionFrontEnd.ml +++ b/analysis/src/CompletionFrontEnd.ml @@ -7,26 +7,6 @@ let rec skipWhite text i = | ' ' | '\n' | '\r' | '\t' -> skipWhite text (i - 1) | _ -> i -let offsetOfLine text line = - let ln = String.length text in - let rec loop i lno = - if i >= ln then None - else - match text.[i] with - | '\n' -> if lno = line - 1 then Some (i + 1) else loop (i + 1) (lno + 1) - | _ -> loop (i + 1) lno - in - match line with - | 0 -> Some 0 - | _ -> loop 0 0 - -let positionToOffset text (line, character) = - match offsetOfLine text line with - | None -> None - | Some bol -> - if bol + character <= String.length text then Some (bol + character) - else None - type prop = { name: string; posStart: int * int; @@ -226,7 +206,7 @@ let completionWithParser1 ~currentFile ~debug ~offset ~path ~posCursor ~text = in let posBeforeCursor = (fst posCursor, max 0 (snd posCursor - 1)) in let charBeforeCursor, blankAfterCursor = - match positionToOffset text posCursor with + match Pos.positionToOffset text posCursor with | Some offset when offset > 0 -> ( let charBeforeCursor = text.[offset - 1] in let charAtCursor = @@ -405,7 +385,9 @@ let completionWithParser1 ~currentFile ~debug ~offset ~path ~posCursor ~text = else if id.loc.loc_ghost then () else if id.loc |> Loc.hasPos ~pos:posBeforeCursor then let posStart, posEnd = Loc.range id.loc in - match (positionToOffset text posStart, positionToOffset text posEnd) with + match + (Pos.positionToOffset text posStart, Pos.positionToOffset text posEnd) + with | Some offsetStart, Some offsetEnd -> (* Can't trust the parser's location E.g. @foo. let x... gives as label @foo.let *) @@ -788,7 +770,7 @@ let completionWithParser1 ~currentFile ~debug ~offset ~path ~posCursor ~text = else None let completionWithParser ~debug ~path ~posCursor ~currentFile ~text = - match positionToOffset text posCursor with + match Pos.positionToOffset text posCursor with | Some offset -> completionWithParser1 ~currentFile ~debug ~offset ~path ~posCursor ~text | None -> None diff --git a/analysis/src/DumpAst.ml b/analysis/src/DumpAst.ml new file mode 100644 index 000000000..cb651dbce --- /dev/null +++ b/analysis/src/DumpAst.ml @@ -0,0 +1,315 @@ +open SharedTypes +(* This is intended to be a debug tool. It's by no means complete. Rather, you're encouraged to extend this with printing whatever types you need printing. *) + +let emptyLocDenom = "" +let hasCursorDenom = "<*>" +let noCursorDenom = "" + +let printLocDenominator loc ~pos = + match loc |> CursorPosition.classifyLoc ~pos with + | EmptyLoc -> emptyLocDenom + | HasCursor -> hasCursorDenom + | NoCursor -> noCursorDenom + +let printLocDenominatorLoc loc ~pos = + match loc |> CursorPosition.classifyLocationLoc ~pos with + | CursorPosition.EmptyLoc -> emptyLocDenom + | HasCursor -> hasCursorDenom + | NoCursor -> noCursorDenom + +let printLocDenominatorPos pos ~posStart ~posEnd = + match CursorPosition.classifyPositions pos ~posStart ~posEnd with + | CursorPosition.EmptyLoc -> emptyLocDenom + | HasCursor -> hasCursorDenom + | NoCursor -> noCursorDenom + +let addIndentation indentation = + let rec indent str indentation = + if indentation < 1 then str else indent (str ^ " ") (indentation - 1) + in + indent "" indentation + +let printAttributes attributes = + match List.length attributes with + | 0 -> "" + | _ -> + "[" + ^ (attributes + |> List.map (fun ({Location.txt}, _payload) -> "@" ^ txt) + |> String.concat ",") + ^ "]" + +let printConstant const = + match const with + | Parsetree.Pconst_integer (s, _) -> "Pconst_integer(" ^ s ^ ")" + | Pconst_char c -> "Pconst_char(" ^ String.make 1 c ^ ")" + | Pconst_string (s, delim) -> + let delim = + match delim with + | None -> "" + | Some delim -> delim ^ " " + in + "Pconst_string(" ^ delim ^ s ^ delim ^ ")" + | Pconst_float (s, _) -> "Pconst_float(" ^ s ^ ")" + +let printCoreType typ ~pos = + printAttributes typ.Parsetree.ptyp_attributes + ^ (typ.ptyp_loc |> printLocDenominator ~pos) + ^ + match typ.ptyp_desc with + | Ptyp_any -> "Ptyp_any" + | Ptyp_var name -> "Ptyp_var(" ^ str name ^ ")" + | Ptyp_constr (loc, _types) -> + "Ptyp_constr(" + ^ (loc |> printLocDenominatorLoc ~pos) + ^ (Utils.flattenLongIdent loc.txt |> ident |> str) + ^ ")" + | Ptyp_variant _ -> "Ptyp_variant()" + | _ -> "" + +let rec printPattern pattern ~pos ~indentation = + printAttributes pattern.Parsetree.ppat_attributes + ^ (pattern.ppat_loc |> printLocDenominator ~pos) + ^ + match pattern.Parsetree.ppat_desc with + | Ppat_or (pat1, pat2) -> + "Ppat_or(\n" + ^ addIndentation (indentation + 1) + ^ printPattern pat1 ~pos ~indentation:(indentation + 2) + ^ ",\n" + ^ addIndentation (indentation + 1) + ^ printPattern pat2 ~pos ~indentation:(indentation + 2) + ^ "\n" ^ addIndentation indentation ^ ")" + | Ppat_extension (({txt} as loc), _) -> + "Ppat_extension(%" ^ (loc |> printLocDenominatorLoc ~pos) ^ txt ^ ")" + | Ppat_var ({txt} as loc) -> + "Ppat_var(" ^ (loc |> printLocDenominatorLoc ~pos) ^ txt ^ ")" + | Ppat_constant const -> "Ppat_constant(" ^ printConstant const ^ ")" + | Ppat_construct (({txt} as loc), maybePat) -> + "Ppat_construct(" + ^ (loc |> printLocDenominatorLoc ~pos) + ^ (Utils.flattenLongIdent txt |> ident |> str) + ^ (match maybePat with + | None -> "" + | Some pat -> "," ^ printPattern pat ~pos ~indentation) + ^ ")" + | Ppat_variant (label, maybePat) -> + "Ppat_variant(" ^ str label + ^ (match maybePat with + | None -> "" + | Some pat -> "," ^ printPattern pat ~pos ~indentation) + ^ ")" + | Ppat_record (fields, _) -> + "Ppat_record(\n" + ^ addIndentation (indentation + 1) + ^ "fields:\n" + ^ (fields + |> List.map (fun ((Location.{txt} as loc), pat) -> + addIndentation (indentation + 2) + ^ (loc |> printLocDenominatorLoc ~pos) + ^ (Utils.flattenLongIdent txt |> ident |> str) + ^ ": " + ^ printPattern pat ~pos ~indentation:(indentation + 2)) + |> String.concat "\n") + ^ "\n" ^ addIndentation indentation ^ ")" + | Ppat_tuple patterns -> + "Ppat_tuple(\n" + ^ (patterns + |> List.map (fun pattern -> + addIndentation (indentation + 2) + ^ (pattern |> printPattern ~pos ~indentation:(indentation + 2))) + |> String.concat ",\n") + ^ "\n" ^ addIndentation indentation ^ ")" + | Ppat_any -> "Ppat_any" + | Ppat_constraint (pattern, typ) -> + "Ppat_constraint(\n" + ^ addIndentation (indentation + 1) + ^ printCoreType typ ~pos ^ ",\n" + ^ addIndentation (indentation + 1) + ^ (pattern |> printPattern ~pos ~indentation:(indentation + 1)) + ^ "\n" ^ addIndentation indentation ^ ")" + | v -> Printf.sprintf "" (Utils.identifyPpat v) + +and printCase case ~pos ~indentation ~caseNum = + addIndentation indentation + ^ Printf.sprintf "case %i:\n" caseNum + ^ addIndentation (indentation + 1) + ^ "pattern" + ^ (case.Parsetree.pc_lhs.ppat_loc |> printLocDenominator ~pos) + ^ ":\n" + ^ addIndentation (indentation + 2) + ^ printPattern case.Parsetree.pc_lhs ~pos ~indentation + ^ "\n" + ^ addIndentation (indentation + 1) + ^ "expr" + ^ (case.Parsetree.pc_rhs.pexp_loc |> printLocDenominator ~pos) + ^ ":\n" + ^ addIndentation (indentation + 2) + ^ printExprItem case.pc_rhs ~pos ~indentation:(indentation + 2) + +and printExprItem expr ~pos ~indentation = + printAttributes expr.Parsetree.pexp_attributes + ^ (expr.pexp_loc |> printLocDenominator ~pos) + ^ + match expr.Parsetree.pexp_desc with + | Pexp_match (matchExpr, cases) -> + "Pexp_match(" + ^ printExprItem matchExpr ~pos ~indentation:0 + ^ ")\n" + ^ (cases + |> List.mapi (fun caseNum case -> + printCase case ~pos ~caseNum:(caseNum + 1) + ~indentation:(indentation + 1)) + |> String.concat "\n") + | Pexp_ident {txt} -> + "Pexp_ident:" ^ (Utils.flattenLongIdent txt |> SharedTypes.ident) + | Pexp_apply (expr, args) -> + let printLabel labelled ~pos = + match labelled with + | None -> "" + | Some labelled -> + printLocDenominatorPos pos ~posStart:labelled.posStart + ~posEnd:labelled.posEnd + ^ "~" + ^ if labelled.opt then "?" else "" ^ labelled.name + in + let args = extractExpApplyArgs ~args in + "Pexp_apply(\n" + ^ addIndentation (indentation + 1) + ^ "expr:\n" + ^ addIndentation (indentation + 2) + ^ printExprItem expr ~pos ~indentation:(indentation + 2) + ^ "\n" + ^ addIndentation (indentation + 1) + ^ "args:\n" + ^ (args + |> List.map (fun arg -> + addIndentation (indentation + 2) + ^ printLabel arg.label ~pos ^ "=\n" + ^ addIndentation (indentation + 3) + ^ printExprItem arg.exp ~pos ~indentation:(indentation + 3)) + |> String.concat ",\n") + ^ "\n" ^ addIndentation indentation ^ ")" + | Pexp_constant constant -> "Pexp_constant(" ^ printConstant constant ^ ")" + | Pexp_construct (({txt} as loc), maybeExpr) -> + "Pexp_construct(" + ^ (loc |> printLocDenominatorLoc ~pos) + ^ (Utils.flattenLongIdent txt |> ident |> str) + ^ (match maybeExpr with + | None -> "" + | Some expr -> ", " ^ printExprItem expr ~pos ~indentation) + ^ ")" + | Pexp_variant (label, maybeExpr) -> + "Pexp_variant(" ^ str label + ^ (match maybeExpr with + | None -> "" + | Some expr -> "," ^ printExprItem expr ~pos ~indentation) + ^ ")" + | Pexp_fun (arg, _maybeDefaultArgExpr, pattern, nextExpr) -> + "Pexp_fun(\n" + ^ addIndentation (indentation + 1) + ^ "arg: " + ^ (match arg with + | Nolabel -> "Nolabel" + | Labelled name -> "Labelled(" ^ name ^ ")" + | Optional name -> "Optional(" ^ name ^ ")") + ^ ",\n" + ^ addIndentation (indentation + 2) + ^ "pattern: " + ^ printPattern pattern ~pos ~indentation:(indentation + 2) + ^ ",\n" + ^ addIndentation (indentation + 1) + ^ "next expr:\n" + ^ addIndentation (indentation + 2) + ^ printExprItem nextExpr ~pos ~indentation:(indentation + 2) + ^ "\n" ^ addIndentation indentation ^ ")" + | Pexp_extension (({txt} as loc), _) -> + "Pexp_extension(%" ^ (loc |> printLocDenominatorLoc ~pos) ^ txt ^ ")" + | Pexp_assert expr -> + "Pexp_assert(" ^ printExprItem expr ~pos ~indentation ^ ")" + | Pexp_field (exp, loc) -> + "Pexp_field(" + ^ (loc |> printLocDenominatorLoc ~pos) + ^ printExprItem exp ~pos ~indentation + ^ ")" + | Pexp_record (fields, _) -> + "Pexp_record(\n" + ^ addIndentation (indentation + 1) + ^ "fields:\n" + ^ (fields + |> List.map (fun ((Location.{txt} as loc), expr) -> + addIndentation (indentation + 2) + ^ (loc |> printLocDenominatorLoc ~pos) + ^ (Utils.flattenLongIdent txt |> ident |> str) + ^ ": " + ^ printExprItem expr ~pos ~indentation:(indentation + 2)) + |> String.concat "\n") + ^ "\n" ^ addIndentation indentation ^ ")" + | Pexp_tuple exprs -> + "Pexp_tuple(\n" + ^ (exprs + |> List.map (fun expr -> + addIndentation (indentation + 2) + ^ (expr |> printExprItem ~pos ~indentation:(indentation + 2))) + |> String.concat ",\n") + ^ "\n" ^ addIndentation indentation ^ ")" + | v -> Printf.sprintf "" (Utils.identifyPexp v) + +let printValueBinding value ~pos ~indentation = + printAttributes value.Parsetree.pvb_attributes + ^ "value" ^ ":\n" + ^ addIndentation (indentation + 1) + ^ (value.pvb_pat |> printPattern ~pos ~indentation:(indentation + 1)) + ^ "\n" ^ addIndentation indentation ^ "expr:\n" + ^ addIndentation (indentation + 1) + ^ printExprItem value.pvb_expr ~pos ~indentation:(indentation + 1) + +let printStructItem structItem ~pos ~source = + match structItem.Parsetree.pstr_loc |> CursorPosition.classifyLoc ~pos with + | HasCursor -> ( + let startOffset = + match Pos.positionToOffset source (structItem.pstr_loc |> Loc.start) with + | None -> 0 + | Some offset -> offset + in + let endOffset = + (* Include the next line of the source since that will hold the ast comment pointing to the position. + Caveat: this only works for single line sources with a comment on the next line. Will need to be + adapted if that's not the only use case.*) + let line, _col = structItem.pstr_loc |> Loc.end_ in + match Pos.positionToOffset source (line + 2, 0) with + | None -> 0 + | Some offset -> offset + in + + ("\nSource:\n// " + ^ String.sub source startOffset (endOffset - startOffset) + ^ "\n") + ^ printLocDenominator structItem.pstr_loc ~pos + ^ + match structItem.pstr_desc with + | Pstr_eval (expr, _attributes) -> + "Pstr_eval(\n" ^ printExprItem expr ~pos ~indentation:1 ^ "\n)" + | Pstr_value (recFlag, values) -> + "Pstr_value(\n" + ^ (match recFlag with + | Recursive -> " rec,\n" + | Nonrecursive -> "") + ^ (values + |> List.map (fun value -> + addIndentation 1 ^ printValueBinding value ~pos ~indentation:1) + |> String.concat ",\n") + ^ "\n)" + | _ -> "") + | _ -> "" + +let dump ~currentFile ~pos = + let {Res_driver.parsetree = structure; source} = + Res_driver.parsingEngine.parseImplementation ~forPrinter:true + ~filename:currentFile + in + + print_endline + (structure + |> List.map (fun structItem -> printStructItem structItem ~pos ~source) + |> String.concat "") \ No newline at end of file diff --git a/analysis/src/Pos.ml b/analysis/src/Pos.ml index cfcf6b7b6..a493946fc 100644 --- a/analysis/src/Pos.ml +++ b/analysis/src/Pos.ml @@ -4,3 +4,23 @@ let ofLexing {Lexing.pos_lnum; pos_cnum; pos_bol} = (pos_lnum - 1, pos_cnum - pos_bol) let toString (loc, col) = Printf.sprintf "%d:%d" loc col + +let offsetOfLine text line = + let ln = String.length text in + let rec loop i lno = + if i >= ln then None + else + match text.[i] with + | '\n' -> if lno = line - 1 then Some (i + 1) else loop (i + 1) (lno + 1) + | _ -> loop (i + 1) lno + in + match line with + | 0 -> Some 0 + | _ -> loop 0 0 + +let positionToOffset text (line, character) = + match offsetOfLine text line with + | None -> None + | Some bol -> + if bol + character <= String.length text then Some (bol + character) + else None diff --git a/analysis/src/SharedTypes.ml b/analysis/src/SharedTypes.ml index 03ad6935e..440ccc811 100644 --- a/analysis/src/SharedTypes.ml +++ b/analysis/src/SharedTypes.ml @@ -1,3 +1,7 @@ +let str s = if s = "" then "\"\"" else s +let list l = "[" ^ (l |> List.map str |> String.concat ", ") ^ "]" +let ident l = l |> List.map str |> String.concat "." + type modulePath = | File of Uri.t * string | NotVisible @@ -442,8 +446,6 @@ module Completable = struct (** E.g. (["M", "Comp"], "id", ["id1", "id2"]) for List.map str |> String.concat ", ") ^ "]" in let completionContextToString = function | Value -> "Value" | Type -> "Type" @@ -479,3 +481,64 @@ module Completable = struct | Cjsx (sl1, s, sl2) -> "Cjsx(" ^ (sl1 |> list) ^ ", " ^ str s ^ ", " ^ (sl2 |> list) ^ ")" end + +module CursorPosition = struct + type t = NoCursor | HasCursor | EmptyLoc + + let classifyLoc loc ~pos = + if loc |> Loc.hasPos ~pos then HasCursor + else if loc |> Loc.end_ = (Location.none |> Loc.end_) then EmptyLoc + else NoCursor + + let classifyLocationLoc (loc : 'a Location.loc) ~pos = + if Loc.start loc.Location.loc <= pos && pos <= Loc.end_ loc.loc then + HasCursor + else if loc.loc |> Loc.end_ = (Location.none |> Loc.end_) then EmptyLoc + else NoCursor + + let classifyPositions pos ~posStart ~posEnd = + if posStart <= pos && pos <= posEnd then HasCursor + else if posEnd = (Location.none |> Loc.end_) then EmptyLoc + else NoCursor +end + +type labelled = { + name: string; + opt: bool; + posStart: int * int; + posEnd: int * int; +} + +type label = labelled option +type arg = {label: label; exp: Parsetree.expression} + +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:[] diff --git a/analysis/src/Utils.ml b/analysis/src/Utils.ml index c785d54f1..ff4299c7b 100644 --- a/analysis/src/Utils.ml +++ b/analysis/src/Utils.ml @@ -71,3 +71,79 @@ let flattenLongIdent ?(jsx = false) ?(cutAtOffset = None) lid = in let path, _ = loop lid in List.rev path + +let identifyPexp pexp = + match pexp with + | Parsetree.Pexp_ident _ -> "Pexp_ident" + | Pexp_constant _ -> "Pexp_constant" + | Pexp_let _ -> "Pexp_let" + | Pexp_function _ -> "Pexp_function" + | Pexp_fun _ -> "Pexp_fun" + | Pexp_apply _ -> "Pexp_apply" + | Pexp_match _ -> "Pexp_match" + | Pexp_try _ -> "Pexp_try" + | Pexp_tuple _ -> "Pexp_tuple" + | Pexp_construct _ -> "Pexp_construct" + | Pexp_variant _ -> "Pexp_variant" + | Pexp_record _ -> "Pexp_record" + | Pexp_field _ -> "Pexp_field" + | Pexp_setfield _ -> "Pexp_setfield" + | Pexp_array _ -> "Pexp_array" + | Pexp_ifthenelse _ -> "Pexp_ifthenelse" + | Pexp_sequence _ -> "Pexp_sequence" + | Pexp_while _ -> "Pexp_while" + | Pexp_for _ -> "Pexp_for" + | Pexp_constraint _ -> "Pexp_constraint" + | Pexp_coerce _ -> "Pexp_coerce" + | Pexp_send _ -> "Pexp_send" + | Pexp_new _ -> "Pexp_new" + | Pexp_setinstvar _ -> "Pexp_setinstvar" + | Pexp_override _ -> "Pexp_override" + | Pexp_letmodule _ -> "Pexp_letmodule" + | Pexp_letexception _ -> "Pexp_letexception" + | Pexp_assert _ -> "Pexp_assert" + | Pexp_lazy _ -> "Pexp_lazy" + | Pexp_poly _ -> "Pexp_poly" + | Pexp_object _ -> "Pexp_object" + | Pexp_newtype _ -> "Pexp_newtype" + | Pexp_pack _ -> "Pexp_pack" + | Pexp_extension _ -> "Pexp_extension" + | Pexp_open _ -> "Pexp_open" + | Pexp_unreachable -> "Pexp_unreachable" + +let identifyPpat pat = + match pat with + | Parsetree.Ppat_any -> "Ppat_any" + | Ppat_var _ -> "Ppat_var" + | Ppat_alias _ -> "Ppat_alias" + | Ppat_constant _ -> "Ppat_constant" + | Ppat_interval _ -> "Ppat_interval" + | Ppat_tuple _ -> "Ppat_tuple" + | Ppat_construct _ -> "Ppat_construct" + | Ppat_variant _ -> "Ppat_variant" + | Ppat_record _ -> "Ppat_record" + | Ppat_array _ -> "Ppat_array" + | Ppat_or _ -> "Ppat_or" + | Ppat_constraint _ -> "Ppat_constraint" + | Ppat_type _ -> "Ppat_type" + | Ppat_lazy _ -> "Ppat_lazy" + | Ppat_unpack _ -> "Ppat_unpack" + | Ppat_exception _ -> "Ppat_exception" + | Ppat_extension _ -> "Ppat_extension" + | Ppat_open _ -> "Ppat_open" + +let identifyType type_desc = + match type_desc with + | Types.Tvar _ -> "Tvar" + | Tarrow _ -> "Tarrow" + | Ttuple _ -> "Ttuple" + | Tconstr _ -> "Tconstr" + | Tobject _ -> "Tobject" + | Tfield _ -> "Tfield" + | Tnil -> "Tnil" + | Tlink _ -> "Tlink" + | Tsubst _ -> "Tsubst" + | Tvariant _ -> "Tvariant" + | Tunivar _ -> "Tunivar" + | Tpoly _ -> "Tpoly" + | Tpackage _ -> "Tpackage" From 2f7c06ed7061f6682457edd9782d97d349ff87ad Mon Sep 17 00:00:00 2001 From: Gabriel Nordeborn Date: Mon, 19 Sep 2022 08:28:51 +0200 Subject: [PATCH 11/19] get rid of hasPosInclusive --- analysis/src/Loc.ml | 2 - analysis/src/SignatureHelp.ml | 56 ++++--- analysis/tests/src/SignatureHelp.res | 12 -- .../tests/src/expected/SignatureHelp.res.txt | 144 +++++------------- 4 files changed, 68 insertions(+), 146 deletions(-) diff --git a/analysis/src/Loc.ml b/analysis/src/Loc.ml index 202ff6041..8e0c72bd6 100644 --- a/analysis/src/Loc.ml +++ b/analysis/src/Loc.ml @@ -8,5 +8,3 @@ let toString (loc : t) = (if loc.loc_ghost then "__ghost__" else "") ^ (loc |> range |> Range.toString) let hasPos ~pos loc = start loc <= pos && pos < end_ loc - -let hasPosInclusive ~pos loc = start loc <= pos && pos <= end_ loc diff --git a/analysis/src/SignatureHelp.ml b/analysis/src/SignatureHelp.ml index c7cb2d3ab..390ccd4a5 100644 --- a/analysis/src/SignatureHelp.ml +++ b/analysis/src/SignatureHelp.ml @@ -6,11 +6,14 @@ let signatureHelp ~path ~pos ~currentFile ~debug = 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 |> Loc.hasPosInclusive ~pos:posBeforeCursor -> + when pexp_loc + |> SharedTypes.CursorPosition.classifyLoc ~pos:posBeforeCursor + == HasCursor -> (* TODO: Move extractExpApplyArgs to a shared module that doesn't look like it's only for completion. *) let extractedArgs = CompletionFrontEnd.extractExpApplyArgs ~args in let argAtCursor = @@ -22,27 +25,32 @@ let signatureHelp ~path ~pos ~currentFile ~debug = 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.hasPosInclusive ~pos:posBeforeCursor - then Some (Unlabelled currentUnlabelledArgCount) + if arg.exp.pexp_loc |> Loc.hasPos ~pos:posBeforeCursor then + Some (Unlabelled currentUnlabelledArgCount) else None - | Some {name; posStart; posEnd} -> + | Some {name; posStart; posEnd} -> ( (* Check for the label identifier itself having the cursor *) - let cursorIsWithinLabelIdPos = - posStart <= pos && pos <= posEnd - in - (* Check for the expr assigned to the label having the cursor *) - let cursorIsWithinArgExprPos = - arg.exp.pexp_loc |> Loc.hasPosInclusive ~pos:posBeforeCursor - in - (* This most likely means that parsing failed and recovered. Happens for empty assignments for example. *) - let argExprIsParserRecovery = - arg.exp.pexp_loc |> Loc.end_ = (Location.none |> Loc.end_) - in - if - cursorIsWithinLabelIdPos || cursorIsWithinArgExprPos - || argExprIsParserRecovery - then Some (Labelled name) - else None) + match + pos + |> SharedTypes.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 + |> SharedTypes.CursorPosition.classifyLoc + ~pos:posBeforeCursor ) + with + | Pexp_extension ({txt = "rescript.exprhole"}, _), _ + | _, HasCursor -> + Some (Labelled name) + | _ -> None))) in setFound (argAtCursor, exp, extractedArgs) | _ -> ()); @@ -53,7 +61,7 @@ let signatureHelp ~path ~pos ~currentFile ~debug = let {Res_driver.parsetree = structure} = parser ~filename:currentFile in iterator.structure iterator structure |> ignore; match !foundFunctionApplicationExpr with - | Some (argAtCursor, exp, extractedArgs) -> ( + | 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 let completions, env, package = @@ -127,14 +135,12 @@ let signatureHelp ~path ~pos ~currentFile ~debug = ptyp_loc; } -> let startOffset = - ptyp_loc |> Loc.start - |> CompletionFrontEnd.positionToOffset label + ptyp_loc |> Loc.start |> Pos.positionToOffset label |> Option.get in let endOffset = argumentTypeExpr.ptyp_loc |> Loc.end_ - |> CompletionFrontEnd.positionToOffset label - |> Option.get + |> 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 = diff --git a/analysis/tests/src/SignatureHelp.res b/analysis/tests/src/SignatureHelp.res index fac0d1926..6d2e81b9b 100644 --- a/analysis/tests/src/SignatureHelp.res +++ b/analysis/tests/src/SignatureHelp.res @@ -23,9 +23,6 @@ let otherFunc = (first: string, second: int, third: float) => { // let _ = someFunc(123, ~two // ^she -// let _ = someFunc(123, ~two= -// ^she - // let _ = someFunc(123, ~two="123" // ^she @@ -41,15 +38,6 @@ let otherFunc = (first: string, second: int, third: float) => { // let _ = otherFunc("123" // ^she -// let _ = otherFunc("123", -// ^she - -// let _ = otherFunc("123", 123 -// ^she - -// let _ = otherFunc("123", 123, -// ^she - // let _ = otherFunc("123", 123, 123.0) // ^she diff --git a/analysis/tests/src/expected/SignatureHelp.res.txt b/analysis/tests/src/expected/SignatureHelp.res.txt index deafaca07..24959a984 100644 --- a/analysis/tests/src/expected/SignatureHelp.res.txt +++ b/analysis/tests/src/expected/SignatureHelp.res.txt @@ -1,6 +1,6 @@ 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] (...[58:0->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 @@ -55,9 +55,9 @@ extracted params: "activeParameter": 1 } -Signature help src/SignatureHelp.res 25:31 -posCursor:[25:19] posNoWhite:[25:18] Found expr:[25:11->25:30] -Pexp_apply ...[25:11->25:19] (...[25:20->25:23], ~two25:26->25:29=...__ghost__[0:-1->0:-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 @@ -74,30 +74,11 @@ extracted params: "activeParameter": 1 } -Signature help src/SignatureHelp.res 28:33 -posCursor:[28:19] posNoWhite:[28:18] Found expr:[28:11->28:35] -Pexp_apply ...[28:11->28:19] (...[28:20->28:23], ~two28:26->28:29=...[28:30->28:35]) +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: ~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]}, {"label": [25, 39]}, {"label": [43, 63]}, {"label": [67, 85]}, {"label": [89, 93]}], - "documentation": {"kind": "markdown", "value": " Does stuff. "} - }], - "activeSignature": 0, - "activeParameter": 1 -} - -Signature help src/SignatureHelp.res 31:38 -posCursor:[31:19] posNoWhite:[31:18] Found expr:[31:11->31:42] -Pexp_apply ...[31:11->31:19] (...[31:20->31:23], ~two31:26->31:29=...[31:30->31:35], ~four31:38->31:42=...[31:38->31:42]) -posCursor:[31:19] posNoWhite:[31:18] Found expr:[31:11->31:19] -Pexp_ident someFunc:[31:11->31:19] argAtCursor: ~four extracted params: [( @@ -112,11 +93,11 @@ extracted params: "activeParameter": 3 } -Signature help src/SignatureHelp.res 34:42 -posCursor:[34:19] posNoWhite:[34:18] Found expr:[34:11->34:44] -Pexp_apply ...[34:11->34:19] (...[34:20->34:23], ~two34:26->34:29=...[34:30->34:35], ~four34:38->34:42=...[34:43->34:44]) -posCursor:[34:19] posNoWhite:[34:18] Found expr:[34:11->34:19] -Pexp_ident someFunc:[34:11->34:19] +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: [( @@ -131,11 +112,11 @@ extracted params: "activeParameter": 3 } -Signature help src/SignatureHelp.res 37:21 -posCursor:[37:20] posNoWhite:[37:19] Found expr:[37:11->37:21] -Pexp_apply ...[37:11->37:20] (...[58:0->37:21]) -posCursor:[37:20] posNoWhite:[37:19] Found expr:[37:11->37:20] -Pexp_ident otherFunc:[37:11->37:20] +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] @@ -148,11 +129,11 @@ extracted params: "activeParameter": -1 } -Signature help src/SignatureHelp.res 40:24 -posCursor:[40:20] posNoWhite:[40:19] Found expr:[40:11->40:26] -Pexp_apply ...[40:11->40:20] (...[40:21->40:26]) -posCursor:[40:20] posNoWhite:[40:19] Found expr:[40:11->40:20] -Pexp_ident otherFunc:[40:11->40:20] +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] @@ -165,62 +146,11 @@ extracted params: "activeParameter": 0 } -Signature help src/SignatureHelp.res 43:28 -posCursor:[43:20] posNoWhite:[43:19] Found expr:[43:11->43:27] -Pexp_apply ...[43:11->43:20] (...[43:21->43:26]) -posCursor:[43:20] posNoWhite:[43:19] Found expr:[43:11->43:20] -Pexp_ident otherFunc:[43:11->43:20] -argAtCursor: none -extracted params: -[(string, int, float] -{ - "signatures": [{ - "label": "let otherFunc: (string, int, float) => unit", - "parameters": [{"label": [15, 22]}, {"label": [24, 27]}, {"label": [29, 34]}] - }], - "activeSignature": 0, - "activeParameter": -1 -} - -Signature help src/SignatureHelp.res 46:30 -posCursor:[46:20] posNoWhite:[46:19] Found expr:[46:11->46:31] -Pexp_apply ...[46:11->46:20] (...[46:21->46:26], ...[46:28->46:31]) -posCursor:[46:20] posNoWhite:[46:19] Found expr:[46:11->46:20] -Pexp_ident otherFunc:[46:11->46:20] -argAtCursor: unlabelled<1> -extracted params: -[(string, int, float] -{ - "signatures": [{ - "label": "let otherFunc: (string, int, float) => unit", - "parameters": [{"label": [15, 22]}, {"label": [24, 27]}, {"label": [29, 34]}] - }], - "activeSignature": 0, - "activeParameter": 1 -} - -Signature help src/SignatureHelp.res 49:33 -posCursor:[49:20] posNoWhite:[49:19] Found expr:[49:11->49:32] -Pexp_apply ...[49:11->49:20] (...[49:21->49:26], ...[49:28->49:31]) -posCursor:[49:20] posNoWhite:[49:19] Found expr:[49:11->49:20] -Pexp_ident otherFunc:[49:11->49:20] -argAtCursor: none -extracted params: -[(string, int, float] -{ - "signatures": [{ - "label": "let otherFunc: (string, int, float) => unit", - "parameters": [{"label": [15, 22]}, {"label": [24, 27]}, {"label": [29, 34]}] - }], - "activeSignature": 0, - "activeParameter": -1 -} - -Signature help src/SignatureHelp.res 52:35 -posCursor:[52:20] posNoWhite:[52:19] Found expr:[52:11->52:39] -Pexp_apply ...[52:11->52:20] (...[52:21->52:26], ...[52:28->52:31], ...[52:33->52:38]) -posCursor:[52:20] posNoWhite:[52:19] Found expr:[52:11->52:20] -Pexp_ident otherFunc:[52:11->52:20] +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] @@ -233,11 +163,11 @@ extracted params: "activeParameter": 2 } -Signature help src/SignatureHelp.res 55:33 -posCursor:[55:29] posNoWhite:[55:28] Found expr:[55:11->55:34] -Pexp_apply ...[55:11->55:29] (~age55:31->55:34=...[55:31->55:34]) -posCursor:[55:29] posNoWhite:[55:28] Found expr:[55:11->55:29] -Pexp_ident Completion.Lib.foo:[55:11->55:29] +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] @@ -250,11 +180,11 @@ extracted params: "activeParameter": 0 } -Signature help src/SignatureHelp.res 62:24 -posCursor:[62:23] posNoWhite:[62:22] Found expr:[62:11->62:24] -Pexp_apply ...[62:11->62:23] (...[65:0->62:24]) -posCursor:[62:23] posNoWhite:[62:22] Found expr:[62:11->62:23] -Pexp_ident iAmSoSpecial:[62:11->62:23] +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] From fc6afe31d23f669b6e293b1e927f39ee25326115 Mon Sep 17 00:00:00 2001 From: Gabriel Nordeborn Date: Mon, 19 Sep 2022 08:32:30 +0200 Subject: [PATCH 12/19] move extractExpApplyArgs to SharedTypes for real --- analysis/src/CompletionFrontEnd.ml | 41 ------------------------------ analysis/src/SignatureHelp.ml | 19 ++++++-------- 2 files changed, 8 insertions(+), 52 deletions(-) diff --git a/analysis/src/CompletionFrontEnd.ml b/analysis/src/CompletionFrontEnd.ml index 5c55baf62..44ae8bf89 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 diff --git a/analysis/src/SignatureHelp.ml b/analysis/src/SignatureHelp.ml index 390ccd4a5..18b68ee7e 100644 --- a/analysis/src/SignatureHelp.ml +++ b/analysis/src/SignatureHelp.ml @@ -1,3 +1,4 @@ +open SharedTypes type cursorAtArg = Unlabelled of int | Labelled of string let signatureHelp ~path ~pos ~currentFile ~debug = @@ -12,15 +13,14 @@ let signatureHelp ~path ~pos ~currentFile ~debug = pexp_loc; } when pexp_loc - |> SharedTypes.CursorPosition.classifyLoc ~pos:posBeforeCursor + |> CursorPosition.classifyLoc ~pos:posBeforeCursor == HasCursor -> - (* TODO: Move extractExpApplyArgs to a shared module that doesn't look like it's only for completion. *) - let extractedArgs = CompletionFrontEnd.extractExpApplyArgs ~args in + let extractedArgs = extractExpApplyArgs ~args in let argAtCursor = let unlabelledArgCount = ref 0 in extractedArgs |> List.find_map (fun arg -> - match arg.CompletionFrontEnd.label with + match arg.label with | None -> let currentUnlabelledArgCount = !unlabelledArgCount in unlabelledArgCount := currentUnlabelledArgCount + 1; @@ -31,9 +31,7 @@ let signatureHelp ~path ~pos ~currentFile ~debug = | Some {name; posStart; posEnd} -> ( (* Check for the label identifier itself having the cursor *) match - pos - |> SharedTypes.CursorPosition.classifyPositions ~posStart - ~posEnd + pos |> CursorPosition.classifyPositions ~posStart ~posEnd with | HasCursor -> Some (Labelled name) | NoCursor | EmptyLoc -> ( @@ -44,8 +42,7 @@ let signatureHelp ~path ~pos ~currentFile ~debug = match ( arg.exp.pexp_desc, arg.exp.pexp_loc - |> SharedTypes.CursorPosition.classifyLoc - ~pos:posBeforeCursor ) + |> CursorPosition.classifyLoc ~pos:posBeforeCursor ) with | Pexp_extension ({txt = "rescript.exprhole"}, _), _ | _, HasCursor -> @@ -82,7 +79,7 @@ let signatureHelp ~path ~pos ~currentFile ~debug = match Cmt.fullFromPath ~path with | None -> ([], None, None) | Some {file; package} -> - let env = SharedTypes.QueryEnv.fromFile file in + let env = QueryEnv.fromFile file in ( completable |> CompletionBackEnd.processCompletable ~debug ~package ~pos ~scope ~env ~forHover:true, @@ -161,7 +158,7 @@ let signatureHelp ~path ~pos ~currentFile ~debug = (parameters |> List.map (fun (start, end_) -> String.sub label start (end_ - start)) - |> SharedTypes.list); + |> list); (* Figure out the active parameter *) let activeParameter = From 8c237f225937c88854a0d1eba771bcbed63b8ce8 Mon Sep 17 00:00:00 2001 From: Gabriel Nordeborn Date: Mon, 19 Sep 2022 08:36:18 +0200 Subject: [PATCH 13/19] shared function --- analysis/src/CompletionFrontEnd.ml | 2 +- analysis/src/Pos.ml | 2 ++ analysis/src/SignatureHelp.ml | 2 +- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/analysis/src/CompletionFrontEnd.ml b/analysis/src/CompletionFrontEnd.ml index 44ae8bf89..bce92250b 100644 --- a/analysis/src/CompletionFrontEnd.ml +++ b/analysis/src/CompletionFrontEnd.ml @@ -163,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/SignatureHelp.ml b/analysis/src/SignatureHelp.ml index 18b68ee7e..bac6a88b4 100644 --- a/analysis/src/SignatureHelp.ml +++ b/analysis/src/SignatureHelp.ml @@ -2,7 +2,7 @@ open SharedTypes type cursorAtArg = Unlabelled of int | Labelled of string let signatureHelp ~path ~pos ~currentFile ~debug = - let posBeforeCursor = (fst pos, max 0 (snd pos - 1)) in + let posBeforeCursor = Pos.posBeforeCursor pos in let foundFunctionApplicationExpr = ref None in let setFound r = foundFunctionApplicationExpr := Some r in let expr (iterator : Ast_iterator.iterator) (expr : Parsetree.expression) = From d2190f830f99c7ed8b54d732c98841fc8095e03d Mon Sep 17 00:00:00 2001 From: Gabriel Nordeborn Date: Mon, 19 Sep 2022 09:40:09 +0200 Subject: [PATCH 14/19] add way to parse via raw source string in parser, and replace current temp file mechanism for parsing signature help type label --- analysis/src/Files.ml | 6 --- analysis/src/SignatureHelp.ml | 8 +--- .../vendor/res_outcome_printer/res_driver.ml | 38 +++++++++++++++++++ .../vendor/res_outcome_printer/res_driver.mli | 12 ++++++ 4 files changed, 52 insertions(+), 12 deletions(-) diff --git a/analysis/src/Files.ml b/analysis/src/Files.ml index 055a8f4b2..9ab016da5 100644 --- a/analysis/src/Files.ml +++ b/analysis/src/Files.ml @@ -102,9 +102,3 @@ let classifySourceFile path = if Filename.check_suffix path ".res" && exists path then Res else if Filename.check_suffix path ".resi" && exists path then Resi else Other - -let writeTempFile ~prefix ~suffix contents = - let tempFile, outChannel = Filename.open_temp_file prefix suffix in - Printf.fprintf outChannel "%s" contents; - close_out outChannel; - tempFile diff --git a/analysis/src/SignatureHelp.ml b/analysis/src/SignatureHelp.ml index bac6a88b4..cd1de91c1 100644 --- a/analysis/src/SignatureHelp.ml +++ b/analysis/src/SignatureHelp.ml @@ -106,14 +106,10 @@ let signatureHelp ~path ~pos ~currentFile ~debug = let label = "let " ^ name ^ ": " ^ Shared.typeToString type_expr in (* TODO: Refactor the parser to support reading string contents directly in addition to taking a file path. For now, create a temp file, pass it to the parser, and then immediately delete it. *) - let fileWithTypeSignature = - Files.writeTempFile ~prefix:"typ_sig" ~suffix:".resi" label - in - let parser = Res_driver.parsingEngine.parseInterface ~forPrinter:false in let {Res_driver.parsetree = signature} = - parser ~filename:fileWithTypeSignature + Res_driver.parseInterfaceFromSource ~forPrinter:false + ~displayFilename:"" ~source:label in - Sys.remove fileWithTypeSignature; let parameters = match signature with 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 -> From 4df6121496f84e28db8233203413c61359df5fc2 Mon Sep 17 00:00:00 2001 From: Gabriel Nordeborn Date: Mon, 19 Sep 2022 16:03:57 +0200 Subject: [PATCH 15/19] refactor logic some --- analysis/src/SignatureHelp.ml | 204 +++++++++++++++++----------------- 1 file changed, 104 insertions(+), 100 deletions(-) diff --git a/analysis/src/SignatureHelp.ml b/analysis/src/SignatureHelp.ml index cd1de91c1..bd640106d 100644 --- a/analysis/src/SignatureHelp.ml +++ b/analysis/src/SignatureHelp.ml @@ -1,6 +1,106 @@ open SharedTypes type cursorAtArg = Unlabelled of int | Labelled of string +let findFunctionType ~currentFile ~debug ~path ~pos = + let completions, env, package = + let textOpt = Files.readFile currentFile in + match textOpt with + | None | Some "" -> ([], None, 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, None) + | Some (completable, scope) -> ( + match Cmt.fullFromPath ~path with + | None -> ([], None, None) + | Some {file; package} -> + let env = QueryEnv.fromFile file in + ( completable + |> CompletionBackEnd.processCompletable ~debug ~package ~pos ~scope + ~env ~forHover:true, + Some env, + Some package ))) + in + match (completions, env, package) with + | {kind = Value type_expr; name; docstring} :: _, Some env, Some package -> + let args, _ = + CompletionBackEnd.extractFunctionType type_expr ~env ~package + in + Some (args, name, docstring, type_expr) + | _ -> 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 @ [(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 signatureHelp ~path ~pos ~currentFile ~debug = let posBeforeCursor = Pos.posBeforeCursor pos in let foundFunctionApplicationExpr = ref None in @@ -61,36 +161,8 @@ let signatureHelp ~path ~pos ~currentFile ~debug = | 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 - let completions, env, package = - let textOpt = Files.readFile currentFile in - match textOpt with - | None | Some "" -> ([], None, 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. *) - (* TODO: This should probably eventually be factored out to its own thing, like "find type for pos", that feels less like abusing completions. *) - match - CompletionFrontEnd.completionWithParser ~debug ~path ~posCursor:pos - ~currentFile ~text - with - | None -> ([], None, None) - | Some (completable, scope) -> ( - match Cmt.fullFromPath ~path with - | None -> ([], None, None) - | Some {file; package} -> - let env = QueryEnv.fromFile file in - ( completable - |> CompletionBackEnd.processCompletable ~debug ~package ~pos - ~scope ~env ~forHover:true, - Some env, - Some package ))) - in - match (completions, env, package) with - | {kind = Value type_expr; name; docstring} :: _, Some env, Some package -> - let args, _ = - CompletionBackEnd.extractFunctionType type_expr ~env ~package - in + match findFunctionType ~currentFile ~debug ~path ~pos with + | Some (args, name, docstring, type_expr) -> if debug then Printf.printf "argAtCursor: %s\n" (match argAtCursor with @@ -104,51 +176,12 @@ let signatureHelp ~path ~pos ~currentFile ~debug = (* 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 - (* TODO: Refactor the parser to support reading string contents directly in addition to taking a file path. - For now, create a temp file, pass it to the parser, and then immediately delete it. *) let {Res_driver.parsetree = signature} = Res_driver.parseInterfaceFromSource ~forPrinter:false ~displayFilename:"" ~source:label in - let parameters = - 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 @ [(startOffset, endOffset)]) - | _ -> params - in - extractParams expr [] - | _ -> [] - in - + let parameters = extractParameters ~signature ~label in if debug then Printf.printf "extracted params: \n%s\n" (parameters @@ -157,36 +190,7 @@ let signatureHelp ~path ~pos ~currentFile ~debug = |> list); (* Figure out the active parameter *) - let activeParameter = - 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 - | [(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) - in + let activeParameter = findActiveParameter ~argAtCursor ~args in Some { Protocol.signatures = From 093ebefb68440c062e0d71b92b18edd5e245369c Mon Sep 17 00:00:00 2001 From: Gabriel Nordeborn Date: Wed, 21 Sep 2022 20:56:58 +0200 Subject: [PATCH 16/19] refactor hover so type expansion can be shared, and integrate into signature help --- analysis/src/Commands.ml | 5 +- analysis/src/Hover.ml | 195 +++++++----------- analysis/src/Markdown.ml | 23 +++ analysis/src/Protocol.ml | 5 +- analysis/src/Shared.ml | 2 - analysis/src/SignatureHelp.ml | 100 +++++++-- analysis/src/Uri.ml | 34 +++ analysis/src/Uri.mli | 1 + .../tests/src/expected/SignatureHelp.res.txt | 22 +- 9 files changed, 232 insertions(+), 155 deletions(-) create mode 100644 analysis/src/Markdown.ml diff --git a/analysis/src/Commands.ml b/analysis/src/Commands.ml index abc997a6f..344346d93 100644 --- a/analysis/src/Commands.ml +++ b/analysis/src/Commands.ml @@ -52,14 +52,15 @@ let hover ~path ~pos ~currentFile ~debug ~supportsMarkdownLinks = match completions with | {kind = Label typString; docstring} :: _ -> let parts = - (if typString = "" then [] else [Hover.codeBlock typString]) + (if typString = "" then [] else [Markdown.codeBlock typString]) @ docstring in Protocol.stringifyHover (String.concat "\n\n" parts) | _ -> ( match CompletionBackEnd.completionsGetTypeEnv completions with | Some (typ, _env) -> - Protocol.stringifyHover (Hover.codeBlock (Shared.typeToString typ)) + Protocol.stringifyHover + (Markdown.codeBlock (Shared.typeToString typ)) | None -> Protocol.null)) | Some locItem -> ( let isModule = diff --git a/analysis/src/Hover.ml b/analysis/src/Hover.ml index 2d0271dec..f3ac92b48 100644 --- a/analysis/src/Hover.ml +++ b/analysis/src/Hover.ml @@ -1,51 +1,5 @@ open SharedTypes -let codeBlock code = Printf.sprintf "```rescript\n%s\n```" code - -(* 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 - general purpose, rather it's currently only for URL encoding the argument list - passed to command links in markdown. *) -let encodeURIComponent text = - let ln = String.length text in - let buf = Buffer.create ln in - let rec loop i = - if i < ln then ( - (match text.[i] with - | '"' -> Buffer.add_string buf "%22" - | '\'' -> Buffer.add_string buf "%22" - | ':' -> Buffer.add_string buf "%3A" - | ';' -> Buffer.add_string buf "%3B" - | '/' -> Buffer.add_string buf "%2F" - | '\\' -> Buffer.add_string buf "%5C" - | ',' -> Buffer.add_string buf "%2C" - | '&' -> Buffer.add_string buf "%26" - | '[' -> Buffer.add_string buf "%5B" - | ']' -> Buffer.add_string buf "%5D" - | '#' -> Buffer.add_string buf "%23" - | '$' -> Buffer.add_string buf "%24" - | '+' -> Buffer.add_string buf "%2B" - | '=' -> Buffer.add_string buf "%3D" - | '?' -> Buffer.add_string buf "%3F" - | '@' -> Buffer.add_string buf "%40" - | '%' -> Buffer.add_string buf "%25" - | c -> Buffer.add_char buf c); - loop (i + 1)) - in - loop 0; - Buffer.contents buf - -type link = {startPos: Protocol.position; file: string; label: string} - -let linkToCommandArgs link = - Printf.sprintf "[\"%s\",%i,%i]" link.file link.startPos.line - link.startPos.character - -let makeGotoCommand link = - Printf.sprintf "[%s](command:rescript-vscode.go_to_location?%s)" link.label - (encodeURIComponent (linkToCommandArgs link)) - let showModuleTopLevel ~docstring ~name (topLevel : Module.item list) = let contents = topLevel @@ -60,7 +14,9 @@ let showModuleTopLevel ~docstring ~name (topLevel : Module.item list) = (* TODO indent *) |> String.concat "\n" in - let full = codeBlock ("module " ^ name ^ " = {" ^ "\n" ^ contents ^ "\n}") in + let full = + Markdown.codeBlock ("module " ^ name ^ " = {" ^ "\n" ^ contents ^ "\n}") + in let doc = match docstring with | [] -> "" @@ -80,11 +36,62 @@ let rec showModule ~docstring ~(file : File.t) ~name | Some {item = Ident path} -> Some ("Unable to resolve module reference " ^ Path.name path) +type extractedType = { + name: string; + path: Path.t; + decl: Types.type_declaration; + env: SharedTypes.QueryEnv.t; + loc: Warnings.loc; +} + +let findRelevantTypesFromType ~file ~package typ = + (* Expand definitions of types mentioned in typ. + If typ itself is a record or variant, search its body *) + let env = QueryEnv.fromFile file in + let envToSearch, typesToSearch = + match typ |> Shared.digConstructor with + | Some path -> ( + let labelDeclarationsTypes lds = + lds |> List.map (fun (ld : Types.label_declaration) -> ld.ld_type) + in + match References.digConstructor ~env ~package path with + | None -> (env, [typ]) + | Some (env1, {item = {decl}}) -> ( + match decl.type_kind with + | Type_record (lds, _) -> (env1, typ :: (lds |> labelDeclarationsTypes)) + | Type_variant cds -> + ( env1, + cds + |> List.map (fun (cd : Types.constructor_declaration) -> + let fromArgs = + match cd.cd_args with + | Cstr_tuple ts -> ts + | Cstr_record lds -> lds |> labelDeclarationsTypes + in + typ + :: + (match cd.cd_res with + | None -> fromArgs + | Some t -> t :: fromArgs)) + |> List.flatten ) + | _ -> (env, [typ]))) + | None -> (env, [typ]) + in + let fromConstructorPath ~env path = + match References.digConstructor ~env ~package path with + | None -> None + | Some (env, {name = {txt}; extentLoc; item = {decl}}) -> + if Utils.isUncurriedInternal path then None + else Some {name = txt; env; loc = extentLoc; decl; path} + in + let constructors = Shared.findTypeConstructors typesToSearch in + constructors |> List.filter_map (fromConstructorPath ~env:envToSearch) + let newHover ~full:{file; package} ~supportsMarkdownLinks locItem = match locItem.locType with | TypeDefinition (name, decl, _stamp) -> let typeDef = Shared.declToString name decl in - Some (codeBlock typeDef) + Some (Markdown.codeBlock typeDef) | LModule (Definition (stamp, _tip)) | LModule (LocalReference (stamp, _tip)) -> ( match Stamps.findModule file.stamps stamp with @@ -132,7 +139,7 @@ let newHover ~full:{file; package} ~supportsMarkdownLinks locItem = | Typed (_, _, Definition (_, (Field _ | Constructor _))) -> None | Constant t -> Some - (codeBlock + (Markdown.codeBlock (match t with | Const_int _ -> "int" | Const_char _ -> "char" @@ -142,81 +149,25 @@ let newHover ~full:{file; package} ~supportsMarkdownLinks locItem = | Const_int64 _ -> "int64" | Const_nativeint _ -> "int")) | Typed (_, t, locKind) -> - let fromConstructorPath ~env path = - match References.digConstructor ~env ~package path with - | None -> None - | Some (env, {extentLoc; item = {decl}}) -> - if Utils.isUncurriedInternal path then None - else - Some - ( decl - |> Shared.declToString ~printNameAsIs:true - (SharedTypes.pathIdentToString path), - extentLoc, - env ) - in let fromType ~docstring typ = - let typeString = codeBlock (typ |> Shared.typeToString) in + let typeString = Markdown.codeBlock (typ |> Shared.typeToString) in + let types = findRelevantTypesFromType typ ~file ~package in let typeDefinitions = - (* Expand definitions of types mentioned in typ. - If typ itself is a record or variant, search its body *) - let env = QueryEnv.fromFile file in - let envToSearch, typesToSearch = - match typ |> Shared.digConstructor with - | Some path -> ( - let labelDeclarationsTypes lds = - lds |> List.map (fun (ld : Types.label_declaration) -> ld.ld_type) - in - match References.digConstructor ~env ~package path with - | None -> (env, [typ]) - | Some (env1, {item = {decl}}) -> ( - match decl.type_kind with - | Type_record (lds, _) -> - (env1, typ :: (lds |> labelDeclarationsTypes)) - | Type_variant cds -> - ( env1, - cds - |> List.map (fun (cd : Types.constructor_declaration) -> - let fromArgs = - match cd.cd_args with - | Cstr_tuple ts -> ts - | Cstr_record lds -> lds |> labelDeclarationsTypes - in - typ - :: - (match cd.cd_res with - | None -> fromArgs - | Some t -> t :: fromArgs)) - |> List.flatten ) - | _ -> (env, [typ]))) - | None -> (env, [typ]) - in - let constructors = Shared.findTypeConstructors typesToSearch in - constructors - |> List.filter_map (fun constructorPath -> - match - constructorPath |> fromConstructorPath ~env:envToSearch - with - | None -> None - | Some (typString, extentLoc, env) -> - let startLine, startCol = Pos.ofLexing extentLoc.loc_start in - let linkToTypeDefinitionStr = - if supportsMarkdownLinks then - "\nGo to: " - ^ makeGotoCommand - { - label = "Type definition"; - file = Uri.toString env.file.uri; - startPos = {line = startLine; character = startCol}; - } - else "" - in - Some - (Shared.markdownSpacing ^ codeBlock typString - ^ linkToTypeDefinitionStr ^ "\n\n---\n")) + types + |> List.map (fun {decl; env; loc; path} -> + let linkToTypeDefinitionStr = + if supportsMarkdownLinks then + Markdown.goToDefinitionText ~env ~pos:loc.Warnings.loc_start + else "" + in + "\n" ^ Markdown.spacing + ^ Markdown.codeBlock + (decl + |> Shared.declToString ~printNameAsIs:true + (SharedTypes.pathIdentToString path)) + ^ linkToTypeDefinitionStr ^ "\n" ^ Markdown.divider) in - let typeString = typeString :: typeDefinitions |> String.concat "\n\n" in - (typeString, docstring) + (typeString :: typeDefinitions |> String.concat "\n", docstring) in let parts = match References.definedForLoc ~file ~package locKind with @@ -238,7 +189,7 @@ let newHover ~full:{file; package} ~supportsMarkdownLinks locItem = |> List.map (fun (t, _) -> Shared.typeToString t) |> String.concat ", " |> Printf.sprintf "(%s)" in - typeString :: codeBlock (txt ^ argsString) :: docstring + typeString :: Markdown.codeBlock (txt ^ argsString) :: docstring | `Field -> let typeString, docstring = t |> fromType ~docstring in typeString :: docstring) diff --git a/analysis/src/Markdown.ml b/analysis/src/Markdown.ml new file mode 100644 index 000000000..0f82050fb --- /dev/null +++ b/analysis/src/Markdown.ml @@ -0,0 +1,23 @@ +let spacing = "\n```\n \n```\n" +let codeBlock code = Printf.sprintf "```rescript\n%s\n```" code +let divider = "\n---\n" + +type link = {startPos: Protocol.position; file: string; label: string} + +let linkToCommandArgs link = + Printf.sprintf "[\"%s\",%i,%i]" link.file link.startPos.line + link.startPos.character + +let makeGotoCommand link = + Printf.sprintf "[%s](command:rescript-vscode.go_to_location?%s)" link.label + (Uri.encodeURIComponent (linkToCommandArgs link)) + +let goToDefinitionText ~env ~pos = + let startLine, startCol = Pos.ofLexing pos in + "\nGo to: " + ^ makeGotoCommand + { + label = "Type definition"; + file = Uri.toString env.SharedTypes.QueryEnv.file.uri; + startPos = {line = startLine; character = startCol}; + } diff --git a/analysis/src/Protocol.ml b/analysis/src/Protocol.ml index ce3e57075..5a40a7d0c 100644 --- a/analysis/src/Protocol.ml +++ b/analysis/src/Protocol.ml @@ -17,7 +17,7 @@ type inlayHint = { } (* https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#parameterInformation *) -type parameterInformation = {label: int * int} +type parameterInformation = {label: int * int; documentation: markupContent} (* https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#signatureInformation *) type signatureInformation = { @@ -189,9 +189,10 @@ let stringifyCodeLens (codeLens : codeLens) = let stringifyParameterInformation (parameterInformation : parameterInformation) = - Printf.sprintf {|{"label": %s}|} + 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) = diff --git a/analysis/src/Shared.ml b/analysis/src/Shared.ml index af5f3748e..18aac6043 100644 --- a/analysis/src/Shared.ml +++ b/analysis/src/Shared.ml @@ -78,5 +78,3 @@ let typeToString ?lineWidth (t : Types.type_expr) = Hashtbl.replace typeTbl (t.id, t) s; s | Some s -> s - -let markdownSpacing = "\n```\n \n```\n" diff --git a/analysis/src/SignatureHelp.ml b/analysis/src/SignatureHelp.ml index bd640106d..857df3f1c 100644 --- a/analysis/src/SignatureHelp.ml +++ b/analysis/src/SignatureHelp.ml @@ -2,10 +2,10 @@ open SharedTypes type cursorAtArg = Unlabelled of int | Labelled of string let findFunctionType ~currentFile ~debug ~path ~pos = - let completions, env, package = + let completables = let textOpt = Files.readFile currentFile in match textOpt with - | None | Some "" -> ([], None, None) + | 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 @@ -14,24 +14,26 @@ let findFunctionType ~currentFile ~debug ~path ~pos = CompletionFrontEnd.completionWithParser ~debug ~path ~posCursor:pos ~currentFile ~text with - | None -> ([], None, None) + | None -> None | Some (completable, scope) -> ( match Cmt.fullFromPath ~path with - | None -> ([], None, None) + | None -> None | Some {file; package} -> let env = QueryEnv.fromFile file in - ( completable - |> CompletionBackEnd.processCompletable ~debug ~package ~pos ~scope - ~env ~forHover:true, - Some env, - Some package ))) + Some + ( completable + |> CompletionBackEnd.processCompletable ~debug ~package ~pos + ~scope ~env ~forHover:true, + env, + package, + file ))) in - match (completions, env, package) with - | {kind = Value type_expr; name; docstring} :: _, Some env, Some package -> + 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) + Some (args, name, docstring, type_expr, package, env, file) | _ -> None (* Extracts all parameters from a parsed function signature *) @@ -64,7 +66,8 @@ let extractParameters ~signature ~label = | Asttypes.Optional _ -> endOffset + 2 | _ -> endOffset in - extractParams nextFunctionExpr (params @ [(startOffset, endOffset)]) + extractParams nextFunctionExpr + (params @ [(argumentLabel, startOffset, endOffset)]) | _ -> params in extractParams expr [] @@ -101,8 +104,55 @@ let findActiveParameter ~argAtCursor ~args = 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) = @@ -162,7 +212,7 @@ let signatureHelp ~path ~pos ~currentFile ~debug = (* 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) -> + | Some (args, name, docstring, type_expr, package, _env, file) -> if debug then Printf.printf "argAtCursor: %s\n" (match argAtCursor with @@ -185,7 +235,7 @@ let signatureHelp ~path ~pos ~currentFile ~debug = if debug then Printf.printf "extracted params: \n%s\n" (parameters - |> List.map (fun (start, end_) -> + |> List.map (fun (_, start, end_) -> String.sub label start (end_ - start)) |> list); @@ -199,7 +249,25 @@ let signatureHelp ~path ~pos ~currentFile ~debug = label; parameters = parameters - |> List.map (fun params -> {Protocol.label = params}); + |> 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 diff --git a/analysis/src/Uri.ml b/analysis/src/Uri.ml index cbe139d5c..4b1d67f54 100644 --- a/analysis/src/Uri.ml +++ b/analysis/src/Uri.ml @@ -22,3 +22,37 @@ let toTopLevelLoc {path} = {Location.loc_start = topPos; Location.loc_end = topPos; loc_ghost = false} 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 + general purpose, rather it's currently only for URL encoding the argument list + passed to command links in markdown. *) +let encodeURIComponent text = + let ln = String.length text in + let buf = Buffer.create ln in + let rec loop i = + if i < ln then ( + (match text.[i] with + | '"' -> Buffer.add_string buf "%22" + | '\'' -> Buffer.add_string buf "%22" + | ':' -> Buffer.add_string buf "%3A" + | ';' -> Buffer.add_string buf "%3B" + | '/' -> Buffer.add_string buf "%2F" + | '\\' -> Buffer.add_string buf "%5C" + | ',' -> Buffer.add_string buf "%2C" + | '&' -> Buffer.add_string buf "%26" + | '[' -> Buffer.add_string buf "%5B" + | ']' -> Buffer.add_string buf "%5D" + | '#' -> Buffer.add_string buf "%23" + | '$' -> Buffer.add_string buf "%24" + | '+' -> Buffer.add_string buf "%2B" + | '=' -> Buffer.add_string buf "%3D" + | '?' -> Buffer.add_string buf "%3F" + | '@' -> Buffer.add_string buf "%40" + | '%' -> Buffer.add_string buf "%25" + | c -> Buffer.add_char buf c); + loop (i + 1)) + in + loop 0; + Buffer.contents buf diff --git a/analysis/src/Uri.mli b/analysis/src/Uri.mli index 5e8013c06..b6e94692b 100644 --- a/analysis/src/Uri.mli +++ b/analysis/src/Uri.mli @@ -6,3 +6,4 @@ val stripPath : bool ref val toPath : t -> string val toString : t -> string val toTopLevelLoc : t -> Location.t +val encodeURIComponent : string -> string diff --git a/analysis/tests/src/expected/SignatureHelp.res.txt b/analysis/tests/src/expected/SignatureHelp.res.txt index 24959a984..897a350a3 100644 --- a/analysis/tests/src/expected/SignatureHelp.res.txt +++ b/analysis/tests/src/expected/SignatureHelp.res.txt @@ -10,7 +10,7 @@ extracted params: { "signatures": [{ "label": "let someFunc: (\n int,\n ~two: string=?,\n ~three: unit => unit,\n ~four: someVariant,\n unit,\n) => unit", - "parameters": [{"label": [14, 21]}, {"label": [25, 39]}, {"label": [43, 63]}, {"label": [67, 85]}, {"label": [89, 93]}], + "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, @@ -29,7 +29,7 @@ extracted params: { "signatures": [{ "label": "let someFunc: (\n int,\n ~two: string=?,\n ~three: unit => unit,\n ~four: someVariant,\n unit,\n) => unit", - "parameters": [{"label": [14, 21]}, {"label": [25, 39]}, {"label": [43, 63]}, {"label": [67, 85]}, {"label": [89, 93]}], + "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, @@ -48,7 +48,7 @@ extracted params: { "signatures": [{ "label": "let someFunc: (\n int,\n ~two: string=?,\n ~three: unit => unit,\n ~four: someVariant,\n unit,\n) => unit", - "parameters": [{"label": [14, 21]}, {"label": [25, 39]}, {"label": [43, 63]}, {"label": [67, 85]}, {"label": [89, 93]}], + "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, @@ -67,7 +67,7 @@ extracted params: { "signatures": [{ "label": "let someFunc: (\n int,\n ~two: string=?,\n ~three: unit => unit,\n ~four: someVariant,\n unit,\n) => unit", - "parameters": [{"label": [14, 21]}, {"label": [25, 39]}, {"label": [43, 63]}, {"label": [67, 85]}, {"label": [89, 93]}], + "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, @@ -86,7 +86,7 @@ extracted params: { "signatures": [{ "label": "let someFunc: (\n int,\n ~two: string=?,\n ~three: unit => unit,\n ~four: someVariant,\n unit,\n) => unit", - "parameters": [{"label": [14, 21]}, {"label": [25, 39]}, {"label": [43, 63]}, {"label": [67, 85]}, {"label": [89, 93]}], + "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, @@ -105,7 +105,7 @@ extracted params: { "signatures": [{ "label": "let someFunc: (\n int,\n ~two: string=?,\n ~three: unit => unit,\n ~four: someVariant,\n unit,\n) => unit", - "parameters": [{"label": [14, 21]}, {"label": [25, 39]}, {"label": [43, 63]}, {"label": [67, 85]}, {"label": [89, 93]}], + "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, @@ -123,7 +123,7 @@ extracted params: { "signatures": [{ "label": "let otherFunc: (string, int, float) => unit", - "parameters": [{"label": [15, 22]}, {"label": [24, 27]}, {"label": [29, 34]}] + "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 @@ -140,7 +140,7 @@ extracted params: { "signatures": [{ "label": "let otherFunc: (string, int, float) => unit", - "parameters": [{"label": [15, 22]}, {"label": [24, 27]}, {"label": [29, 34]}] + "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 @@ -157,7 +157,7 @@ extracted params: { "signatures": [{ "label": "let otherFunc: (string, int, float) => unit", - "parameters": [{"label": [15, 22]}, {"label": [24, 27]}, {"label": [29, 34]}] + "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 @@ -174,7 +174,7 @@ extracted params: { "signatures": [{ "label": "let foo: (~age: int, ~name: string) => string", - "parameters": [{"label": [9, 19]}, {"label": [21, 34]}] + "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 @@ -191,7 +191,7 @@ extracted params: { "signatures": [{ "label": "let iAmSoSpecial: string => unit", - "parameters": [{"label": [18, 24]}] + "parameters": [{"label": [18, 24], "documentation": {"kind": "markdown", "value": "```rescript\nstring\n```"}}] }], "activeSignature": 0, "activeParameter": 0 From f02ebefedc59087d0c315d94a80afdaded8efae2 Mon Sep 17 00:00:00 2001 From: Gabriel Nordeborn Date: Thu, 22 Sep 2022 10:23:19 +0200 Subject: [PATCH 17/19] gate new experimental signature help behind configuration option --- README.md | 1 + client/src/extension.ts | 19 ++++++++++--------- package.json | 5 +++++ server/src/server.ts | 16 ++++++++++++---- 4 files changed, 28 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 37ad6e6eb..66c007084 100644 --- a/README.md +++ b/README.md @@ -110,6 +110,7 @@ You'll find all ReScript specific settings under the scope `rescript.settings`. | 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` | +| 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/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 1d6d7fc64..92c79d670 100644 --- a/server/src/server.ts +++ b/server/src/server.ts @@ -34,6 +34,9 @@ interface extensionConfiguration { }; codeLens: boolean; binaryPath: string | null; + signatureHelp: { + enable: boolean; + }; } // This holds client capabilities specific to our extension, and not necessarily @@ -55,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; @@ -1152,10 +1158,12 @@ function onMessage(msg: p.Message) { workDoneProgress: false, } : undefined, - signatureHelpProvider: { - triggerCharacters: ["("], - retriggerCharacters: ["=", ","], - }, + signatureHelpProvider: extensionConfiguration.signatureHelp?.enable + ? { + triggerCharacters: ["("], + retriggerCharacters: ["=", ","], + } + : undefined, }, }; let response: p.ResponseMessage = { From c9fe0347ee106bb580131592c6a5a44fd0b5fd23 Mon Sep 17 00:00:00 2001 From: Gabriel Nordeborn Date: Thu, 22 Sep 2022 12:38:35 +0200 Subject: [PATCH 18/19] comment --- analysis/src/SignatureHelp.ml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/analysis/src/SignatureHelp.ml b/analysis/src/SignatureHelp.ml index 857df3f1c..fdefee2a1 100644 --- a/analysis/src/SignatureHelp.ml +++ b/analysis/src/SignatureHelp.ml @@ -221,7 +221,7 @@ let signatureHelp ~path ~pos ~currentFile ~debug = | 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'll leverage the parser to figure the offsets out by parsing the label, and extract the + 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. *) From 0368f1d3906c130f093bd0226ec9a7231f6bfc7e Mon Sep 17 00:00:00 2001 From: Gabriel Nordeborn Date: Thu, 22 Sep 2022 12:43:42 +0200 Subject: [PATCH 19/19] changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) 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