Skip to content

Commit e9756ee

Browse files
authored
Merge pull request #513 from rescript-lang/functions-codelens
Code Lens
2 parents 35ac5f2 + 980beeb commit e9756ee

15 files changed

+194
-10
lines changed

CHANGELOG.md

+3-1
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,9 @@
1414

1515
#### :rocket: New Feature
1616

17-
- Inlay Hints (experimetal). `rescript.settings.inlayHints.enable: true`
17+
- Inlay Hints (experimetal). `rescript.settings.inlayHints.enable: true`. Turned off by default.
18+
- Code Lenses for functions (experimetal). `rescript.settings.codeLens: true`. Turned off by default.
19+
1820
## v1.4.2
1921

2022
#### :bug: Bug Fix

README.md

+9
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,15 @@ rescript.settings.inlayHints.enable: true
117117
rescript.settings.inlayHints.maxLength: 25
118118
```
119119

120+
### Code Lens (experimental)
121+
122+
This tells the editor to add code lenses to function definitions, showing its full type above the definition.
123+
124+
```jsonc
125+
// Enable (experimental) code lens.
126+
rescript.settings.codeLens: true
127+
```
128+
120129
### Hide generated files
121130

122131
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.

analysis/src/Cli.ml

+7-1
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@ API examples:
1111
./rescript-editor-analysis.exe references src/MyFile.res 10 2
1212
./rescript-editor-analysis.exe rename src/MyFile.res 10 2 foo
1313
./rescript-editor-analysis.exe diagnosticSyntax src/MyFile.res
14-
/rescript-editor-analysis.exe inlayHint src/MyFile.res 0 3 25
14+
./rescript-editor-analysis.exe inlayHint src/MyFile.res 0 3 25
15+
./rescript-editor-analysis.exe codeLens src/MyFile.res
1516

1617
Dev-time examples:
1718
./rescript-editor-analysis.exe dump src/MyFile.res src/MyFile2.res
@@ -70,6 +71,10 @@ Options:
7071

7172
./rescript-editor-analysis.exe inlayHint src/MyFile.res 0 3 25
7273

74+
codeLens: get all code lens entries for file src/MyFile.res
75+
76+
./rescript-editor-analysis.exe codeLens src/MyFile.res
77+
7378
test: run tests specified by special comments in file src/MyFile.res
7479

7580
./rescript-editor-analysis.exe test src/src/MyFile.res
@@ -98,6 +103,7 @@ let main () =
98103
Commands.inlayhint ~path
99104
~pos:(int_of_string line_start, int_of_string line_end)
100105
~maxLength ~debug:false
106+
| [_; "codeLens"; path] -> Commands.codeLens ~path ~debug:false
101107
| [_; "codeAction"; path; line; col; currentFile] ->
102108
Commands.codeAction ~path
103109
~pos:(int_of_string line, int_of_string col)

analysis/src/Commands.ml

+7
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,10 @@ let inlayhint ~path ~pos ~maxLength ~debug =
3232
let result = Hint.inlay ~path ~pos ~maxLength ~debug |> Protocol.array in
3333
print_endline result
3434

35+
let codeLens ~path ~debug =
36+
let result = Hint.codeLens ~path ~debug |> Protocol.array in
37+
print_endline result
38+
3539
let hover ~path ~pos ~currentFile ~debug =
3640
let result =
3741
match Cmt.fullFromPath ~path with
@@ -391,6 +395,9 @@ let test ~path =
391395
let line_end = 6 in
392396
print_endline ("Inlay Hint " ^ path ^ " " ^ string_of_int line_start ^ ":" ^ string_of_int line_end);
393397
inlayhint ~path ~pos:(line_start, line_end) ~maxLength:"25" ~debug:false)
398+
| "cle" ->
399+
print_endline ("Code Lens " ^ path);
400+
codeLens ~path ~debug:false
394401
| _ -> ());
395402
print_newline ())
396403
in

analysis/src/Hint.ml

+57-1
Original file line numberDiff line numberDiff line change
@@ -119,4 +119,60 @@ let inlay ~path ~pos ~maxLength ~debug =
119119
| Some value ->
120120
if String.length label > value then None else Some result
121121
| None -> Some result)
122-
| None -> None)))
122+
| None -> None)))
123+
124+
let codeLens ~path ~debug =
125+
let lenses = ref [] in
126+
let push loc =
127+
let range = Utils.cmtLocToRange loc in
128+
lenses := range :: !lenses
129+
in
130+
(* Code lenses are only emitted for functions right now. So look for value bindings that are functions,
131+
and use the loc of the value binding itself so we can look up the full function type for our code lens. *)
132+
let value_binding (iterator : Ast_iterator.iterator)
133+
(vb : Parsetree.value_binding) =
134+
(match vb with
135+
| {
136+
pvb_pat = {ppat_desc = Ppat_var _; ppat_loc};
137+
pvb_expr = {pexp_desc = Pexp_fun _};
138+
} ->
139+
push ppat_loc
140+
| _ -> ());
141+
Ast_iterator.default_iterator.value_binding iterator vb
142+
in
143+
let iterator = {Ast_iterator.default_iterator with value_binding} in
144+
(* We only print code lenses in implementation files. This is because they'd be redundant in interface files,
145+
where the definition itself will be the same thing as what would've been printed in the code lens. *)
146+
(if Filename.check_suffix path ".res" then
147+
let parser =
148+
Res_driver.parsingEngine.parseImplementation ~forPrinter:false
149+
in
150+
let {Res_driver.parsetree = structure} = parser ~filename:path in
151+
iterator.structure iterator structure |> ignore);
152+
!lenses
153+
|> List.filter_map (fun (range : Protocol.range) ->
154+
match Cmt.fullFromPath ~path with
155+
| None -> None
156+
| Some full -> (
157+
match
158+
References.getLocItem ~full
159+
~pos:(range.start.line, range.start.character + 1)
160+
~debug
161+
with
162+
| Some {locType = Typed (_, typeExpr, _)} ->
163+
Some
164+
(Protocol.stringifyCodeLens
165+
{
166+
range;
167+
command =
168+
Some
169+
{
170+
(* Code lenses can run commands. An empty command string means we just want the editor
171+
to print the text, not link to running a command. *)
172+
command = "";
173+
(* Print the type with a huge line width, because the code lens always prints on a
174+
single line in the editor. *)
175+
title = typeExpr |> Shared.typeToString ~lineWidth:400;
176+
};
177+
})
178+
| _ -> None))

analysis/src/PrintType.ml

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
let printExpr typ =
1+
let printExpr ?(lineWidth = 60) typ =
22
Printtyp.reset_names ();
3-
Res_doc.toString ~width:60
3+
Res_doc.toString ~width:lineWidth
44
(Res_outcome_printer.printOutTypeDoc (Printtyp.tree_of_typexp false typ))
55

66
let printDecl ~recStatus name decl =

analysis/src/Protocol.ml

+23
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,13 @@
11
type position = {line: int; character: int}
22
type range = {start: position; end_: position}
33
type markupContent = {kind: string; value: string}
4+
5+
(* https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#command *)
6+
type command = {title: string; command: string}
7+
8+
(* https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#codeLens *)
9+
type codeLens = {range: range; command: command option}
10+
411
type inlayHint = {
512
position: position;
613
label: string;
@@ -147,6 +154,22 @@ let stringifyHint hint =
147154
(stringifyPosition hint.position)
148155
(Json.escape hint.label) hint.kind hint.paddingLeft hint.paddingRight
149156

157+
let stringifyCommand (command : command) =
158+
Printf.sprintf {|{"title": "%s", "command": "%s"}|}
159+
(Json.escape command.title)
160+
(Json.escape command.command)
161+
162+
let stringifyCodeLens (codeLens : codeLens) =
163+
Printf.sprintf
164+
{|{
165+
"range": %s,
166+
"command": %s
167+
}|}
168+
(stringifyRange codeLens.range)
169+
(match codeLens.command with
170+
| None -> ""
171+
| Some command -> stringifyCommand command)
172+
150173
(* https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#diagnostic *)
151174
let stringifyDiagnostic d =
152175
Printf.sprintf

analysis/src/Shared.ml

+2-2
Original file line numberDiff line numberDiff line change
@@ -49,12 +49,12 @@ let declToString ?(recStatus = Types.Trec_not) name t =
4949
let cacheTypeToString = ref false
5050
let typeTbl = Hashtbl.create 1
5151

52-
let typeToString (t : Types.type_expr) =
52+
let typeToString ?lineWidth (t : Types.type_expr) =
5353
match
5454
if !cacheTypeToString then Hashtbl.find_opt typeTbl (t.id, t) else None
5555
with
5656
| None ->
57-
let s = PrintType.printExpr t in
57+
let s = PrintType.printExpr ?lineWidth t in
5858
Hashtbl.replace typeTbl (t.id, t) s;
5959
s
6060
| Some s -> s

analysis/tests/src/CodeLens.res

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
let add = (x, y) => x + y
2+
3+
let foo = (~age, ~name) => name ++ string_of_int(age)
4+
5+
let ff = (~opt1=0, ~a, ~b, (), ~opt2=0, (), ~c) => a + b + c + opt1 + opt2
6+
7+
let compFF = Completion.ff
8+
9+
@react.component
10+
let make = (~name) => React.string(name)
11+
//^cle
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
Code Lens src/CodeLens.res
2+
[{
3+
"range": {"start": {"line": 9, "character": 4}, "end": {"line": 9, "character": 8}},
4+
"command": {"title": "{\"name\": string} => React.element", "command": ""}
5+
}, {
6+
"range": {"start": {"line": 4, "character": 4}, "end": {"line": 4, "character": 6}},
7+
"command": {"title": "(~opt1: int=?, ~a: int, ~b: int, unit, ~opt2: int=?, unit, ~c: int) => int", "command": ""}
8+
}, {
9+
"range": {"start": {"line": 2, "character": 4}, "end": {"line": 2, "character": 7}},
10+
"command": {"title": "(~age: int, ~name: string) => string", "command": ""}
11+
}, {
12+
"range": {"start": {"line": 0, "character": 4}, "end": {"line": 0, "character": 7}},
13+
"command": {"title": "(int, int) => int", "command": ""}
14+
}]
15+
+1-1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
DCE src/Dce.res
2-
issues:243
2+
issues:249
33

analysis/tests/src/expected/Debug.res.txt

+2-1
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,9 @@ Dependencies: @rescript/react
44
Source directories: ./node_modules/@rescript/react/src ./node_modules/@rescript/react/src/legacy
55
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
66
Source directories: ./src ./src/expected
7-
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
7+
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
88
Impl cmt:./lib/bs/src/Auto.cmt res:./src/Auto.res
9+
Impl cmt:./lib/bs/src/CodeLens.cmt res:./src/CodeLens.res
910
Impl cmt:./lib/bs/src/CompletePrioritize1.cmt res:./src/CompletePrioritize1.res
1011
Impl cmt:./lib/bs/src/CompletePrioritize2.cmt res:./src/CompletePrioritize2.res
1112
Impl cmt:./lib/bs/src/Completion.cmt res:./src/Completion.res

client/src/extension.ts

+9-1
Original file line numberDiff line numberDiff line change
@@ -240,7 +240,15 @@ export function activate(context: ExtensionContext) {
240240
// language client, and because of that requires a full restart.
241241
context.subscriptions.push(
242242
workspace.onDidChangeConfiguration(({ affectsConfiguration }) => {
243-
if (affectsConfiguration("rescript.settings.inlayHints")) {
243+
// Put any configuration that, when changed, requires a full restart of
244+
// the server here. That will typically be any configuration that affects
245+
// the capabilities declared by the server, since those cannot be updated
246+
// on the fly, and require a full restart with new capabilities set when
247+
// initializing.
248+
if (
249+
affectsConfiguration("rescript.settings.inlayHints") ||
250+
affectsConfiguration("rescript.settings.codeLens")
251+
) {
244252
commands.executeCommand("rescript-vscode.restart_language_server");
245253
}
246254
})

package.json

+5
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,11 @@
146146
],
147147
"minimum": 0
148148
},
149+
"rescript.settings.codeLens.enable": {
150+
"type": "boolean",
151+
"default": false,
152+
"description": "Enable (experimental) code lens for function definitions."
153+
},
149154
"rescript.settings.binaryPath": {
150155
"type": ["string", "null"],
151156
"default": null,

server/src/server.ts

+41
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
DidChangeConfigurationNotification,
1414
InitializeParams,
1515
InlayHintParams,
16+
CodeLensParams,
1617
} from "vscode-languageserver-protocol";
1718
import * as utils from "./utils";
1819
import * as codeActions from "./codeActions";
@@ -30,14 +31,19 @@ interface extensionConfiguration {
3031
enable: boolean;
3132
maxLength: number | null;
3233
};
34+
codeLens: boolean;
3335
binaryPath: string | null;
3436
}
37+
38+
// All values here are temporary, and will be overridden as the server is
39+
// initialized, and the current config is received from the client.
3540
let extensionConfiguration: extensionConfiguration = {
3641
askToStartBuild: true,
3742
inlayHints: {
3843
enable: false,
3944
maxLength: 25
4045
},
46+
codeLens: false,
4147
binaryPath: null,
4248
};
4349
let pullConfigurationPeriodically: NodeJS.Timeout | null = null;
@@ -229,6 +235,9 @@ let compilerLogsWatcher = chokidar
229235
if (extensionConfiguration.inlayHints.enable === true) {
230236
sendInlayHintsRefresh();
231237
}
238+
if (extensionConfiguration.codeLens === true) {
239+
sendCodeLensRefresh();
240+
}
232241
});
233242
let stopWatchingCompilerLog = () => {
234243
// TODO: cleanup of compilerLogs?
@@ -411,6 +420,27 @@ function sendInlayHintsRefresh() {
411420
send(request);
412421
}
413422

423+
function codeLens(msg: p.RequestMessage) {
424+
const params = msg.params as p.CodeLensParams;
425+
const filePath = fileURLToPath(params.textDocument.uri);
426+
427+
const response = utils.runAnalysisCommand(
428+
filePath,
429+
["codeLens", filePath],
430+
msg
431+
);
432+
return response;
433+
}
434+
435+
function sendCodeLensRefresh() {
436+
let request: p.RequestMessage = {
437+
jsonrpc: c.jsonrpcVersion,
438+
method: p.CodeLensRefreshRequest.method,
439+
id: serverSentRequestIdCounter++,
440+
};
441+
send(request);
442+
}
443+
414444
function definition(msg: p.RequestMessage) {
415445
// https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_definition
416446
let params = msg.params as p.DefinitionParams;
@@ -1001,6 +1031,11 @@ function onMessage(msg: p.Message) {
10011031
full: true,
10021032
},
10031033
inlayHintProvider: extensionConfiguration.inlayHints.enable,
1034+
codeLensProvider: extensionConfiguration.codeLens
1035+
? {
1036+
workDoneProgress: false,
1037+
}
1038+
: undefined,
10041039
},
10051040
};
10061041
let response: p.ResponseMessage = {
@@ -1086,6 +1121,12 @@ function onMessage(msg: p.Message) {
10861121
if (extName === c.resExt) {
10871122
send(inlayHint(msg));
10881123
}
1124+
} else if (msg.method === p.CodeLensRequest.method) {
1125+
let params = msg.params as CodeLensParams;
1126+
let extName = path.extname(params.textDocument.uri);
1127+
if (extName === c.resExt) {
1128+
send(codeLens(msg));
1129+
}
10891130
} else {
10901131
let response: p.ResponseMessage = {
10911132
jsonrpc: c.jsonrpcVersion,

0 commit comments

Comments
 (0)