umple-lsp-server 0.4.2 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +10 -0
- package/README.md +35 -0
- package/completions.scm +9 -3
- package/definitions.scm +5 -0
- package/highlights.scm +487 -0
- package/out/codeActions.d.ts +31 -0
- package/out/codeActions.js +361 -0
- package/out/codeActions.js.map +1 -0
- package/out/completionAnalysis.d.ts +9 -1
- package/out/completionAnalysis.js +1211 -64
- package/out/completionAnalysis.js.map +1 -1
- package/out/completionBuilder.d.ts +1 -1
- package/out/completionBuilder.js +463 -319
- package/out/completionBuilder.js.map +1 -1
- package/out/completionTriggers.d.ts +20 -0
- package/out/completionTriggers.js +69 -0
- package/out/completionTriggers.js.map +1 -0
- package/out/diagnosticSources.d.ts +3 -0
- package/out/diagnosticSources.js +11 -0
- package/out/diagnosticSources.js.map +1 -0
- package/out/diagramNavigation.js +3 -3
- package/out/diagramNavigation.js.map +1 -1
- package/out/documentSymbolBuilder.js +2 -37
- package/out/documentSymbolBuilder.js.map +1 -1
- package/out/formatter.d.ts +13 -1
- package/out/formatter.js +303 -10
- package/out/formatter.js.map +1 -1
- package/out/hoverBuilder.js +90 -23
- package/out/hoverBuilder.js.map +1 -1
- package/out/inlayHints.d.ts +21 -0
- package/out/inlayHints.js +98 -0
- package/out/inlayHints.js.map +1 -0
- package/out/referenceSearch.d.ts +1 -1
- package/out/referenceSearch.js +134 -7
- package/out/referenceSearch.js.map +1 -1
- package/out/resolver.js +82 -3
- package/out/resolver.js.map +1 -1
- package/out/semanticTokens.d.ts +32 -0
- package/out/semanticTokens.js +228 -0
- package/out/semanticTokens.js.map +1 -0
- package/out/server.js +216 -36
- package/out/server.js.map +1 -1
- package/out/snippets.d.ts +39 -0
- package/out/snippets.js +328 -0
- package/out/snippets.js.map +1 -0
- package/out/symbolIndex.d.ts +50 -0
- package/out/symbolIndex.js +170 -7
- package/out/symbolIndex.js.map +1 -1
- package/out/symbolPresentation.d.ts +3 -0
- package/out/symbolPresentation.js +45 -0
- package/out/symbolPresentation.js.map +1 -0
- package/out/symbolTypes.d.ts +1 -0
- package/out/tokenAnalysis.js +77 -4
- package/out/tokenAnalysis.js.map +1 -1
- package/out/tokenTypes.d.ts +8 -1
- package/out/tokenTypes.js +2 -0
- package/out/tokenTypes.js.map +1 -1
- package/out/treeUtils.js +17 -4
- package/out/treeUtils.js.map +1 -1
- package/out/workspaceSymbolBuilder.d.ts +3 -0
- package/out/workspaceSymbolBuilder.js +117 -0
- package/out/workspaceSymbolBuilder.js.map +1 -0
- package/package.json +5 -2
- package/references.scm +31 -3
- package/tree-sitter-umple.wasm +0 -0
- package/out/bin.d.ts +0 -2
- package/out/bin.js +0 -5
- package/out/bin.js.map +0 -1
- package/out/log.d.ts +0 -7
- package/out/log.js +0 -22
- package/out/log.js.map +0 -1
- package/out/tsconfig.tsbuildinfo +0 -1
|
@@ -51,6 +51,61 @@ const STRUCTURAL_TOKENS = new Set([
|
|
|
51
51
|
function isOperatorToken(name) {
|
|
52
52
|
return /^[<>-]/.test(name) && name.length > 1;
|
|
53
53
|
}
|
|
54
|
+
// ── Typed-prefix detection helpers (topic 048 phase 1) ──────────────────────
|
|
55
|
+
/**
|
|
56
|
+
* The standard gate for typed-prefix detection: letter-leading identifier
|
|
57
|
+
* nodes. Shared by every typed-prefix classification block. Letter-leading
|
|
58
|
+
* ensures we don't misclassify digit-leading tokens (multiplicity bits, etc.)
|
|
59
|
+
* that the parser occasionally recovers as `identifier`.
|
|
60
|
+
*/
|
|
61
|
+
function isLetterLeadingIdentifier(node) {
|
|
62
|
+
return node.type === "identifier" && /^[A-Za-z_]/.test(node.text);
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Walk up from `start` looking for a `type_name` node. On hit, return true
|
|
66
|
+
* iff type_name's parent rule is in `parentTypes` AND the child field name
|
|
67
|
+
* equals `fieldName`. Bails at any node in `boundaryTypes`. Used by the
|
|
68
|
+
* declaration-type and return-type typed-prefix detections, whose shapes are
|
|
69
|
+
* `identifier < qualified_name < type_name < <parent-rule>` with a specific
|
|
70
|
+
* grammar-level field binding the type_name into its parent.
|
|
71
|
+
*/
|
|
72
|
+
function isInsideTypeNameFieldSlot(start, parentTypes, fieldName, boundaryTypes) {
|
|
73
|
+
let n = start.parent;
|
|
74
|
+
while (n) {
|
|
75
|
+
if (n.type === "type_name") {
|
|
76
|
+
const p = n.parent;
|
|
77
|
+
if (p && parentTypes.has(p.type)) {
|
|
78
|
+
for (let i = 0; i < p.childCount; i++) {
|
|
79
|
+
if (p.child(i)?.id === n.id) {
|
|
80
|
+
return p.fieldNameForChild(i) === fieldName;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
if (boundaryTypes.has(n.type))
|
|
87
|
+
return false;
|
|
88
|
+
n = n.parent;
|
|
89
|
+
}
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
const TYPE_SLOT_BOUNDARIES = new Set([
|
|
93
|
+
"class_definition",
|
|
94
|
+
"trait_definition",
|
|
95
|
+
"interface_definition",
|
|
96
|
+
"association_class_definition",
|
|
97
|
+
"source_file",
|
|
98
|
+
]);
|
|
99
|
+
const DECL_TYPE_PARENTS = new Set([
|
|
100
|
+
"attribute_declaration",
|
|
101
|
+
"const_declaration",
|
|
102
|
+
]);
|
|
103
|
+
const RETURN_TYPE_PARENTS = new Set([
|
|
104
|
+
"method_declaration",
|
|
105
|
+
"abstract_method_declaration",
|
|
106
|
+
"method_signature",
|
|
107
|
+
"trait_method_signature",
|
|
108
|
+
]);
|
|
54
109
|
// ── Main analysis function ──────────────────────────────────────────────────
|
|
55
110
|
/**
|
|
56
111
|
* Analyze the completion context at a given position in a parsed tree.
|
|
@@ -79,6 +134,32 @@ function analyzeCompletion(tree, language, completionsQuery, content, line, colu
|
|
|
79
134
|
if (nodeAtCursor && isInsideComment(nodeAtCursor)) {
|
|
80
135
|
return { ...empty, isComment: true };
|
|
81
136
|
}
|
|
137
|
+
// Pre-compute prevLeaf — needed by both the suppression ladder
|
|
138
|
+
// (`isInsideMethodParamListStart` distinguishes param-type slots from
|
|
139
|
+
// suppress positions using prevLeaf) and the rest of the analyzer.
|
|
140
|
+
const earlyPrevLeaf = findPreviousLeaf(tree, content, line, column);
|
|
141
|
+
const cursorOffset = offsetAt(content, line, column);
|
|
142
|
+
if (earlyPrevLeaf?.type === "->" && earlyPrevLeaf.endIndex === cursorOffset) {
|
|
143
|
+
return empty;
|
|
144
|
+
}
|
|
145
|
+
// --- Topic 050: contexts where any popup would be wrong ────────────────
|
|
146
|
+
// Each guard returns `empty` (symbolKinds=null, keywords=[], operators=[])
|
|
147
|
+
// so completionBuilder shows nothing. Order matters: java_annotation
|
|
148
|
+
// first because it can wrap content that resembles other contexts; the
|
|
149
|
+
// structural ones (param-list, attribute initializer) before the textual
|
|
150
|
+
// identifier-fragment guard.
|
|
151
|
+
if (nodeAtCursor && isInsideJavaAnnotation(nodeAtCursor))
|
|
152
|
+
return empty;
|
|
153
|
+
if (nodeAtCursor && isInsideMethodParamListStart(nodeAtCursor, earlyPrevLeaf))
|
|
154
|
+
return empty;
|
|
155
|
+
if (nodeAtCursor && isInsideAttributeInitializer(nodeAtCursor))
|
|
156
|
+
return empty;
|
|
157
|
+
if (isInsideMalformedDashIdentifier(content, line, column, nodeAtCursor))
|
|
158
|
+
return empty;
|
|
159
|
+
if (nodeAtCursor && isBareCompleteMultiplicityAtEnd(nodeAtCursor, content, line, column))
|
|
160
|
+
return empty;
|
|
161
|
+
if (nodeAtCursor && isInsideBrokenMethodNameSlot(nodeAtCursor))
|
|
162
|
+
return empty;
|
|
82
163
|
// --- Extract prefix from the token at cursor ---
|
|
83
164
|
let prefix = "";
|
|
84
165
|
if (column > 0 &&
|
|
@@ -96,7 +177,7 @@ function analyzeCompletion(tree, language, completionsQuery, content, line, colu
|
|
|
96
177
|
return { ...empty, isDefinitionName: true };
|
|
97
178
|
}
|
|
98
179
|
// --- LookaheadIterator for keywords ---
|
|
99
|
-
const prevLeaf =
|
|
180
|
+
const prevLeaf = earlyPrevLeaf;
|
|
100
181
|
const stateId = prevLeaf
|
|
101
182
|
? prevLeaf.nextParseState
|
|
102
183
|
: (nodeAtCursor?.parseState ?? 0);
|
|
@@ -147,7 +228,21 @@ function analyzeCompletion(tree, language, completionsQuery, content, line, colu
|
|
|
147
228
|
}
|
|
148
229
|
// --- Trace completion fallback for zero-identifier case ("trace |") ---
|
|
149
230
|
// Trace entity fallbacks for zero-identifier recovery
|
|
150
|
-
const TRACE_PREFIX_KEYWORDS = new Set([
|
|
231
|
+
const TRACE_PREFIX_KEYWORDS = new Set([
|
|
232
|
+
"trace",
|
|
233
|
+
"set",
|
|
234
|
+
"get",
|
|
235
|
+
"onlyGet",
|
|
236
|
+
"onlySet",
|
|
237
|
+
"in",
|
|
238
|
+
"out",
|
|
239
|
+
"entry",
|
|
240
|
+
"exit",
|
|
241
|
+
"cardinality",
|
|
242
|
+
"add",
|
|
243
|
+
"remove",
|
|
244
|
+
"transition",
|
|
245
|
+
]);
|
|
151
246
|
if (prevLeaf && TRACE_PREFIX_KEYWORDS.has(prevLeaf.type)) {
|
|
152
247
|
// Check if inside a trace_statement or ERROR under class body
|
|
153
248
|
let inTrace = prevLeaf.parent?.type === "trace_statement";
|
|
@@ -199,24 +294,373 @@ function analyzeCompletion(tree, language, completionsQuery, content, line, colu
|
|
|
199
294
|
if (prevLeaf?.type === "isA" && prevLeaf.parent?.type === "ERROR") {
|
|
200
295
|
const errorParent = prevLeaf.parent.parent;
|
|
201
296
|
if (errorParent && CLASS_LIKE_TYPES.has(errorParent.type)) {
|
|
202
|
-
|
|
297
|
+
// Topic 052 item 1 — route blank `isA |` through the same scalar
|
|
298
|
+
// scope used by `isA P|` (typed prefix) and `isA T,|` (comma
|
|
299
|
+
// continuation). The array form fell through to the fallback path
|
|
300
|
+
// and emitted built-ins + `void`, none of which are valid `isA`
|
|
301
|
+
// parents. The scalar takes the symbol-only builder branch that
|
|
302
|
+
// returns class / interface / trait symbols only.
|
|
303
|
+
symbolKinds = "isa_typed_prefix";
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
// --- Typed-prefix on isa_declaration type identifier (topic 047 item 1) ---
|
|
307
|
+
// Once the user types a prefix inside the inheritance type list, the existing
|
|
308
|
+
// (isa_declaration) @scope.class_interface_trait scope query still matches,
|
|
309
|
+
// but the array form lets LookaheadIterator append class-body / top-level
|
|
310
|
+
// starters (ERROR, namespace, Java, generate, ...). Force the scalar
|
|
311
|
+
// `isa_typed_prefix` so completionBuilder takes the symbol-only early-return
|
|
312
|
+
// branch — mirrors topic 043's association_typed_prefix pattern.
|
|
313
|
+
//
|
|
314
|
+
// Walk shape is specialized (direct-ancestor match + trait_sm_binding hard-
|
|
315
|
+
// stop + ERROR-recovery fallback), so only the identifier gate is shared
|
|
316
|
+
// with items 2 / 3 via isLetterLeadingIdentifier.
|
|
317
|
+
if (nodeAtCursor && isLetterLeadingIdentifier(nodeAtCursor)) {
|
|
318
|
+
// Primary: identifier is inside an isa_declaration's isa_type_list.
|
|
319
|
+
// Hard-stop at trait_sm_binding so `isA T<sm as S|` is never misclassified.
|
|
320
|
+
let n = nodeAtCursor.parent;
|
|
321
|
+
let insideIsaDecl = false;
|
|
322
|
+
while (n) {
|
|
323
|
+
if (n.type === "trait_sm_binding")
|
|
324
|
+
break;
|
|
325
|
+
if (n.type === "isa_declaration") {
|
|
326
|
+
insideIsaDecl = true;
|
|
327
|
+
break;
|
|
328
|
+
}
|
|
329
|
+
if (TYPE_SLOT_BOUNDARIES.has(n.type))
|
|
330
|
+
break;
|
|
331
|
+
n = n.parent;
|
|
332
|
+
}
|
|
333
|
+
// Fallback: ERROR recovery where isa_declaration didn't form at all
|
|
334
|
+
// (e.g. `class S { isA P` with no closing brace). findPreviousLeaf backs
|
|
335
|
+
// up over the current identifier, so prevLeaf for `isA P|` is `isA` and
|
|
336
|
+
// for `isA Person, P|` is `,`. For trait-SM angle-bracket positions
|
|
337
|
+
// (`isA T<sm|`, `isA T<sm as S|`) prevLeaf is `<` / `as`, so the gate
|
|
338
|
+
// excludes them even when they share an ERROR region with `isA`.
|
|
339
|
+
if (!insideIsaDecl &&
|
|
340
|
+
(prevLeaf?.type === "isA" || prevLeaf?.type === ",")) {
|
|
341
|
+
let err = nodeAtCursor.parent;
|
|
342
|
+
while (err && err.type !== "ERROR")
|
|
343
|
+
err = err.parent;
|
|
344
|
+
if (err) {
|
|
345
|
+
// For the comma case, additionally confirm the enclosing ERROR has
|
|
346
|
+
// `isA` as an earlier child — rules out stray commas in unrelated
|
|
347
|
+
// broken constructs.
|
|
348
|
+
for (let i = 0; i < err.childCount; i++) {
|
|
349
|
+
const c = err.child(i);
|
|
350
|
+
if (!c)
|
|
351
|
+
continue;
|
|
352
|
+
if (c.startIndex >= nodeAtCursor.startIndex)
|
|
353
|
+
break;
|
|
354
|
+
if (c.type === "isA") {
|
|
355
|
+
insideIsaDecl = true;
|
|
356
|
+
break;
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
}
|
|
203
360
|
}
|
|
361
|
+
if (insideIsaDecl)
|
|
362
|
+
symbolKinds = "isa_typed_prefix";
|
|
363
|
+
}
|
|
364
|
+
// --- Typed-prefix on declaration-type identifier (topic 047 item 2) ---
|
|
365
|
+
// Same class of bug as isa_typed_prefix: when the user types a prefix
|
|
366
|
+
// inside an attribute_declaration or const_declaration's type_name, the
|
|
367
|
+
// scope query has no dedicated capture so completion falls through to
|
|
368
|
+
// generic class_body (54+ LookaheadIterator keywords, zero type symbols).
|
|
369
|
+
// Force scalar `decl_type_typed_prefix` to take a symbol-only builder
|
|
370
|
+
// branch that offers built-in types + class / interface / trait / enum.
|
|
371
|
+
//
|
|
372
|
+
// ERROR-recovery where type_name sits under ERROR (e.g. class body without
|
|
373
|
+
// closing brace) is deliberately NOT matched — too ambiguous with isA /
|
|
374
|
+
// modifier prefixes.
|
|
375
|
+
if (nodeAtCursor &&
|
|
376
|
+
isLetterLeadingIdentifier(nodeAtCursor) &&
|
|
377
|
+
isInsideTypeNameFieldSlot(nodeAtCursor, DECL_TYPE_PARENTS, "type", TYPE_SLOT_BOUNDARIES)) {
|
|
378
|
+
symbolKinds = "decl_type_typed_prefix";
|
|
379
|
+
}
|
|
380
|
+
// --- Typed-prefix on method return-type identifier (topic 047 item 3) ---
|
|
381
|
+
// Same pattern as decl_type_typed_prefix, but for method return-type slots
|
|
382
|
+
// across method_declaration, abstract_method_declaration, method_signature,
|
|
383
|
+
// and trait_method_signature. All four rules use a `return_type` field.
|
|
384
|
+
// Parameter types sit under `param`, not a method rule, so the field/slot
|
|
385
|
+
// check excludes them cleanly without extra logic. Void IS valid here
|
|
386
|
+
// (unlike decl_type_typed_prefix, where it is reserved for this item).
|
|
387
|
+
if (nodeAtCursor &&
|
|
388
|
+
isLetterLeadingIdentifier(nodeAtCursor) &&
|
|
389
|
+
isInsideTypeNameFieldSlot(nodeAtCursor, RETURN_TYPE_PARENTS, "return_type", TYPE_SLOT_BOUNDARIES)) {
|
|
390
|
+
symbolKinds = "return_type_typed_prefix";
|
|
391
|
+
}
|
|
392
|
+
// Topic 052 item 4 — parameter-type completion. Three positive shapes
|
|
393
|
+
// covered by isInsideMethodParamTypeSlot (blank `(`, single-id param,
|
|
394
|
+
// blank continuation after `,`). The matching topic-050 suppressors
|
|
395
|
+
// already step aside via the same predicate, so this assignment is the
|
|
396
|
+
// sole authority for those positions.
|
|
397
|
+
if (nodeAtCursor && isInsideMethodParamTypeSlot(nodeAtCursor, prevLeaf)) {
|
|
398
|
+
symbolKinds = "param_type_typed_prefix";
|
|
204
399
|
}
|
|
205
400
|
if ((prevLeaf?.type === "before" || prevLeaf?.type === "after") &&
|
|
206
401
|
prevLeaf.parent?.type === "ERROR") {
|
|
207
402
|
const errorParent = prevLeaf.parent.parent;
|
|
208
403
|
if (errorParent && CLASS_LIKE_TYPES.has(errorParent.type)) {
|
|
209
|
-
|
|
404
|
+
// Topic 052 item 2 — route blank `before |` / `after |` and the
|
|
405
|
+
// typed-prefix `before p|` cases through a method-symbol-only scalar
|
|
406
|
+
// scope. The array form fell through to the fallback path and
|
|
407
|
+
// emitted ~177 LookaheadIterator keywords (ERROR, namespace, Java,
|
|
408
|
+
// ...) ahead of the actual method symbols.
|
|
409
|
+
symbolKinds = "code_injection_method";
|
|
210
410
|
}
|
|
211
411
|
}
|
|
412
|
+
// Topic 052 item 3 — `filter { include ... }` target completion.
|
|
413
|
+
//
|
|
414
|
+
// Two recovery shapes:
|
|
415
|
+
// 1. Blank `include |`: parses as `filter_definition < ERROR(include)`
|
|
416
|
+
// and the scope query falls through to `filter_body` (the keyword-
|
|
417
|
+
// starters list). Detection: prevLeaf is the `include` keyword
|
|
418
|
+
// whose parent is ERROR whose parent is filter_definition.
|
|
419
|
+
// 2. Typed `include S|`: parses cleanly as
|
|
420
|
+
// `filter_definition < filter_statement < filter_value <
|
|
421
|
+
// (include, filter_pattern "S", ;)`. The existing
|
|
422
|
+
// `(filter_value) @scope.class` capture in completions.scm produces
|
|
423
|
+
// `["class"]`, which leaks built-ins via the fallback path. Detection:
|
|
424
|
+
// walk up from nodeAtCursor; if ancestor is `filter_value` and its
|
|
425
|
+
// first child is the `include` keyword, override the scope.
|
|
426
|
+
//
|
|
427
|
+
// Both routes set `filter_include_target` — class-symbol-only, no
|
|
428
|
+
// built-ins, no `void`. Negative scopes stay untouched: blank filter
|
|
429
|
+
// body keeps `filter_body`, `includeFilter |` keeps `filter_body`,
|
|
430
|
+
// `namespace |` keeps `null`.
|
|
431
|
+
if (prevLeaf?.type === "include" &&
|
|
432
|
+
prevLeaf.parent?.type === "ERROR" &&
|
|
433
|
+
prevLeaf.parent.parent?.type === "filter_definition") {
|
|
434
|
+
symbolKinds = "filter_include_target";
|
|
435
|
+
}
|
|
436
|
+
else if (nodeAtCursor) {
|
|
437
|
+
let n = nodeAtCursor;
|
|
438
|
+
while (n) {
|
|
439
|
+
if (n.type === "filter_value") {
|
|
440
|
+
// First non-extra child decides — the `include` literal opens
|
|
441
|
+
// the filter_value variant we want.
|
|
442
|
+
for (let i = 0; i < n.childCount; i++) {
|
|
443
|
+
const c = n.child(i);
|
|
444
|
+
if (!c || c.isExtra)
|
|
445
|
+
continue;
|
|
446
|
+
if (c.type === "include")
|
|
447
|
+
symbolKinds = "filter_include_target";
|
|
448
|
+
break;
|
|
449
|
+
}
|
|
450
|
+
break;
|
|
451
|
+
}
|
|
452
|
+
if (n.type === "filter_definition" || n.type === "source_file")
|
|
453
|
+
break;
|
|
454
|
+
n = n.parent;
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
// Topic 055 — `as |` blank slot inside an ERROR, recovered for two distinct
|
|
458
|
+
// syntaxes:
|
|
459
|
+
//
|
|
460
|
+
// 1. `class C { sm name as |` → referenced_statemachine target
|
|
461
|
+
// 2. `class C { isA T<sm as |` → trait_sm_binding target
|
|
462
|
+
//
|
|
463
|
+
// Disambiguation: the trait binding ERROR carries a `<` token sibling
|
|
464
|
+
// because the parser consumed `isA T<` but couldn't close it. The
|
|
465
|
+
// referenced_statemachine ERROR doesn't. Both routes are scalar so the
|
|
466
|
+
// builder never falls through to the raw-lookahead path that historically
|
|
467
|
+
// emitted ~175 keyword junk items.
|
|
212
468
|
if (prevLeaf?.type === "as" && prevLeaf.parent?.type === "ERROR") {
|
|
213
|
-
const
|
|
469
|
+
const errorNode = prevLeaf.parent;
|
|
470
|
+
const errorParent = errorNode.parent;
|
|
214
471
|
if (errorParent &&
|
|
215
472
|
(CLASS_LIKE_TYPES.has(errorParent.type) || errorParent.type === "attribute_declaration")) {
|
|
216
|
-
|
|
473
|
+
let isTraitBinding = false;
|
|
474
|
+
for (let i = 0; i < errorNode.childCount; i++) {
|
|
475
|
+
const c = errorNode.child(i);
|
|
476
|
+
if (c && c.type === "<") {
|
|
477
|
+
isTraitBinding = true;
|
|
478
|
+
break;
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
symbolKinds = isTraitBinding
|
|
482
|
+
? "trait_sm_binding_target"
|
|
483
|
+
: "referenced_sm_target";
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
// Topic 055 — `... as Sm.|` dotted-state continuation inside an ERROR.
|
|
487
|
+
// Cursor lands on the `.` token; tree-sitter typically wraps the bare dot
|
|
488
|
+
// in its own ERROR node, separated from the preceding trait_sm_binding
|
|
489
|
+
// ERROR. Detection: walk previous siblings of the dot's container looking
|
|
490
|
+
// for an enclosing `trait_sm_binding` (preferred) or an ERROR that
|
|
491
|
+
// contains the `<` and `as` tokens of an in-progress trait binding.
|
|
492
|
+
// Capture the SM name segment so the builder can resolve states.
|
|
493
|
+
// Topic 055 — capture SM name + remaining state-path prefix for the
|
|
494
|
+
// trait_sm_binding_state_target builder.
|
|
495
|
+
let traitSmBindingSmName;
|
|
496
|
+
let traitSmBindingStatePrefix;
|
|
497
|
+
if (prevLeaf?.type === "." && prevLeaf.parent) {
|
|
498
|
+
const dotContainer = prevLeaf.parent;
|
|
499
|
+
let sawAngle = false;
|
|
500
|
+
let sawAs = false;
|
|
501
|
+
let valuePathSegments;
|
|
502
|
+
// Helper: extract all identifier names from a qualified_name in document
|
|
503
|
+
// order. The first becomes the SM name, the rest the state prefix.
|
|
504
|
+
const collectSegments = (qn) => {
|
|
505
|
+
const out = [];
|
|
506
|
+
for (let i = 0; i < qn.childCount; i++) {
|
|
507
|
+
const c = qn.child(i);
|
|
508
|
+
if (c && c.type === "identifier")
|
|
509
|
+
out.push(c.text);
|
|
510
|
+
}
|
|
511
|
+
return out;
|
|
512
|
+
};
|
|
513
|
+
let sib = dotContainer.previousSibling;
|
|
514
|
+
let hops = 0;
|
|
515
|
+
while (sib && hops < 6) {
|
|
516
|
+
hops++;
|
|
517
|
+
if (sib.type === "trait_sm_binding") {
|
|
518
|
+
for (let i = 0; i < sib.childCount; i++) {
|
|
519
|
+
const c = sib.child(i);
|
|
520
|
+
if (!c)
|
|
521
|
+
continue;
|
|
522
|
+
const fieldName = sib.fieldNameForChild(i);
|
|
523
|
+
if (fieldName === "value" && c.type === "qualified_name") {
|
|
524
|
+
valuePathSegments = collectSegments(c);
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
sawAngle = sawAs = true;
|
|
528
|
+
break;
|
|
529
|
+
}
|
|
530
|
+
if (sib.type === "ERROR") {
|
|
531
|
+
for (let i = 0; i < sib.childCount; i++) {
|
|
532
|
+
const c = sib.child(i);
|
|
533
|
+
if (!c)
|
|
534
|
+
continue;
|
|
535
|
+
if (c.type === "<")
|
|
536
|
+
sawAngle = true;
|
|
537
|
+
if (c.type === "as")
|
|
538
|
+
sawAs = true;
|
|
539
|
+
if (c.type === "trait_sm_binding") {
|
|
540
|
+
for (let j = 0; j < c.childCount; j++) {
|
|
541
|
+
const cc = c.child(j);
|
|
542
|
+
if (!cc)
|
|
543
|
+
continue;
|
|
544
|
+
const fieldName = c.fieldNameForChild(j);
|
|
545
|
+
if (fieldName === "value" && cc.type === "qualified_name") {
|
|
546
|
+
valuePathSegments = collectSegments(cc);
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
sawAngle = sawAs = true;
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
if (sawAngle && sawAs && valuePathSegments && valuePathSegments.length > 0)
|
|
553
|
+
break;
|
|
554
|
+
}
|
|
555
|
+
sib = sib.previousSibling;
|
|
556
|
+
}
|
|
557
|
+
if (sawAngle &&
|
|
558
|
+
sawAs &&
|
|
559
|
+
valuePathSegments &&
|
|
560
|
+
valuePathSegments.length > 0) {
|
|
561
|
+
symbolKinds = "trait_sm_binding_state_target";
|
|
562
|
+
traitSmBindingSmName = valuePathSegments[0];
|
|
563
|
+
traitSmBindingStatePrefix = valuePathSegments.slice(1);
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
// Topic 055 — bare dot inside an already-formed `trait_sm_binding > value
|
|
567
|
+
// (qualified_name)`. This fires when the user types `<sm as Sm.|>;` (or
|
|
568
|
+
// when the parser otherwise reformed the qualified_name despite the dot
|
|
569
|
+
// being mid-edit). Detection: prevLeaf is `.`, prevLeaf.parent is
|
|
570
|
+
// qualified_name, qualified_name.parent is trait_sm_binding (value path).
|
|
571
|
+
// Capture the SM name and any preceding state-prefix segments so the
|
|
572
|
+
// builder can resolve `Sm` → top-level / class-local SM and descend.
|
|
573
|
+
if (prevLeaf?.type === "." &&
|
|
574
|
+
prevLeaf.parent?.type === "qualified_name" &&
|
|
575
|
+
prevLeaf.parent.parent?.type === "trait_sm_binding") {
|
|
576
|
+
const tsb = prevLeaf.parent.parent;
|
|
577
|
+
let isValuePath = false;
|
|
578
|
+
for (let i = 0; i < tsb.childCount; i++) {
|
|
579
|
+
const c = tsb.child(i);
|
|
580
|
+
if (!c)
|
|
581
|
+
continue;
|
|
582
|
+
const fieldName = tsb.fieldNameForChild(i);
|
|
583
|
+
if (fieldName === "value" && c.id === prevLeaf.parent.id) {
|
|
584
|
+
isValuePath = true;
|
|
585
|
+
break;
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
if (isValuePath) {
|
|
589
|
+
const qn = prevLeaf.parent;
|
|
590
|
+
const precedingIdentifiers = [];
|
|
591
|
+
for (let i = 0; i < qn.childCount; i++) {
|
|
592
|
+
const c = qn.child(i);
|
|
593
|
+
if (!c)
|
|
594
|
+
continue;
|
|
595
|
+
if (c.id === prevLeaf.id)
|
|
596
|
+
break;
|
|
597
|
+
if (c.type === "identifier")
|
|
598
|
+
precedingIdentifiers.push(c.text);
|
|
599
|
+
}
|
|
600
|
+
if (precedingIdentifiers.length > 0) {
|
|
601
|
+
symbolKinds = "trait_sm_binding_state_target";
|
|
602
|
+
traitSmBindingSmName = precedingIdentifiers[0];
|
|
603
|
+
traitSmBindingStatePrefix = precedingIdentifiers.slice(1);
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
// Topic 055 — typed dotted continuation: cursor on identifier inside
|
|
608
|
+
// `trait_sm_binding > qualified_name`. The query-based scope already routes
|
|
609
|
+
// first-identifier captures to `trait_sm_binding_target`; here we promote
|
|
610
|
+
// non-first identifiers (i.e., the dotted-state segment) to the dedicated
|
|
611
|
+
// state target. Detection: walk siblings of the cursor identifier inside
|
|
612
|
+
// the qualified_name; if any preceding sibling is a `.` token we're past
|
|
613
|
+
// the SM-name segment.
|
|
614
|
+
if (nodeAtCursor &&
|
|
615
|
+
nodeAtCursor.type === "identifier" &&
|
|
616
|
+
nodeAtCursor.parent?.type === "qualified_name" &&
|
|
617
|
+
nodeAtCursor.parent.parent?.type === "trait_sm_binding") {
|
|
618
|
+
const tsb = nodeAtCursor.parent.parent;
|
|
619
|
+
// Only consider the value path, not the param path.
|
|
620
|
+
let isValuePath = false;
|
|
621
|
+
for (let i = 0; i < tsb.childCount; i++) {
|
|
622
|
+
const c = tsb.child(i);
|
|
623
|
+
if (!c)
|
|
624
|
+
continue;
|
|
625
|
+
const fieldName = tsb.fieldNameForChild(i);
|
|
626
|
+
if (fieldName === "value" && c.id === nodeAtCursor.parent.id) {
|
|
627
|
+
isValuePath = true;
|
|
628
|
+
break;
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
if (isValuePath) {
|
|
632
|
+
const qn = nodeAtCursor.parent;
|
|
633
|
+
const precedingIdentifiers = [];
|
|
634
|
+
let pastDot = false;
|
|
635
|
+
for (let i = 0; i < qn.childCount; i++) {
|
|
636
|
+
const c = qn.child(i);
|
|
637
|
+
if (!c)
|
|
638
|
+
continue;
|
|
639
|
+
if (c.id === nodeAtCursor.id)
|
|
640
|
+
break;
|
|
641
|
+
if (c.type === ".")
|
|
642
|
+
pastDot = true;
|
|
643
|
+
else if (c.type === "identifier")
|
|
644
|
+
precedingIdentifiers.push(c.text);
|
|
645
|
+
}
|
|
646
|
+
if (pastDot) {
|
|
647
|
+
symbolKinds = "trait_sm_binding_state_target";
|
|
648
|
+
// First identifier is the SM name; subsequent are state-path segments
|
|
649
|
+
// BEFORE the cursor (state-prefix). The cursor identifier itself is
|
|
650
|
+
// the typed prefix the client filters on.
|
|
651
|
+
if (precedingIdentifiers.length > 0) {
|
|
652
|
+
traitSmBindingSmName = precedingIdentifiers[0];
|
|
653
|
+
traitSmBindingStatePrefix = precedingIdentifiers.slice(1);
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
else {
|
|
657
|
+
symbolKinds = "trait_sm_binding_target";
|
|
658
|
+
}
|
|
217
659
|
}
|
|
218
660
|
}
|
|
219
|
-
if (prevLeaf?.type === "->" &&
|
|
661
|
+
if (prevLeaf?.type === "->" &&
|
|
662
|
+
prevLeaf.parent?.type === "ERROR" &&
|
|
663
|
+
prevLeaf.endIndex < cursorOffset) {
|
|
220
664
|
let n = prevLeaf.parent;
|
|
221
665
|
while (n) {
|
|
222
666
|
if (n.type === "state_machine" || n.type === "statemachine_definition") {
|
|
@@ -227,13 +671,45 @@ function analyzeCompletion(tree, language, completionsQuery, content, line, colu
|
|
|
227
671
|
break;
|
|
228
672
|
n = n.parent;
|
|
229
673
|
}
|
|
674
|
+
// Topic 050 case 3 — `status { s1 -> }` parses as `enumerated_attribute
|
|
675
|
+
// < ERROR(->)` rather than reaching state_machine recovery, so the walk
|
|
676
|
+
// above bails at class_definition. Detect the `enumerated_attribute`
|
|
677
|
+
// ancestor and treat the position as transition_target. The completion
|
|
678
|
+
// builder's transition_target branch returns an empty list when no
|
|
679
|
+
// enclosingStateMachine is present — better empty than 48 wrong items.
|
|
680
|
+
if (symbolKinds !== "transition_target") {
|
|
681
|
+
let walk = prevLeaf.parent;
|
|
682
|
+
while (walk) {
|
|
683
|
+
if (walk.type === "enumerated_attribute") {
|
|
684
|
+
// Confirm class-like outer container.
|
|
685
|
+
let outer = walk.parent;
|
|
686
|
+
while (outer) {
|
|
687
|
+
if (CLASS_LIKE_DEF_TYPES.has(outer.type)) {
|
|
688
|
+
symbolKinds = "transition_target";
|
|
689
|
+
break;
|
|
690
|
+
}
|
|
691
|
+
if (outer.type === "source_file")
|
|
692
|
+
break;
|
|
693
|
+
outer = outer.parent;
|
|
694
|
+
}
|
|
695
|
+
break;
|
|
696
|
+
}
|
|
697
|
+
if (walk.type === "class_definition" || walk.type === "source_file")
|
|
698
|
+
break;
|
|
699
|
+
walk = walk.parent;
|
|
700
|
+
}
|
|
701
|
+
}
|
|
230
702
|
}
|
|
231
|
-
// --- Partial
|
|
232
|
-
// While the user is mid-typing an association inside a class-like body:
|
|
233
|
-
// `1 -> |`
|
|
234
|
-
//
|
|
235
|
-
// `1 -> * |`
|
|
703
|
+
// --- Partial association completion ---
|
|
704
|
+
// While the user is mid-typing an inline association inside a class-like body:
|
|
705
|
+
// `1 -> |` -> ERROR wraps (multiplicity)(arrow); offer right-
|
|
706
|
+
// multiplicities (1 / * / 0..1 / 1..* / 0..*).
|
|
707
|
+
// `1 -> * |` -> ERROR wraps (multiplicity)(arrow)(multiplicity); offer
|
|
236
708
|
// class symbols for the right_type slot.
|
|
709
|
+
// Standalone association blocks have a left_type slot after the left
|
|
710
|
+
// multiplicity:
|
|
711
|
+
// `association { 1 | }` -> offer class names, not arrows.
|
|
712
|
+
// `association { 1 Other | }` -> offer arrows.
|
|
237
713
|
// Once the user starts typing the type identifier the parser forms a full
|
|
238
714
|
// `association_inline`, and the existing (association_inline) @scope.* in
|
|
239
715
|
// completions.scm takes over. This fallback only fires while the partial is
|
|
@@ -245,37 +721,53 @@ function analyzeCompletion(tree, language, completionsQuery, content, line, colu
|
|
|
245
721
|
// $.arrow, association_definition doesn't), and prevLeaf can land inside a
|
|
246
722
|
// multiplicity token or on the multiplicity node itself depending on width.
|
|
247
723
|
{
|
|
248
|
-
const
|
|
724
|
+
const INLINE_ASSOC_CONTAINERS = new Set([
|
|
249
725
|
"class_definition", "trait_definition", "interface_definition",
|
|
250
726
|
"association_class_definition", "mixset_definition",
|
|
251
|
-
"association_definition",
|
|
252
727
|
]);
|
|
253
728
|
const ASSOC_SM_STOPS = new Set([
|
|
254
729
|
"state_machine", "statemachine_definition", "state", "transition",
|
|
255
730
|
]);
|
|
256
|
-
const
|
|
731
|
+
const associationCompletionMode = (node) => {
|
|
257
732
|
let n = node;
|
|
258
733
|
while (n) {
|
|
259
734
|
if (ASSOC_SM_STOPS.has(n.type))
|
|
260
|
-
return
|
|
261
|
-
if (
|
|
262
|
-
return
|
|
735
|
+
return null;
|
|
736
|
+
if (n.type === "association_definition")
|
|
737
|
+
return "standalone";
|
|
738
|
+
if (INLINE_ASSOC_CONTAINERS.has(n.type))
|
|
739
|
+
return "inline";
|
|
263
740
|
if (n.type === "source_file")
|
|
264
|
-
return
|
|
741
|
+
return null;
|
|
265
742
|
n = n.parent;
|
|
266
743
|
}
|
|
267
|
-
return
|
|
744
|
+
return null;
|
|
268
745
|
};
|
|
269
746
|
const findEnclosingError = (n) => {
|
|
747
|
+
let found = null;
|
|
270
748
|
while (n) {
|
|
271
749
|
if (n.type === "ERROR")
|
|
272
|
-
|
|
750
|
+
found = n;
|
|
273
751
|
n = n.parent;
|
|
274
752
|
}
|
|
275
|
-
return
|
|
753
|
+
return found;
|
|
754
|
+
};
|
|
755
|
+
const pushErrorFlattenedChildren = (node, out) => {
|
|
756
|
+
if (node.type !== "ERROR") {
|
|
757
|
+
if (!node.isExtra)
|
|
758
|
+
out.push(node);
|
|
759
|
+
return;
|
|
760
|
+
}
|
|
761
|
+
for (let i = 0; i < node.childCount; i++) {
|
|
762
|
+
const c = node.child(i);
|
|
763
|
+
if (c && (!c.isExtra || c.type === "ERROR")) {
|
|
764
|
+
pushErrorFlattenedChildren(c, out);
|
|
765
|
+
}
|
|
766
|
+
}
|
|
276
767
|
};
|
|
277
768
|
const errorNode = prevLeaf ? findEnclosingError(prevLeaf) : null;
|
|
278
|
-
|
|
769
|
+
const assocMode = errorNode ? associationCompletionMode(errorNode) : null;
|
|
770
|
+
if (prevLeaf && errorNode && assocMode) {
|
|
279
771
|
// Anchor classification on prevLeaf's position within the ERROR's non-
|
|
280
772
|
// extras children. Multiple in-progress associations in the same block
|
|
281
773
|
// (`association { 1 ->\n 1 -> *\n 0..1 -> 1..* }`) collapse into one
|
|
@@ -295,6 +787,7 @@ function analyzeCompletion(tree, language, completionsQuery, content, line, colu
|
|
|
295
787
|
// Letter-leading identifiers DON'T count, so `e ->` in class body
|
|
296
788
|
// (attribute-shaped, no multiplicity) stays on class_body.
|
|
297
789
|
const isArrow = (c) => c.type === "arrow" || c.type === "->";
|
|
790
|
+
const isLetterLeadingId = (c) => c.type === "identifier" && /^[A-Za-z_]/.test(c.text);
|
|
298
791
|
const mightBeMult = (c) => c.type === "multiplicity" ||
|
|
299
792
|
c.type === "*" ||
|
|
300
793
|
c.type === ".." ||
|
|
@@ -303,8 +796,9 @@ function analyzeCompletion(tree, language, completionsQuery, content, line, colu
|
|
|
303
796
|
const children = [];
|
|
304
797
|
for (let i = 0; i < errorNode.childCount; i++) {
|
|
305
798
|
const c = errorNode.child(i);
|
|
306
|
-
if (c && !c.isExtra)
|
|
307
|
-
|
|
799
|
+
if (c && (!c.isExtra || c.type === "ERROR")) {
|
|
800
|
+
pushErrorFlattenedChildren(c, children);
|
|
801
|
+
}
|
|
308
802
|
}
|
|
309
803
|
let prevIdx = -1;
|
|
310
804
|
for (let i = children.length - 1; i >= 0; i--) {
|
|
@@ -315,30 +809,121 @@ function analyzeCompletion(tree, language, completionsQuery, content, line, colu
|
|
|
315
809
|
}
|
|
316
810
|
}
|
|
317
811
|
if (prevIdx >= 0) {
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
812
|
+
// All slot detection is segment-bounded. The segment is the range of
|
|
813
|
+
// children since the last `;` boundary up to and including prevIdx —
|
|
814
|
+
// i.e. the current association attempt only. Without this, cascades
|
|
815
|
+
// in `association { 1 -> * Other; 1 |}` would inherit the prior
|
|
816
|
+
// association's arrow and misclassify the new left-mult as slot 2.
|
|
817
|
+
let segStart = 0;
|
|
818
|
+
for (let i = prevIdx - 1; i >= 0; i--) {
|
|
819
|
+
if (children[i].type === ";") {
|
|
820
|
+
segStart = i + 1;
|
|
821
|
+
break;
|
|
822
|
+
}
|
|
823
|
+
if (children[i].endPosition.row < prevLeaf.startPosition.row) {
|
|
824
|
+
segStart = i + 1;
|
|
825
|
+
break;
|
|
321
826
|
}
|
|
322
827
|
}
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
828
|
+
const segment = children.slice(segStart, prevIdx + 1);
|
|
829
|
+
const segArrowIdxInSeg = segment.findIndex(isArrow);
|
|
830
|
+
const prevInSeg = segment.length - 1; // prevIdx in segment terms
|
|
831
|
+
const last = segment[prevInSeg];
|
|
832
|
+
const onlyOneCompleteMultAtCursor = segment.length === 1 &&
|
|
833
|
+
last.type === "multiplicity" &&
|
|
834
|
+
last.endIndex === cursorOffset;
|
|
835
|
+
// `1 -> *|` is still the multiplicity token. The right-type slot starts
|
|
836
|
+
// after whitespace (`1 -> * |`) or after a typed identifier (`1 -> * O|`).
|
|
837
|
+
const isRightMultiplicityWithoutTypeBoundary = (node) => !isLetterLeadingId(node) && mightBeMult(node) && node.endIndex === cursorOffset;
|
|
838
|
+
if (assocMode === "standalone") {
|
|
839
|
+
const leftSide = segArrowIdxInSeg >= 0
|
|
840
|
+
? segment.slice(0, segArrowIdxInSeg)
|
|
841
|
+
: segment;
|
|
842
|
+
const hasLeftMult = leftSide.some(mightBeMult);
|
|
843
|
+
const hasLeftType = leftSide.some(isLetterLeadingId);
|
|
844
|
+
if (isArrow(last)) {
|
|
845
|
+
// Standalone slot 2: `1 Other -> |` needs right multiplicity.
|
|
846
|
+
if (last.endIndex === cursorOffset) {
|
|
847
|
+
symbolKinds = null;
|
|
848
|
+
}
|
|
849
|
+
else if (hasLeftMult && hasLeftType) {
|
|
850
|
+
symbolKinds = "association_multiplicity";
|
|
329
851
|
}
|
|
852
|
+
else if (hasLeftMult) {
|
|
853
|
+
// Recovery for malformed `association { 1 -> | }`: the left
|
|
854
|
+
// type is still the missing piece.
|
|
855
|
+
symbolKinds = "association_type";
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
else if (segArrowIdxInSeg >= 0 && hasLeftMult && hasLeftType) {
|
|
859
|
+
// Standalone slot 3: `1 Other -> * |` needs right type.
|
|
860
|
+
const child = segment[prevInSeg];
|
|
861
|
+
if (isRightMultiplicityWithoutTypeBoundary(child)) {
|
|
862
|
+
symbolKinds = null;
|
|
863
|
+
}
|
|
864
|
+
else {
|
|
865
|
+
symbolKinds = isLetterLeadingId(child)
|
|
866
|
+
? "association_typed_prefix"
|
|
867
|
+
: "association_type";
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
else if (hasLeftMult && hasLeftType) {
|
|
871
|
+
// Standalone slot 1b: `1 Other |` or `1 Other -|` needs arrow.
|
|
872
|
+
symbolKinds = "association_arrow";
|
|
873
|
+
}
|
|
874
|
+
else {
|
|
875
|
+
// Standalone slot 1a: `1 |` needs a left type, not an arrow.
|
|
876
|
+
const PARTIAL_ARROW_TYPES = new Set(["-", "<", ">", "@"]);
|
|
877
|
+
const partial = mightBeMult(last) ||
|
|
878
|
+
PARTIAL_ARROW_TYPES.has(last.type) ||
|
|
879
|
+
last.type === "req_free_text_punct";
|
|
880
|
+
if (partial && hasLeftMult && !onlyOneCompleteMultAtCursor) {
|
|
881
|
+
symbolKinds = "association_type";
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
else if (isArrow(segment[prevInSeg])) {
|
|
886
|
+
// Inline slot 1: cursor IS an arrow -> right-multiplicity slot.
|
|
887
|
+
if (segment[prevInSeg].endIndex === cursorOffset) {
|
|
888
|
+
symbolKinds = null;
|
|
330
889
|
}
|
|
331
|
-
if (
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
890
|
+
else if (segment.slice(0, prevInSeg).some(mightBeMult)) {
|
|
891
|
+
symbolKinds = "association_multiplicity";
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
else if (segArrowIdxInSeg >= 0
|
|
895
|
+
&& segment.slice(0, segArrowIdxInSeg).some(mightBeMult)) {
|
|
896
|
+
// Inline slot 2: arrow appears before prevLeaf in the segment -> right-type
|
|
897
|
+
// slot. typed-prefix vs blank-multiplicity disambiguation matches
|
|
898
|
+
// topic 043's heuristic.
|
|
899
|
+
const child = segment[prevInSeg];
|
|
900
|
+
if (isRightMultiplicityWithoutTypeBoundary(child)) {
|
|
901
|
+
symbolKinds = null;
|
|
902
|
+
}
|
|
903
|
+
else {
|
|
904
|
+
symbolKinds = isLetterLeadingId(child)
|
|
338
905
|
? "association_typed_prefix"
|
|
339
906
|
: "association_type";
|
|
340
907
|
}
|
|
341
908
|
}
|
|
909
|
+
else {
|
|
910
|
+
// Inline slot 0: no arrow in this segment yet. If prevLeaf is
|
|
911
|
+
// mult-like OR a partial-arrow character (`-`, `<`, `>`, `@`,
|
|
912
|
+
// `req_free_text_punct` for things like `<@`), AND the segment has
|
|
913
|
+
// a mult-like -> arrow slot.
|
|
914
|
+
const PARTIAL_ARROW_TYPES = new Set(["-", "<", ">", "@"]);
|
|
915
|
+
const partial = mightBeMult(last) ||
|
|
916
|
+
PARTIAL_ARROW_TYPES.has(last.type) ||
|
|
917
|
+
last.type === "req_free_text_punct";
|
|
918
|
+
// Topic 050 case 2 — bare `0..*` (a single complete multiplicity
|
|
919
|
+
// node with cursor exactly at its end) is not yet starting an
|
|
920
|
+
// association. Without intervening whitespace there's no arrow
|
|
921
|
+
// intent. `1 |` (cursor after a space) still fires because the
|
|
922
|
+
// cursor offset is past the digit token's end.
|
|
923
|
+
if (partial && segment.some(mightBeMult) && !onlyOneCompleteMultAtCursor) {
|
|
924
|
+
symbolKinds = "association_arrow";
|
|
925
|
+
}
|
|
926
|
+
}
|
|
342
927
|
}
|
|
343
928
|
}
|
|
344
929
|
// --- Typed-prefix on the right-type identifier (cursor INSIDE the
|
|
@@ -351,11 +936,13 @@ function analyzeCompletion(tree, language, completionsQuery, content, line, colu
|
|
|
351
936
|
nodeAtCursor.type === "identifier" &&
|
|
352
937
|
/^[A-Za-z_]/.test(nodeAtCursor.text)) {
|
|
353
938
|
const errAnc = findEnclosingError(nodeAtCursor);
|
|
354
|
-
|
|
939
|
+
const typedAssocMode = errAnc ? associationCompletionMode(errAnc) : null;
|
|
940
|
+
if (errAnc && typedAssocMode) {
|
|
355
941
|
// Sanity: ERROR contains an arrow with a mult-like before it (i.e.
|
|
356
|
-
// we're really in an association
|
|
357
|
-
//
|
|
942
|
+
// we're really in an association type identifier slot, not some
|
|
943
|
+
// unrelated identifier under a recovery ERROR).
|
|
358
944
|
const isArrow2 = (c) => c.type === "arrow" || c.type === "->";
|
|
945
|
+
const isLetterLeadingId2 = (c) => c.type === "identifier" && /^[A-Za-z_]/.test(c.text);
|
|
359
946
|
const mightBeMult2 = (c) => c.type === "multiplicity" ||
|
|
360
947
|
c.type === "*" ||
|
|
361
948
|
c.type === ".." ||
|
|
@@ -364,24 +951,76 @@ function analyzeCompletion(tree, language, completionsQuery, content, line, colu
|
|
|
364
951
|
const errChildren = [];
|
|
365
952
|
for (let i = 0; i < errAnc.childCount; i++) {
|
|
366
953
|
const c = errAnc.child(i);
|
|
367
|
-
if (c && !c.isExtra)
|
|
368
|
-
|
|
954
|
+
if (c && (!c.isExtra || c.type === "ERROR")) {
|
|
955
|
+
pushErrorFlattenedChildren(c, errChildren);
|
|
956
|
+
}
|
|
369
957
|
}
|
|
370
|
-
let
|
|
371
|
-
let
|
|
372
|
-
|
|
373
|
-
if (c.id === nodeAtCursor.id
|
|
958
|
+
let typedIdx = -1;
|
|
959
|
+
for (let i = 0; i < errChildren.length; i++) {
|
|
960
|
+
const c = errChildren[i];
|
|
961
|
+
if (c.id === nodeAtCursor.id ||
|
|
962
|
+
(c.startIndex <= nodeAtCursor.startIndex && nodeAtCursor.endIndex <= c.endIndex)) {
|
|
963
|
+
typedIdx = i;
|
|
374
964
|
break;
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
if (typedIdx >= 0) {
|
|
968
|
+
let segStart = 0;
|
|
969
|
+
for (let i = typedIdx - 1; i >= 0; i--) {
|
|
970
|
+
if (errChildren[i].type === ";") {
|
|
971
|
+
segStart = i + 1;
|
|
972
|
+
break;
|
|
973
|
+
}
|
|
974
|
+
if (errChildren[i].endPosition.row < nodeAtCursor.startPosition.row) {
|
|
975
|
+
segStart = i + 1;
|
|
976
|
+
break;
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
const typedSegment = errChildren.slice(segStart, typedIdx + 1);
|
|
980
|
+
const arrowIdx = typedSegment.findIndex(isArrow2);
|
|
981
|
+
if (typedAssocMode === "standalone" && arrowIdx < 0) {
|
|
982
|
+
// Left_type typed prefix: `association { 1 O| }`.
|
|
983
|
+
if (typedSegment.slice(0, -1).some(mightBeMult2)) {
|
|
984
|
+
symbolKinds = "association_typed_prefix";
|
|
985
|
+
}
|
|
986
|
+
}
|
|
987
|
+
else if (arrowIdx >= 0) {
|
|
988
|
+
const beforeArrow = typedSegment.slice(0, arrowIdx);
|
|
989
|
+
const hasLeftMult = beforeArrow.some(mightBeMult2);
|
|
990
|
+
const hasLeftType = typedAssocMode === "inline" ||
|
|
991
|
+
beforeArrow.some(isLetterLeadingId2);
|
|
992
|
+
if (hasLeftMult && hasLeftType) {
|
|
993
|
+
symbolKinds = "association_typed_prefix";
|
|
994
|
+
}
|
|
995
|
+
}
|
|
379
996
|
}
|
|
380
|
-
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
// Corpus association syntax also permits standalone association ends:
|
|
1000
|
+
// association { 1 Person; * Employee role; }
|
|
1001
|
+
// That clean parse means `association { 1 O| }` and `1 Other |` are no
|
|
1002
|
+
// longer wrapped in ERROR, so preserve the pre-existing completion
|
|
1003
|
+
// behavior explicitly: typed identifiers complete class-like symbols,
|
|
1004
|
+
// and whitespace after the left type still offers arrows for users who
|
|
1005
|
+
// are building the arrow-form association member.
|
|
1006
|
+
const isSingleEndInAssociationDefinition = (node) => node?.type === "single_association_end" && node.parent?.type === "association_definition";
|
|
1007
|
+
if (nodeAtCursor && isLetterLeadingIdentifier(nodeAtCursor)) {
|
|
1008
|
+
let n = nodeAtCursor.parent;
|
|
1009
|
+
while (n) {
|
|
1010
|
+
if (isSingleEndInAssociationDefinition(n)) {
|
|
381
1011
|
symbolKinds = "association_typed_prefix";
|
|
1012
|
+
break;
|
|
382
1013
|
}
|
|
1014
|
+
if (n.type === "association_definition" || n.type === "source_file")
|
|
1015
|
+
break;
|
|
1016
|
+
n = n.parent;
|
|
383
1017
|
}
|
|
384
1018
|
}
|
|
1019
|
+
if (prevLeaf?.type === "identifier" &&
|
|
1020
|
+
isSingleEndInAssociationDefinition(prevLeaf.parent) &&
|
|
1021
|
+
cursorOffset > prevLeaf.endIndex) {
|
|
1022
|
+
symbolKinds = "association_arrow";
|
|
1023
|
+
}
|
|
385
1024
|
}
|
|
386
1025
|
// --- Structured req body: slot-ready starter completion ---
|
|
387
1026
|
// The completions.scm query can't distinguish "cursor between tags" (slot-
|
|
@@ -450,17 +1089,35 @@ function analyzeCompletion(tree, language, completionsQuery, content, line, colu
|
|
|
450
1089
|
symbolKinds = ["requirement"];
|
|
451
1090
|
break;
|
|
452
1091
|
}
|
|
1092
|
+
// Topic 051 item 1 — isA comma continuation. After `isA Person,|` the
|
|
1093
|
+
// user is starting another type name. Route to the same scalar
|
|
1094
|
+
// typed-prefix scope as `isa_typed_prefix` so completionBuilder
|
|
1095
|
+
// returns class/interface/trait symbols only (no class_body junk).
|
|
1096
|
+
if (n.type === "isa_declaration") {
|
|
1097
|
+
symbolKinds = "isa_typed_prefix";
|
|
1098
|
+
break;
|
|
1099
|
+
}
|
|
453
1100
|
if (n.type === "ERROR") {
|
|
454
1101
|
// Look for a sibling `implementsReq` keyword anywhere in this ERROR
|
|
455
1102
|
// recovery region — that's the signature of a partial req_implementation.
|
|
1103
|
+
// Same scan also catches partial isa_declaration recovery.
|
|
1104
|
+
let foundReq = false;
|
|
1105
|
+
let foundIsA = false;
|
|
456
1106
|
for (let i = 0; i < n.childCount; i++) {
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
1107
|
+
const c = n.child(i);
|
|
1108
|
+
if (c?.type === "implementsReq")
|
|
1109
|
+
foundReq = true;
|
|
1110
|
+
if (c?.type === "isA")
|
|
1111
|
+
foundIsA = true;
|
|
1112
|
+
}
|
|
1113
|
+
if (foundReq) {
|
|
1114
|
+
symbolKinds = ["requirement"];
|
|
1115
|
+
break;
|
|
461
1116
|
}
|
|
462
|
-
if (
|
|
1117
|
+
if (foundIsA) {
|
|
1118
|
+
symbolKinds = "isa_typed_prefix";
|
|
463
1119
|
break;
|
|
1120
|
+
}
|
|
464
1121
|
}
|
|
465
1122
|
if (n.type === "class_definition" || n.type === "source_file")
|
|
466
1123
|
break;
|
|
@@ -486,6 +1143,23 @@ function analyzeCompletion(tree, language, completionsQuery, content, line, colu
|
|
|
486
1143
|
traitSmContext = { traitName };
|
|
487
1144
|
}
|
|
488
1145
|
}
|
|
1146
|
+
// Topic 053 — `isA T<-|` / `isA T<+|` recovery shape parses as a flat
|
|
1147
|
+
// ERROR under class_definition (no isa_declaration / type_list ancestor),
|
|
1148
|
+
// so the helpers above don't reach the trait name. Recover from the
|
|
1149
|
+
// ERROR's child sequence directly. Bare `isA T<|` (no -/+ marker yet)
|
|
1150
|
+
// suppresses to avoid the class_body keyword leak.
|
|
1151
|
+
if (!traitSmContext && nodeAtCursor) {
|
|
1152
|
+
const recovered = recoverTraitSmOpFromIsAError(nodeAtCursor);
|
|
1153
|
+
if (recovered) {
|
|
1154
|
+
if (recovered.mode === "op") {
|
|
1155
|
+
symbolKinds = "trait_sm_op_sm";
|
|
1156
|
+
traitSmContext = { traitName: recovered.traitName };
|
|
1157
|
+
}
|
|
1158
|
+
else {
|
|
1159
|
+
symbolKinds = "suppress";
|
|
1160
|
+
}
|
|
1161
|
+
}
|
|
1162
|
+
}
|
|
489
1163
|
if (!traitSmContext && prevLeaf?.type === ".") {
|
|
490
1164
|
// After dot in trait SM operation paths. The dot is inside ERROR,
|
|
491
1165
|
// with the preceding path in a sibling node.
|
|
@@ -610,7 +1284,8 @@ function analyzeCompletion(tree, language, completionsQuery, content, line, colu
|
|
|
610
1284
|
const traceStmt = findAncestorOfType(cursorNode, "trace_statement");
|
|
611
1285
|
if (traceStmt) {
|
|
612
1286
|
const STATE_PREFIXES = new Set(["entry", "exit"]);
|
|
613
|
-
const ATTR_PREFIXES = new Set(["set", "get"]);
|
|
1287
|
+
const ATTR_PREFIXES = new Set(["set", "get", "onlyGet", "onlySet"]);
|
|
1288
|
+
const EVENT_PREFIXES = new Set(["transition"]);
|
|
614
1289
|
const ASSOC_PREFIXES = new Set(["add", "remove", "cardinality"]);
|
|
615
1290
|
let prefixType = null;
|
|
616
1291
|
for (let i = 0; i < traceStmt.childCount; i++) {
|
|
@@ -627,6 +1302,10 @@ function analyzeCompletion(tree, language, completionsQuery, content, line, colu
|
|
|
627
1302
|
prefixType = "attribute";
|
|
628
1303
|
break;
|
|
629
1304
|
}
|
|
1305
|
+
if (EVENT_PREFIXES.has(child.type)) {
|
|
1306
|
+
prefixType = "event";
|
|
1307
|
+
break;
|
|
1308
|
+
}
|
|
630
1309
|
if (ASSOC_PREFIXES.has(child.type)) {
|
|
631
1310
|
prefixType = "suppress";
|
|
632
1311
|
break;
|
|
@@ -650,6 +1329,9 @@ function analyzeCompletion(tree, language, completionsQuery, content, line, colu
|
|
|
650
1329
|
else if (prefixType === "attribute") {
|
|
651
1330
|
symbolKinds = "trace_attribute";
|
|
652
1331
|
}
|
|
1332
|
+
else if (prefixType === "event") {
|
|
1333
|
+
symbolKinds = "trace_event";
|
|
1334
|
+
}
|
|
653
1335
|
else if (prefixType === "suppress") {
|
|
654
1336
|
symbolKinds = "suppress";
|
|
655
1337
|
}
|
|
@@ -667,6 +1349,8 @@ function analyzeCompletion(tree, language, completionsQuery, content, line, colu
|
|
|
667
1349
|
dottedStatePrefix,
|
|
668
1350
|
sortedKeyOwner,
|
|
669
1351
|
traitSmContext,
|
|
1352
|
+
traitSmBindingSmName,
|
|
1353
|
+
traitSmBindingStatePrefix,
|
|
670
1354
|
};
|
|
671
1355
|
}
|
|
672
1356
|
function findAncestorOfType(node, type) {
|
|
@@ -689,6 +1373,328 @@ function isInsideComment(node) {
|
|
|
689
1373
|
}
|
|
690
1374
|
return false;
|
|
691
1375
|
}
|
|
1376
|
+
// ── Topic 050 suppression guards ────────────────────────────────────────────
|
|
1377
|
+
const CLASS_LIKE_DEF_TYPES = new Set([
|
|
1378
|
+
"class_definition",
|
|
1379
|
+
"trait_definition",
|
|
1380
|
+
"interface_definition",
|
|
1381
|
+
"association_class_definition",
|
|
1382
|
+
"mixset_definition",
|
|
1383
|
+
]);
|
|
1384
|
+
/** Cursor sits inside a `java_annotation` subtree (e.g. `@Override`). */
|
|
1385
|
+
function isInsideJavaAnnotation(node) {
|
|
1386
|
+
let n = node;
|
|
1387
|
+
while (n) {
|
|
1388
|
+
if (n.type === "java_annotation")
|
|
1389
|
+
return true;
|
|
1390
|
+
n = n.parent;
|
|
1391
|
+
}
|
|
1392
|
+
return false;
|
|
1393
|
+
}
|
|
1394
|
+
/**
|
|
1395
|
+
* Cursor sits inside a method param list at a NON-completable position
|
|
1396
|
+
* (param-name slot, or whitespace after a complete param). Topic 052
|
|
1397
|
+
* item 4 narrowed this from "any position inside param list" to "only
|
|
1398
|
+
* positions that are NOT a parameter-type slot" — type slots route to
|
|
1399
|
+
* `param_type_typed_prefix` via `isInsideMethodParamTypeSlot` instead.
|
|
1400
|
+
*
|
|
1401
|
+
* Now suppresses ONLY:
|
|
1402
|
+
* - cursor on a param's NAME identifier (param has both type_name and
|
|
1403
|
+
* identifier; cursor is on the second one)
|
|
1404
|
+
* - cursor in `param_list` whitespace (no param ancestor)
|
|
1405
|
+
*
|
|
1406
|
+
* The two old broken-recovery shapes (`void f(|` no-closing-paren / `,`
|
|
1407
|
+
* in ERROR sibling of param_list) are now handled positively by
|
|
1408
|
+
* `isInsideMethodParamTypeSlot` and pre-empt this suppressor.
|
|
1409
|
+
*/
|
|
1410
|
+
function isInsideMethodParamListStart(node, prevLeaf = null) {
|
|
1411
|
+
// If this is a param-type slot, the positive scope handles it; do NOT
|
|
1412
|
+
// suppress.
|
|
1413
|
+
if (isInsideMethodParamTypeSlot(node, prevLeaf))
|
|
1414
|
+
return false;
|
|
1415
|
+
// Otherwise, only suppress when cursor is genuinely inside a param ancestor
|
|
1416
|
+
// at a non-type-slot position (the param-name slot after a full
|
|
1417
|
+
// type_name + identifier pair) or inside param_list whitespace.
|
|
1418
|
+
let n = node;
|
|
1419
|
+
while (n) {
|
|
1420
|
+
if (n.type === "param") {
|
|
1421
|
+
let hasTypeName = false;
|
|
1422
|
+
let hasIdentifier = false;
|
|
1423
|
+
for (let i = 0; i < n.childCount; i++) {
|
|
1424
|
+
const c = n.child(i);
|
|
1425
|
+
if (!c)
|
|
1426
|
+
continue;
|
|
1427
|
+
if (c.type === "type_name")
|
|
1428
|
+
hasTypeName = true;
|
|
1429
|
+
if (c.type === "identifier")
|
|
1430
|
+
hasIdentifier = true;
|
|
1431
|
+
}
|
|
1432
|
+
// Suppress only when the param has both type_name and identifier
|
|
1433
|
+
// (cursor on the name slot). Single-identifier params are type-slots
|
|
1434
|
+
// and have already returned false above via isInsideMethodParamTypeSlot.
|
|
1435
|
+
return hasTypeName && hasIdentifier;
|
|
1436
|
+
}
|
|
1437
|
+
if (n.type === "param_list") {
|
|
1438
|
+
// Cursor in param_list whitespace after a complete param — suppress.
|
|
1439
|
+
return true;
|
|
1440
|
+
}
|
|
1441
|
+
if (n.type === "method_declaration" || n.type === "class_definition" ||
|
|
1442
|
+
n.type === "source_file")
|
|
1443
|
+
break;
|
|
1444
|
+
n = n.parent;
|
|
1445
|
+
}
|
|
1446
|
+
return false;
|
|
1447
|
+
}
|
|
1448
|
+
/**
|
|
1449
|
+
* Topic 052 item 4 — cursor sits at a parameter-type slot inside a method
|
|
1450
|
+
* declaration. Three recovery shapes:
|
|
1451
|
+
*
|
|
1452
|
+
* 1. Blank first slot `void f(|)` — cursor at `(` whose parent is
|
|
1453
|
+
* `method_declaration`. No `param_list` formed yet.
|
|
1454
|
+
* 2. Typed inside single-identifier param `void f(P|)` /
|
|
1455
|
+
* `void f(int a, P|)` — cursor on an `identifier` whose parent is
|
|
1456
|
+
* `param`, and the param has only one identifier child (no separate
|
|
1457
|
+
* type_name yet). The single identifier is the type-being-typed.
|
|
1458
|
+
* 3. Blank continuation `void f(int a, |)` — cursor sits between `,`
|
|
1459
|
+
* (in ERROR sibling of param_list) and `)`. Detection: prevLeaf is
|
|
1460
|
+
* `,` whose parent ERROR is a sibling of param_list under
|
|
1461
|
+
* method_declaration.
|
|
1462
|
+
*
|
|
1463
|
+
* Returns true if any of these match. Caller sets
|
|
1464
|
+
* `symbolKinds = "param_type_typed_prefix"`.
|
|
1465
|
+
*/
|
|
1466
|
+
function isInsideMethodParamTypeSlot(node, prevLeaf = null) {
|
|
1467
|
+
// Shape 1 — cursor at `(` of method_declaration. Two parse states:
|
|
1468
|
+
// 1a. clean: `void f() {}` — `(` directly under method_declaration.
|
|
1469
|
+
// 1b. broken: `void f(` (no closing) — the entire method shape lives
|
|
1470
|
+
// under an ERROR. The ERROR contains a `type_name` (return type),
|
|
1471
|
+
// identifier (method name), and `(`. This is the topic-050
|
|
1472
|
+
// broken-`(` shape, now redirected from suppress to positive.
|
|
1473
|
+
if (node.type === "(") {
|
|
1474
|
+
if (node.parent?.type === "method_declaration")
|
|
1475
|
+
return true;
|
|
1476
|
+
if (node.parent?.type === "ERROR") {
|
|
1477
|
+
const err = node.parent;
|
|
1478
|
+
let hasTypeName = false;
|
|
1479
|
+
let hasNameAfterTypeName = false;
|
|
1480
|
+
for (let i = 0; i < err.childCount; i++) {
|
|
1481
|
+
const c = err.child(i);
|
|
1482
|
+
if (!c)
|
|
1483
|
+
continue;
|
|
1484
|
+
if (c.id === node.id)
|
|
1485
|
+
break;
|
|
1486
|
+
if (c.type === "type_name")
|
|
1487
|
+
hasTypeName = true;
|
|
1488
|
+
if (c.type === "identifier" && hasTypeName)
|
|
1489
|
+
hasNameAfterTypeName = true;
|
|
1490
|
+
}
|
|
1491
|
+
if (hasTypeName && hasNameAfterTypeName) {
|
|
1492
|
+
// Confirm class-like ancestor.
|
|
1493
|
+
let p = err.parent;
|
|
1494
|
+
while (p) {
|
|
1495
|
+
if (CLASS_LIKE_DEF_TYPES.has(p.type))
|
|
1496
|
+
return true;
|
|
1497
|
+
if (p.type === "source_file")
|
|
1498
|
+
return false;
|
|
1499
|
+
p = p.parent;
|
|
1500
|
+
}
|
|
1501
|
+
}
|
|
1502
|
+
}
|
|
1503
|
+
}
|
|
1504
|
+
// Shape 2 — cursor on an identifier inside a single-identifier param.
|
|
1505
|
+
if (node.type === "identifier") {
|
|
1506
|
+
const param = node.parent;
|
|
1507
|
+
if (param?.type === "param") {
|
|
1508
|
+
let typeNameCount = 0;
|
|
1509
|
+
let idCount = 0;
|
|
1510
|
+
for (let i = 0; i < param.childCount; i++) {
|
|
1511
|
+
const c = param.child(i);
|
|
1512
|
+
if (!c)
|
|
1513
|
+
continue;
|
|
1514
|
+
if (c.type === "type_name")
|
|
1515
|
+
typeNameCount++;
|
|
1516
|
+
if (c.type === "identifier")
|
|
1517
|
+
idCount++;
|
|
1518
|
+
}
|
|
1519
|
+
// Single identifier, no type_name → cursor is on the in-progress
|
|
1520
|
+
// type token.
|
|
1521
|
+
return typeNameCount === 0 && idCount === 1;
|
|
1522
|
+
}
|
|
1523
|
+
}
|
|
1524
|
+
// Shape 3 — cursor after a comma in the ERROR-sibling-of-param_list shape.
|
|
1525
|
+
if (prevLeaf?.type === "," && prevLeaf.parent?.type === "ERROR") {
|
|
1526
|
+
const err = prevLeaf.parent;
|
|
1527
|
+
if (err.parent?.type === "method_declaration")
|
|
1528
|
+
return true;
|
|
1529
|
+
}
|
|
1530
|
+
return false;
|
|
1531
|
+
}
|
|
1532
|
+
/**
|
|
1533
|
+
* Cursor sits inside an attribute-initializer expression after `=` and
|
|
1534
|
+
* before the terminating `;`. The grammar's `_attribute_value` is hidden,
|
|
1535
|
+
* and broken/incomplete expressions live entirely under an ERROR. Codex's
|
|
1536
|
+
* invariant: in a class-like container, enclosing ERROR has `=` before
|
|
1537
|
+
* cursor with no association arrow before cursor → suppress.
|
|
1538
|
+
*/
|
|
1539
|
+
function isInsideAttributeInitializer(node) {
|
|
1540
|
+
let err = node;
|
|
1541
|
+
while (err && err.type !== "ERROR")
|
|
1542
|
+
err = err.parent;
|
|
1543
|
+
if (!err)
|
|
1544
|
+
return false;
|
|
1545
|
+
// ERROR must sit inside a class-like container.
|
|
1546
|
+
let p = err.parent;
|
|
1547
|
+
let inClassLike = false;
|
|
1548
|
+
while (p) {
|
|
1549
|
+
if (CLASS_LIKE_DEF_TYPES.has(p.type)) {
|
|
1550
|
+
inClassLike = true;
|
|
1551
|
+
break;
|
|
1552
|
+
}
|
|
1553
|
+
if (p.type === "source_file")
|
|
1554
|
+
return false;
|
|
1555
|
+
p = p.parent;
|
|
1556
|
+
}
|
|
1557
|
+
if (!inClassLike)
|
|
1558
|
+
return false;
|
|
1559
|
+
// Walk ERROR children before the cursor: must contain `=` and no arrow.
|
|
1560
|
+
let sawEquals = false;
|
|
1561
|
+
let sawArrow = false;
|
|
1562
|
+
for (let i = 0; i < err.childCount; i++) {
|
|
1563
|
+
const c = err.child(i);
|
|
1564
|
+
if (!c)
|
|
1565
|
+
continue;
|
|
1566
|
+
if (c.startIndex >= node.startIndex)
|
|
1567
|
+
break;
|
|
1568
|
+
if (c.type === "=")
|
|
1569
|
+
sawEquals = true;
|
|
1570
|
+
if (c.type === "->" || c.type === "arrow")
|
|
1571
|
+
sawArrow = true;
|
|
1572
|
+
}
|
|
1573
|
+
return sawEquals && !sawArrow;
|
|
1574
|
+
}
|
|
1575
|
+
/**
|
|
1576
|
+
* Cursor sits inside or at the end of a contiguous identifier-character run
|
|
1577
|
+
* (`[A-Za-z0-9_-]+`) that contains both a `-` and a letter — i.e. a
|
|
1578
|
+
* malformed identifier the parser couldn't classify. Examples: `req-|foo`,
|
|
1579
|
+
* `req|-foo`, `req-foo|`. Negatives: `Int|`, `isA P|`, `1 -> * O|`,
|
|
1580
|
+
* `1 -|` (no letter in the run).
|
|
1581
|
+
*
|
|
1582
|
+
* Mostly textual; the recovery AST is not stable enough at these positions
|
|
1583
|
+
* to drive the decision off node types alone. We do, however, exempt two
|
|
1584
|
+
* legitimate dash-bearing contexts:
|
|
1585
|
+
*
|
|
1586
|
+
* 1. AST: cursor inside a `req_id`, `req_implementation`, or
|
|
1587
|
+
* `requirement_definition` subtree — `req_id` allows hyphens, so
|
|
1588
|
+
* `implementsReq L01-|License` is a valid completion target.
|
|
1589
|
+
*
|
|
1590
|
+
* 2. Textual fallback (when the AST doesn't form because of a missing
|
|
1591
|
+
* `;`): scan the current line backward from the cursor; if we encounter
|
|
1592
|
+
* `implementsReq` or `req` keyword before any `;`, `}`, or line start,
|
|
1593
|
+
* the user is editing a requirement-id position — don't suppress.
|
|
1594
|
+
*
|
|
1595
|
+
* Codex-approved narrow form — does NOT suppress legitimate typed-prefix
|
|
1596
|
+
* completions because those scopes have their own positive entries above
|
|
1597
|
+
* this guard.
|
|
1598
|
+
*/
|
|
1599
|
+
function isInsideMalformedDashIdentifier(content, line, column, nodeAtCursor) {
|
|
1600
|
+
const lines = content.split("\n");
|
|
1601
|
+
const lineText = lines[line] ?? "";
|
|
1602
|
+
const isRunChar = (c) => /[A-Za-z0-9_-]/.test(c);
|
|
1603
|
+
const isLetter = (c) => /[A-Za-z_]/.test(c);
|
|
1604
|
+
// Scan the contiguous run that contains (or is adjacent to) the cursor.
|
|
1605
|
+
let start = column;
|
|
1606
|
+
while (start > 0 && isRunChar(lineText[start - 1]))
|
|
1607
|
+
start--;
|
|
1608
|
+
let end = column;
|
|
1609
|
+
while (end < lineText.length && isRunChar(lineText[end]))
|
|
1610
|
+
end++;
|
|
1611
|
+
if (start === end)
|
|
1612
|
+
return false; // empty run on either side
|
|
1613
|
+
const run = lineText.slice(start, end);
|
|
1614
|
+
if (!run.includes("-") || ![...run].some(isLetter))
|
|
1615
|
+
return false;
|
|
1616
|
+
// Exempt #1 — AST shows the cursor is in a real requirement context.
|
|
1617
|
+
if (nodeAtCursor) {
|
|
1618
|
+
let n = nodeAtCursor;
|
|
1619
|
+
while (n) {
|
|
1620
|
+
if (n.type === "req_id" ||
|
|
1621
|
+
n.type === "req_implementation" ||
|
|
1622
|
+
n.type === "requirement_definition")
|
|
1623
|
+
return false;
|
|
1624
|
+
n = n.parent;
|
|
1625
|
+
}
|
|
1626
|
+
}
|
|
1627
|
+
// Exempt #2 — same-line back-scan for `implementsReq` / `req` keyword
|
|
1628
|
+
// before any statement boundary. Covers the AST-didn't-form case (no `;`
|
|
1629
|
+
// closing the partial req_implementation).
|
|
1630
|
+
const before = lineText.slice(0, start);
|
|
1631
|
+
// If before contains `;` or `}`, that closes any prior context — bail.
|
|
1632
|
+
if (/[;}]/.test(before))
|
|
1633
|
+
return true;
|
|
1634
|
+
if (/\b(?:implementsReq|req)\b/.test(before))
|
|
1635
|
+
return false;
|
|
1636
|
+
return true;
|
|
1637
|
+
}
|
|
1638
|
+
/**
|
|
1639
|
+
* Topic 050 case 2 — bare complete multiplicity sitting alone in a class
|
|
1640
|
+
* body or association block, with cursor at the exact end of the
|
|
1641
|
+
* multiplicity token. Examples: `0..*|`, `1|`, `0..1|` placed alone in a
|
|
1642
|
+
* class-body line. Without further context (arrow, type) this isn't a real
|
|
1643
|
+
* completion target — the partial-association refinement keeps it from
|
|
1644
|
+
* classifying as `association_arrow`, but the analyzer would still fall
|
|
1645
|
+
* through to `class_body` and surface 47 wrong keywords. Suppress.
|
|
1646
|
+
*
|
|
1647
|
+
* Negatives this guard does NOT match:
|
|
1648
|
+
* `0..* |` (cursor past whitespace) — segments differ; partial-association
|
|
1649
|
+
* logic upstream picks it up as a real arrow slot.
|
|
1650
|
+
* `1 -> *|` (after the right multiplicity of an in-progress association)
|
|
1651
|
+
* — segment has an arrow; this guard requires no arrow before cursor.
|
|
1652
|
+
*/
|
|
1653
|
+
function isBareCompleteMultiplicityAtEnd(node, content, line, column) {
|
|
1654
|
+
if (node.type !== "multiplicity")
|
|
1655
|
+
return false;
|
|
1656
|
+
const lines = content.split("\n");
|
|
1657
|
+
let offset = 0;
|
|
1658
|
+
for (let i = 0; i < line; i++)
|
|
1659
|
+
offset += lines[i].length + 1;
|
|
1660
|
+
offset += column;
|
|
1661
|
+
if (node.endIndex !== offset)
|
|
1662
|
+
return false;
|
|
1663
|
+
const err = node.parent;
|
|
1664
|
+
if (!err || err.type !== "ERROR")
|
|
1665
|
+
return false;
|
|
1666
|
+
// No arrow or `;` before this multiplicity in the ERROR
|
|
1667
|
+
for (let i = 0; i < err.childCount; i++) {
|
|
1668
|
+
const c = err.child(i);
|
|
1669
|
+
if (!c)
|
|
1670
|
+
continue;
|
|
1671
|
+
if (c.id === node.id)
|
|
1672
|
+
break;
|
|
1673
|
+
if (c.type === "arrow" || c.type === "->")
|
|
1674
|
+
return false;
|
|
1675
|
+
if (c.type === ";")
|
|
1676
|
+
return false;
|
|
1677
|
+
}
|
|
1678
|
+
// ERROR must sit in a class-like or association container
|
|
1679
|
+
let p = err.parent;
|
|
1680
|
+
while (p) {
|
|
1681
|
+
if (CLASS_LIKE_DEF_TYPES.has(p.type) || p.type === "association_definition") {
|
|
1682
|
+
return true;
|
|
1683
|
+
}
|
|
1684
|
+
if (p.type === "source_file")
|
|
1685
|
+
return false;
|
|
1686
|
+
p = p.parent;
|
|
1687
|
+
}
|
|
1688
|
+
return false;
|
|
1689
|
+
}
|
|
1690
|
+
function offsetAt(content, line, column) {
|
|
1691
|
+
const lines = content.split("\n");
|
|
1692
|
+
let offset = 0;
|
|
1693
|
+
for (let i = 0; i < line && i < lines.length; i++) {
|
|
1694
|
+
offset += lines[i].length + 1;
|
|
1695
|
+
}
|
|
1696
|
+
return offset + Math.min(column, lines[line]?.length ?? 0);
|
|
1697
|
+
}
|
|
692
1698
|
function findPreviousLeaf(tree, content, line, column) {
|
|
693
1699
|
const lines = content.split("\n");
|
|
694
1700
|
let offset = 0;
|
|
@@ -793,6 +1799,26 @@ function resolveCompletionScope(completionsQuery, tree, line, column) {
|
|
|
793
1799
|
return "association_type";
|
|
794
1800
|
if (kindStr === "association_typed_prefix")
|
|
795
1801
|
return "association_typed_prefix";
|
|
1802
|
+
if (kindStr === "isa_typed_prefix")
|
|
1803
|
+
return "isa_typed_prefix";
|
|
1804
|
+
if (kindStr === "decl_type_typed_prefix")
|
|
1805
|
+
return "decl_type_typed_prefix";
|
|
1806
|
+
if (kindStr === "return_type_typed_prefix")
|
|
1807
|
+
return "return_type_typed_prefix";
|
|
1808
|
+
if (kindStr === "code_injection_method")
|
|
1809
|
+
return "code_injection_method";
|
|
1810
|
+
if (kindStr === "filter_include_target")
|
|
1811
|
+
return "filter_include_target";
|
|
1812
|
+
if (kindStr === "param_type_typed_prefix")
|
|
1813
|
+
return "param_type_typed_prefix";
|
|
1814
|
+
if (kindStr === "association_arrow")
|
|
1815
|
+
return "association_arrow";
|
|
1816
|
+
if (kindStr === "referenced_sm_target")
|
|
1817
|
+
return "referenced_sm_target";
|
|
1818
|
+
if (kindStr === "trait_sm_binding_target")
|
|
1819
|
+
return "trait_sm_binding_target";
|
|
1820
|
+
if (kindStr === "trait_sm_binding_state_target")
|
|
1821
|
+
return "trait_sm_binding_state_target";
|
|
796
1822
|
if (kindStr === "none")
|
|
797
1823
|
return null;
|
|
798
1824
|
return kindStr.split("_");
|
|
@@ -922,11 +1948,132 @@ function isInsideTraitSmOpContext(node) {
|
|
|
922
1948
|
}
|
|
923
1949
|
return false;
|
|
924
1950
|
}
|
|
1951
|
+
/**
|
|
1952
|
+
* Topic 053 — `isA T<` recovery shapes parse as a flat ERROR under
|
|
1953
|
+
* class_definition (no `isa_declaration` / `type_list` / `type_name`
|
|
1954
|
+
* ancestor). Returns the matched recovery info if the ERROR shape is
|
|
1955
|
+
* `[isA, qualified_name(trait), <, ...rest]` AND cursor sits at/after
|
|
1956
|
+
* the `<`. The `mode` field tells the caller whether we have a
|
|
1957
|
+
* trait-SM operation marker (`-` / `+`) or just the bare `<`.
|
|
1958
|
+
*/
|
|
1959
|
+
function recoverTraitSmOpFromIsAError(nodeAtCursor) {
|
|
1960
|
+
// Bail when cursor is already inside a formed `trait_sm_binding` —
|
|
1961
|
+
// the parse has progressed past the `T<` recovery shape and the user
|
|
1962
|
+
// is editing a deeper position (e.g. `isA T<sm as S|`). Existing
|
|
1963
|
+
// logic for those positions should run.
|
|
1964
|
+
let walk = nodeAtCursor;
|
|
1965
|
+
while (walk) {
|
|
1966
|
+
if (walk.type === "trait_sm_binding")
|
|
1967
|
+
return null;
|
|
1968
|
+
if (walk.type === "ERROR")
|
|
1969
|
+
break;
|
|
1970
|
+
walk = walk.parent;
|
|
1971
|
+
}
|
|
1972
|
+
// Find the enclosing ERROR.
|
|
1973
|
+
let err = nodeAtCursor;
|
|
1974
|
+
while (err && err.type !== "ERROR")
|
|
1975
|
+
err = err.parent;
|
|
1976
|
+
if (!err)
|
|
1977
|
+
return null;
|
|
1978
|
+
// Must sit directly under a class-like container.
|
|
1979
|
+
let p = err.parent;
|
|
1980
|
+
let inClassLike = false;
|
|
1981
|
+
while (p) {
|
|
1982
|
+
if (CLASS_LIKE_DEF_TYPES.has(p.type)) {
|
|
1983
|
+
inClassLike = true;
|
|
1984
|
+
break;
|
|
1985
|
+
}
|
|
1986
|
+
if (p.type === "source_file")
|
|
1987
|
+
break;
|
|
1988
|
+
p = p.parent;
|
|
1989
|
+
}
|
|
1990
|
+
if (!inClassLike)
|
|
1991
|
+
return null;
|
|
1992
|
+
// Walk children: find the [isA, qualified_name, <] prefix BEFORE cursor.
|
|
1993
|
+
let sawIsA = false;
|
|
1994
|
+
let traitName;
|
|
1995
|
+
let sawAngle = false;
|
|
1996
|
+
let sawOpMarker = false;
|
|
1997
|
+
for (let i = 0; i < err.childCount; i++) {
|
|
1998
|
+
const c = err.child(i);
|
|
1999
|
+
if (!c || c.isExtra)
|
|
2000
|
+
continue;
|
|
2001
|
+
if (c.startIndex >= nodeAtCursor.startIndex && c.id !== nodeAtCursor.id)
|
|
2002
|
+
break;
|
|
2003
|
+
if (c.type === "isA") {
|
|
2004
|
+
sawIsA = true;
|
|
2005
|
+
continue;
|
|
2006
|
+
}
|
|
2007
|
+
if (sawIsA && !traitName && c.type === "qualified_name") {
|
|
2008
|
+
const lastId = c.namedChild(c.namedChildCount - 1);
|
|
2009
|
+
if (lastId?.type === "identifier")
|
|
2010
|
+
traitName = lastId.text;
|
|
2011
|
+
continue;
|
|
2012
|
+
}
|
|
2013
|
+
if (sawIsA && traitName && c.type === "<") {
|
|
2014
|
+
sawAngle = true;
|
|
2015
|
+
continue;
|
|
2016
|
+
}
|
|
2017
|
+
if (sawAngle && (c.type === "-" || c.type === "+")) {
|
|
2018
|
+
sawOpMarker = true;
|
|
2019
|
+
continue;
|
|
2020
|
+
}
|
|
2021
|
+
}
|
|
2022
|
+
if (!sawIsA || !traitName || !sawAngle)
|
|
2023
|
+
return null;
|
|
2024
|
+
return { traitName, mode: sawOpMarker ? "op" : "bare" };
|
|
2025
|
+
}
|
|
2026
|
+
/**
|
|
2027
|
+
* Topic 053 — broken method-name slot in a class body when the method has
|
|
2028
|
+
* no `{}` body. Parses as `class_definition < ERROR < [type_name,
|
|
2029
|
+
* identifier(name), (, )]`. Cursor on the name identifier is the broken
|
|
2030
|
+
* equivalent of `void method|() {}` which is normally caught by
|
|
2031
|
+
* isAtAttributeNamePosition. Suppress.
|
|
2032
|
+
*/
|
|
2033
|
+
function isInsideBrokenMethodNameSlot(node) {
|
|
2034
|
+
if (node.type !== "identifier")
|
|
2035
|
+
return false;
|
|
2036
|
+
const err = node.parent;
|
|
2037
|
+
if (err?.type !== "ERROR")
|
|
2038
|
+
return false;
|
|
2039
|
+
// ERROR's children must include type_name BEFORE this identifier, and
|
|
2040
|
+
// `(` after — confirms the method-shape recovery.
|
|
2041
|
+
let sawTypeName = false;
|
|
2042
|
+
let sawNameAfterTypeName = false;
|
|
2043
|
+
let sawOpenParenAfterName = false;
|
|
2044
|
+
for (let i = 0; i < err.childCount; i++) {
|
|
2045
|
+
const c = err.child(i);
|
|
2046
|
+
if (!c || c.isExtra)
|
|
2047
|
+
continue;
|
|
2048
|
+
if (c.type === "type_name")
|
|
2049
|
+
sawTypeName = true;
|
|
2050
|
+
else if (c.id === node.id) {
|
|
2051
|
+
if (sawTypeName)
|
|
2052
|
+
sawNameAfterTypeName = true;
|
|
2053
|
+
}
|
|
2054
|
+
else if (c.type === "(") {
|
|
2055
|
+
if (sawNameAfterTypeName)
|
|
2056
|
+
sawOpenParenAfterName = true;
|
|
2057
|
+
}
|
|
2058
|
+
}
|
|
2059
|
+
if (!sawNameAfterTypeName || !sawOpenParenAfterName)
|
|
2060
|
+
return false;
|
|
2061
|
+
// Confirm class-like container.
|
|
2062
|
+
let p = err.parent;
|
|
2063
|
+
while (p) {
|
|
2064
|
+
if (CLASS_LIKE_DEF_TYPES.has(p.type))
|
|
2065
|
+
return true;
|
|
2066
|
+
if (p.type === "source_file")
|
|
2067
|
+
return false;
|
|
2068
|
+
p = p.parent;
|
|
2069
|
+
}
|
|
2070
|
+
return false;
|
|
2071
|
+
}
|
|
925
2072
|
/** Check if a node is inside angle brackets of a type_name (trait type arguments). */
|
|
926
2073
|
function isInsideTraitAngleBrackets(node) {
|
|
927
2074
|
let n = node;
|
|
928
2075
|
while (n) {
|
|
929
|
-
if (n.type === "type_name" || n.type === "type_list")
|
|
2076
|
+
if (n.type === "type_name" || n.type === "type_list" || n.type === "isa_type_list")
|
|
930
2077
|
return true;
|
|
931
2078
|
// ERROR nodes inside isA declarations with angle brackets
|
|
932
2079
|
if (n.type === "isa_declaration")
|
|
@@ -955,8 +2102,8 @@ function extractTraitNameFromAngleBrackets(node) {
|
|
|
955
2102
|
const isa = n.parent;
|
|
956
2103
|
for (let i = 0; i < isa.namedChildCount; i++) {
|
|
957
2104
|
const child = isa.namedChild(i);
|
|
958
|
-
if (child?.type === "type_list") {
|
|
959
|
-
// type_list > type_name > qualified_name
|
|
2105
|
+
if (child?.type === "type_list" || child?.type === "isa_type_list") {
|
|
2106
|
+
// type_list/isa_type_list > type_name > qualified_name
|
|
960
2107
|
const typeName = child.namedChild(0);
|
|
961
2108
|
if (typeName?.type === "type_name") {
|
|
962
2109
|
const qn = typeName.childForFieldName("name") ?? typeName.namedChild(0);
|