Skip to content

Commit 7b3285c

Browse files
committed
feat(lsp): Add code completion
1 parent bb0764e commit 7b3285c

File tree

5 files changed

+374
-0
lines changed

5 files changed

+374
-0
lines changed
Lines changed: 298 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,298 @@
1+
open Grain_utils;
2+
open Grain_typed;
3+
open Grain_diagnostics;
4+
5+
// This is the full enumeration of all CompletionItemKind as declared by the language server
6+
// protocol (https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#completionItemKind),
7+
// but not all will be used by Grain LSP
8+
[@deriving (enum, yojson)]
9+
type completion_item_kind =
10+
// Since these are using ppx_deriving enum, order matters
11+
| [@value 1] CompletionItemKindText
12+
| CompletionItemKindMethod
13+
| CompletionItemKindFunction
14+
| CompletionItemKindConstructor
15+
| CompletionItemKindField
16+
| CompletionItemKindVariable
17+
| CompletionItemKindClass
18+
| CompletionItemKindInterface
19+
| CompletionItemKindModule
20+
| CompletionItemKindProperty
21+
| CompletionItemKindUnit
22+
| CompletionItemKindValue
23+
| CompletionItemKindEnum
24+
| CompletionItemKindKeyword
25+
| CompletionItemKindSnippet
26+
| CompletionItemKindColor
27+
| CompletionItemKindFile
28+
| CompletionItemKindReference
29+
| CompletionItemKindFolder
30+
| CompletionItemKindEnumMember
31+
| CompletionItemKindConstant
32+
| CompletionItemKindStruct
33+
| CompletionItemKindEvent
34+
| CompletionItemKindOperator
35+
| CompletionItemKindTypeParameter;
36+
37+
let completion_item_kind_to_yojson = severity =>
38+
completion_item_kind_to_enum(severity) |> [%to_yojson: int];
39+
let completion_item_kind_of_yojson = json =>
40+
Result.bind(json |> [%of_yojson: int], value => {
41+
switch (completion_item_kind_of_enum(value)) {
42+
| Some(severity) => Ok(severity)
43+
| None => Result.Error("Invalid enum value")
44+
}
45+
});
46+
47+
[@deriving yojson]
48+
type completion_item = {
49+
label: string,
50+
kind: completion_item_kind,
51+
detail: string,
52+
documentation: string,
53+
};
54+
55+
// https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#completionParams
56+
module RequestParams = {
57+
[@deriving yojson({strict: false})]
58+
type t = {
59+
[@key "textDocument"]
60+
text_document: Protocol.text_document_identifier,
61+
position: Protocol.position,
62+
};
63+
};
64+
65+
// https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#completionList
66+
module ResponseResult = {
67+
[@deriving yojson]
68+
type t = {
69+
isIncomplete: bool,
70+
items: list(completion_item),
71+
};
72+
};
73+
74+
// Original implementation https://github.com/jaredly/reason-language-server/blob/ce1b3f8ddb554b6498c2a83ea9c53a6bdf0b6081/src/analyze/PartialParser.re#L178-L198
75+
let find_completable = (text, offset) => {
76+
let rec loop = i => {
77+
i < 0
78+
? Some(String_utils.slice(~first=0, ~last=offset + 1, text))
79+
: (
80+
switch (String_utils.char_at(text, i)) {
81+
| Some('a' .. 'z' | 'A' .. 'Z' | '0' .. '9' | '.' | '_') =>
82+
loop(i - 1)
83+
| _ =>
84+
i == offset - 1
85+
? None
86+
: Some(
87+
String_utils.slice(
88+
~first=i + 1,
89+
~last=offset - (i + 1),
90+
text,
91+
),
92+
)
93+
}
94+
);
95+
};
96+
loop(offset - 1);
97+
};
98+
99+
let get_original_text = (documents, uri, line, char) =>
100+
// try and find the code we are completing in the original source
101+
switch (Hashtbl.find_opt(documents, uri)) {
102+
| None => None
103+
| Some(source_code) =>
104+
let lines = String.split_on_char('\n', source_code);
105+
let line = List.nth_opt(lines, line);
106+
Option.bind(line, line => find_completable(line, char));
107+
};
108+
109+
// maps Grain types to LSP CompletionItemKind
110+
let rec get_kind = (desc: Types.type_desc) =>
111+
switch (desc) {
112+
| TTyVar(_) => CompletionItemKindVariable
113+
| TTyArrow(_) => CompletionItemKindFunction
114+
| TTyTuple(_) => CompletionItemKindStruct
115+
| TTyRecord(_) => CompletionItemKindStruct
116+
| TTyConstr(_) => CompletionItemKindConstructor
117+
| TTySubst(s) => get_kind(s.desc)
118+
| TTyLink(t) => get_kind(t.desc)
119+
| _ => CompletionItemKindText
120+
};
121+
122+
let send_completion =
123+
(~id: Protocol.message_id, completions: list(completion_item)) => {
124+
Protocol.response(
125+
~id,
126+
ResponseResult.to_yojson({isIncomplete: false, items: completions}),
127+
);
128+
};
129+
130+
module Resolution = {
131+
// https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#completionItem
132+
module RequestParams = {
133+
// TODO: implement the rest of the fields
134+
[@deriving yojson({strict: false})]
135+
type t = {label: string};
136+
};
137+
138+
// As per https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_completion
139+
// If computing full completion items is expensive, servers can additionally provide a handler for
140+
// the completion item resolve request (‘completionItem/resolve’). This request is sent when a
141+
// completion item is selected in the user interface.
142+
let process =
143+
(
144+
~id: Protocol.message_id,
145+
~compiled_code: Hashtbl.t(Protocol.uri, Typedtree.typed_program),
146+
~cached_code: Hashtbl.t(Protocol.uri, Typedtree.typed_program),
147+
~documents: Hashtbl.t(Protocol.uri, string),
148+
params: RequestParams.t,
149+
) => {
150+
// Right now we just resolve nothing to clear the client's request
151+
// In future we may want to send more details back with Graindoc details for example
152+
send_completion(
153+
~id,
154+
[],
155+
);
156+
};
157+
};
158+
159+
let process =
160+
(
161+
~id: Protocol.message_id,
162+
~compiled_code: Hashtbl.t(Protocol.uri, Typedtree.typed_program),
163+
~cached_code: Hashtbl.t(Protocol.uri, Typedtree.typed_program),
164+
~documents: Hashtbl.t(Protocol.uri, string),
165+
params: RequestParams.t,
166+
) => {
167+
let completable =
168+
get_original_text(
169+
documents,
170+
params.text_document.uri,
171+
params.position.line,
172+
params.position.character,
173+
);
174+
switch (completable) {
175+
| None => send_completion(~id, [])
176+
| Some(text) =>
177+
switch (Hashtbl.find_opt(cached_code, params.text_document.uri)) {
178+
| None => send_completion(~id, [])
179+
| Some(compiled_code) =>
180+
let modules =
181+
Env.fold_modules(
182+
(tag, path, decl, acc) => {
183+
List.append(acc, [Printtyp.string_of_path(path)])
184+
},
185+
None,
186+
compiled_code.env,
187+
[],
188+
);
189+
190+
let completions =
191+
switch (String_utils.char_at(text, 0)) {
192+
| Some('A' .. 'Z') =>
193+
// autocomplete modules
194+
switch (String.rindex(text, '.')) {
195+
| exception exn =>
196+
let types =
197+
Env.fold_types(
198+
(tag, path, (type_decl, type_descs), acc) => {
199+
List.append(acc, [Printtyp.string_of_path(path)])
200+
},
201+
None,
202+
compiled_code.env,
203+
[],
204+
);
205+
206+
let converted_modules =
207+
List.map(
208+
(m: string) => {
209+
let item: completion_item = {
210+
label: m,
211+
kind: CompletionItemKindModule,
212+
detail: "",
213+
documentation: "",
214+
};
215+
item;
216+
},
217+
modules,
218+
);
219+
220+
let converted_types =
221+
List.map(
222+
(t: string) => {
223+
let item: completion_item = {
224+
label: t,
225+
kind: CompletionItemKindStruct,
226+
detail: "",
227+
documentation: "",
228+
};
229+
item;
230+
},
231+
types,
232+
);
233+
234+
converted_modules @ converted_types;
235+
| pos =>
236+
// find module name
237+
238+
let mod_name = String_utils.slice(~first=0, ~last=pos, text);
239+
let ident: Ident.t = {name: mod_name, stamp: 0, flags: 0};
240+
241+
// only look up completions for imported modules
242+
if (!List.exists((m: string) => m == mod_name, modules)) {
243+
[];
244+
} else {
245+
List.map(
246+
(m: Modules.export) => {
247+
let kind =
248+
switch (m.kind) {
249+
| Function => CompletionItemKindFunction
250+
| Value => CompletionItemKindValue
251+
| Record => CompletionItemKindStruct
252+
| Enum => CompletionItemKindEnum
253+
| Abstract => CompletionItemKindTypeParameter
254+
| Exception => CompletionItemKindTypeParameter
255+
};
256+
257+
{
258+
label: m.name,
259+
kind,
260+
detail: m.signature,
261+
documentation: "",
262+
};
263+
},
264+
Modules.get_exports(~path=PIdent(ident), compiled_code),
265+
);
266+
};
267+
}
268+
269+
| _ =>
270+
// Autocompete anything in scope
271+
let values: list((string, Types.value_description)) =
272+
Env.fold_values(
273+
(tag, path, vd, acc) => {
274+
List.append(acc, [(Printtyp.string_of_path(path), vd)])
275+
},
276+
None,
277+
compiled_code.env,
278+
[],
279+
);
280+
281+
List.map(
282+
((i: string, l: Types.value_description)) => {
283+
let item: completion_item = {
284+
label: i,
285+
kind: get_kind(l.val_type.desc),
286+
detail: Printtyp.string_of_type_scheme(l.val_type),
287+
documentation: "",
288+
};
289+
item;
290+
},
291+
values,
292+
);
293+
};
294+
295+
send_completion(~id, completions);
296+
}
297+
};
298+
};
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
open Grain_typed;
2+
3+
// https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#completionParams
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/#completionList
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, Typedtree.typed_program),
19+
~cached_code: Hashtbl.t(Protocol.uri, Typedtree.typed_program),
20+
~documents: Hashtbl.t(Protocol.uri, string),
21+
RequestParams.t
22+
) =>
23+
unit;
24+
25+
module Resolution: {
26+
// https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#completionItem
27+
module RequestParams: {
28+
[@deriving yojson({strict: false})]
29+
type t;
30+
};
31+
32+
let process:
33+
(
34+
~id: Protocol.message_id,
35+
~compiled_code: Hashtbl.t(Protocol.uri, Typedtree.typed_program),
36+
~cached_code: Hashtbl.t(Protocol.uri, Typedtree.typed_program),
37+
~documents: Hashtbl.t(Protocol.uri, string),
38+
RequestParams.t
39+
) =>
40+
unit;
41+
};

compiler/src/language_server/initialize.re

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,14 @@ module RequestParams = {
2727

2828
// https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#initializeResult
2929
module ResponseResult = {
30+
[@deriving yojson]
31+
type completion_values = {
32+
[@key "resolveProvider"]
33+
resolve_provider: bool,
34+
[@key "triggerCharacters"]
35+
trigger_characters: list(string),
36+
};
37+
3038
[@deriving yojson]
3139
type code_values = {
3240
[@key "resolveProvider"]
@@ -41,6 +49,8 @@ module ResponseResult = {
4149
text_document_sync: Protocol.text_document_sync_kind,
4250
[@key "hoverProvider"]
4351
hover_provider: bool,
52+
[@key "completionProvider"]
53+
completion_provider: completion_values,
4454
[@key "definitionProvider"]
4555
definition_provider: bool,
4656
[@key "typeDefinitionProvider"]
@@ -60,13 +70,18 @@ module ResponseResult = {
6070
[@key "renameProvider"]
6171
rename_provider: bool,
6272
};
73+
6374
[@deriving yojson]
6475
type t = {capabilities: lsp_capabilities};
6576

6677
let capabilities = {
6778
document_formatting_provider: true,
6879
text_document_sync: Full,
6980
hover_provider: true,
81+
completion_provider: {
82+
resolve_provider: true,
83+
trigger_characters: ["."],
84+
},
7085
definition_provider: false, // disabled until we can resolve the external module location
7186
type_definition_provider: false,
7287
references_provider: false,

0 commit comments

Comments
 (0)