Skip to content

Commit e4caac2

Browse files
authored
feat(lsp): Implement document symbol outline (#2298)
1 parent ac892d1 commit e4caac2

File tree

11 files changed

+374
-2
lines changed

11 files changed

+374
-2
lines changed

compiler/src/language_server/driver.re

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,9 @@ let process = msg => {
2828
| TextDocumentInlayHint(id, params) when is_initialized^ =>
2929
Inlayhint.process(~id, ~compiled_code, ~documents, params);
3030
Reading;
31+
| TextDocumentSymbol(id, params) when is_initialized^ =>
32+
Symbol.process(~id, ~compiled_code, ~documents, params);
33+
Reading;
3134
| TextDocumentCodeLens(id, params) when is_initialized^ =>
3235
Lenses.process(~id, ~compiled_code, ~documents, params);
3336
Reading;

compiler/src/language_server/initialize.re

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ module ResponseResult = {
7474
},
7575
type_definition_provider: true,
7676
references_provider: false,
77-
document_symbol_provider: false,
77+
document_symbol_provider: true,
7878
code_action_provider: true,
7979
code_lens_provider: {
8080
resolve_provider: true,

compiler/src/language_server/message.re

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ type t =
99
| TextDocumentDidOpen(Protocol.uri, Code_file.DidOpen.RequestParams.t)
1010
| TextDocumentDidChange(Protocol.uri, Code_file.DidChange.RequestParams.t)
1111
| TextDocumentInlayHint(Protocol.message_id, Inlayhint.RequestParams.t)
12+
| TextDocumentSymbol(Protocol.message_id, Symbol.RequestParams.t)
1213
| Formatting(Protocol.message_id, Formatting.RequestParams.t)
1314
| Goto(Protocol.message_id, Goto.goto_request_type, Goto.RequestParams.t)
1415
| CodeAction(Protocol.message_id, Code_action.RequestParams.t)
@@ -33,6 +34,15 @@ let of_request = (msg: Protocol.request_message): t => {
3334
| Ok(params) => TextDocumentInlayHint(id, params)
3435
| Error(msg) => Error(msg)
3536
}
37+
| {
38+
method: "textDocument/documentSymbol",
39+
id: Some(id),
40+
params: Some(params),
41+
} =>
42+
switch (Symbol.RequestParams.of_yojson(params)) {
43+
| Ok(params) => TextDocumentSymbol(id, params)
44+
| Error(msg) => Error(msg)
45+
}
3646
| {method: "textDocument/codeLens", id: Some(id), params: Some(params)} =>
3747
switch (Lenses.RequestParams.of_yojson(params)) {
3848
| Ok(params) => TextDocumentCodeLens(id, params)

compiler/src/language_server/message.rei

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ type t =
77
| TextDocumentDidOpen(Protocol.uri, Code_file.DidOpen.RequestParams.t)
88
| TextDocumentDidChange(Protocol.uri, Code_file.DidChange.RequestParams.t)
99
| TextDocumentInlayHint(Protocol.message_id, Inlayhint.RequestParams.t)
10+
| TextDocumentSymbol(Protocol.message_id, Symbol.RequestParams.t)
1011
| Formatting(Protocol.message_id, Formatting.RequestParams.t)
1112
| Goto(Protocol.message_id, Goto.goto_request_type, Goto.RequestParams.t)
1213
| CodeAction(Protocol.message_id, Code_action.RequestParams.t)
Lines changed: 246 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,246 @@
1+
open Grain_typed;
2+
3+
// https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#documentSymbolParams
4+
module RequestParams = {
5+
[@deriving yojson({strict: false})]
6+
type t = {
7+
[@key "textDocument"]
8+
text_document: Protocol.text_document_identifier,
9+
};
10+
};
11+
12+
// https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#documentSymbol
13+
module ResponseResult = {
14+
/** A symbol kind. */
15+
[@deriving (enum, yojson)]
16+
type symbol_kind =
17+
| [@value 1] File
18+
| [@value 2] Module
19+
| [@value 3] Namespace
20+
| [@value 4] Package
21+
| [@value 5] Class
22+
| [@value 6] Method
23+
| [@value 7] Property
24+
| [@value 8] Field
25+
| [@value 9] Constructor
26+
| [@value 10] Enum
27+
| [@value 11] Interface
28+
| [@value 12] Function
29+
| [@value 13] Variable
30+
| [@value 14] Constant
31+
| [@value 15] String
32+
| [@value 16] Number
33+
| [@value 17] Boolean
34+
| [@value 18] Array
35+
| [@value 19] Object
36+
| [@value 20] Key
37+
| [@value 21] Null
38+
| [@value 22] EnumMember
39+
| [@value 23] Struct
40+
| [@value 24] Event
41+
| [@value 25] Operator
42+
| [@value 26] TypeParameter;
43+
let symbol_kind_to_yojson = symbol_kind =>
44+
symbol_kind_to_enum(symbol_kind) |> [%to_yojson: int];
45+
let symbol_kind_of_yojson = json =>
46+
Result.bind(json |> [%of_yojson: int], value => {
47+
switch (symbol_kind_of_enum(value)) {
48+
| Some(symbol_kind) => Ok(symbol_kind)
49+
| None => Result.Error("Invalid enum value")
50+
}
51+
});
52+
53+
/**
54+
* Symbol tags are extra annotations that tweak the rendering of a symbol.
55+
*/
56+
[@deriving (enum, yojson)]
57+
type symbol_tag =
58+
| /**
59+
* Render a symbol as obsolete, usually using a strike-out.
60+
*/
61+
[@value 1] Deprecated;
62+
/**
63+
* Represents programming constructs like variables, classes, interfaces etc.
64+
* that appear in a document. Document symbols can be hierarchical and they
65+
* have two ranges: one that encloses its definition and one that points to its
66+
* most interesting range, e.g. the range of an identifier.
67+
*/
68+
[@deriving yojson]
69+
type document_symbol = {
70+
/**
71+
* The name of this symbol. Will be displayed in the user interface and
72+
* therefore must not be an empty string or a string only consisting of
73+
* white spaces.
74+
*/
75+
name: string,
76+
/**
77+
* More detail for this symbol, e.g the signature of a function.
78+
*/
79+
[@default None]
80+
detail: option(string),
81+
/** The kind of this symbol. */
82+
kind: symbol_kind,
83+
/** Tags for this document symbol. */
84+
[@default None]
85+
tags: option(list(symbol_tag)),
86+
/**
87+
* The range enclosing this symbol not including leading/trailing whitespace
88+
* but everything else like comments. This information is typically used to
89+
* determine if the clients cursor is inside the symbol to reveal it in the
90+
* UI.
91+
*/
92+
range: Protocol.range,
93+
/**
94+
* The range that should be selected and revealed when this symbol is being
95+
* picked, e.g. the name of a function. Must be contained by the `range`.
96+
*/
97+
[@key "selectionRange"]
98+
selection_range: Protocol.range,
99+
/**
100+
* Children of this symbol, e.g. properties of a class.
101+
*/
102+
[@default None]
103+
children: option(list(document_symbol)),
104+
};
105+
[@deriving yojson]
106+
type t = list(document_symbol);
107+
};
108+
109+
// Outline building functions
110+
let rec build_toplevel_outline = (statement: Typedtree.toplevel_stmt) => {
111+
switch (statement.ttop_desc) {
112+
| TTopForeign(_)
113+
| TTopInclude(_)
114+
| TTopProvide(_)
115+
| TTopException(_)
116+
| TTopExpr(_) => None
117+
| TTopData(data_decls) =>
118+
Some(
119+
List.filter_map(
120+
(data_decl: Typedtree.data_declaration) => {
121+
let kind: option(ResponseResult.symbol_kind) =
122+
switch (data_decl.data_kind) {
123+
| TDataRecord(_) => Some(Struct)
124+
| TDataVariant(_) => Some(Enum)
125+
| TDataAbstract => None
126+
};
127+
switch (kind) {
128+
| Some(kind) =>
129+
Some(
130+
{
131+
name: Ident.name(data_decl.data_id),
132+
detail: None,
133+
kind,
134+
tags: None,
135+
range: Utils.loc_to_range(data_decl.data_loc),
136+
selection_range: Utils.loc_to_range(data_decl.data_loc),
137+
children: None,
138+
}: ResponseResult.document_symbol,
139+
)
140+
| None => None
141+
};
142+
},
143+
data_decls,
144+
),
145+
)
146+
| TTopLet(_, _, binds) =>
147+
Some(
148+
List.filter_map(
149+
(bind: Typedtree.value_binding) => {
150+
switch (bind.vb_pat.pat_desc) {
151+
| TPatVar(ident, _) =>
152+
Some(
153+
{
154+
name: Ident.name(ident),
155+
detail:
156+
Some(
157+
Printtyp.string_of_type_scheme(bind.vb_expr.exp_type),
158+
),
159+
kind:
160+
switch (bind.vb_expr.exp_desc) {
161+
| TExpLambda(_, _) => Function
162+
| _ => Variable
163+
},
164+
tags: None,
165+
range: Utils.loc_to_range(bind.vb_loc),
166+
selection_range: Utils.loc_to_range(bind.vb_loc),
167+
children: None,
168+
}: ResponseResult.document_symbol,
169+
)
170+
| _ => None
171+
}
172+
},
173+
binds,
174+
),
175+
)
176+
| TTopModule(module_decl) =>
177+
Some([
178+
(
179+
{
180+
name: Ident.name(module_decl.tmod_id),
181+
detail: None,
182+
kind: Module,
183+
tags: None,
184+
range: Utils.loc_to_range(statement.ttop_loc),
185+
selection_range: Utils.loc_to_range(statement.ttop_loc),
186+
children:
187+
Some(
188+
List.fold_left(
189+
(acc, statement) => {
190+
switch (build_toplevel_outline(statement)) {
191+
| None => acc
192+
| Some(symbol) => List.append(acc, symbol)
193+
}
194+
},
195+
[],
196+
module_decl.tmod_statements,
197+
),
198+
),
199+
}: ResponseResult.document_symbol
200+
),
201+
])
202+
};
203+
};
204+
205+
let build_program_outline = (program: Typedtree.typed_program) => {
206+
[
207+
(
208+
{
209+
name: program.module_name.txt,
210+
detail: None,
211+
kind: Module,
212+
tags: None,
213+
range: Utils.loc_to_range(program.mod_loc),
214+
selection_range: Utils.loc_to_range(program.mod_loc),
215+
children:
216+
Some(
217+
List.fold_left(
218+
(acc, statement) => {
219+
switch (build_toplevel_outline(statement)) {
220+
| None => acc
221+
| Some(symbol) => List.append(acc, symbol)
222+
}
223+
},
224+
[],
225+
program.statements,
226+
),
227+
),
228+
}: ResponseResult.document_symbol
229+
),
230+
];
231+
};
232+
233+
let process =
234+
(
235+
~id: Protocol.message_id,
236+
~compiled_code: Hashtbl.t(Protocol.uri, Lsp_types.code),
237+
~documents: Hashtbl.t(Protocol.uri, string),
238+
params: RequestParams.t,
239+
) => {
240+
switch (Hashtbl.find_opt(compiled_code, params.text_document.uri)) {
241+
| None => Protocol.response(~id, `Null)
242+
| Some({program}) =>
243+
let outline = build_program_outline(program);
244+
Protocol.response(~id, ResponseResult.to_yojson(outline));
245+
};
246+
};
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
open Grain_typed;
2+
3+
// https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#documentSymbolParams
4+
module RequestParams: {
5+
[@deriving yojson({strict: false})]
6+
type t;
7+
};
8+
9+
// https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#documentSymbol
10+
module ResponseResult: {
11+
[@deriving yojson]
12+
type t;
13+
};
14+
15+
let process:
16+
(
17+
~id: Protocol.message_id,
18+
~compiled_code: Hashtbl.t(Protocol.uri, Lsp_types.code),
19+
~documents: Hashtbl.t(Protocol.uri, string),
20+
RequestParams.t
21+
) =>
22+
unit;

compiler/src/typed/typedtree.re

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -631,6 +631,7 @@ type typed_program = {
631631
signature: Cmi_format.cmi_infos,
632632
comments: list(comment),
633633
prog_loc: Location.t,
634+
mod_loc: Location.t,
634635
};
635636

636637
let iter_pattern_desc = (f, patt) =>

compiler/src/typed/typedtree.rei

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -591,6 +591,7 @@ type typed_program = {
591591
signature: Cmi_format.cmi_infos,
592592
comments: list(comment),
593593
prog_loc: Location.t,
594+
mod_loc: Location.t,
594595
};
595596

596597
/* Auxiliary functions over the AST */

compiler/src/typed/typemod.re

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1075,6 +1075,7 @@ let type_implementation = (prog: Parsetree.parsed_program) => {
10751075
signature,
10761076
comments: prog.comments,
10771077
prog_loc: prog.prog_loc,
1078+
mod_loc: prog.prog_core_loc,
10781079
};
10791080
};
10801081

compiler/test/runner.re

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -726,7 +726,7 @@ let assert_lsp_responses =
726726
lsp_expected_response(
727727
lsp_success_response(
728728
Yojson.Safe.from_string(
729-
{|{"capabilities":{"documentFormattingProvider":true,"textDocumentSync":1,"hoverProvider":true,"definitionProvider":{"linkSupport":true},"typeDefinitionProvider":true,"referencesProvider":false,"documentSymbolProvider":false,"codeActionProvider":true,"codeLensProvider":{"resolveProvider":true},"documentHighlightProvider":false,"documentRangeFormattingProvider":false,"renameProvider":false,"inlayHintProvider":{"resolveProvider":false}}}|},
729+
{|{"capabilities":{"documentFormattingProvider":true,"textDocumentSync":1,"hoverProvider":true,"definitionProvider":{"linkSupport":true},"typeDefinitionProvider":true,"referencesProvider":false,"documentSymbolProvider":true,"codeActionProvider":true,"codeLensProvider":{"resolveProvider":true},"documentHighlightProvider":false,"documentRangeFormattingProvider":false,"renameProvider":false,"inlayHintProvider":{"resolveProvider":false}}}|},
730730
),
731731
),
732732
);

0 commit comments

Comments
 (0)