Skip to content

Commit 26c8df8

Browse files
authored
Feature: Support Inlay Hint (#453)
1 parent 8dd86d1 commit 26c8df8

File tree

14 files changed

+318
-30
lines changed

14 files changed

+318
-30
lines changed

CHANGELOG.md

+3
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@
1212
1313
## master
1414

15+
#### :rocket: New Feature
16+
17+
- Inlay Hints (experimetal). `rescript.settings.inlayHints.enable: true`
1518
## v1.4.2
1619

1720
#### :bug: Bug Fix

README.md

+12
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,18 @@ The extension will look for the existence of a `/node_modules/.bin/rescript` fil
105105

106106
To override this lookup process, the path can be configured explicitly using the setting `rescript.settings.binaryPath`.
107107

108+
### Inlay Hints (experimental)
109+
110+
This allows an editor to place annotations inline with text to display type hints.
111+
112+
```jsonc
113+
// Enable (experimental) inlay hints.
114+
rescript.settings.inlayHints.enable: true
115+
116+
// Maximum length of character for inlay hints. Set to null to have an unlimited length. Inlay hints that exceed the maximum length will not be shown
117+
rescript.settings.inlayHints.maxLength: 25
118+
```
119+
108120
### Hide generated files
109121

110122
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/examples/larger-project/.merlin

+7-7
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
####{BSB GENERATED: NO EDIT
2-
FLG -ppx '/Users/cristianocalcagno/GitHub/rescript-vscode/analysis/examples/larger-project/node_modules/rescript/darwin/bsc.exe -as-ppx -bs-jsx 3'
3-
S /Users/cristianocalcagno/GitHub/rescript-vscode/analysis/examples/larger-project/node_modules/rescript/lib/ocaml
4-
B /Users/cristianocalcagno/GitHub/rescript-vscode/analysis/examples/larger-project/node_modules/rescript/lib/ocaml
2+
FLG -ppx '/home/pedro/Desktop/Projects/rescript-vscode/analysis/examples/larger-project/node_modules/rescript/linux/bsc.exe -as-ppx -bs-jsx 3'
3+
S /home/pedro/Desktop/Projects/rescript-vscode/analysis/examples/larger-project/node_modules/rescript/lib/ocaml
4+
B /home/pedro/Desktop/Projects/rescript-vscode/analysis/examples/larger-project/node_modules/rescript/lib/ocaml
55
FLG -w +a-4-9-20-40-41-42-50-61-102
6-
S /Users/cristianocalcagno/GitHub/rescript-vscode/analysis/examples/larger-project/node_modules/@rescript/react/lib/ocaml
7-
B /Users/cristianocalcagno/GitHub/rescript-vscode/analysis/examples/larger-project/node_modules/@rescript/react/lib/ocaml
8-
S /Users/cristianocalcagno/GitHub/rescript-vscode/analysis/examples/larger-project/node_modules/@glennsl/bs-json/lib/ocaml
9-
B /Users/cristianocalcagno/GitHub/rescript-vscode/analysis/examples/larger-project/node_modules/@glennsl/bs-json/lib/ocaml
6+
S /home/pedro/Desktop/Projects/rescript-vscode/analysis/examples/larger-project/node_modules/@rescript/react/lib/ocaml
7+
B /home/pedro/Desktop/Projects/rescript-vscode/analysis/examples/larger-project/node_modules/@rescript/react/lib/ocaml
8+
S /home/pedro/Desktop/Projects/rescript-vscode/analysis/examples/larger-project/node_modules/@glennsl/bs-json/lib/ocaml
9+
B /home/pedro/Desktop/Projects/rescript-vscode/analysis/examples/larger-project/node_modules/@glennsl/bs-json/lib/ocaml
1010
S src
1111
B lib/bs/src
1212
S src/exception

analysis/src/Cli.ml

+9
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ 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
1415

1516
Dev-time examples:
1617
./rescript-editor-analysis.exe dump src/MyFile.res src/MyFile2.res
@@ -65,6 +66,10 @@ Options:
6566

6667
./rescript-editor-analysis.exe diagnosticSyntax src/MyFile.res
6768

69+
inlayHint: get all inlay Hint between line 0 and 3 declared in MyFile.res. Last argument is maximum of character length for inlay hints
70+
71+
./rescript-editor-analysis.exe inlayHint src/MyFile.res 0 3 25
72+
6873
test: run tests specified by special comments in file src/MyFile.res
6974

7075
./rescript-editor-analysis.exe test src/src/MyFile.res
@@ -89,6 +94,10 @@ let main () =
8994
Commands.hover ~path
9095
~pos:(int_of_string line, int_of_string col)
9196
~currentFile ~debug:false
97+
| [_; "inlayHint"; path; line_start; line_end; maxLength] ->
98+
Commands.inlayhint ~path
99+
~pos:(int_of_string line_start, int_of_string line_end)
100+
~maxLength ~debug:false
92101
| [_; "codeAction"; path; line; col; currentFile] ->
93102
Commands.codeAction ~path
94103
~pos:(int_of_string line, int_of_string col)

analysis/src/Commands.ml

+9
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,10 @@ let completion ~debug ~path ~pos ~currentFile =
2828
|> List.map Protocol.stringifyCompletionItem
2929
|> Protocol.array)
3030

31+
let inlayhint ~path ~pos ~maxLength ~debug =
32+
let result = Hint.inlay ~path ~pos ~maxLength ~debug |> Protocol.array in
33+
print_endline result
34+
3135
let hover ~path ~pos ~currentFile ~debug =
3236
let result =
3337
match Cmt.fullFromPath ~path with
@@ -382,6 +386,11 @@ let test ~path =
382386
(Protocol.stringifyRange range)
383387
indent indent newText)))
384388
| "dia" -> diagnosticSyntax ~path
389+
| "hin" -> (
390+
let line_start = 0 in
391+
let line_end = 6 in
392+
print_endline ("Inlay Hint " ^ path ^ " " ^ string_of_int line_start ^ ":" ^ string_of_int line_end);
393+
inlayhint ~path ~pos:(line_start, line_end) ~maxLength:"25" ~debug:false)
385394
| _ -> ());
386395
print_newline ())
387396
in

analysis/src/Hint.ml

+122
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
open SharedTypes
2+
3+
type inlayHintKind = Type | Parameter
4+
let inlayKindToNumber = function
5+
| Type -> 1
6+
| Parameter -> 2
7+
8+
let locItemToTypeHint ~full:{file; package} locItem =
9+
match locItem.locType with
10+
| Constant t ->
11+
Some
12+
(match t with
13+
| Const_int _ -> "int"
14+
| Const_char _ -> "char"
15+
| Const_string _ -> "string"
16+
| Const_float _ -> "float"
17+
| Const_int32 _ -> "int32"
18+
| Const_int64 _ -> "int64"
19+
| Const_nativeint _ -> "int")
20+
| Typed (_, t, locKind) ->
21+
let fromType typ =
22+
typ |> Shared.typeToString
23+
|> Str.global_replace (Str.regexp "[\r\n\t]") ""
24+
in
25+
Some
26+
(match References.definedForLoc ~file ~package locKind with
27+
| None -> fromType t
28+
| Some (_, res) -> (
29+
match res with
30+
| `Declared -> fromType t
31+
| `Constructor _ -> fromType t
32+
| `Field -> fromType t))
33+
| _ -> None
34+
35+
let inlay ~path ~pos ~maxLength ~debug =
36+
let maxlen = try Some (int_of_string maxLength) with Failure _ -> None in
37+
let hints = ref [] in
38+
let start_line, end_line = pos in
39+
let push loc kind =
40+
let range = Utils.cmtLocToRange loc in
41+
if start_line <= range.end_.line && end_line >= range.start.line then
42+
hints := (range, kind) :: !hints
43+
in
44+
let rec processFunction (exp : Parsetree.expression) =
45+
match exp.pexp_desc with
46+
| Pexp_fun (_, _, pat_exp, e) -> (
47+
match pat_exp with
48+
| {ppat_desc = Ppat_var _} ->
49+
push pat_exp.ppat_loc Type;
50+
processFunction e
51+
| _ -> processFunction e)
52+
| _ -> ()
53+
in
54+
let value_binding (iterator : Ast_iterator.iterator)
55+
(vb : Parsetree.value_binding) =
56+
(match vb with
57+
| {
58+
pvb_pat = {ppat_desc = Ppat_var _};
59+
pvb_expr =
60+
{
61+
pexp_desc =
62+
( Pexp_constant _ | Pexp_tuple _ | Pexp_record _ | Pexp_variant _
63+
| Pexp_apply _ | Pexp_match _ | Pexp_construct _ | Pexp_ifthenelse _
64+
| Pexp_array _ | Pexp_ident _ | Pexp_try _ | Pexp_lazy _
65+
| Pexp_send _ | Pexp_field _ | Pexp_open _ );
66+
};
67+
} ->
68+
push vb.pvb_pat.ppat_loc Type
69+
| {pvb_pat = {ppat_desc = Ppat_tuple tuples}} ->
70+
List.iter
71+
(fun (tuple : Parsetree.pattern) -> push tuple.ppat_loc Type)
72+
tuples
73+
| {
74+
pvb_pat = {ppat_desc = Ppat_var _};
75+
pvb_expr = {pexp_desc = Pexp_fun (_, _, pat, e)};
76+
} ->
77+
(match pat with
78+
| {ppat_desc = Ppat_var _} -> push pat.ppat_loc Type
79+
| _ -> ());
80+
processFunction e
81+
| _ -> ());
82+
Ast_iterator.default_iterator.value_binding iterator vb
83+
in
84+
let iterator = {Ast_iterator.default_iterator with value_binding} in
85+
(if Filename.check_suffix path ".res" then
86+
let parser =
87+
Res_driver.parsingEngine.parseImplementation ~forPrinter:false
88+
in
89+
let {Res_driver.parsetree = structure} = parser ~filename:path in
90+
iterator.structure iterator structure |> ignore);
91+
!hints
92+
|> List.filter_map (fun ((range : Protocol.range), hintKind) ->
93+
match Cmt.fullFromPath ~path with
94+
| None -> None
95+
| Some full -> (
96+
match
97+
References.getLocItem ~full
98+
~pos:(range.start.line, range.start.character + 1)
99+
~debug
100+
with
101+
| None -> None
102+
| Some locItem -> (
103+
let position : Protocol.position =
104+
{line = range.start.line; character = range.end_.character}
105+
in
106+
match locItemToTypeHint locItem ~full with
107+
| Some label -> (
108+
let result =
109+
Protocol.stringifyHint
110+
{
111+
kind = inlayKindToNumber hintKind;
112+
position;
113+
paddingLeft = true;
114+
paddingRight = false;
115+
label = ": " ^ label;
116+
}
117+
in
118+
match maxlen with
119+
| Some value ->
120+
if String.length label > value then None else Some result
121+
| None -> Some result)
122+
| None -> None)))

analysis/src/Protocol.ml

+19
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+
type inlayHint = {
5+
position: position;
6+
label: string;
7+
kind: int;
8+
paddingLeft: bool;
9+
paddingRight: bool;
10+
}
411

512
type completionItem = {
613
label: string;
@@ -128,6 +135,18 @@ let stringifyCodeAction ca =
128135
(codeActionKindToString ca.codeActionKind)
129136
(ca.edit |> stringifyCodeActionEdit)
130137

138+
let stringifyHint hint =
139+
Printf.sprintf
140+
{|{
141+
"position": %s,
142+
"label": "%s",
143+
"kind": %i,
144+
"paddingLeft": %b,
145+
"paddingRight": %b
146+
}|}
147+
(stringifyPosition hint.position)
148+
(Json.escape hint.label) hint.kind hint.paddingLeft hint.paddingRight
149+
131150
(* https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#diagnostic *)
132151
let stringifyDiagnostic d =
133152
Printf.sprintf

analysis/tests/src/InlayHint.res

+20
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
let string = "ReScript"
2+
let number = 1
3+
let float = 1.1
4+
let char = 'c'
5+
6+
let add = (x, y) => x + y
7+
8+
let my_sum = 3->add(1)->add(1)->add(1)->add(8)
9+
10+
let withAs = (~xx as yyy) => yyy + 1
11+
12+
13+
@react.component
14+
let make = (~name) => React.string(name)
15+
16+
let tuple = ("ReScript", "lol")
17+
18+
let (lang, _) = tuple
19+
20+
//^hin
+1-1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
DCE src/Dce.res
2-
issues:235
2+
issues:243
33

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

+2-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ 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/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/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
99
Impl cmt:./lib/bs/src/CompletePrioritize1.cmt res:./src/CompletePrioritize1.res
1010
Impl cmt:./lib/bs/src/CompletePrioritize2.cmt res:./src/CompletePrioritize2.res
@@ -21,6 +21,7 @@ Impl cmt:./lib/bs/src/DocumentSymbol.cmt res:./src/DocumentSymbol.res
2121
Impl cmt:./lib/bs/src/Fragment.cmt res:./src/Fragment.res
2222
Impl cmt:./lib/bs/src/Highlight.cmt res:./src/Highlight.res
2323
Impl cmt:./lib/bs/src/Hover.cmt res:./src/Hover.res
24+
Impl cmt:./lib/bs/src/InlayHint.cmt res:./src/InlayHint.res
2425
IntfAndImpl cmti:./lib/bs/src/Jsx.cmti resi:./src/Jsx.resi cmt:./lib/bs/src/Jsx.cmt res:./src/Jsx.res
2526
Impl cmt:./lib/bs/src/LongIdentTest.cmt res:./src/LongIdentTest.res
2627
Impl cmt:./lib/bs/src/Object.cmt res:./src/Object.res
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
Inlay Hint src/InlayHint.res 0:6
2+
[{
3+
"position": {"line": 5, "character": 15},
4+
"label": ": int",
5+
"kind": 1,
6+
"paddingLeft": true,
7+
"paddingRight": false
8+
}, {
9+
"position": {"line": 5, "character": 12},
10+
"label": ": int",
11+
"kind": 1,
12+
"paddingLeft": true,
13+
"paddingRight": false
14+
}, {
15+
"position": {"line": 3, "character": 8},
16+
"label": ": char",
17+
"kind": 1,
18+
"paddingLeft": true,
19+
"paddingRight": false
20+
}, {
21+
"position": {"line": 2, "character": 9},
22+
"label": ": float",
23+
"kind": 1,
24+
"paddingLeft": true,
25+
"paddingRight": false
26+
}, {
27+
"position": {"line": 1, "character": 10},
28+
"label": ": int",
29+
"kind": 1,
30+
"paddingLeft": true,
31+
"paddingRight": false
32+
}, {
33+
"position": {"line": 0, "character": 10},
34+
"label": ": string",
35+
"kind": 1,
36+
"paddingLeft": true,
37+
"paddingRight": false
38+
}]
39+

client/src/extension.ts

+11
Original file line numberDiff line numberDiff line change
@@ -235,6 +235,17 @@ export function activate(context: ExtensionContext) {
235235
// Start the client. This will also launch the server
236236
client.start();
237237

238+
// Restart the language client automatically when certain configuration
239+
// changes. These are typically settings that affect the capabilities of the
240+
// language client, and because of that requires a full restart.
241+
context.subscriptions.push(
242+
workspace.onDidChangeConfiguration(({ affectsConfiguration }) => {
243+
if (affectsConfiguration("rescript.settings.inlayHints")) {
244+
commands.executeCommand("rescript-vscode.restart_language_server");
245+
}
246+
})
247+
);
248+
238249
// Autostart code analysis if wanted
239250
if (
240251
workspace

package.json

+14
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,20 @@
132132
"default": false,
133133
"description": "Automatically start ReScript's code analysis."
134134
},
135+
"rescript.settings.inlayHints.enable": {
136+
"type": "boolean",
137+
"default": false,
138+
"description": "Enable (experimental) inlay hints."
139+
},
140+
"rescript.settings.inlayHints.maxLength": {
141+
"markdownDescription": "Maximum length of character for inlay hints. Set to null to have an unlimited length. Inlay hints that exceed the maximum length will not be shown.",
142+
"default": 25,
143+
"type": [
144+
"null",
145+
"integer"
146+
],
147+
"minimum": 0
148+
},
135149
"rescript.settings.binaryPath": {
136150
"type": ["string", "null"],
137151
"default": null,

0 commit comments

Comments
 (0)