@domainlang/language 0.6.0 → 0.8.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/README.md +1 -1
- package/out/domain-lang-module.d.ts +2 -0
- package/out/domain-lang-module.js +23 -2
- package/out/domain-lang-module.js.map +1 -1
- package/out/lsp/domain-lang-completion.d.ts +142 -1
- package/out/lsp/domain-lang-completion.js +620 -22
- package/out/lsp/domain-lang-completion.js.map +1 -1
- package/out/lsp/domain-lang-document-symbol-provider.d.ts +79 -0
- package/out/lsp/domain-lang-document-symbol-provider.js +210 -0
- package/out/lsp/domain-lang-document-symbol-provider.js.map +1 -0
- package/out/lsp/domain-lang-index-manager.d.ts +98 -1
- package/out/lsp/domain-lang-index-manager.js +214 -7
- package/out/lsp/domain-lang-index-manager.js.map +1 -1
- package/out/lsp/domain-lang-node-kind-provider.d.ts +27 -0
- package/out/lsp/domain-lang-node-kind-provider.js +87 -0
- package/out/lsp/domain-lang-node-kind-provider.js.map +1 -0
- package/out/lsp/domain-lang-scope-provider.d.ts +100 -0
- package/out/lsp/domain-lang-scope-provider.js +170 -0
- package/out/lsp/domain-lang-scope-provider.js.map +1 -0
- package/out/lsp/domain-lang-workspace-manager.d.ts +46 -0
- package/out/lsp/domain-lang-workspace-manager.js +148 -4
- package/out/lsp/domain-lang-workspace-manager.js.map +1 -1
- package/out/lsp/hover/domain-lang-hover.d.ts +16 -6
- package/out/lsp/hover/domain-lang-hover.js +160 -134
- package/out/lsp/hover/domain-lang-hover.js.map +1 -1
- package/out/lsp/hover/hover-builders.d.ts +57 -0
- package/out/lsp/hover/hover-builders.js +171 -0
- package/out/lsp/hover/hover-builders.js.map +1 -0
- package/out/main.js +116 -20
- package/out/main.js.map +1 -1
- package/out/sdk/index.d.ts +2 -1
- package/out/sdk/index.js +1 -1
- package/out/sdk/index.js.map +1 -1
- package/out/sdk/loader-node.js +1 -1
- package/out/sdk/loader-node.js.map +1 -1
- package/out/sdk/loader.d.ts +55 -2
- package/out/sdk/loader.js +87 -28
- package/out/sdk/loader.js.map +1 -1
- package/out/sdk/query.js +14 -11
- package/out/sdk/query.js.map +1 -1
- package/out/services/import-resolver.d.ts +29 -6
- package/out/services/import-resolver.js +48 -9
- package/out/services/import-resolver.js.map +1 -1
- package/out/services/package-boundary-detector.d.ts +101 -0
- package/out/services/package-boundary-detector.js +211 -0
- package/out/services/package-boundary-detector.js.map +1 -0
- package/out/services/performance-optimizer.js +6 -2
- package/out/services/performance-optimizer.js.map +1 -1
- package/out/services/types.d.ts +24 -0
- package/out/services/types.js.map +1 -1
- package/out/services/workspace-manager.d.ts +73 -6
- package/out/services/workspace-manager.js +210 -57
- package/out/services/workspace-manager.js.map +1 -1
- package/out/utils/import-utils.d.ts +9 -6
- package/out/utils/import-utils.js +26 -15
- package/out/utils/import-utils.js.map +1 -1
- package/out/validation/constants.d.ts +20 -0
- package/out/validation/constants.js +39 -3
- package/out/validation/constants.js.map +1 -1
- package/out/validation/import.d.ts +22 -1
- package/out/validation/import.js +104 -16
- package/out/validation/import.js.map +1 -1
- package/out/validation/maps.js +101 -3
- package/out/validation/maps.js.map +1 -1
- package/package.json +5 -5
- package/src/domain-lang-module.ts +26 -3
- package/src/lsp/domain-lang-completion.ts +736 -27
- package/src/lsp/domain-lang-document-symbol-provider.ts +254 -0
- package/src/lsp/domain-lang-index-manager.ts +250 -7
- package/src/lsp/domain-lang-node-kind-provider.ts +119 -0
- package/src/lsp/domain-lang-scope-provider.ts +250 -0
- package/src/lsp/domain-lang-workspace-manager.ts +187 -4
- package/src/lsp/hover/domain-lang-hover.ts +189 -131
- package/src/lsp/hover/hover-builders.ts +208 -0
- package/src/main.ts +156 -23
- package/src/sdk/index.ts +2 -1
- package/src/sdk/loader-node.ts +2 -1
- package/src/sdk/loader.ts +125 -34
- package/src/sdk/query.ts +15 -11
- package/src/services/import-resolver.ts +60 -9
- package/src/services/package-boundary-detector.ts +238 -0
- package/src/services/performance-optimizer.ts +6 -2
- package/src/services/types.ts +25 -0
- package/src/services/workspace-manager.ts +259 -62
- package/src/utils/import-utils.ts +27 -15
- package/src/validation/constants.ts +47 -6
- package/src/validation/import.ts +124 -16
- package/src/validation/maps.ts +118 -4
|
@@ -6,12 +6,18 @@
|
|
|
6
6
|
* - Grammar-aligned: Completions match grammar structure exactly
|
|
7
7
|
* - Simple: Uses parent node to determine context
|
|
8
8
|
* - Maintainable: Clear mapping from grammar to completions
|
|
9
|
+
* - Import-aware: Provides completions for local paths, aliases, and dependencies
|
|
9
10
|
*/
|
|
10
11
|
|
|
11
|
-
import type { AstNode } from 'langium';
|
|
12
|
+
import type { AstNode, AstNodeDescription, LangiumDocument, ReferenceInfo } from 'langium';
|
|
13
|
+
import { AstUtils, GrammarAST } from 'langium';
|
|
12
14
|
import { CompletionAcceptor, CompletionContext, DefaultCompletionProvider, NextFeature } from 'langium/lsp';
|
|
13
|
-
import { CompletionItemKind, InsertTextFormat } from 'vscode-languageserver';
|
|
15
|
+
import { CompletionItemKind, CompletionList, InsertTextFormat, TextEdit } from 'vscode-languageserver';
|
|
16
|
+
import type { CancellationToken, CompletionItem, CompletionParams } from 'vscode-languageserver-protocol';
|
|
14
17
|
import * as ast from '../generated/ast.js';
|
|
18
|
+
import type { DomainLangServices } from '../domain-lang-module.js';
|
|
19
|
+
import type { WorkspaceManager } from '../services/workspace-manager.js';
|
|
20
|
+
import type { ModelManifest, DependencySpec } from '../services/types.js';
|
|
15
21
|
|
|
16
22
|
/**
|
|
17
23
|
* Top-level snippet templates for creating new AST nodes.
|
|
@@ -118,33 +124,446 @@ const TOP_LEVEL_SNIPPETS = [
|
|
|
118
124
|
] as const;
|
|
119
125
|
|
|
120
126
|
export class DomainLangCompletionProvider extends DefaultCompletionProvider {
|
|
121
|
-
|
|
127
|
+
private readonly workspaceManager: WorkspaceManager;
|
|
128
|
+
|
|
129
|
+
override readonly completionOptions = {
|
|
130
|
+
triggerCharacters: ['.']
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
constructor(services: DomainLangServices) {
|
|
134
|
+
super(services);
|
|
135
|
+
this.workspaceManager = services.imports.WorkspaceManager;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Override getCompletion to handle import string completions for incomplete strings.
|
|
140
|
+
*
|
|
141
|
+
* **Why this override is necessary:**
|
|
142
|
+
* When the cursor sits inside an incomplete string token (e.g. `import "partial`)
|
|
143
|
+
* Langium's lexer cannot produce a valid STRING token, so `completionFor()` never
|
|
144
|
+
* fires for the `uri` property. This override detects the incomplete-string case
|
|
145
|
+
* via regex and returns completions directly. For all other positions the parent
|
|
146
|
+
* implementation (which routes through `completionFor`) is used.
|
|
147
|
+
*/
|
|
148
|
+
override async getCompletion(
|
|
149
|
+
document: LangiumDocument,
|
|
150
|
+
params: CompletionParams,
|
|
151
|
+
cancelToken?: CancellationToken
|
|
152
|
+
): Promise<CompletionList | undefined> {
|
|
153
|
+
const text = document.textDocument.getText();
|
|
154
|
+
const offset = document.textDocument.offsetAt(params.position);
|
|
155
|
+
const textBefore = text.substring(0, offset);
|
|
156
|
+
|
|
157
|
+
// Pattern: import "partial_text (opening quote, no closing quote)
|
|
158
|
+
const importStringPattern = /\b(import|Import)\s+"([^"]*)$/;
|
|
159
|
+
const match = importStringPattern.exec(textBefore);
|
|
160
|
+
|
|
161
|
+
if (match) {
|
|
162
|
+
const currentInput = match[2];
|
|
163
|
+
const items = await this.collectImportItems(currentInput);
|
|
164
|
+
return CompletionList.create(items, true);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const result = await super.getCompletion(document, params, cancelToken);
|
|
168
|
+
return this.segmentDottedCompletions(result, text, offset, document);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Post-process completion results to replace full-FQN items with segmented items
|
|
173
|
+
* when the cursor is at a dotted path.
|
|
174
|
+
*
|
|
175
|
+
* **Why this is necessary:**
|
|
176
|
+
* Langium's `buildContexts` creates a "data type rule" context that triggers our
|
|
177
|
+
* `completionForCrossReference` override, which correctly produces segmented items.
|
|
178
|
+
* However, when the CST is broken (e.g., partial `Core.B` doesn't fully parse as
|
|
179
|
+
* a QualifiedName), `findDataTypeRuleStart` returns `undefined` and only token-based
|
|
180
|
+
* contexts fire. These contexts have features that are ID terminals (not cross-references),
|
|
181
|
+
* so `completionForCrossReference` is never called. Langium's default pipeline then
|
|
182
|
+
* produces full-FQN items like `Core.Baunwalls.Jannie`.
|
|
183
|
+
*
|
|
184
|
+
* This post-processing step catches those FQN items and segments them,
|
|
185
|
+
* ensuring consistent behavior regardless of parse state.
|
|
186
|
+
*/
|
|
187
|
+
private segmentDottedCompletions(
|
|
188
|
+
result: CompletionList | undefined,
|
|
189
|
+
text: string,
|
|
190
|
+
offset: number,
|
|
191
|
+
_document: LangiumDocument
|
|
192
|
+
): CompletionList | undefined {
|
|
193
|
+
if (!result?.items?.length) return result;
|
|
194
|
+
|
|
195
|
+
// Detect dotted path at cursor by scanning backwards
|
|
196
|
+
const dottedPath = this.extractDottedPathAtCursor(text, offset);
|
|
197
|
+
if (!dottedPath) return result;
|
|
198
|
+
|
|
199
|
+
const { fullTyped, fullStart } = dottedPath;
|
|
200
|
+
const lastDotIndex = fullTyped.lastIndexOf('.');
|
|
201
|
+
const prefix = fullTyped.substring(0, lastDotIndex + 1);
|
|
202
|
+
const partial = fullTyped.substring(lastDotIndex + 1);
|
|
203
|
+
|
|
204
|
+
// If completionForCrossReference already produced segmented items, just clean up
|
|
205
|
+
if (this.hasSegmentedItems(result.items, prefix)) {
|
|
206
|
+
return this.removeLeakedFqnItems(result, prefix);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// No segmented items — transform FQN items into segmented items
|
|
210
|
+
const positions = this.calculateTextPositions(text, offset, fullStart);
|
|
211
|
+
const newItems = this.transformToSegmentedItems(
|
|
212
|
+
result.items, prefix, partial, fullTyped, positions
|
|
213
|
+
);
|
|
214
|
+
return CompletionList.create(newItems, true);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/** Check if any items are already segmented by our completionForCrossReference. */
|
|
218
|
+
private hasSegmentedItems(items: CompletionItem[], prefix: string): boolean {
|
|
219
|
+
return items.some(item =>
|
|
220
|
+
!item.label.includes('.') && item.filterText?.startsWith(prefix)
|
|
221
|
+
);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/** Remove full-FQN items that leaked alongside segmented items. */
|
|
225
|
+
private removeLeakedFqnItems(
|
|
226
|
+
result: CompletionList,
|
|
227
|
+
prefix: string
|
|
228
|
+
): CompletionList {
|
|
229
|
+
const prefixRoot = prefix.substring(0, prefix.length - 1);
|
|
230
|
+
result.items = result.items.filter(item =>
|
|
231
|
+
!item.label.includes('.') || !item.label.startsWith(prefixRoot)
|
|
232
|
+
);
|
|
233
|
+
return result;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/** Calculate line/character positions from text offsets. */
|
|
237
|
+
private calculateTextPositions(
|
|
238
|
+
text: string,
|
|
239
|
+
offset: number,
|
|
240
|
+
fullStart: number
|
|
241
|
+
): { startPos: { line: number; character: number }; endPos: { line: number; character: number } } {
|
|
242
|
+
const startPos = { line: 0, character: 0 };
|
|
243
|
+
const endPos = { line: 0, character: 0 };
|
|
244
|
+
let line = 0;
|
|
245
|
+
let col = 0;
|
|
246
|
+
for (let i = 0; i < text.length && i <= offset; i++) {
|
|
247
|
+
if (i === fullStart) { startPos.line = line; startPos.character = col; }
|
|
248
|
+
if (i === offset) { endPos.line = line; endPos.character = col; }
|
|
249
|
+
if (text[i] === '\n') { line++; col = 0; } else { col++; }
|
|
250
|
+
}
|
|
251
|
+
if (offset === text.length) { endPos.line = line; endPos.character = col; }
|
|
252
|
+
return { startPos, endPos };
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/** Transform FQN completion items into segmented (next-segment-only) items. */
|
|
256
|
+
private transformToSegmentedItems(
|
|
257
|
+
items: CompletionItem[],
|
|
258
|
+
prefix: string,
|
|
259
|
+
partial: string,
|
|
260
|
+
fullTyped: string,
|
|
261
|
+
positions: { startPos: { line: number; character: number }; endPos: { line: number; character: number } }
|
|
262
|
+
): CompletionItem[] {
|
|
263
|
+
const seenSegments = new Set<string>();
|
|
264
|
+
const newItems: CompletionItem[] = [];
|
|
265
|
+
|
|
266
|
+
for (const item of items) {
|
|
267
|
+
const segmented = this.segmentSingleItem(
|
|
268
|
+
item, prefix, partial, fullTyped, positions, seenSegments
|
|
269
|
+
);
|
|
270
|
+
if (segmented) newItems.push(segmented);
|
|
271
|
+
}
|
|
272
|
+
return newItems;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/** Transform a single FQN item into a segmented item, or keep non-matching items. */
|
|
276
|
+
private segmentSingleItem(
|
|
277
|
+
item: CompletionItem,
|
|
278
|
+
prefix: string,
|
|
279
|
+
partial: string,
|
|
280
|
+
fullTyped: string,
|
|
281
|
+
positions: { startPos: { line: number; character: number }; endPos: { line: number; character: number } },
|
|
282
|
+
seenSegments: Set<string>
|
|
283
|
+
): CompletionItem | undefined {
|
|
284
|
+
const itemName = item.label;
|
|
285
|
+
|
|
286
|
+
// Keep non-matching items (keywords, snippets, etc.)
|
|
287
|
+
if (!itemName.startsWith(prefix)) {
|
|
288
|
+
if (!itemName.includes('.') || !this.sharesDottedPrefix(itemName, fullTyped)) {
|
|
289
|
+
return item;
|
|
290
|
+
}
|
|
291
|
+
return undefined;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
const remainder = itemName.substring(prefix.length);
|
|
295
|
+
const dotIndex = remainder.indexOf('.');
|
|
296
|
+
const segment = dotIndex === -1 ? remainder : remainder.substring(0, dotIndex);
|
|
297
|
+
|
|
298
|
+
if (!segment || seenSegments.has(segment)) return undefined;
|
|
299
|
+
if (partial && !segment.toLowerCase().startsWith(partial.toLowerCase())) return undefined;
|
|
300
|
+
|
|
301
|
+
seenSegments.add(segment);
|
|
302
|
+
|
|
303
|
+
const isLeaf = dotIndex === -1;
|
|
304
|
+
const fullInsertText = prefix + segment;
|
|
305
|
+
return {
|
|
306
|
+
label: segment,
|
|
307
|
+
kind: isLeaf ? (item.kind ?? CompletionItemKind.Reference) : CompletionItemKind.Module,
|
|
308
|
+
detail: isLeaf ? itemName : 'Namespace',
|
|
309
|
+
sortText: segment,
|
|
310
|
+
filterText: fullInsertText,
|
|
311
|
+
textEdit: TextEdit.replace(
|
|
312
|
+
{ start: positions.startPos, end: positions.endPos },
|
|
313
|
+
fullInsertText
|
|
314
|
+
),
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* Check if two dotted names share a common prefix up to the first differing segment.
|
|
320
|
+
*/
|
|
321
|
+
private sharesDottedPrefix(a: string, b: string): boolean {
|
|
322
|
+
const aParts = a.split('.');
|
|
323
|
+
const bParts = b.split('.');
|
|
324
|
+
return aParts.length > 0 && bParts.length > 0 && aParts[0] === bParts[0];
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* Scan backwards from cursor to find a dotted identifier path.
|
|
329
|
+
* Returns the full typed text and start position, or undefined if no dots found.
|
|
330
|
+
*
|
|
331
|
+
* This is intentionally cursor-based (not tokenOffset-based) to be robust
|
|
332
|
+
* across all Langium completion contexts.
|
|
333
|
+
*/
|
|
334
|
+
private extractDottedPathAtCursor(
|
|
335
|
+
text: string,
|
|
336
|
+
offset: number
|
|
337
|
+
): { fullTyped: string; fullStart: number } | undefined {
|
|
338
|
+
// Walk backwards from cursor through the current partial identifier
|
|
339
|
+
let pos = offset - 1;
|
|
340
|
+
while (pos >= 0 && /\w/.test(text[pos])) pos--;
|
|
341
|
+
|
|
342
|
+
// Walk backwards through `.ID` pairs to find start of dotted path
|
|
343
|
+
pos = this.walkBackThroughDotIdPairs(text, pos);
|
|
344
|
+
|
|
345
|
+
const fullStart = pos + 1;
|
|
346
|
+
const fullTyped = text.substring(fullStart, offset);
|
|
347
|
+
|
|
348
|
+
if (!fullTyped.includes('.')) return undefined;
|
|
349
|
+
return { fullTyped, fullStart };
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/** Walk backwards through `.ID` pairs from the given position. */
|
|
353
|
+
private walkBackThroughDotIdPairs(text: string, pos: number): number {
|
|
354
|
+
while (pos >= 0) {
|
|
355
|
+
const preSpace = pos;
|
|
356
|
+
while (pos >= 0 && text[pos] === ' ') pos--;
|
|
357
|
+
if (pos < 0 || text[pos] !== '.') return preSpace;
|
|
358
|
+
pos--; // skip dot
|
|
359
|
+
while (pos >= 0 && text[pos] === ' ') pos--;
|
|
360
|
+
if (pos < 0 || !/\w/.test(text[pos])) return preSpace;
|
|
361
|
+
while (pos >= 0 && /\w/.test(text[pos])) pos--;
|
|
362
|
+
}
|
|
363
|
+
return pos;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* Collect import completion items for a given partial input string.
|
|
368
|
+
*
|
|
369
|
+
* Shared by both the `getCompletion` override (incomplete string case)
|
|
370
|
+
* and the `completionFor` path (normal Langium feature-based routing).
|
|
371
|
+
*/
|
|
372
|
+
private async collectImportItems(currentInput: string): Promise<CompletionItem[]> {
|
|
373
|
+
let manifest: ModelManifest | undefined;
|
|
374
|
+
try {
|
|
375
|
+
manifest = await this.workspaceManager.ensureManifestLoaded();
|
|
376
|
+
} catch {
|
|
377
|
+
// Continue with undefined manifest – will show basic starters
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
const items: CompletionItem[] = [];
|
|
381
|
+
const collector = (_ctx: unknown, item: CompletionItem): void => { items.push(item); };
|
|
382
|
+
|
|
383
|
+
// Re-use the acceptor-based helpers by wrapping the collector
|
|
384
|
+
// We pass `undefined as unknown` for context since the collector ignores it
|
|
385
|
+
const ctx = undefined as unknown as CompletionContext;
|
|
386
|
+
const accept = ((_context: CompletionContext, item: CompletionItem): void => {
|
|
387
|
+
collector(undefined, item);
|
|
388
|
+
}) as CompletionAcceptor;
|
|
389
|
+
|
|
390
|
+
if (currentInput === '' || !currentInput) {
|
|
391
|
+
this.addAllStarterOptions(ctx, accept, manifest);
|
|
392
|
+
} else if (currentInput.startsWith('@')) {
|
|
393
|
+
this.addAliasCompletions(ctx, accept, currentInput, manifest);
|
|
394
|
+
} else if (currentInput.startsWith('./') || currentInput.startsWith('../')) {
|
|
395
|
+
this.addLocalPathStarters(ctx, accept);
|
|
396
|
+
} else if (currentInput.includes('/') && !currentInput.startsWith('.')) {
|
|
397
|
+
this.addDependencyCompletions(ctx, accept, currentInput, manifest);
|
|
398
|
+
} else {
|
|
399
|
+
this.addFilteredOptions(ctx, accept, currentInput, manifest);
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
return items;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
protected override async completionFor(
|
|
122
406
|
context: CompletionContext,
|
|
123
407
|
next: NextFeature,
|
|
124
408
|
acceptor: CompletionAcceptor
|
|
125
|
-
): void {
|
|
409
|
+
): Promise<void> {
|
|
126
410
|
try {
|
|
127
|
-
this.safeCompletionFor(context, next, acceptor);
|
|
411
|
+
await this.safeCompletionFor(context, next, acceptor);
|
|
128
412
|
} catch (error) {
|
|
129
413
|
console.error('Error in completionFor:', error);
|
|
130
414
|
// Fall back to default completion on error
|
|
131
|
-
super.completionFor(context, next, acceptor);
|
|
415
|
+
await super.completionFor(context, next, acceptor);
|
|
132
416
|
}
|
|
133
417
|
}
|
|
134
418
|
|
|
135
|
-
|
|
419
|
+
/**
|
|
420
|
+
* Override cross-reference completion to provide dot-segmented completions.
|
|
421
|
+
*
|
|
422
|
+
* When the user types a dotted prefix (e.g., `Core.`), only the next
|
|
423
|
+
* namespace segment is shown instead of the full qualified name —
|
|
424
|
+
* matching how modern IDEs handle hierarchical completions.
|
|
425
|
+
*
|
|
426
|
+
* Example: Scope contains `Core.CoreDomain`, `Core.BaunWalls.Jannie`, `Core.BaunWalls.Anna`
|
|
427
|
+
* - No dots typed → default FQN labels: `Core.CoreDomain`, `Core.BaunWalls.Jannie`, ...
|
|
428
|
+
* - `Core.` typed → segmented: `CoreDomain`, `BaunWalls`
|
|
429
|
+
* - `Core.BaunWalls.` typed → segmented: `Jannie`, `Anna`
|
|
430
|
+
*/
|
|
431
|
+
protected override completionForCrossReference(
|
|
136
432
|
context: CompletionContext,
|
|
137
|
-
next: NextFeature
|
|
433
|
+
next: NextFeature<GrammarAST.CrossReference>,
|
|
434
|
+
acceptor: CompletionAcceptor
|
|
435
|
+
): void | Promise<void> {
|
|
436
|
+
const text = context.textDocument.getText();
|
|
437
|
+
const fullStart = this.findDottedPathStart(text, context.tokenOffset);
|
|
438
|
+
const fullTyped = text.substring(fullStart, context.offset);
|
|
439
|
+
|
|
440
|
+
// Without dots, use Langium's default FQN-based completion
|
|
441
|
+
if (!fullTyped.includes('.')) {
|
|
442
|
+
return super.completionForCrossReference(context, next, acceptor);
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
// Build ReferenceInfo to query the scope — replicating what super does
|
|
446
|
+
// but without going through super's fuzzy-matching pipeline.
|
|
447
|
+
const assignment = AstUtils.getContainerOfType(
|
|
448
|
+
next.feature, GrammarAST.isAssignment
|
|
449
|
+
);
|
|
450
|
+
if (!assignment || !context.node) return;
|
|
451
|
+
|
|
452
|
+
let node: AstNode = context.node;
|
|
453
|
+
if (next.type) {
|
|
454
|
+
node = {
|
|
455
|
+
$type: next.type,
|
|
456
|
+
$container: node,
|
|
457
|
+
$containerProperty: next.property,
|
|
458
|
+
} as AstNode;
|
|
459
|
+
AstUtils.assignMandatoryProperties(this.astReflection, node);
|
|
460
|
+
}
|
|
461
|
+
const reference = { $refText: '' } as unknown;
|
|
462
|
+
const refInfo: ReferenceInfo = {
|
|
463
|
+
reference: reference as ReferenceInfo['reference'],
|
|
464
|
+
container: node,
|
|
465
|
+
property: assignment.feature,
|
|
466
|
+
};
|
|
467
|
+
|
|
468
|
+
const candidates = this.getReferenceCandidates(refInfo, context);
|
|
469
|
+
this.acceptSegmentedCandidates(context, candidates, fullTyped, fullStart, acceptor);
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
/**
|
|
473
|
+
* Walk backwards from `tokenOffset` through preceding `.ID` pairs
|
|
474
|
+
* to find the start of the full dotted path.
|
|
475
|
+
* QualifiedName = ID ('.' ID)* — Langium tokenises each ID separately.
|
|
476
|
+
*/
|
|
477
|
+
private findDottedPathStart(text: string, tokenOffset: number): number {
|
|
478
|
+
let fullStart = tokenOffset;
|
|
479
|
+
while (fullStart > 0) {
|
|
480
|
+
let pos = fullStart - 1;
|
|
481
|
+
while (pos >= 0 && text[pos] === ' ') pos--;
|
|
482
|
+
if (pos < 0 || text[pos] !== '.') break;
|
|
483
|
+
const idEnd = pos;
|
|
484
|
+
pos--;
|
|
485
|
+
while (pos >= 0 && text[pos] === ' ') pos--;
|
|
486
|
+
while (pos >= 0 && /\w/.test(text[pos])) pos--;
|
|
487
|
+
pos++;
|
|
488
|
+
if (pos >= idEnd) break;
|
|
489
|
+
fullStart = pos;
|
|
490
|
+
}
|
|
491
|
+
return fullStart;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
/**
|
|
495
|
+
* Iterate scope candidates and emit segmented completion items.
|
|
496
|
+
* Splits FQN candidates by the typed prefix, extracting only the next segment.
|
|
497
|
+
*/
|
|
498
|
+
private acceptSegmentedCandidates(
|
|
499
|
+
context: CompletionContext,
|
|
500
|
+
candidates: ReturnType<DefaultCompletionProvider['getReferenceCandidates']>,
|
|
501
|
+
fullTyped: string,
|
|
502
|
+
fullStart: number,
|
|
138
503
|
acceptor: CompletionAcceptor
|
|
139
504
|
): void {
|
|
505
|
+
const lastDotIndex = fullTyped.lastIndexOf('.');
|
|
506
|
+
const prefix = fullTyped.substring(0, lastDotIndex + 1);
|
|
507
|
+
const partial = fullTyped.substring(lastDotIndex + 1);
|
|
508
|
+
|
|
509
|
+
const seenSegments = new Set<string>();
|
|
510
|
+
const startPos = context.textDocument.positionAt(fullStart);
|
|
511
|
+
const endPos = context.textDocument.positionAt(context.offset);
|
|
512
|
+
|
|
513
|
+
candidates.forEach((candidate: AstNodeDescription) => {
|
|
514
|
+
const fullName = candidate.name;
|
|
515
|
+
if (!fullName.startsWith(prefix)) return;
|
|
516
|
+
|
|
517
|
+
const remainder = fullName.substring(prefix.length);
|
|
518
|
+
const dotIndex = remainder.indexOf('.');
|
|
519
|
+
const segment = dotIndex === -1 ? remainder : remainder.substring(0, dotIndex);
|
|
520
|
+
|
|
521
|
+
if (!segment || seenSegments.has(segment)) return;
|
|
522
|
+
if (partial && !segment.toLowerCase().startsWith(partial.toLowerCase())) return;
|
|
523
|
+
|
|
524
|
+
seenSegments.add(segment);
|
|
525
|
+
|
|
526
|
+
const isLeaf = dotIndex === -1;
|
|
527
|
+
const fullInsertText = prefix + segment;
|
|
528
|
+
acceptor(context, {
|
|
529
|
+
label: segment,
|
|
530
|
+
kind: isLeaf ? this.nodeKindProvider.getCompletionItemKind(candidate) : CompletionItemKind.Module,
|
|
531
|
+
detail: isLeaf ? fullName : 'Namespace',
|
|
532
|
+
sortText: segment,
|
|
533
|
+
// filterText MUST include the full dotted prefix so VS Code's
|
|
534
|
+
// client-side filter can match "Core.B" against "Core.BaunWalls".
|
|
535
|
+
// Without this, VS Code uses `label` ('BaunWalls') for filtering,
|
|
536
|
+
// which fails to match the typed text 'Core.B'.
|
|
537
|
+
filterText: fullInsertText,
|
|
538
|
+
textEdit: {
|
|
539
|
+
newText: fullInsertText,
|
|
540
|
+
range: { start: startPos, end: endPos },
|
|
541
|
+
},
|
|
542
|
+
});
|
|
543
|
+
});
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
private async safeCompletionFor(
|
|
547
|
+
context: CompletionContext,
|
|
548
|
+
next: NextFeature,
|
|
549
|
+
acceptor: CompletionAcceptor
|
|
550
|
+
): Promise<void> {
|
|
140
551
|
const node = context.node;
|
|
141
552
|
if (!node) {
|
|
142
|
-
super.completionFor(context, next, acceptor);
|
|
553
|
+
await super.completionFor(context, next, acceptor);
|
|
143
554
|
return;
|
|
144
555
|
}
|
|
145
556
|
|
|
146
557
|
// Strategy: Check node type and container to determine what's allowed at cursor position
|
|
147
558
|
|
|
559
|
+
// Handle import statement completions
|
|
560
|
+
if (this.isImportUriCompletion(node, context, next)) {
|
|
561
|
+
// Add async import completions (ensures manifest is loaded)
|
|
562
|
+
await this.addImportCompletions(context, acceptor, node);
|
|
563
|
+
// Don't call super - we handle import string completions ourselves
|
|
564
|
+
return;
|
|
565
|
+
}
|
|
566
|
+
|
|
148
567
|
// Check if cursor is after the node (for top-level positioning)
|
|
149
568
|
const offset = context.offset;
|
|
150
569
|
const nodeEnd = node.$cstNode?.end ?? 0;
|
|
@@ -154,76 +573,366 @@ export class DomainLangCompletionProvider extends DefaultCompletionProvider {
|
|
|
154
573
|
if ((ast.isBoundedContext(node) || ast.isDomain(node)) && isAfterNode) {
|
|
155
574
|
this.addTopLevelSnippets(acceptor, context);
|
|
156
575
|
// Let Langium provide keywords like "bc", "Domain", etc.
|
|
157
|
-
super.completionFor(context, next, acceptor);
|
|
576
|
+
await super.completionFor(context, next, acceptor);
|
|
158
577
|
return;
|
|
159
578
|
}
|
|
160
579
|
|
|
161
580
|
// Handle node-level completions
|
|
162
|
-
if (this.handleNodeCompletions(node, acceptor, context, next)) {
|
|
581
|
+
if (await this.handleNodeCompletions(node, acceptor, context, next)) {
|
|
163
582
|
return;
|
|
164
583
|
}
|
|
165
584
|
|
|
166
585
|
// Handle container-level completions
|
|
167
586
|
const container = node.$container;
|
|
168
|
-
if (this.handleContainerCompletions(container, node, acceptor, context, next)) {
|
|
587
|
+
if (await this.handleContainerCompletions(container, node, acceptor, context, next)) {
|
|
169
588
|
return;
|
|
170
589
|
}
|
|
171
590
|
|
|
172
591
|
// Let Langium handle default completions
|
|
173
|
-
super.completionFor(context, next, acceptor);
|
|
592
|
+
await super.completionFor(context, next, acceptor);
|
|
174
593
|
}
|
|
175
594
|
|
|
176
|
-
|
|
595
|
+
/**
|
|
596
|
+
* Detect if we're completing inside an import statement's uri property.
|
|
597
|
+
*
|
|
598
|
+
* This checks:
|
|
599
|
+
* 1. The NextFeature's type and property (when completing STRING for uri)
|
|
600
|
+
* 2. The current AST node (when inside an ImportStatement)
|
|
601
|
+
* 3. Text-based pattern matching (fallback for edge cases)
|
|
602
|
+
*/
|
|
603
|
+
private isImportUriCompletion(
|
|
177
604
|
node: AstNode,
|
|
178
|
-
acceptor: CompletionAcceptor,
|
|
179
605
|
context: CompletionContext,
|
|
180
606
|
next: NextFeature
|
|
181
607
|
): boolean {
|
|
608
|
+
// Check 1: NextFeature indicates we're completing uri property of ImportStatement
|
|
609
|
+
if (next.type === 'ImportStatement' && next.property === 'uri') {
|
|
610
|
+
return true;
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
// Check 2: The feature is an Assignment to 'uri' property
|
|
614
|
+
if (GrammarAST.isAssignment(next.feature) && next.feature.feature === 'uri') {
|
|
615
|
+
return true;
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
// Check 3: Any ancestor (including self) is ImportStatement
|
|
619
|
+
if (this.isInImportStatementHierarchy(node)) {
|
|
620
|
+
return true;
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
// Check 4: Text-based pattern matching (fallback for edge cases)
|
|
624
|
+
return this.isImportTextPattern(context);
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
/** Check if the node or any ancestor is an ImportStatement. */
|
|
628
|
+
private isInImportStatementHierarchy(node: AstNode): boolean {
|
|
629
|
+
let current: AstNode | undefined = node;
|
|
630
|
+
while (current) {
|
|
631
|
+
if (ast.isImportStatement(current)) return true;
|
|
632
|
+
current = current.$container;
|
|
633
|
+
}
|
|
634
|
+
return false;
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
/** Check if text before cursor matches an import string pattern. */
|
|
638
|
+
private isImportTextPattern(context: CompletionContext): boolean {
|
|
639
|
+
if (typeof context.textDocument?.getText !== 'function') return false;
|
|
640
|
+
try {
|
|
641
|
+
const textBefore = context.textDocument.getText().substring(0, context.offset);
|
|
642
|
+
return /\bimport\s+"[^"]*$/i.test(textBefore);
|
|
643
|
+
} catch {
|
|
644
|
+
return false;
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
/**
|
|
649
|
+
* Add import completions asynchronously.
|
|
650
|
+
* This method ensures the manifest is loaded before providing completions.
|
|
651
|
+
*/
|
|
652
|
+
private async addImportCompletions(
|
|
653
|
+
context: CompletionContext,
|
|
654
|
+
acceptor: CompletionAcceptor,
|
|
655
|
+
_node: AstNode
|
|
656
|
+
): Promise<void> {
|
|
657
|
+
// Extract what user has typed inside the import string
|
|
658
|
+
const currentInput = this.extractImportInput(context);
|
|
659
|
+
|
|
660
|
+
// Ensure manifest is loaded (async)
|
|
661
|
+
let manifest: ModelManifest | undefined;
|
|
662
|
+
try {
|
|
663
|
+
manifest = await this.workspaceManager.ensureManifestLoaded();
|
|
664
|
+
} catch {
|
|
665
|
+
// Continue with undefined manifest – will show basic starters
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
if (currentInput.startsWith('@')) {
|
|
669
|
+
// Alias completions
|
|
670
|
+
this.addAliasCompletions(context, acceptor, currentInput, manifest);
|
|
671
|
+
} else if (currentInput.startsWith('./') || currentInput.startsWith('../')) {
|
|
672
|
+
// Local path completions
|
|
673
|
+
this.addLocalPathStarters(context, acceptor);
|
|
674
|
+
} else if (currentInput === '' || !currentInput) {
|
|
675
|
+
// Show all starter options
|
|
676
|
+
this.addAllStarterOptions(context, acceptor, manifest);
|
|
677
|
+
} else if (currentInput.includes('/') && !currentInput.startsWith('.')) {
|
|
678
|
+
// External dependency - filter by partial input
|
|
679
|
+
this.addDependencyCompletions(context, acceptor, currentInput, manifest);
|
|
680
|
+
} else {
|
|
681
|
+
// Show all options for partial input (e.g., typing 'l' should show matching items)
|
|
682
|
+
this.addFilteredOptions(context, acceptor, currentInput, manifest);
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
/**
|
|
687
|
+
* Extract the current input inside the import string.
|
|
688
|
+
*/
|
|
689
|
+
private extractImportInput(context: CompletionContext): string {
|
|
690
|
+
try {
|
|
691
|
+
const text = context.textDocument.getText();
|
|
692
|
+
const offset = context.offset;
|
|
693
|
+
const textBefore = text.substring(0, offset);
|
|
694
|
+
|
|
695
|
+
const importPattern = /\bimport\s+"([^"]*)$/i;
|
|
696
|
+
const match = importPattern.exec(textBefore);
|
|
697
|
+
return match ? match[1] : '';
|
|
698
|
+
} catch {
|
|
699
|
+
return '';
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
/**
|
|
704
|
+
* Add local path starters.
|
|
705
|
+
*/
|
|
706
|
+
private addLocalPathStarters(context: CompletionContext, acceptor: CompletionAcceptor): void {
|
|
707
|
+
// Would need async fs access to list directories
|
|
708
|
+
// For now, just acknowledge the path exists
|
|
709
|
+
acceptor(context, {
|
|
710
|
+
label: '(type path)',
|
|
711
|
+
kind: CompletionItemKind.Text,
|
|
712
|
+
insertText: '',
|
|
713
|
+
documentation: 'Continue typing the file path',
|
|
714
|
+
sortText: 'z_'
|
|
715
|
+
});
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
/**
|
|
719
|
+
* Add all starter options when input is empty.
|
|
720
|
+
*/
|
|
721
|
+
private addAllStarterOptions(
|
|
722
|
+
context: CompletionContext,
|
|
723
|
+
acceptor: CompletionAcceptor,
|
|
724
|
+
manifest?: ModelManifest
|
|
725
|
+
): void {
|
|
726
|
+
// Local starters
|
|
727
|
+
acceptor(context, {
|
|
728
|
+
label: './',
|
|
729
|
+
kind: CompletionItemKind.Folder,
|
|
730
|
+
insertText: './',
|
|
731
|
+
documentation: 'Import from current directory',
|
|
732
|
+
sortText: '0_local_current'
|
|
733
|
+
});
|
|
734
|
+
acceptor(context, {
|
|
735
|
+
label: '../',
|
|
736
|
+
kind: CompletionItemKind.Folder,
|
|
737
|
+
insertText: '../',
|
|
738
|
+
documentation: 'Import from parent directory',
|
|
739
|
+
sortText: '0_local_parent'
|
|
740
|
+
});
|
|
741
|
+
|
|
742
|
+
// Add aliases if available
|
|
743
|
+
if (manifest?.paths) {
|
|
744
|
+
for (const alias of Object.keys(manifest.paths)) {
|
|
745
|
+
acceptor(context, {
|
|
746
|
+
label: alias,
|
|
747
|
+
kind: CompletionItemKind.Module,
|
|
748
|
+
detail: `→ ${manifest.paths[alias]}`,
|
|
749
|
+
documentation: `Path alias from model.yaml`,
|
|
750
|
+
insertText: alias,
|
|
751
|
+
sortText: `1_alias_${alias}`
|
|
752
|
+
});
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
// Add dependencies if available
|
|
757
|
+
if (manifest?.dependencies) {
|
|
758
|
+
for (const [depKey, depSpec] of Object.entries(manifest.dependencies)) {
|
|
759
|
+
const dep: DependencySpec = depSpec;
|
|
760
|
+
const depName = typeof dep === 'string' ? depKey : (dep.source ?? depKey);
|
|
761
|
+
const version = typeof dep === 'string' ? dep : (dep.ref ?? 'latest');
|
|
762
|
+
|
|
763
|
+
acceptor(context, {
|
|
764
|
+
label: depName,
|
|
765
|
+
kind: CompletionItemKind.Module,
|
|
766
|
+
detail: `📦 ${version}`,
|
|
767
|
+
documentation: `External dependency from model.yaml`,
|
|
768
|
+
insertText: depName,
|
|
769
|
+
sortText: `2_dep_${depName}`
|
|
770
|
+
});
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
/**
|
|
776
|
+
* Add alias completions that match the current input.
|
|
777
|
+
*/
|
|
778
|
+
private addAliasCompletions(
|
|
779
|
+
context: CompletionContext,
|
|
780
|
+
acceptor: CompletionAcceptor,
|
|
781
|
+
currentInput: string,
|
|
782
|
+
manifest?: ModelManifest
|
|
783
|
+
): void {
|
|
784
|
+
if (!manifest?.paths) {
|
|
785
|
+
return;
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
const inputLower = currentInput.toLowerCase();
|
|
789
|
+
|
|
790
|
+
for (const [alias, targetPath] of Object.entries(manifest.paths)) {
|
|
791
|
+
if (alias.toLowerCase().startsWith(inputLower)) {
|
|
792
|
+
acceptor(context, {
|
|
793
|
+
label: alias,
|
|
794
|
+
kind: CompletionItemKind.Module,
|
|
795
|
+
detail: `→ ${targetPath}`,
|
|
796
|
+
documentation: `Path alias defined in model.yaml\nMaps to: ${targetPath}`,
|
|
797
|
+
insertText: alias,
|
|
798
|
+
sortText: `1_alias_${alias}`
|
|
799
|
+
});
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
/**
|
|
805
|
+
* Add dependency completions that match the current input.
|
|
806
|
+
*/
|
|
807
|
+
private addDependencyCompletions(
|
|
808
|
+
context: CompletionContext,
|
|
809
|
+
acceptor: CompletionAcceptor,
|
|
810
|
+
currentInput: string,
|
|
811
|
+
manifest?: ModelManifest
|
|
812
|
+
): void {
|
|
813
|
+
if (!manifest?.dependencies) {
|
|
814
|
+
return;
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
const inputLower = currentInput.toLowerCase();
|
|
818
|
+
|
|
819
|
+
for (const [depKey, depSpec] of Object.entries(manifest.dependencies)) {
|
|
820
|
+
const dep: DependencySpec = depSpec;
|
|
821
|
+
const depName = typeof dep === 'string' ? depKey : (dep.source ?? depKey);
|
|
822
|
+
const version = typeof dep === 'string' ? dep : (dep.ref ?? 'latest');
|
|
823
|
+
|
|
824
|
+
if (depName.toLowerCase().startsWith(inputLower)) {
|
|
825
|
+
acceptor(context, {
|
|
826
|
+
label: depName,
|
|
827
|
+
kind: CompletionItemKind.Module,
|
|
828
|
+
detail: `📦 ${version}`,
|
|
829
|
+
documentation: `External dependency from model.yaml\nVersion: ${version}`,
|
|
830
|
+
insertText: depName,
|
|
831
|
+
sortText: `2_dep_${depName}`
|
|
832
|
+
});
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
/**
|
|
838
|
+
* Add filtered options for partial input that doesn't start with special characters.
|
|
839
|
+
* Shows aliases and dependencies that match the user's partial input.
|
|
840
|
+
*/
|
|
841
|
+
private addFilteredOptions(
|
|
842
|
+
context: CompletionContext,
|
|
843
|
+
acceptor: CompletionAcceptor,
|
|
844
|
+
currentInput: string,
|
|
845
|
+
manifest?: ModelManifest
|
|
846
|
+
): void {
|
|
847
|
+
// Offer local path starters when the partial input could match ./ or ../
|
|
848
|
+
if ('./'.startsWith(currentInput) || '../'.startsWith(currentInput)) {
|
|
849
|
+
acceptor(context, {
|
|
850
|
+
label: './',
|
|
851
|
+
kind: CompletionItemKind.Folder,
|
|
852
|
+
insertText: './',
|
|
853
|
+
documentation: 'Import from current directory',
|
|
854
|
+
sortText: '0_local_current'
|
|
855
|
+
});
|
|
856
|
+
acceptor(context, {
|
|
857
|
+
label: '../',
|
|
858
|
+
kind: CompletionItemKind.Folder,
|
|
859
|
+
insertText: '../',
|
|
860
|
+
documentation: 'Import from parent directory',
|
|
861
|
+
sortText: '0_local_parent'
|
|
862
|
+
});
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
// Delegate to existing helpers for alias and dependency filtering
|
|
866
|
+
this.addAliasCompletions(context, acceptor, currentInput, manifest);
|
|
867
|
+
this.addDependencyCompletions(context, acceptor, currentInput, manifest);
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
private async handleNodeCompletions(
|
|
871
|
+
node: AstNode,
|
|
872
|
+
acceptor: CompletionAcceptor,
|
|
873
|
+
context: CompletionContext,
|
|
874
|
+
next: NextFeature
|
|
875
|
+
): Promise<boolean> {
|
|
182
876
|
// If we're AT a BoundedContext node: only BC documentation blocks
|
|
183
877
|
if (ast.isBoundedContext(node)) {
|
|
184
878
|
this.addBoundedContextCompletions(node, acceptor, context);
|
|
185
|
-
super.completionFor(context, next, acceptor);
|
|
879
|
+
await super.completionFor(context, next, acceptor);
|
|
186
880
|
return true;
|
|
187
881
|
}
|
|
188
882
|
|
|
189
883
|
// If we're AT a Domain node: only Domain documentation blocks
|
|
190
884
|
if (ast.isDomain(node)) {
|
|
191
885
|
this.addDomainCompletions(node, acceptor, context);
|
|
192
|
-
super.completionFor(context, next, acceptor);
|
|
886
|
+
await super.completionFor(context, next, acceptor);
|
|
193
887
|
return true;
|
|
194
888
|
}
|
|
195
889
|
|
|
196
890
|
// If we're AT a ContextMap node: relationships and contains
|
|
197
891
|
if (ast.isContextMap(node)) {
|
|
198
892
|
this.addContextMapCompletions(node, acceptor, context);
|
|
199
|
-
super.completionFor(context, next, acceptor);
|
|
893
|
+
await super.completionFor(context, next, acceptor);
|
|
200
894
|
return true;
|
|
201
895
|
}
|
|
202
896
|
|
|
203
897
|
// If we're AT a DomainMap node: contains
|
|
204
898
|
if (ast.isDomainMap(node)) {
|
|
205
899
|
this.addDomainMapCompletions(node, acceptor, context);
|
|
206
|
-
super.completionFor(context, next, acceptor);
|
|
900
|
+
await super.completionFor(context, next, acceptor);
|
|
207
901
|
return true;
|
|
208
902
|
}
|
|
209
903
|
|
|
210
904
|
// If we're AT the Model or NamespaceDeclaration level: all top-level constructs
|
|
211
905
|
if (ast.isModel(node) || ast.isNamespaceDeclaration(node)) {
|
|
212
906
|
this.addTopLevelSnippets(acceptor, context);
|
|
213
|
-
|
|
907
|
+
this.addImportSnippet(acceptor, context);
|
|
908
|
+
await super.completionFor(context, next, acceptor);
|
|
214
909
|
return true;
|
|
215
910
|
}
|
|
216
911
|
|
|
217
912
|
return false;
|
|
218
913
|
}
|
|
219
914
|
|
|
220
|
-
|
|
915
|
+
/**
|
|
916
|
+
* Add import statement snippet at top level.
|
|
917
|
+
*/
|
|
918
|
+
private addImportSnippet(acceptor: CompletionAcceptor, context: CompletionContext): void {
|
|
919
|
+
acceptor(context, {
|
|
920
|
+
label: '⚡ import',
|
|
921
|
+
kind: CompletionItemKind.Snippet,
|
|
922
|
+
insertText: 'import "${1:./path}"',
|
|
923
|
+
insertTextFormat: InsertTextFormat.Snippet,
|
|
924
|
+
documentation: '📝 Snippet: Import another DomainLang file',
|
|
925
|
+
sortText: '0_snippet_import'
|
|
926
|
+
});
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
private async handleContainerCompletions(
|
|
221
930
|
container: AstNode | undefined,
|
|
222
931
|
node: AstNode,
|
|
223
932
|
acceptor: CompletionAcceptor,
|
|
224
933
|
context: CompletionContext,
|
|
225
934
|
next: NextFeature
|
|
226
|
-
): boolean {
|
|
935
|
+
): Promise<boolean> {
|
|
227
936
|
if (!container) {
|
|
228
937
|
return false;
|
|
229
938
|
}
|
|
@@ -231,34 +940,34 @@ export class DomainLangCompletionProvider extends DefaultCompletionProvider {
|
|
|
231
940
|
// Inside BoundedContext body: suggest missing scalar properties and collections
|
|
232
941
|
if (ast.isBoundedContext(container)) {
|
|
233
942
|
this.addBoundedContextCompletions(container, acceptor, context);
|
|
234
|
-
super.completionFor(context, next, acceptor);
|
|
943
|
+
await super.completionFor(context, next, acceptor);
|
|
235
944
|
return true;
|
|
236
945
|
}
|
|
237
946
|
|
|
238
947
|
// Inside Domain body: suggest missing scalar properties
|
|
239
948
|
if (ast.isDomain(container)) {
|
|
240
949
|
this.addDomainCompletions(container, acceptor, context);
|
|
241
|
-
super.completionFor(context, next, acceptor);
|
|
950
|
+
await super.completionFor(context, next, acceptor);
|
|
242
951
|
return true;
|
|
243
952
|
}
|
|
244
953
|
|
|
245
954
|
// Inside ContextMap body: relationships and contains
|
|
246
955
|
if (ast.isContextMap(container)) {
|
|
247
956
|
this.addContextMapCompletions(container, acceptor, context);
|
|
248
|
-
super.completionFor(context, next, acceptor);
|
|
957
|
+
await super.completionFor(context, next, acceptor);
|
|
249
958
|
return true;
|
|
250
959
|
}
|
|
251
960
|
|
|
252
961
|
// Inside DomainMap body: contains
|
|
253
962
|
if (ast.isDomainMap(container)) {
|
|
254
963
|
this.addDomainMapCompletions(container, acceptor, context);
|
|
255
|
-
super.completionFor(context, next, acceptor);
|
|
964
|
+
await super.completionFor(context, next, acceptor);
|
|
256
965
|
return true;
|
|
257
966
|
}
|
|
258
967
|
|
|
259
968
|
if (ast.isRelationship(node) || ast.isRelationship(container)) {
|
|
260
969
|
this.addRelationshipCompletions(acceptor, context);
|
|
261
|
-
super.completionFor(context, next, acceptor);
|
|
970
|
+
await super.completionFor(context, next, acceptor);
|
|
262
971
|
return true;
|
|
263
972
|
}
|
|
264
973
|
|