Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature: Support Inlay Hint #453

Merged
merged 48 commits into from
Jul 23, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
84fa4dd
feat(inlayHint): basic support
aspeddro Jun 10, 2022
19d6585
feat(inlayHint): remove log
aspeddro Jun 11, 2022
a4fae36
feat(inlayHint): add pos argument
aspeddro Jun 11, 2022
5e0648c
feat(inlayHint): simplify hint
aspeddro Jun 11, 2022
a75a2b8
feat(inlayHint): remove import extension
aspeddro Jun 11, 2022
b4172dc
feat(inlayHint): add rescript test file
aspeddro Jun 11, 2022
3105227
cleanup
aspeddro Jun 11, 2022
c73fa08
cleanup rm client.ts
aspeddro Jun 11, 2022
49b79df
Merge branch 'master' into feat-inlay-hint
zth Jun 16, 2022
935a8df
add config option for inlay hints
zth Jun 16, 2022
8b41f21
wire up inline hints again
zth Jun 16, 2022
f46998e
honor inline hints configuration
zth Jun 16, 2022
a2562c6
refactor hint analysis
aspeddro Jun 18, 2022
834af28
fix merge
aspeddro Jun 18, 2022
4c2bad3
add docstring to tooltip
aspeddro Jun 19, 2022
a3457d9
fix merge
aspeddro Jul 4, 2022
f43ef11
rewrite inlay hints
aspeddro Jul 8, 2022
f4e9f1d
add more exp
aspeddro Jul 8, 2022
0e38c52
sync with master
aspeddro Jul 14, 2022
29b77d1
refactor
aspeddro Jul 20, 2022
fefee51
add tests
aspeddro Jul 20, 2022
b995ac2
simplify typehint
aspeddro Jul 20, 2022
91ecfe8
Merge branch 'master' into feat-inlay-hint
aspeddro Jul 20, 2022
516f701
fix test
aspeddro Jul 20, 2022
5ee6f66
add cli help
aspeddro Jul 20, 2022
13be958
fix test
aspeddro Jul 20, 2022
7bd68f2
fix test
aspeddro Jul 20, 2022
02f080a
update expected test
aspeddro Jul 20, 2022
a4353ef
trim whitespace
aspeddro Jul 20, 2022
674e8be
format
aspeddro Jul 20, 2022
9a30439
remove autoRunCodeAnalysis
aspeddro Jul 20, 2022
b8d1ece
add maxLength option
aspeddro Jul 21, 2022
bd4cfdd
keep whitespace
aspeddro Jul 21, 2022
bc5494c
fix cli arg
aspeddro Jul 21, 2022
6921165
Update tests.
cristianoc Jul 22, 2022
13b804a
Simple cleanup.
cristianoc Jul 22, 2022
2d03db2
sync with master
aspeddro Jul 22, 2022
58d3d67
fix server.ts
aspeddro Jul 22, 2022
83a7d74
add range params
aspeddro Jul 22, 2022
c185c0a
update Cli
aspeddro Jul 22, 2022
9b4bac9
update readme
aspeddro Jul 22, 2022
fc3e3f9
add test
aspeddro Jul 23, 2022
99bd073
fix test
aspeddro Jul 23, 2022
65fe0ec
fix test
aspeddro Jul 23, 2022
21338b0
update changelog
aspeddro Jul 23, 2022
de032db
update changelog
aspeddro Jul 23, 2022
dfe05f1
fix comment
zth Jul 23, 2022
4df48ae
clarify maxLegth config
aspeddro Jul 23, 2022
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@

## master

#### :rocket: New Feature

- Inlay Hints (experimetal). `rescript.settings.inlayHints.enable: true`
## v1.4.2

#### :bug: Bug Fix
Expand Down
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,18 @@ The extension will look for the existence of a `/node_modules/.bin/rescript` fil

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.

```jsonc
// Enable (experimental) inlay hints.
rescript.settings.inlayHints.enable: true

// 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
rescript.settings.inlayHints.maxLength: 25
```

### 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.
Expand Down
14 changes: 7 additions & 7 deletions analysis/examples/larger-project/.merlin
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
####{BSB GENERATED: NO EDIT
FLG -ppx '/Users/cristianocalcagno/GitHub/rescript-vscode/analysis/examples/larger-project/node_modules/rescript/darwin/bsc.exe -as-ppx -bs-jsx 3'
S /Users/cristianocalcagno/GitHub/rescript-vscode/analysis/examples/larger-project/node_modules/rescript/lib/ocaml
B /Users/cristianocalcagno/GitHub/rescript-vscode/analysis/examples/larger-project/node_modules/rescript/lib/ocaml
FLG -ppx '/home/pedro/Desktop/Projects/rescript-vscode/analysis/examples/larger-project/node_modules/rescript/linux/bsc.exe -as-ppx -bs-jsx 3'
S /home/pedro/Desktop/Projects/rescript-vscode/analysis/examples/larger-project/node_modules/rescript/lib/ocaml
B /home/pedro/Desktop/Projects/rescript-vscode/analysis/examples/larger-project/node_modules/rescript/lib/ocaml
FLG -w +a-4-9-20-40-41-42-50-61-102
S /Users/cristianocalcagno/GitHub/rescript-vscode/analysis/examples/larger-project/node_modules/@rescript/react/lib/ocaml
B /Users/cristianocalcagno/GitHub/rescript-vscode/analysis/examples/larger-project/node_modules/@rescript/react/lib/ocaml
S /Users/cristianocalcagno/GitHub/rescript-vscode/analysis/examples/larger-project/node_modules/@glennsl/bs-json/lib/ocaml
B /Users/cristianocalcagno/GitHub/rescript-vscode/analysis/examples/larger-project/node_modules/@glennsl/bs-json/lib/ocaml
S /home/pedro/Desktop/Projects/rescript-vscode/analysis/examples/larger-project/node_modules/@rescript/react/lib/ocaml
B /home/pedro/Desktop/Projects/rescript-vscode/analysis/examples/larger-project/node_modules/@rescript/react/lib/ocaml
S /home/pedro/Desktop/Projects/rescript-vscode/analysis/examples/larger-project/node_modules/@glennsl/bs-json/lib/ocaml
B /home/pedro/Desktop/Projects/rescript-vscode/analysis/examples/larger-project/node_modules/@glennsl/bs-json/lib/ocaml
S src
B lib/bs/src
S src/exception
Expand Down
9 changes: 9 additions & 0 deletions analysis/src/Cli.ml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ 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

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

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

inlayHint: get all inlay Hint between line 0 and 3 declared in MyFile.res. Last argument is maximum of character length for inlay hints

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

test: run tests specified by special comments in file src/MyFile.res

./rescript-editor-analysis.exe test src/src/MyFile.res
Expand All @@ -89,6 +94,10 @@ let main () =
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)
~maxLength ~debug:false
| [_; "codeAction"; path; line; col; currentFile] ->
Commands.codeAction ~path
~pos:(int_of_string line, int_of_string col)
Expand Down
9 changes: 9 additions & 0 deletions analysis/src/Commands.ml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,10 @@ let completion ~debug ~path ~pos ~currentFile =
|> List.map Protocol.stringifyCompletionItem
|> Protocol.array)

let inlayhint ~path ~pos ~maxLength ~debug =
let result = Hint.inlay ~path ~pos ~maxLength ~debug |> Protocol.array in
print_endline result

let hover ~path ~pos ~currentFile ~debug =
let result =
match Cmt.fullFromPath ~path with
Expand Down Expand Up @@ -382,6 +386,11 @@ let test ~path =
(Protocol.stringifyRange range)
indent indent newText)))
| "dia" -> diagnosticSyntax ~path
| "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_newline ())
in
Expand Down
122 changes: 122 additions & 0 deletions analysis/src/Hint.ml
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
open SharedTypes

type inlayHintKind = Type | Parameter
let inlayKindToNumber = function
| Type -> 1
| Parameter -> 2

let locItemToTypeHint ~full:{file; package} locItem =
match locItem.locType with
| Constant t ->
Some
(match t with
| Const_int _ -> "int"
| Const_char _ -> "char"
| Const_string _ -> "string"
| Const_float _ -> "float"
| Const_int32 _ -> "int32"
| Const_int64 _ -> "int64"
| Const_nativeint _ -> "int")
| Typed (_, t, locKind) ->
let fromType typ =
typ |> Shared.typeToString
|> Str.global_replace (Str.regexp "[\r\n\t]") ""
in
Some
(match References.definedForLoc ~file ~package locKind with
| None -> fromType t
| Some (_, res) -> (
match res with
| `Declared -> fromType t
| `Constructor _ -> fromType t
| `Field -> fromType t))
| _ -> None

let inlay ~path ~pos ~maxLength ~debug =
let maxlen = try Some (int_of_string maxLength) with Failure _ -> None in
let hints = ref [] in
let start_line, end_line = pos in
let push loc kind =
let range = Utils.cmtLocToRange loc in
if start_line <= range.end_.line && end_line >= range.start.line then
hints := (range, kind) :: !hints
in
let rec processFunction (exp : Parsetree.expression) =
match exp.pexp_desc with
| Pexp_fun (_, _, pat_exp, e) -> (
match pat_exp with
| {ppat_desc = Ppat_var _} ->
push pat_exp.ppat_loc Type;
processFunction e
| _ -> processFunction e)
| _ -> ()
in
let value_binding (iterator : Ast_iterator.iterator)
(vb : Parsetree.value_binding) =
(match vb with
| {
pvb_pat = {ppat_desc = Ppat_var _};
pvb_expr =
{
pexp_desc =
( Pexp_constant _ | Pexp_tuple _ | Pexp_record _ | Pexp_variant _
| Pexp_apply _ | Pexp_match _ | Pexp_construct _ | Pexp_ifthenelse _
| Pexp_array _ | Pexp_ident _ | Pexp_try _ | Pexp_lazy _
| Pexp_send _ | Pexp_field _ | Pexp_open _ );
};
} ->
push vb.pvb_pat.ppat_loc Type
| {pvb_pat = {ppat_desc = Ppat_tuple tuples}} ->
List.iter
(fun (tuple : Parsetree.pattern) -> push tuple.ppat_loc Type)
tuples
| {
pvb_pat = {ppat_desc = Ppat_var _};
pvb_expr = {pexp_desc = Pexp_fun (_, _, pat, e)};
} ->
(match pat with
| {ppat_desc = Ppat_var _} -> push pat.ppat_loc Type
| _ -> ());
processFunction e
| _ -> ());
Ast_iterator.default_iterator.value_binding iterator vb
in
let iterator = {Ast_iterator.default_iterator with value_binding} in
(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);
!hints
|> List.filter_map (fun ((range : Protocol.range), hintKind) ->
match Cmt.fullFromPath ~path with
| None -> None
| Some full -> (
match
References.getLocItem ~full
~pos:(range.start.line, range.start.character + 1)
~debug
with
| None -> None
| Some locItem -> (
let position : Protocol.position =
{line = range.start.line; character = range.end_.character}
in
match locItemToTypeHint locItem ~full with
| Some label -> (
let result =
Protocol.stringifyHint
{
kind = inlayKindToNumber hintKind;
position;
paddingLeft = true;
paddingRight = false;
label = ": " ^ label;
}
in
match maxlen with
| Some value ->
if String.length label > value then None else Some result
| None -> Some result)
| None -> None)))
19 changes: 19 additions & 0 deletions analysis/src/Protocol.ml
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
type position = {line: int; character: int}
type range = {start: position; end_: position}
type markupContent = {kind: string; value: string}
type inlayHint = {
position: position;
label: string;
kind: int;
paddingLeft: bool;
paddingRight: bool;
}

type completionItem = {
label: string;
Expand Down Expand Up @@ -128,6 +135,18 @@ let stringifyCodeAction ca =
(codeActionKindToString ca.codeActionKind)
(ca.edit |> stringifyCodeActionEdit)

let stringifyHint hint =
Printf.sprintf
{|{
"position": %s,
"label": "%s",
"kind": %i,
"paddingLeft": %b,
"paddingRight": %b
}|}
(stringifyPosition hint.position)
(Json.escape hint.label) hint.kind hint.paddingLeft hint.paddingRight

(* https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#diagnostic *)
let stringifyDiagnostic d =
Printf.sprintf
Expand Down
20 changes: 20 additions & 0 deletions analysis/tests/src/InlayHint.res
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
let string = "ReScript"
let number = 1
let float = 1.1
let char = 'c'

let add = (x, y) => x + y

let my_sum = 3->add(1)->add(1)->add(1)->add(8)

let withAs = (~xx as yyy) => yyy + 1


@react.component
let make = (~name) => React.string(name)

let tuple = ("ReScript", "lol")

let (lang, _) = tuple

//^hin
2 changes: 1 addition & 1 deletion analysis/tests/src/expected/Dce.res.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
DCE src/Dce.res
issues:235
issues:243

3 changes: 2 additions & 1 deletion analysis/tests/src/expected/Debug.res.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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/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
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
Impl cmt:./lib/bs/src/Auto.cmt res:./src/Auto.res
Impl cmt:./lib/bs/src/CompletePrioritize1.cmt res:./src/CompletePrioritize1.res
Impl cmt:./lib/bs/src/CompletePrioritize2.cmt res:./src/CompletePrioritize2.res
Expand All @@ -21,6 +21,7 @@ Impl cmt:./lib/bs/src/DocumentSymbol.cmt res:./src/DocumentSymbol.res
Impl cmt:./lib/bs/src/Fragment.cmt res:./src/Fragment.res
Impl cmt:./lib/bs/src/Highlight.cmt res:./src/Highlight.res
Impl cmt:./lib/bs/src/Hover.cmt res:./src/Hover.res
Impl cmt:./lib/bs/src/InlayHint.cmt res:./src/InlayHint.res
IntfAndImpl cmti:./lib/bs/src/Jsx.cmti resi:./src/Jsx.resi cmt:./lib/bs/src/Jsx.cmt res:./src/Jsx.res
Impl cmt:./lib/bs/src/LongIdentTest.cmt res:./src/LongIdentTest.res
Impl cmt:./lib/bs/src/Object.cmt res:./src/Object.res
Expand Down
39 changes: 39 additions & 0 deletions analysis/tests/src/expected/InlayHint.res.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
Inlay Hint src/InlayHint.res 0:6
[{
"position": {"line": 5, "character": 15},
"label": ": int",
"kind": 1,
"paddingLeft": true,
"paddingRight": false
}, {
"position": {"line": 5, "character": 12},
"label": ": int",
"kind": 1,
"paddingLeft": true,
"paddingRight": false
}, {
"position": {"line": 3, "character": 8},
"label": ": char",
"kind": 1,
"paddingLeft": true,
"paddingRight": false
}, {
"position": {"line": 2, "character": 9},
"label": ": float",
"kind": 1,
"paddingLeft": true,
"paddingRight": false
}, {
"position": {"line": 1, "character": 10},
"label": ": int",
"kind": 1,
"paddingLeft": true,
"paddingRight": false
}, {
"position": {"line": 0, "character": 10},
"label": ": string",
"kind": 1,
"paddingLeft": true,
"paddingRight": false
}]

11 changes: 11 additions & 0 deletions client/src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,17 @@ export function activate(context: ExtensionContext) {
// Start the client. This will also launch the server
client.start();

// Restart the language client automatically when certain configuration
// changes. These are typically settings that affect the capabilities of the
// language client, and because of that requires a full restart.
context.subscriptions.push(
workspace.onDidChangeConfiguration(({ affectsConfiguration }) => {
if (affectsConfiguration("rescript.settings.inlayHints")) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a special reason to restart only when this specific setting changes?
I imagine as a user I would be quite confused "I think last time I could just change setting, this time I need to restart, strange...".

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's only restarted automatically when required. Some configs can be changed on the fly, but other configs require restarting the server because they affect how the lsp is instantiated. If we don't have this then the user would need to be promoted to restart the server manually to have their config take effect.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The question is why not the other changes too.
How do I know other configs don't need restarting the server?
And if they don't need restating the server today, how do I know they don't need to restart the server tomorrow.
And if tomorrow they will need a restart, what are the chances the person doing the change will think of that, realise this piece of code exists, and come here to update it?

Overall, why special case thins config, is the question.

commands.executeCommand("rescript-vscode.restart_language_server");
}
})
);

// Autostart code analysis if wanted
if (
workspace
Expand Down
14 changes: 14 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,20 @@
"default": false,
"description": "Automatically start ReScript's code analysis."
},
"rescript.settings.inlayHints.enable": {
"type": "boolean",
"default": false,
"description": "Enable (experimental) inlay hints."
},
"rescript.settings.inlayHints.maxLength": {
"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.",
"default": 25,
"type": [
"null",
"integer"
],
"minimum": 0
},
"rescript.settings.binaryPath": {
"type": ["string", "null"],
"default": null,
Expand Down
Loading