Skip to content
← Back

src/syntax/process/completer.ghul

1
namespace Syntax.Process is
2
use LAZY = System.Lazy;
3
4
use IO.Std;
5
6
use Pair = Collections.KeyValuePair;
7
8
use Logging;
9
use Source;
10
11
// One context-appropriate keyword to offer, optionally with a snippet body.
12
// The body uses LSP tabstop syntax (`$1`, `$2`, `$0`); empty body means
13
// "insert `name` verbatim". The COMPLETION_HANDLER forwards both fields
14
// through to the client; the VSCE turns a non-empty body into a tabbable
15
// snippet insertion.
16
class KEYWORD_COMPLETION is
17
name: string public;
18
snippet: string public;
19
20
init(name: string, snippet: string) is
21
self.name = name;
22
self.snippet = snippet;
23
si
24
si
25
26
class COMPLETER: ScopedVisitor is
27
_target_line: int;
28
_target_column: int;
29
30
_dotnet_symbol_table: LAZY[Semantic.DotNet.SYMBOL_TABLE];
31
32
_results: Collections.MAP[string,Semantic.Symbols.Symbol];
33
_keyword_results: Collections.LIST[KEYWORD_COMPLETION];
34
35
_have_hit: bool;
36
37
// Sticky: set to true when the cursor sits inside any
38
// type-expression node, so the final result set can be
39
// filtered down to type-like kinds (classes, traits,
40
// structs, enums, namespaces, type parameters). Values
41
// — locals, fields, functions, properties, constants —
42
// never belong in a type-expression position.
43
_in_type_position: bool;
44
45
keyword_results: Collections.Iterable[KEYWORD_COMPLETION] => _keyword_results;
46
47
init(
48
logger: Logger,
49
symbol_table: Semantic.SYMBOL_TABLE,
50
namespaces: Semantic.NAMESPACES,
51
dotnet_symbol_table: LAZY[Semantic.DotNet.SYMBOL_TABLE]
52
)
53
is
54
super.init(logger, symbol_table, namespaces);
55
56
_dotnet_symbol_table = dotnet_symbol_table;
57
si
58
59
find_completions(root: Trees.Node, target_line: int, target_column: int) -> Collections.Iterable[Pair[string,Semantic.Symbols.Symbol]] is
60
_have_hit = false;
61
_in_type_position = false;
62
63
_target_line = target_line;
64
_target_column = target_column;
65
66
_results = Collections.MAP[string,Semantic.Symbols.Symbol]();
67
_keyword_results = Collections.LIST[KEYWORD_COMPLETION]();
68
69
root.walk(self);
70
71
_apply_type_position_filter();
72
73
return _results;
74
si
75
76
// Note when the cursor sits inside a type-expression node.
77
// The flag is sticky once set: every TypeExpression pre
78
// override below funnels through here, and the cursor can
79
// only sit inside one of them at a time.
80
_check_type_position(node: Trees.Node) is
81
if !_in_type_position /\ node.location.contains(_target_line, _target_column) then
82
_in_type_position = true;
83
fi
84
si
85
86
// If the cursor settled in a type-expression position, drop
87
// every non-type-like symbol from the result set. Allowed
88
// kinds: CLASS, INTERFACE (traits), STRUCT, ENUM, MODULE
89
// (namespaces), TYPE_PARAMETER. Locals, fields, functions,
90
// properties, constants, enum members and operators are
91
// never useful in a type expression. Statement keywords
92
// (`if`, `while`, `let`, …) get cleared too — none of them
93
// start a type expression.
94
_apply_type_position_filter() is
95
if !_in_type_position then
96
return;
97
fi
98
99
let kept = Collections.MAP[string,Semantic.Symbols.Symbol]();
100
101
for pair in _results do
102
if _is_type_like(pair.value.completion_kind) then
103
kept.add(pair.key, pair.value);
104
fi
105
od
106
107
_results = kept;
108
_keyword_results = Collections.LIST[KEYWORD_COMPLETION]();
109
si
110
111
_is_type_like(kind: Semantic.Symbols.CompletionKind) -> bool =>
112
kind == Semantic.Symbols.CompletionKind.CLASS \/
113
kind == Semantic.Symbols.CompletionKind.INTERFACE \/
114
kind == Semantic.Symbols.CompletionKind.STRUCT \/
115
kind == Semantic.Symbols.CompletionKind.ENUM \/
116
kind == Semantic.Symbols.CompletionKind.MODULE \/
117
kind == Semantic.Symbols.CompletionKind.TYPE_PARAMETER;
118
119
leave_scope(node: Trees.ScopeCarrier) is
120
if !_have_hit /\ node.location.contains(_target_line, _target_column) then
121
add_keyword_matches(node);
122
add_scope_matches();
123
fi
124
125
super.leave_scope(node);
126
si
127
128
// The innermost scope containing the cursor is left first, so `node`
129
// here is the tightest enclosing definition / statement scope. Offer
130
// the keywords that can lead a construct in that context. Keywords
131
// whose canonical shape is a multi-line construct carry a snippet
132
// body the client can expand as a tabbable template; the rest insert
133
// their name verbatim.
134
//
135
// Snippet bodies use LSP tabstop syntax: `${N:placeholder}` for an
136
// ordered tabstop with a selectable default, `$0` for the final
137
// cursor position. The cursor starts at `$1` and ends at `$0`. For
138
// multi-line constructs `$0` sits in the body so the user lands
139
// ready to type the block contents after filling the header.
140
add_keyword_matches(node: Trees.ScopeCarrier) is
141
if isa Trees.Definitions.NAMESPACE(node) then
142
_snippet("namespace", "namespace ${1:Name} is\n\t$0\nsi");
143
_plain ("use");
144
_snippet("class", "class ${1:Name} is\n\t$0\nsi");
145
_snippet("trait", "trait ${1:Name} is\n\t$0\nsi");
146
_snippet("struct", "struct ${1:Name} is\n\t$0\nsi");
147
_snippet("union", "union ${1:Name} is\n\t$0\nsi");
148
_snippet("enum", "enum ${1:Name} is\n\t$0\nsi");
149
elif isa Trees.Definitions.ENUM(node) then
150
// an enum body holds enum members, not keyword-led constructs
151
elif isa Trees.Definitions.Classy(node) then
152
_plain ("use");
153
_snippet("class", "class ${1:Name} is\n\t$0\nsi");
154
_snippet("trait", "trait ${1:Name} is\n\t$0\nsi");
155
_snippet("struct", "struct ${1:Name} is\n\t$0\nsi");
156
_snippet("union", "union ${1:Name} is\n\t$0\nsi");
157
_snippet("enum", "enum ${1:Name} is\n\t$0\nsi");
158
_plain ("public");
159
_plain ("private");
160
_plain ("protected");
161
_plain ("static");
162
_plain ("field");
163
_plain ("innate");
164
else
165
_plain ("assert");
166
_snippet("break", "break;\n$0");
167
_snippet("case", "case ${1:expression}\n\twhen ${2:pattern} then\n\t\t$0\nesac");
168
_plain ("cast");
169
_snippet("continue", "continue;\n$0");
170
_snippet("do", "do\n\t$0\nod");
171
_plain ("false");
172
_snippet("for", "for ${1:element} in ${2:iterable} do\n\t$0\nod");
173
_snippet("if", "if ${1:condition} then\n\t$0\nfi");
174
_plain ("isa");
175
_snippet("let", "let $0");
176
_plain ("null");
177
_plain ("return");
178
_plain ("self");
179
_plain ("super");
180
_plain ("throw");
181
_plain ("true");
182
_snippet("try", "try\n\t$0\nyrt");
183
_plain ("typeof");
184
_snippet("while", "while ${1:condition} do\n\t$0\nod");
185
fi
186
si
187
188
_plain(name: string) is
189
_keyword_results.add(KEYWORD_COMPLETION(name, ""));
190
si
191
192
_snippet(name: string, body: string) is
193
_keyword_results.add(KEYWORD_COMPLETION(name, body));
194
si
195
196
add_scope_matches() is
197
find_matches("", _results);
198
199
_namespaces.find_root_matches(_results);
200
_dotnet_symbol_table.value.find_root_matches(_results);
201
202
_have_hit = true;
203
si
204
205
visit_literal(location: LOCATION) is
206
if
207
location.contains(_target_line, _target_column) \/
208
location.contains(_target_line, _target_column - 1)
209
then
210
_have_hit = true;
211
fi
212
si
213
214
pre(`use: Trees.Definitions.USE) -> bool => true;
215
visit(`use: Trees.Definitions.USE) is
216
if !`use.location.contains(_target_line, _target_column) then
217
return;
218
fi
219
220
let identifier = `use.`use;
221
222
if identifier? /\ identifier.location.contains(_target_line, _target_column) /\ isa Trees.Identifiers.QUALIFIED(identifier) then
223
add_qualified_identifier_matches(identifier);
224
else
225
add_scope_matches();
226
fi
227
si
228
229
visit(literal: Trees.Expressions.Literals.INTEGER) is
230
visit_literal(literal.location);
231
si
232
233
visit(literal: Trees.Expressions.Literals.FLOAT) is
234
visit_literal(literal.location);
235
si
236
237
pre(interpolation: Trees.Expressions.STRING_INTERPOLATION) -> bool is
238
if !interpolation.location.contains(_target_line, _target_column) then
239
return true;
240
fi
241
242
for f in interpolation.values do
243
if
244
f.is_expression /\
245
f.expression.location.contains(_target_line, _target_column)
246
then
247
// cursor is in an iterpolated expression - allow the tree walk
248
// to continue and let the appropriate expression node handle it
249
return false;
250
fi
251
od
252
253
// none of the expressions contain the cursor - block the tree
254
// walk from continuing
255
_have_hit = true;
256
return true;
257
si
258
259
visit(literal: Trees.Expressions.STRING_INTERPOLATION) is
260
si
261
262
visit(literal: Trees.Expressions.Literals.CHARACTER) is
263
visit_literal(literal.location);
264
si
265
266
visit(literal: Trees.Expressions.Literals.BOOLEAN) is
267
visit_literal(literal.location);
268
si
269
270
visit(qualified: Trees.Identifiers.QUALIFIED) is
271
if !qualified.completion_target.contains(_target_line, _target_column) then
272
return;
273
fi
274
275
add_qualified_identifier_matches(qualified);
276
si
277
278
add_qualified_identifier_matches(qualified: Trees.Identifiers.QUALIFIED) is
279
let symbol = find(qualified.qualifier);
280
let namespace_name = qualified.qualifier.to_string();
281
282
if !symbol? then
283
_namespaces.find_namespace_matches(namespace_name, _results);
284
_dotnet_symbol_table.value.find_member_matches(namespace_name, _results);
285
286
_have_hit = true;
287
return;
288
fi
289
290
if !isa Semantic.Symbols.Scoped(symbol) then
291
return;
292
fi
293
294
symbol.find_member_matches("", _results);
295
296
_namespaces.find_namespace_matches(namespace_name, _results);
297
298
_dotnet_symbol_table.value.find_member_matches(namespace_name, _results);
299
300
_have_hit = true;
301
si
302
303
visit(member: Trees.Expressions.MEMBER) is
304
if !member.completion_target.contains(_target_line, _target_column) then
305
return;
306
fi
307
308
// The cursor sits inside this member-access expression's
309
// completion target. Anything we return is "members of the
310
// LHS", never a scope dump — claim the hit up front so the
311
// enclosing leave_scope fallback can't add unrelated
312
// identifiers, keywords, or root namespaces.
313
_have_hit = true;
314
315
if member.left == null then
316
return;
317
fi
318
319
let symbol = _resolve_member_lhs_symbol(member.left);
320
321
if !symbol? then
322
return;
323
fi
324
325
symbol.find_member_matches("", _results);
326
si
327
328
// Type-expression pre overrides — every subclass funnels
329
// through `_check_type_position` so the sticky type-position
330
// flag is set whenever the cursor lands in one. The walk
331
// continues into children normally (return false).
332
pre(named: Trees.TypeExpressions.NAMED) -> bool is _check_type_position(named); return false; si
333
pre(generic: Trees.TypeExpressions.GENERIC) -> bool is _check_type_position(generic); return false; si
334
pre(member: Trees.TypeExpressions.MEMBER) -> bool is _check_type_position(member); return false; si
335
pre(function: Trees.TypeExpressions.FUNCTION) -> bool is _check_type_position(function); return false; si
336
pre(functions: Trees.TypeExpressions.FUNCTION_GROUP) -> bool is _check_type_position(functions); return false; si
337
pre(tuple: Trees.TypeExpressions.TUPLE) -> bool is _check_type_position(tuple); return false; si
338
pre(array: Trees.TypeExpressions.ARRAY_) -> bool is _check_type_position(array); return false; si
339
pre(pointer: Trees.TypeExpressions.POINTER) -> bool is _check_type_position(pointer); return false; si
340
pre(optional: Trees.TypeExpressions.OPTIONAL) -> bool is _check_type_position(optional); return false; si
341
pre(reference: Trees.TypeExpressions.REFERENCE) -> bool is _check_type_position(reference); return false; si
342
pre(infer: Trees.TypeExpressions.INFER) -> bool is _check_type_position(infer); return false; si
343
pre(undefined: Trees.TypeExpressions.UNDEFINED) -> bool is _check_type_position(undefined); return false; si
344
pre(constraint: Trees.TypeExpressions.TYPE_PARAMETER_CONSTRAINT) -> bool is _check_type_position(constraint); return false; si
345
346
// A NAMED_TUPLE_ELEMENT is either a tuple element (`(x: int,
347
// y: int)`), a function literal parameter (`(x: int) -> int`),
348
// or a generic type parameter declaration (`[T: Foo]`). The
349
// `name` slot in every one of those is a name the user is
350
// *introducing* — not a position to suggest existing symbols.
351
// When the cursor sits there, claim the hit and block the
352
// walk into children so the scope fallback can't dump.
353
// Otherwise treat the element as ordinary type-position
354
// context.
355
pre(element: Trees.TypeExpressions.NAMED_TUPLE_ELEMENT) -> bool is
356
if element.name.location.contains(_target_line, _target_column) then
357
_have_hit = true;
358
return true;
359
fi
360
361
_check_type_position(element);
362
return false;
363
si
364
365
// When the user has typed `let x:` with nothing after, the
366
// parser produces an UNDEFINED type expression sitting on
367
// the next concrete token (often a line below) — its
368
// location doesn't cover the cursor, so the type-expression
369
// pre hooks above never fire. Recognise the type-position
370
// gap explicitly: a VARIABLE whose declared type is
371
// explicit, where the cursor is strictly past the variable
372
// name's end and strictly before the initializer's start
373
// (or there is no initializer), must be in the type slot.
374
pre(variable: Trees.Variables.VARIABLE) -> bool is
375
if !variable.is_explicit_type \/ !variable.location.contains(_target_line, _target_column) then
376
return false;
377
fi
378
379
let cursor = Source.LOCATION.pair(_target_line, _target_column);
380
381
if variable.left.location.end >= cursor then
382
return false;
383
fi
384
385
if variable.initializer? /\ variable.initializer.location.start <= cursor then
386
return false;
387
fi
388
389
_in_type_position = true;
390
return false;
391
si
392
393
// Mirror the VARIABLE gap rule for a function's return type:
394
// the parser may park an UNDEFINED return type on the next
395
// token, away from the cursor. When the cursor sits inside
396
// the FUNCTION but strictly past the arguments' close-paren
397
// and strictly before the body's start (or there is no
398
// body), it's in the return-type slot. Forward to super so
399
// the function's scope still gets entered.
400
pre(function: Trees.Definitions.FUNCTION) -> bool is
401
let result = super.pre(function);
402
403
if !function.location.contains(_target_line, _target_column) then
404
return result;
405
fi
406
407
let cursor = Source.LOCATION.pair(_target_line, _target_column);
408
409
if function.arguments.location.end >= cursor then
410
return result;
411
fi
412
413
if function.body? /\ function.body.location.start <= cursor then
414
return result;
415
fi
416
417
_in_type_position = true;
418
return result;
419
si
420
421
// Resolve the LHS of a member-access expression to the symbol
422
// whose members the completer should enumerate. Three sources,
423
// in order: the IR value's type for evaluated values (the
424
// common `local.|` case); the named identifier the LHS copies
425
// to for type / namespace references (`TYPE.|`, `Outer.Inner.|`,
426
// `IO.|`); null when neither resolves.
427
_resolve_member_lhs_symbol(left: Trees.Expressions.Expression) -> Semantic.Symbols.Symbol? is
428
if let value = left.value /\ value.type? /\ value.type!.is_named then
429
let type = value.type;
430
431
if type? /\ type.is_ref then
432
// is_ref implies an element type; the runtime
433
// invariant guarantees get_element_type() non-null.
434
return type.get_element_type()!.symbol;
435
fi
436
437
return type!.symbol;
438
fi
439
440
let identifier = left.try_copy_as_identifer();
441
442
if identifier? then
443
return try_find(identifier);
444
fi
445
446
return null;
447
si
448
si
449
si