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
// Set when the cursor sits in the right-hand side of a `|>`,
46
// where the useful candidates are the functions the threaded
47
// subject can be passed to rather than every name in scope.
48
// Carries the subject's type when it has one, so the filter can
49
// judge each candidate's first parameter; null when the subject
50
// did not compile.
51
_thread_first_subject_type: Semantic.Types.Type?
52
_in_thread_first_position: bool
53
54
_thread_first_filter: THREAD_FIRST_CANDIDATE_FILTER
55
56
// Set when the results are the members of something to the left
57
// of a `.` — a member access or a qualified name — rather than
58
// the enclosing scope. A client that asked because the user
59
// typed a `.` reads this to tell an empty answer apart from an
60
// answer about the wrong thing.
61
_is_member_completion: bool
62
63
keyword_results: Collections.Iterable[KEYWORD_COMPLETION] => _keyword_results
64
65
is_member_completion: bool => _is_member_completion
66
67
// The result collections belong to one find_completions call and
68
// are rebuilt at the top of each.
69
@suppress("field-definite-assignment")
70
init(
71
logger: Logger,
72
symbol_table: Semantic.SYMBOL_TABLE,
73
namespaces: Semantic.NAMESPACES,
74
dotnet_symbol_table: LAZY[Semantic.DotNet.SYMBOL_TABLE]
75
)
76
is
77
super.init(logger, symbol_table, namespaces)
78
79
_dotnet_symbol_table = dotnet_symbol_table
80
_thread_first_filter = THREAD_FIRST_CANDIDATE_FILTER()
81
si
82
83
find_completions(root: Trees.Node, target_line: int, target_column: int) -> Collections.Iterable[Pair[string,Semantic.Symbols.Symbol]] is
84
_have_hit = false
85
_in_type_position = false
86
_is_member_completion = false
87
_in_thread_first_position = false
88
_thread_first_subject_type = null
89
90
_target_line = target_line
91
_target_column = target_column
92
93
_results = Collections.MAP[string,Semantic.Symbols.Symbol]()
94
_keyword_results = Collections.LIST[KEYWORD_COMPLETION]()
95
96
root.walk(self)
97
98
_apply_thread_first_filter()
99
_apply_type_position_filter()
100
101
return _results
102
si
103
104
// Note when the cursor sits inside a type-expression node.
105
// The flag is sticky once set: every TypeExpression pre
106
// override below funnels through here, and the cursor can
107
// only sit inside one of them at a time.
108
_check_type_position(node: Trees.Node) is
109
if !_in_type_position /\ node.location.contains(_target_line, _target_column) then
110
_in_type_position = true
111
fi
112
si
113
114
// If the cursor settled in a type-expression position, drop
115
// every non-type-like symbol from the result set. Allowed
116
// kinds: CLASS, INTERFACE (traits), STRUCT, ENUM, MODULE
117
// (namespaces), TYPE_PARAMETER. Locals, fields, functions,
118
// properties, constants, enum members and operators are
119
// never useful in a type expression. Statement keywords
120
// (`if`, `while`, `let`, …) get cleared too — none of them
121
// start a type expression.
122
_apply_type_position_filter() is
123
if !_in_type_position then
124
return
125
fi
126
127
let kept = Collections.MAP[string,Semantic.Symbols.Symbol]()
128
129
for pair in _results do
130
if _is_type_like(pair.value.completion_kind) then
131
kept.add(pair.key, pair.value)
132
fi
133
od
134
135
_results = kept
136
_keyword_results = Collections.LIST[KEYWORD_COMPLETION]()
137
si
138
139
// Reduce the scope dump to the names that could lead the call the
140
// `|>` is on its way to. Keywords go with them: none of them start
141
// a call.
142
_apply_thread_first_filter() is
143
// Only the scope dump is filtered. A `|>` whose call is written
144
// against a receiver or a namespace (`x |> box.combine(a)`)
145
// puts the cursor in a member position inside the same
146
// completion target, and those results are already the right
147
// ones.
148
if !_in_thread_first_position \/ _is_member_completion then
149
return
150
fi
151
152
_results = _thread_first_filter.apply(_results, _thread_first_subject_type)
153
_keyword_results = Collections.LIST[KEYWORD_COMPLETION]()
154
si
155
156
_is_type_like(kind: Semantic.Symbols.CompletionKind) -> bool =>
157
kind == Semantic.Symbols.CompletionKind.CLASS \/
158
kind == Semantic.Symbols.CompletionKind.INTERFACE \/
159
kind == Semantic.Symbols.CompletionKind.STRUCT \/
160
kind == Semantic.Symbols.CompletionKind.ENUM \/
161
kind == Semantic.Symbols.CompletionKind.MODULE \/
162
kind == Semantic.Symbols.CompletionKind.TYPE_PARAMETER
163
164
leave_scope(node: Trees.ScopeCarrier) is
165
if !_have_hit /\ !_is_synthesized_wrapper(node) /\ node.location.contains(_target_line, _target_column) then
166
add_keyword_matches(node)
167
add_scope_matches()
168
fi
169
170
super.leave_scope(node)
171
si
172
173
_is_synthesized_wrapper(node: Trees.ScopeCarrier) -> bool =>
174
if let function = cast Trees.Definitions.FUNCTION?(node) then function.is_synthesized_main_wrapper else false fi
175
176
// A namespace is left through leave_namespace rather than through the
177
// ScopeCarrier overload above, so a cursor in a namespace body - between
178
// definitions, or on the line after a `use` - reaches no scope at all
179
// without this.
180
visit(`namespace: Trees.Definitions.NAMESPACE) is
181
if !_have_hit /\ `namespace.location.contains(_target_line, _target_column) then
182
add_keyword_matches(`namespace)
183
add_scope_matches()
184
fi
185
186
super.visit(`namespace)
187
si
188
189
// The innermost scope containing the cursor is left first, so `node`
190
// here is the tightest enclosing definition / statement scope. Offer
191
// the keywords that can lead a construct in that context. Keywords
192
// whose canonical shape is a multi-line construct carry a snippet
193
// body the client can expand as a tabbable template; the rest insert
194
// their name verbatim.
195
//
196
// Snippet bodies use LSP tabstop syntax: `${N:placeholder}` for an
197
// ordered tabstop with a selectable default, `$0` for the final
198
// cursor position. The cursor starts at `$1` and ends at `$0`. For
199
// multi-line constructs `$0` sits in the body so the user lands
200
// ready to type the block contents after filling the header.
201
add_keyword_matches(node: Trees.ScopeCarrier) is
202
if isa Trees.Definitions.NAMESPACE(node) then
203
_snippet("namespace", "namespace ${1:Name} is\n\t$0\nsi")
204
_plain ("use")
205
_snippet("class", "class ${1:Name} is\n\t$0\nsi")
206
_snippet("trait", "trait ${1:Name} is\n\t$0\nsi")
207
_snippet("struct", "struct ${1:Name} is\n\t$0\nsi")
208
_snippet("union", "union ${1:Name} is\n\t$0\nsi")
209
_snippet("enum", "enum ${1:Name} is\n\t$0\nsi")
210
elif isa Trees.Definitions.ENUM(node) then
211
// an enum body holds enum members, not keyword-led constructs
212
elif isa Trees.Definitions.Classy(node) then
213
_plain ("use")
214
_snippet("class", "class ${1:Name} is\n\t$0\nsi")
215
_snippet("trait", "trait ${1:Name} is\n\t$0\nsi")
216
_snippet("struct", "struct ${1:Name} is\n\t$0\nsi")
217
_snippet("union", "union ${1:Name} is\n\t$0\nsi")
218
_snippet("enum", "enum ${1:Name} is\n\t$0\nsi")
219
_plain ("public")
220
_plain ("private")
221
_plain ("protected")
222
_plain ("static")
223
_plain ("field")
224
_plain ("innate")
225
else
226
_plain ("assert")
227
_snippet("break", "break;\n$0")
228
_snippet("case", "case ${1:expression}\n\twhen ${2:pattern} then\n\t\t$0\nesac")
229
_plain ("cast")
230
_snippet("continue", "continue;\n$0")
231
_snippet("do", "do\n\t$0\nod")
232
_plain ("false")
233
_snippet("for", "for ${1:element} in ${2:iterable} do\n\t$0\nod")
234
_snippet("if", "if ${1:condition} then\n\t$0\nfi")
235
_plain ("isa")
236
_snippet("let", "let $0")
237
_plain ("null")
238
_plain ("return")
239
_plain ("self")
240
_plain ("super")
241
_plain ("throw")
242
_plain ("true")
243
_snippet("try", "try\n\t$0\nyrt")
244
_plain ("typeof")
245
_snippet("while", "while ${1:condition} do\n\t$0\nod")
246
fi
247
si
248
249
_plain(name: string) is
250
_keyword_results.add(KEYWORD_COMPLETION(name, ""))
251
si
252
253
_snippet(name: string, body: string) is
254
_keyword_results.add(KEYWORD_COMPLETION(name, body))
255
si
256
257
add_scope_matches() is
258
find_matches("", _results)
259
260
_namespaces.find_root_matches(_results)
261
_dotnet_symbol_table.value.find_root_matches(_results)
262
263
_have_hit = true
264
si
265
266
visit_literal(location: LOCATION) is
267
if
268
location.contains(_target_line, _target_column) \/
269
location.contains(_target_line, _target_column - 1)
270
then
271
_have_hit = true
272
fi
273
si
274
275
pre(`use: Trees.Definitions.USE) -> bool => true
276
visit(`use: Trees.Definitions.USE) is
277
if !`use.location.contains(_target_line, _target_column) then
278
return
279
fi
280
281
let identifier = `use.`use
282
283
if identifier? /\ identifier.location.contains(_target_line, _target_column) /\ isa Trees.Identifiers.QUALIFIED(identifier) then
284
add_qualified_identifier_matches(identifier)
285
else
286
add_scope_matches()
287
fi
288
si
289
290
visit(literal: Trees.Expressions.Literals.INTEGER) is
291
visit_literal(literal.location)
292
si
293
294
visit(literal: Trees.Expressions.Literals.FLOAT) is
295
visit_literal(literal.location)
296
si
297
298
pre(interpolation: Trees.Expressions.STRING_INTERPOLATION) -> bool is
299
if !interpolation.location.contains(_target_line, _target_column) then
300
return true
301
fi
302
303
for f in interpolation.values do
304
if
305
f.is_expression /\
306
f.expression.location.contains(_target_line, _target_column)
307
then
308
// cursor is in an iterpolated expression - allow the tree walk
309
// to continue and let the appropriate expression node handle it
310
return false
311
fi
312
od
313
314
// none of the expressions contain the cursor - block the tree
315
// walk from continuing
316
_have_hit = true
317
return true
318
si
319
320
visit(literal: Trees.Expressions.STRING_INTERPOLATION) is
321
si
322
323
visit(literal: Trees.Expressions.Literals.CHARACTER) is
324
visit_literal(literal.location)
325
si
326
327
visit(literal: Trees.Expressions.Literals.BOOLEAN) is
328
visit_literal(literal.location)
329
si
330
331
visit(qualified: Trees.Identifiers.QUALIFIED) is
332
if !qualified.completion_target.contains(_target_line, _target_column) then
333
return
334
fi
335
336
add_qualified_identifier_matches(qualified)
337
si
338
339
add_qualified_identifier_matches(qualified: Trees.Identifiers.QUALIFIED) is
340
_is_member_completion = true
341
342
let symbol = find(qualified.qualifier)
343
let namespace_name = qualified.qualifier.to_string()
344
345
if !symbol? then
346
_namespaces.find_namespace_matches(namespace_name, _results)
347
_dotnet_symbol_table.value.find_member_matches(namespace_name, _results)
348
349
Semantic.DotNet.HIDDEN_SYMBOLS.prune_hidden(namespace_name, _results)
350
351
_have_hit = true
352
return
353
fi
354
355
if !isa Semantic.Symbols.Scoped(symbol) then
356
return
357
fi
358
359
symbol.find_member_matches("", _results)
360
361
_namespaces.find_namespace_matches(namespace_name, _results)
362
363
_dotnet_symbol_table.value.find_member_matches(namespace_name, _results)
364
365
Semantic.DotNet.HIDDEN_SYMBOLS.prune_hidden(namespace_name, _results)
366
367
_keep_offered_members(false)
368
369
_have_hit = true
370
si
371
372
// The right-hand side of a `|>`, whether or not the call is
373
// finished being typed. The subject has already been threaded in
374
// as argument 0, so its compiled type is what the candidates are
375
// judged against. The walk still continues: a cursor inside the
376
// remaining arguments is an ordinary expression position, and the
377
// completion target covers only the operator and the called name.
378
pre(call: Trees.Expressions.CALL) -> bool is
379
let target = call.completion_target
380
381
if
382
!call.is_thread_first \/ !target? \/
383
!target.contains(_target_line, _target_column)
384
then
385
return false
386
fi
387
388
_in_thread_first_position = true
389
390
let subject = call.arguments.expressions
391
392
if subject.count > 0 then
393
if let value = subject[0].value then
394
_thread_first_subject_type = value.type
395
fi
396
fi
397
398
return false
399
si
400
401
visit(member: Trees.Expressions.MEMBER) is
402
if !member.completion_target.contains(_target_line, _target_column) then
403
return
404
fi
405
406
// The cursor sits inside this member-access expression's
407
// completion target. Anything we return is "members of the
408
// LHS", never a scope dump — claim the hit up front so the
409
// enclosing leave_scope fallback can't add unrelated
410
// identifiers, keywords, or root namespaces.
411
_have_hit = true
412
_is_member_completion = true
413
414
let symbol = _resolve_member_lhs_symbol(member.left)
415
416
if !symbol? then
417
return
418
fi
419
420
symbol.find_member_matches("", _results)
421
422
_keep_offered_members(isa Trees.Expressions.SELF(member.left) \/ isa Trees.Expressions.SUPER(member.left))
423
si
424
425
// Drops what a program cannot write after this dot.
426
_keep_offered_members(from_inside: bool) is
427
let kept = Collections.MAP[string,Semantic.Symbols.Symbol]()
428
429
for pair in _results do
430
if MEMBER_COMPLETION_FILTER.is_offered(pair.key, pair.value, from_inside) then
431
kept.add(pair.key, pair.value)
432
fi
433
od
434
435
_results = kept
436
si
437
438
// Type-expression pre overrides — every subclass funnels
439
// through `_check_type_position` so the sticky type-position
440
// flag is set whenever the cursor lands in one. The walk
441
// continues into children normally (return false).
442
pre(named: Trees.TypeExpressions.NAMED) -> bool is _check_type_position(named); return false; si
443
pre(generic: Trees.TypeExpressions.GENERIC) -> bool is _check_type_position(generic); return false; si
444
pre(member: Trees.TypeExpressions.MEMBER) -> bool is _check_type_position(member); return false; si
445
pre(function: Trees.TypeExpressions.FUNCTION) -> bool is _check_type_position(function); return false; si
446
pre(functions: Trees.TypeExpressions.FUNCTION_GROUP) -> bool is _check_type_position(functions); return false; si
447
pre(tuple: Trees.TypeExpressions.TUPLE) -> bool is _check_type_position(tuple); return false; si
448
pre(array: Trees.TypeExpressions.ARRAY_) -> bool is _check_type_position(array); return false; si
449
pre(pointer: Trees.TypeExpressions.POINTER) -> bool is _check_type_position(pointer); return false; si
450
pre(optional: Trees.TypeExpressions.OPTIONAL) -> bool is _check_type_position(optional); return false; si
451
pre(reference: Trees.TypeExpressions.REFERENCE) -> bool is _check_type_position(reference); return false; si
452
pre(infer: Trees.TypeExpressions.INFER) -> bool is _check_type_position(infer); return false; si
453
pre(undefined: Trees.TypeExpressions.UNDEFINED) -> bool is _check_type_position(undefined); return false; si
454
pre(constraint: Trees.TypeExpressions.TYPE_PARAMETER_CONSTRAINT) -> bool is _check_type_position(constraint); return false; si
455
456
// A NAMED_TUPLE_ELEMENT is either a tuple element (`(x: int,
457
// y: int)`), a function literal parameter (`(x: int) -> int`),
458
// or a generic type parameter declaration (`[T: Foo]`). The
459
// `name` slot in every one of those is a name the user is
460
// *introducing* — not a position to suggest existing symbols.
461
// When the cursor sits there, claim the hit and block the
462
// walk into children so the scope fallback can't dump.
463
// Otherwise treat the element as ordinary type-position
464
// context.
465
pre(element: Trees.TypeExpressions.NAMED_TUPLE_ELEMENT) -> bool is
466
if element.name.location.contains(_target_line, _target_column) then
467
_have_hit = true
468
return true
469
fi
470
471
_check_type_position(element)
472
return false
473
si
474
475
// When the user has typed `let x:` with nothing after, the
476
// parser produces an UNDEFINED type expression sitting on
477
// the next concrete token (often a line below) — its
478
// location doesn't cover the cursor, so the type-expression
479
// pre hooks above never fire. Recognise the type-position
480
// gap explicitly: a VARIABLE whose declared type is
481
// explicit, where the cursor is strictly past the variable
482
// name's end and strictly before the initializer's start
483
// (or there is no initializer), must be in the type slot.
484
pre(variable: Trees.Variables.VARIABLE) -> bool is
485
if !variable.is_explicit_type \/ !variable.location.contains(_target_line, _target_column) then
486
return false
487
fi
488
489
let cursor = Source.LOCATION.pair(_target_line, _target_column)
490
491
if variable.left.location.end >= cursor then
492
return false
493
fi
494
495
if variable.initializer? /\ variable.initializer.location.start <= cursor then
496
return false
497
fi
498
499
_in_type_position = true
500
return false
501
si
502
503
// Mirror the VARIABLE gap rule for a function's return type:
504
// the parser may park an UNDEFINED return type on the next
505
// token, away from the cursor. When the cursor sits inside
506
// the FUNCTION but strictly past the arguments' close-paren
507
// and strictly before the body's start (or there is no
508
// body), it's in the return-type slot. Forward to super so
509
// the function's scope still gets entered.
510
pre(function: Trees.Definitions.FUNCTION) -> bool is
511
let result = super.pre(function)
512
513
// A function the compiler writes around top-level statements is
514
// located at those statements, as are the parameters and return
515
// type it gives the entry, so none of them can be what the cursor
516
// is on. Only the statements themselves are walked.
517
if function.is_synthesized_main_wrapper then
518
return true
519
fi
520
521
if function.is_top_level_entry then
522
if let body = function.body then
523
body.walk(self)
524
525
// The statements end at their last token, so a cursor
526
// just after it - where a name being typed at the end
527
// of the file is completed - lies in no scope. It is
528
// still in the statements, and completes from them.
529
if !_have_hit /\ body.location.start <= Source.LOCATION.pair(_target_line, _target_column) then
530
add_keyword_matches(function)
531
add_scope_matches()
532
fi
533
fi
534
535
return true
536
fi
537
538
if !function.location.contains(_target_line, _target_column) then
539
return result
540
fi
541
542
let cursor = Source.LOCATION.pair(_target_line, _target_column)
543
544
if function.arguments.location.end >= cursor then
545
return result
546
fi
547
548
if function.body? /\ function.body.location.start <= cursor then
549
return result
550
fi
551
552
_in_type_position = true
553
return result
554
si
555
556
// Resolve the LHS of a member-access expression to the symbol
557
// whose members the completer should enumerate. Three sources,
558
// in order: the IR value's type for evaluated values (the
559
// common `local.|` case); the named identifier the LHS copies
560
// to for type / namespace references (`TYPE.|`, `Outer.Inner.|`,
561
// `IO.|`); null when neither resolves.
562
_resolve_member_lhs_symbol(left: Trees.Expressions.Expression) -> Semantic.Symbols.Symbol? is
563
if let value = left.value /\ value.type? /\ value.type.is_named then
564
let type = value.type
565
566
if type.is_ref then
567
// is_ref implies an element type; the runtime
568
// invariant guarantees get_element_type() non-null.
569
return type.get_element_type()!.symbol
570
fi
571
572
return type.symbol
573
fi
574
575
let identifier = left.try_copy_as_identifer()
576
577
if identifier? then
578
return try_find(identifier)
579
fi
580
581
return null
582
si
583
si
584
si