diff --git a/CHANGELOG.md b/CHANGELOG.md index 5bb7cd495..f227d4717 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,7 +14,9 @@ #### :rocket: New Feature -- Inlay Hints (experimetal). `rescript.settings.inlayHints.enable: true` +- 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. + ## v1.4.2 #### :bug: Bug Fix diff --git a/README.md b/README.md index ed047c0d0..d51428d8a 100644 --- a/README.md +++ b/README.md @@ -117,6 +117,15 @@ rescript.settings.inlayHints.enable: true rescript.settings.inlayHints.maxLength: 25 ``` +### Code Lens (experimental) + +This tells the editor to add code lenses to function definitions, showing its full type above the definition. + +```jsonc +// Enable (experimental) code lens. +rescript.settings.codeLens: true +``` + ### Hide generated files You can configure VSCode to collapse the JavaScript files ReScript generates under its source ReScript file. This will "hide" the generated files in the VSCode file explorer, but still leaving them accessible by expanding the source ReScript file they belong to. diff --git a/analysis/src/Cli.ml b/analysis/src/Cli.ml index 9acc01417..90b82b1c8 100644 --- a/analysis/src/Cli.ml +++ b/analysis/src/Cli.ml @@ -11,7 +11,8 @@ API examples: ./rescript-editor-analysis.exe references src/MyFile.res 10 2 ./rescript-editor-analysis.exe rename src/MyFile.res 10 2 foo ./rescript-editor-analysis.exe diagnosticSyntax src/MyFile.res - /rescript-editor-analysis.exe inlayHint src/MyFile.res 0 3 25 + ./rescript-editor-analysis.exe inlayHint src/MyFile.res 0 3 25 + ./rescript-editor-analysis.exe codeLens src/MyFile.res Dev-time examples: ./rescript-editor-analysis.exe dump src/MyFile.res src/MyFile2.res @@ -70,6 +71,10 @@ Options: ./rescript-editor-analysis.exe inlayHint src/MyFile.res 0 3 25 + codeLens: get all code lens entries for file src/MyFile.res + + ./rescript-editor-analysis.exe codeLens src/MyFile.res + test: run tests specified by special comments in file src/MyFile.res ./rescript-editor-analysis.exe test src/src/MyFile.res @@ -98,6 +103,7 @@ let main () = Commands.inlayhint ~path ~pos:(int_of_string line_start, int_of_string line_end) ~maxLength ~debug:false + | [_; "codeLens"; path] -> Commands.codeLens ~path ~debug:false | [_; "codeAction"; path; line; col; currentFile] -> Commands.codeAction ~path ~pos:(int_of_string line, int_of_string col) diff --git a/analysis/src/Commands.ml b/analysis/src/Commands.ml index 3dd16c468..ad9ad4a89 100644 --- a/analysis/src/Commands.ml +++ b/analysis/src/Commands.ml @@ -32,6 +32,10 @@ let inlayhint ~path ~pos ~maxLength ~debug = let result = Hint.inlay ~path ~pos ~maxLength ~debug |> Protocol.array in print_endline result +let codeLens ~path ~debug = + let result = Hint.codeLens ~path ~debug |> Protocol.array in + print_endline result + let hover ~path ~pos ~currentFile ~debug = let result = match Cmt.fullFromPath ~path with @@ -391,6 +395,9 @@ let test ~path = 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) + | "cle" -> + print_endline ("Code Lens " ^ path); + codeLens ~path ~debug:false | _ -> ()); print_newline ()) in diff --git a/analysis/src/Hint.ml b/analysis/src/Hint.ml index c3d8a9682..0f90ff8ea 100644 --- a/analysis/src/Hint.ml +++ b/analysis/src/Hint.ml @@ -119,4 +119,60 @@ let inlay ~path ~pos ~maxLength ~debug = | Some value -> if String.length label > value then None else Some result | None -> Some result) - | None -> None))) \ No newline at end of file + | None -> None))) + +let codeLens ~path ~debug = + let lenses = ref [] in + let push loc = + let range = Utils.cmtLocToRange loc in + lenses := range :: !lenses + in + (* Code lenses are only emitted for functions right now. So look for value bindings that are functions, + and use the loc of the value binding itself so we can look up the full function type for our code lens. *) + let value_binding (iterator : Ast_iterator.iterator) + (vb : Parsetree.value_binding) = + (match vb with + | { + pvb_pat = {ppat_desc = Ppat_var _; ppat_loc}; + pvb_expr = {pexp_desc = Pexp_fun _}; + } -> + push ppat_loc + | _ -> ()); + Ast_iterator.default_iterator.value_binding iterator vb + in + let iterator = {Ast_iterator.default_iterator with value_binding} in + (* We only print code lenses in implementation files. This is because they'd be redundant in interface files, + where the definition itself will be the same thing as what would've been printed in the code lens. *) + (if Filename.check_suffix path ".res" then + let parser = + Res_driver.parsingEngine.parseImplementation ~forPrinter:false + in + let {Res_driver.parsetree = structure} = parser ~filename:path in + iterator.structure iterator structure |> ignore); + !lenses + |> List.filter_map (fun (range : Protocol.range) -> + match Cmt.fullFromPath ~path with + | None -> None + | Some full -> ( + match + References.getLocItem ~full + ~pos:(range.start.line, range.start.character + 1) + ~debug + with + | Some {locType = Typed (_, typeExpr, _)} -> + Some + (Protocol.stringifyCodeLens + { + range; + command = + Some + { + (* Code lenses can run commands. An empty command string means we just want the editor + to print the text, not link to running a command. *) + command = ""; + (* Print the type with a huge line width, because the code lens always prints on a + single line in the editor. *) + title = typeExpr |> Shared.typeToString ~lineWidth:400; + }; + }) + | _ -> None)) diff --git a/analysis/src/PrintType.ml b/analysis/src/PrintType.ml index b146430fb..3da8293c0 100644 --- a/analysis/src/PrintType.ml +++ b/analysis/src/PrintType.ml @@ -1,6 +1,6 @@ -let printExpr typ = +let printExpr ?(lineWidth = 60) typ = Printtyp.reset_names (); - Res_doc.toString ~width:60 + Res_doc.toString ~width:lineWidth (Res_outcome_printer.printOutTypeDoc (Printtyp.tree_of_typexp false typ)) let printDecl ~recStatus name decl = diff --git a/analysis/src/Protocol.ml b/analysis/src/Protocol.ml index 6838a29ca..a1440aa85 100644 --- a/analysis/src/Protocol.ml +++ b/analysis/src/Protocol.ml @@ -1,6 +1,13 @@ type position = {line: int; character: int} type range = {start: position; end_: position} type markupContent = {kind: string; value: string} + +(* https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#command *) +type command = {title: string; command: string} + +(* https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#codeLens *) +type codeLens = {range: range; command: command option} + type inlayHint = { position: position; label: string; @@ -147,6 +154,22 @@ let stringifyHint hint = (stringifyPosition hint.position) (Json.escape hint.label) hint.kind hint.paddingLeft hint.paddingRight +let stringifyCommand (command : command) = + Printf.sprintf {|{"title": "%s", "command": "%s"}|} + (Json.escape command.title) + (Json.escape command.command) + +let stringifyCodeLens (codeLens : codeLens) = + Printf.sprintf + {|{ + "range": %s, + "command": %s + }|} + (stringifyRange codeLens.range) + (match codeLens.command with + | None -> "" + | Some command -> stringifyCommand command) + (* https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#diagnostic *) let stringifyDiagnostic d = Printf.sprintf diff --git a/analysis/src/Shared.ml b/analysis/src/Shared.ml index 21b84a125..467238629 100644 --- a/analysis/src/Shared.ml +++ b/analysis/src/Shared.ml @@ -49,12 +49,12 @@ let declToString ?(recStatus = Types.Trec_not) name t = let cacheTypeToString = ref false let typeTbl = Hashtbl.create 1 -let typeToString (t : Types.type_expr) = +let typeToString ?lineWidth (t : Types.type_expr) = match if !cacheTypeToString then Hashtbl.find_opt typeTbl (t.id, t) else None with | None -> - let s = PrintType.printExpr t in + let s = PrintType.printExpr ?lineWidth t in Hashtbl.replace typeTbl (t.id, t) s; s | Some s -> s diff --git a/analysis/tests/src/CodeLens.res b/analysis/tests/src/CodeLens.res new file mode 100644 index 000000000..a63bed6df --- /dev/null +++ b/analysis/tests/src/CodeLens.res @@ -0,0 +1,11 @@ +let add = (x, y) => x + y + +let foo = (~age, ~name) => name ++ string_of_int(age) + +let ff = (~opt1=0, ~a, ~b, (), ~opt2=0, (), ~c) => a + b + c + opt1 + opt2 + +let compFF = Completion.ff + +@react.component +let make = (~name) => React.string(name) +//^cle diff --git a/analysis/tests/src/expected/CodeLens.res.txt b/analysis/tests/src/expected/CodeLens.res.txt new file mode 100644 index 000000000..06472d5e4 --- /dev/null +++ b/analysis/tests/src/expected/CodeLens.res.txt @@ -0,0 +1,15 @@ +Code Lens src/CodeLens.res +[{ + "range": {"start": {"line": 9, "character": 4}, "end": {"line": 9, "character": 8}}, + "command": {"title": "{\"name\": string} => React.element", "command": ""} + }, { + "range": {"start": {"line": 4, "character": 4}, "end": {"line": 4, "character": 6}}, + "command": {"title": "(~opt1: int=?, ~a: int, ~b: int, unit, ~opt2: int=?, unit, ~c: int) => int", "command": ""} + }, { + "range": {"start": {"line": 2, "character": 4}, "end": {"line": 2, "character": 7}}, + "command": {"title": "(~age: int, ~name: string) => string", "command": ""} + }, { + "range": {"start": {"line": 0, "character": 4}, "end": {"line": 0, "character": 7}}, + "command": {"title": "(int, int) => int", "command": ""} + }] + diff --git a/analysis/tests/src/expected/Dce.res.txt b/analysis/tests/src/expected/Dce.res.txt index 441bb5807..9e47ef48c 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:243 +issues:249 diff --git a/analysis/tests/src/expected/Debug.res.txt b/analysis/tests/src/expected/Debug.res.txt index ff23fe834..a95597eae 100644 --- a/analysis/tests/src/expected/Debug.res.txt +++ b/analysis/tests/src/expected/Debug.res.txt @@ -4,8 +4,9 @@ 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/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/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 Impl cmt:./lib/bs/src/CompletePrioritize2.cmt res:./src/CompletePrioritize2.res Impl cmt:./lib/bs/src/Completion.cmt res:./src/Completion.res diff --git a/client/src/extension.ts b/client/src/extension.ts index 95d3387de..7bd85b918 100644 --- a/client/src/extension.ts +++ b/client/src/extension.ts @@ -240,7 +240,15 @@ export function activate(context: ExtensionContext) { // language client, and because of that requires a full restart. context.subscriptions.push( workspace.onDidChangeConfiguration(({ affectsConfiguration }) => { - if (affectsConfiguration("rescript.settings.inlayHints")) { + // 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 + // on the fly, and require a full restart with new capabilities set when + // initializing. + if ( + affectsConfiguration("rescript.settings.inlayHints") || + affectsConfiguration("rescript.settings.codeLens") + ) { commands.executeCommand("rescript-vscode.restart_language_server"); } }) diff --git a/package.json b/package.json index f24f89a35..181499112 100644 --- a/package.json +++ b/package.json @@ -146,6 +146,11 @@ ], "minimum": 0 }, + "rescript.settings.codeLens.enable": { + "type": "boolean", + "default": false, + "description": "Enable (experimental) code lens for function definitions." + }, "rescript.settings.binaryPath": { "type": ["string", "null"], "default": null, diff --git a/server/src/server.ts b/server/src/server.ts index 542d1010b..072195bf4 100644 --- a/server/src/server.ts +++ b/server/src/server.ts @@ -13,6 +13,7 @@ import { DidChangeConfigurationNotification, InitializeParams, InlayHintParams, + CodeLensParams, } from "vscode-languageserver-protocol"; import * as utils from "./utils"; import * as codeActions from "./codeActions"; @@ -30,14 +31,19 @@ interface extensionConfiguration { enable: boolean; maxLength: number | null; }; + codeLens: boolean; binaryPath: string | null; } + +// All values here are temporary, and will be overridden as the server is +// initialized, and the current config is received from the client. let extensionConfiguration: extensionConfiguration = { askToStartBuild: true, inlayHints: { enable: false, maxLength: 25 }, + codeLens: false, binaryPath: null, }; let pullConfigurationPeriodically: NodeJS.Timeout | null = null; @@ -229,6 +235,9 @@ let compilerLogsWatcher = chokidar if (extensionConfiguration.inlayHints.enable === true) { sendInlayHintsRefresh(); } + if (extensionConfiguration.codeLens === true) { + sendCodeLensRefresh(); + } }); let stopWatchingCompilerLog = () => { // TODO: cleanup of compilerLogs? @@ -411,6 +420,27 @@ function sendInlayHintsRefresh() { send(request); } +function codeLens(msg: p.RequestMessage) { + const params = msg.params as p.CodeLensParams; + const filePath = fileURLToPath(params.textDocument.uri); + + const response = utils.runAnalysisCommand( + filePath, + ["codeLens", filePath], + msg + ); + return response; +} + +function sendCodeLensRefresh() { + let request: p.RequestMessage = { + jsonrpc: c.jsonrpcVersion, + method: p.CodeLensRefreshRequest.method, + id: serverSentRequestIdCounter++, + }; + send(request); +} + function definition(msg: p.RequestMessage) { // https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_definition let params = msg.params as p.DefinitionParams; @@ -1001,6 +1031,11 @@ function onMessage(msg: p.Message) { full: true, }, inlayHintProvider: extensionConfiguration.inlayHints.enable, + codeLensProvider: extensionConfiguration.codeLens + ? { + workDoneProgress: false, + } + : undefined, }, }; let response: p.ResponseMessage = { @@ -1086,6 +1121,12 @@ function onMessage(msg: p.Message) { if (extName === c.resExt) { send(inlayHint(msg)); } + } else if (msg.method === p.CodeLensRequest.method) { + let params = msg.params as CodeLensParams; + let extName = path.extname(params.textDocument.uri); + if (extName === c.resExt) { + send(codeLens(msg)); + } } else { let response: p.ResponseMessage = { jsonrpc: c.jsonrpcVersion,