@domainlang/language 0.7.0 → 0.9.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/out/domain-lang-module.d.ts +2 -0
- package/out/domain-lang-module.js +21 -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 +34 -5
- package/out/lsp/domain-lang-index-manager.js +66 -27
- 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 +53 -20
- package/out/lsp/domain-lang-scope-provider.js +119 -44
- package/out/lsp/domain-lang-scope-provider.js.map +1 -1
- package/out/lsp/domain-lang-workspace-manager.d.ts +23 -2
- package/out/lsp/domain-lang-workspace-manager.js +51 -6
- 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 +2 -1
- package/out/main.js.map +1 -1
- package/out/sdk/index.d.ts +31 -11
- package/out/sdk/index.js +30 -11
- package/out/sdk/index.js.map +1 -1
- package/out/sdk/loader-node.d.ts +2 -0
- package/out/sdk/loader-node.js +3 -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/sdk/validator.d.ts +134 -0
- package/out/sdk/validator.js +249 -0
- package/out/sdk/validator.js.map +1 -0
- 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 +7 -0
- package/out/validation/constants.js +21 -3
- package/out/validation/constants.js.map +1 -1
- package/out/validation/import.d.ts +11 -1
- package/out/validation/import.js +42 -14
- package/out/validation/import.js.map +1 -1
- package/out/validation/maps.js +50 -1
- package/out/validation/maps.js.map +1 -1
- package/package.json +8 -9
- package/src/domain-lang-module.ts +24 -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 +79 -27
- package/src/lsp/domain-lang-node-kind-provider.ts +119 -0
- package/src/lsp/domain-lang-scope-provider.ts +171 -55
- package/src/lsp/domain-lang-workspace-manager.ts +64 -6
- package/src/lsp/hover/domain-lang-hover.ts +189 -131
- package/src/lsp/hover/hover-builders.ts +208 -0
- package/src/main.ts +3 -1
- package/src/sdk/index.ts +33 -11
- package/src/sdk/loader-node.ts +6 -1
- package/src/sdk/loader.ts +125 -34
- package/src/sdk/query.ts +15 -11
- package/src/sdk/validator.ts +358 -0
- 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 +23 -6
- package/src/validation/import.ts +49 -14
- package/src/validation/maps.ts +59 -2
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Custom DocumentSymbolProvider for DomainLang.
|
|
3
|
+
*
|
|
4
|
+
* Extends Langium's DefaultDocumentSymbolProvider to add meaningful
|
|
5
|
+
* `detail` text to outline items, improving the Outline view, breadcrumbs,
|
|
6
|
+
* and Go to Symbol experience.
|
|
7
|
+
*
|
|
8
|
+
* The default provider handles the full AST walk, child nesting, and
|
|
9
|
+
* range computation. We only override `getSymbol` to enrich the detail
|
|
10
|
+
* property with DDD-relevant information (descriptions, visions, counts).
|
|
11
|
+
*
|
|
12
|
+
* @module lsp/domain-lang-document-symbol-provider
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import type { AstNode, LangiumDocument } from 'langium';
|
|
16
|
+
import { DocumentSymbol, SymbolKind } from 'vscode-languageserver';
|
|
17
|
+
import { CstUtils } from 'langium';
|
|
18
|
+
import { DefaultDocumentSymbolProvider } from 'langium/lsp';
|
|
19
|
+
import {
|
|
20
|
+
isDomain,
|
|
21
|
+
isBoundedContext,
|
|
22
|
+
isContextMap,
|
|
23
|
+
isDomainMap,
|
|
24
|
+
isNamespaceDeclaration,
|
|
25
|
+
isRelationship,
|
|
26
|
+
isThisRef,
|
|
27
|
+
} from '../generated/ast.js';
|
|
28
|
+
import type { BoundedContext, Relationship, MetadataEntry } from '../generated/ast.js';
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Enriches document symbols with DDD-specific detail text and grouping.
|
|
32
|
+
*
|
|
33
|
+
* Detail text shown in the Outline view next to each symbol:
|
|
34
|
+
* - Domain: vision or description
|
|
35
|
+
* - BoundedContext: description or domain name
|
|
36
|
+
* - ContextMap: number of contained contexts
|
|
37
|
+
* - DomainMap: number of contained domains
|
|
38
|
+
* - Namespace: qualified namespace name
|
|
39
|
+
* - Relationship: formatted endpoint summary (e.g., "OrderContext -> PaymentContext")
|
|
40
|
+
*
|
|
41
|
+
* Grouping: Creates synthetic folder nodes for collections in the grammar:
|
|
42
|
+
* - BoundedContext: decisions, terminology, relationships, metadata
|
|
43
|
+
*
|
|
44
|
+
* Note: Relationship and MetadataEntry symbols are created manually (not via NameProvider)
|
|
45
|
+
* to avoid polluting the global scope/reference system. These are display-only synthetic symbols.
|
|
46
|
+
*/
|
|
47
|
+
export class DomainLangDocumentSymbolProvider extends DefaultDocumentSymbolProvider {
|
|
48
|
+
|
|
49
|
+
protected override getSymbol(document: LangiumDocument, astNode: AstNode): DocumentSymbol[] {
|
|
50
|
+
try {
|
|
51
|
+
const symbols = super.getSymbol(document, astNode);
|
|
52
|
+
const detail = this.getDetailText(astNode);
|
|
53
|
+
if (detail !== undefined) {
|
|
54
|
+
for (const symbol of symbols) {
|
|
55
|
+
symbol.detail = detail;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return symbols;
|
|
59
|
+
} catch (error) {
|
|
60
|
+
console.error('Error in DomainLangDocumentSymbolProvider.getSymbol:', error);
|
|
61
|
+
return super.getSymbol(document, astNode);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Override to add synthetic grouping folders for collections.
|
|
67
|
+
* Groups decisions, terminology, relationships, and metadata under folder nodes.
|
|
68
|
+
*/
|
|
69
|
+
protected override getChildSymbols(document: LangiumDocument, astNode: AstNode): DocumentSymbol[] | undefined {
|
|
70
|
+
// Only group for BoundedContext nodes
|
|
71
|
+
if (!isBoundedContext(astNode)) {
|
|
72
|
+
return super.getChildSymbols(document, astNode);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const grouped: DocumentSymbol[] = [];
|
|
76
|
+
|
|
77
|
+
// Process each collection type
|
|
78
|
+
this.addDecisionsFolder(document, astNode, grouped);
|
|
79
|
+
this.addTerminologyFolder(document, astNode, grouped);
|
|
80
|
+
this.addRelationshipsFolder(astNode, grouped);
|
|
81
|
+
this.addMetadataFolder(astNode, grouped);
|
|
82
|
+
|
|
83
|
+
return grouped.length > 0 ? grouped : undefined;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** Adds decisions folder if collection is non-empty. */
|
|
87
|
+
private addDecisionsFolder(document: LangiumDocument, bc: BoundedContext, grouped: DocumentSymbol[]): void {
|
|
88
|
+
if (bc.decisions && bc.decisions.length > 0) {
|
|
89
|
+
const symbols = bc.decisions.flatMap(d => this.getSymbol(document, d));
|
|
90
|
+
if (symbols.length > 0) {
|
|
91
|
+
grouped.push(this.createFolderSymbol('decisions', symbols));
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Adds terminology folder if collection is non-empty. */
|
|
97
|
+
private addTerminologyFolder(document: LangiumDocument, bc: BoundedContext, grouped: DocumentSymbol[]): void {
|
|
98
|
+
if (bc.terminology && bc.terminology.length > 0) {
|
|
99
|
+
const symbols = bc.terminology.flatMap(t => this.getSymbol(document, t));
|
|
100
|
+
if (symbols.length > 0) {
|
|
101
|
+
grouped.push(this.createFolderSymbol('terminology', symbols));
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** Adds relationships folder with manually created symbols. */
|
|
107
|
+
private addRelationshipsFolder(bc: BoundedContext, grouped: DocumentSymbol[]): void {
|
|
108
|
+
if (bc.relationships && bc.relationships.length > 0) {
|
|
109
|
+
const symbols = bc.relationships.map(r => this.createRelationshipSymbol(r)).filter((s): s is DocumentSymbol => s !== undefined);
|
|
110
|
+
if (symbols.length > 0) {
|
|
111
|
+
grouped.push(this.createFolderSymbol('relationships', symbols));
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** Adds metadata folder with manually created symbols. */
|
|
117
|
+
private addMetadataFolder(bc: BoundedContext, grouped: DocumentSymbol[]): void {
|
|
118
|
+
if (bc.metadata && bc.metadata.length > 0) {
|
|
119
|
+
const symbols = bc.metadata.map(m => this.createMetadataSymbol(m)).filter((s): s is DocumentSymbol => s !== undefined);
|
|
120
|
+
if (symbols.length > 0) {
|
|
121
|
+
grouped.push(this.createFolderSymbol('metadata', symbols));
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Creates a synthetic folder DocumentSymbol for grouping children.
|
|
128
|
+
*/
|
|
129
|
+
private createFolderSymbol(name: string, children: DocumentSymbol[]): DocumentSymbol {
|
|
130
|
+
// Use the first child's range for the folder
|
|
131
|
+
const firstChild = children[0];
|
|
132
|
+
|
|
133
|
+
return DocumentSymbol.create(
|
|
134
|
+
name,
|
|
135
|
+
`${children.length} items`,
|
|
136
|
+
SymbolKind.Object,
|
|
137
|
+
firstChild.range,
|
|
138
|
+
firstChild.selectionRange,
|
|
139
|
+
children
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Creates a DocumentSymbol for a Relationship node.
|
|
145
|
+
*/
|
|
146
|
+
private createRelationshipSymbol(rel: Relationship): DocumentSymbol | undefined {
|
|
147
|
+
const cstNode = rel.$cstNode;
|
|
148
|
+
if (!cstNode) return undefined;
|
|
149
|
+
|
|
150
|
+
const left = isThisRef(rel.left) ? 'this' : rel.left?.link?.ref?.name ?? '?';
|
|
151
|
+
const right = isThisRef(rel.right) ? 'this' : rel.right?.link?.ref?.name ?? '?';
|
|
152
|
+
const name = `${left} → ${right}`;
|
|
153
|
+
|
|
154
|
+
const range = CstUtils.toDocumentSegment(cstNode).range;
|
|
155
|
+
return DocumentSymbol.create(
|
|
156
|
+
name,
|
|
157
|
+
undefined,
|
|
158
|
+
SymbolKind.Interface,
|
|
159
|
+
range,
|
|
160
|
+
range
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Creates a DocumentSymbol for a MetadataEntry node.
|
|
166
|
+
*/
|
|
167
|
+
private createMetadataSymbol(meta: MetadataEntry): DocumentSymbol | undefined {
|
|
168
|
+
const cstNode = meta.$cstNode;
|
|
169
|
+
if (!cstNode) return undefined;
|
|
170
|
+
|
|
171
|
+
const name = meta.key?.ref?.name ?? 'unknown';
|
|
172
|
+
const range = CstUtils.toDocumentSegment(cstNode).range;
|
|
173
|
+
|
|
174
|
+
return DocumentSymbol.create(
|
|
175
|
+
name,
|
|
176
|
+
meta.value,
|
|
177
|
+
SymbolKind.Field,
|
|
178
|
+
range,
|
|
179
|
+
range
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Returns DDD-specific detail text for a given AST node.
|
|
185
|
+
* Shown alongside the symbol name in the Outline view.
|
|
186
|
+
*/
|
|
187
|
+
private getDetailText(node: AstNode): string | undefined {
|
|
188
|
+
if (isDomain(node)) return "Domain — " + (node.vision ?? node.description);
|
|
189
|
+
if (isBoundedContext(node)) return this.getBcDetail(node);
|
|
190
|
+
if (isContextMap(node)) return this.pluralize('context', node.boundedContexts?.length ?? 0);
|
|
191
|
+
if (isDomainMap(node)) return this.pluralize('domain', node.domains?.length ?? 0);
|
|
192
|
+
if (isNamespaceDeclaration(node)) return node.name;
|
|
193
|
+
if (isRelationship(node)) return this.formatRelationshipDetail(node);
|
|
194
|
+
return undefined;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/** Builds BC detail: "BC for DomainName — description". */
|
|
198
|
+
private getBcDetail(node: BoundedContext): string | undefined {
|
|
199
|
+
const parts: string[] = [];
|
|
200
|
+
if (node.domain?.ref?.name) {
|
|
201
|
+
parts.push(`BC for ${node.domain.ref.name}`);
|
|
202
|
+
}
|
|
203
|
+
if (node.description) {
|
|
204
|
+
parts.push(node.description);
|
|
205
|
+
}
|
|
206
|
+
return parts.length > 0 ? parts.join(' — ') : undefined;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/** Returns "N item(s)" or undefined when count is 0. */
|
|
210
|
+
private pluralize(label: string, count: number): string | undefined {
|
|
211
|
+
if (count === 0) return undefined;
|
|
212
|
+
const suffix = count === 1 ? '' : 's';
|
|
213
|
+
return `${count} ${label}${suffix}`;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Formats a relationship as a compact detail string:
|
|
218
|
+
* e.g., "OrderContext -> PaymentContext"
|
|
219
|
+
*/
|
|
220
|
+
private formatRelationshipDetail(
|
|
221
|
+
node: ReturnType<typeof Object> & { left?: unknown; right?: unknown; arrow?: string }
|
|
222
|
+
): string | undefined {
|
|
223
|
+
try {
|
|
224
|
+
// We know this is a Relationship node thanks to isRelationship guard above
|
|
225
|
+
const rel = node as { left?: { link?: { $refText?: string } }; right?: { link?: { $refText?: string } }; arrow?: string };
|
|
226
|
+
const leftName = this.getRefName(rel.left);
|
|
227
|
+
const rightName = this.getRefName(rel.right);
|
|
228
|
+
const arrow = rel.arrow ?? '->';
|
|
229
|
+
if (leftName && rightName) {
|
|
230
|
+
return `${leftName} ${arrow} ${rightName}`;
|
|
231
|
+
}
|
|
232
|
+
return undefined;
|
|
233
|
+
} catch {
|
|
234
|
+
return undefined;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Gets a display name from a BoundedContextRef.
|
|
240
|
+
*/
|
|
241
|
+
private getRefName(ref: unknown): string | undefined {
|
|
242
|
+
if (!ref || typeof ref !== 'object') return undefined;
|
|
243
|
+
|
|
244
|
+
// Check for ThisRef
|
|
245
|
+
const refObj = ref as Record<string, unknown>;
|
|
246
|
+
if (refObj.$type === 'ThisRef' || isThisRef(ref as AstNode)) {
|
|
247
|
+
return 'this';
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Check for named reference
|
|
251
|
+
const link = refObj.link as { $refText?: string } | undefined;
|
|
252
|
+
return link?.$refText;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
@@ -3,6 +3,9 @@ import { DefaultIndexManager, DocumentState } from 'langium';
|
|
|
3
3
|
import { CancellationToken } from 'vscode-jsonrpc';
|
|
4
4
|
import { resolveImportPath } from '../utils/import-utils.js';
|
|
5
5
|
import type { Model } from '../generated/ast.js';
|
|
6
|
+
import type { ImportResolver } from '../services/import-resolver.js';
|
|
7
|
+
import type { DomainLangServices } from '../domain-lang-module.js';
|
|
8
|
+
import type { ImportInfo } from '../services/types.js';
|
|
6
9
|
|
|
7
10
|
/**
|
|
8
11
|
* Custom IndexManager that extends Langium's default to:
|
|
@@ -37,12 +40,12 @@ export class DomainLangIndexManager extends DefaultIndexManager {
|
|
|
37
40
|
private readonly importDependencies = new Map<string, Set<string>>();
|
|
38
41
|
|
|
39
42
|
/**
|
|
40
|
-
* Maps document URI to its import
|
|
41
|
-
* Used
|
|
43
|
+
* Maps document URI to its import information (specifier, alias, resolved URI).
|
|
44
|
+
* Used for scope resolution with aliases and detecting when file moves affect imports.
|
|
42
45
|
* Key: importing document URI
|
|
43
|
-
* Value:
|
|
46
|
+
* Value: Array of ImportInfo objects
|
|
44
47
|
*/
|
|
45
|
-
private readonly
|
|
48
|
+
private readonly documentImportInfo = new Map<string, ImportInfo[]>();
|
|
46
49
|
|
|
47
50
|
/**
|
|
48
51
|
* Tracks documents that have had their imports loaded to avoid redundant work.
|
|
@@ -55,11 +58,41 @@ export class DomainLangIndexManager extends DefaultIndexManager {
|
|
|
55
58
|
*/
|
|
56
59
|
private readonly sharedServices: LangiumSharedCoreServices;
|
|
57
60
|
|
|
61
|
+
/**
|
|
62
|
+
* DI-injected import resolver. Set via late-binding because
|
|
63
|
+
* IndexManager (shared module) is created before ImportResolver (language module).
|
|
64
|
+
* Falls back to standalone resolveImportPath when not set.
|
|
65
|
+
*/
|
|
66
|
+
private importResolver: ImportResolver | undefined;
|
|
67
|
+
|
|
58
68
|
constructor(services: LangiumSharedCoreServices) {
|
|
59
69
|
super(services);
|
|
60
70
|
this.sharedServices = services;
|
|
61
71
|
}
|
|
62
72
|
|
|
73
|
+
/**
|
|
74
|
+
* Late-binds the language-specific services after DI initialization.
|
|
75
|
+
* Called from `createDomainLangServices()` after the language module is created.
|
|
76
|
+
*
|
|
77
|
+
* This is necessary because the IndexManager lives in the shared module,
|
|
78
|
+
* which is created before the language module that provides ImportResolver.
|
|
79
|
+
*/
|
|
80
|
+
setLanguageServices(services: DomainLangServices): void {
|
|
81
|
+
this.importResolver = services.imports.ImportResolver;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Resolves an import path using the DI-injected ImportResolver when available,
|
|
86
|
+
* falling back to the standalone resolver for backwards compatibility.
|
|
87
|
+
*/
|
|
88
|
+
private async resolveImport(document: LangiumDocument, specifier: string): Promise<URI> {
|
|
89
|
+
if (this.importResolver) {
|
|
90
|
+
return this.importResolver.resolveForDocument(document, specifier);
|
|
91
|
+
}
|
|
92
|
+
// Fallback for contexts where language services aren't wired (e.g., tests)
|
|
93
|
+
return resolveImportPath(document, specifier);
|
|
94
|
+
}
|
|
95
|
+
|
|
63
96
|
/**
|
|
64
97
|
* Extends the default content update to:
|
|
65
98
|
* 1. Ensure all imported documents are loaded
|
|
@@ -126,7 +159,7 @@ export class DomainLangIndexManager extends DefaultIndexManager {
|
|
|
126
159
|
* Tracks import dependencies for a document.
|
|
127
160
|
* For each import in the document, records:
|
|
128
161
|
* 1. That the imported URI is depended upon (for direct change detection)
|
|
129
|
-
* 2. The import specifier
|
|
162
|
+
* 2. The import specifier and alias (for scope resolution)
|
|
130
163
|
*/
|
|
131
164
|
private async trackImportDependencies(document: LangiumDocument): Promise<void> {
|
|
132
165
|
const importingUri = document.uri.toString();
|
|
@@ -134,7 +167,7 @@ export class DomainLangIndexManager extends DefaultIndexManager {
|
|
|
134
167
|
// First, remove old dependencies from this document
|
|
135
168
|
// (in case imports changed)
|
|
136
169
|
this.removeDocumentFromDependencies(importingUri);
|
|
137
|
-
this.
|
|
170
|
+
this.documentImportInfo.delete(importingUri);
|
|
138
171
|
|
|
139
172
|
// Skip if document isn't ready (no parse result)
|
|
140
173
|
if (document.state < DocumentState.Parsed) {
|
|
@@ -146,17 +179,21 @@ export class DomainLangIndexManager extends DefaultIndexManager {
|
|
|
146
179
|
return;
|
|
147
180
|
}
|
|
148
181
|
|
|
149
|
-
const
|
|
182
|
+
const importInfoList: ImportInfo[] = [];
|
|
150
183
|
|
|
151
184
|
for (const imp of model.imports) {
|
|
152
185
|
if (!imp.uri) continue;
|
|
153
186
|
|
|
154
187
|
try {
|
|
155
|
-
const resolvedUri = await
|
|
188
|
+
const resolvedUri = await this.resolveImport(document, imp.uri);
|
|
156
189
|
const importedUri = resolvedUri.toString();
|
|
157
190
|
|
|
158
|
-
// Track the specifier
|
|
159
|
-
|
|
191
|
+
// Track the full import info (specifier, alias, resolved URI)
|
|
192
|
+
importInfoList.push({
|
|
193
|
+
specifier: imp.uri,
|
|
194
|
+
alias: imp.alias,
|
|
195
|
+
resolvedUri: importedUri
|
|
196
|
+
});
|
|
160
197
|
|
|
161
198
|
// Add to reverse dependency graph: importedUri → importingUri
|
|
162
199
|
let dependents = this.importDependencies.get(importedUri);
|
|
@@ -167,12 +204,16 @@ export class DomainLangIndexManager extends DefaultIndexManager {
|
|
|
167
204
|
dependents.add(importingUri);
|
|
168
205
|
} catch {
|
|
169
206
|
// Import resolution failed - still track the specifier with empty resolution
|
|
170
|
-
|
|
207
|
+
importInfoList.push({
|
|
208
|
+
specifier: imp.uri,
|
|
209
|
+
alias: imp.alias,
|
|
210
|
+
resolvedUri: ''
|
|
211
|
+
});
|
|
171
212
|
}
|
|
172
213
|
}
|
|
173
214
|
|
|
174
|
-
if (
|
|
175
|
-
this.
|
|
215
|
+
if (importInfoList.length > 0) {
|
|
216
|
+
this.documentImportInfo.set(importingUri, importInfoList);
|
|
176
217
|
}
|
|
177
218
|
}
|
|
178
219
|
|
|
@@ -210,7 +251,7 @@ export class DomainLangIndexManager extends DefaultIndexManager {
|
|
|
210
251
|
if (!imp.uri) continue;
|
|
211
252
|
|
|
212
253
|
try {
|
|
213
|
-
const resolvedUri = await
|
|
254
|
+
const resolvedUri = await this.resolveImport(document, imp.uri);
|
|
214
255
|
const importedUriString = resolvedUri.toString();
|
|
215
256
|
|
|
216
257
|
// Skip if already loaded
|
|
@@ -267,7 +308,7 @@ export class DomainLangIndexManager extends DefaultIndexManager {
|
|
|
267
308
|
*/
|
|
268
309
|
clearImportDependencies(): void {
|
|
269
310
|
this.importDependencies.clear();
|
|
270
|
-
this.
|
|
311
|
+
this.documentImportInfo.clear();
|
|
271
312
|
this.importsLoaded.clear();
|
|
272
313
|
}
|
|
273
314
|
|
|
@@ -298,21 +339,32 @@ export class DomainLangIndexManager extends DefaultIndexManager {
|
|
|
298
339
|
* @returns Set of resolved import URIs, or empty set if none
|
|
299
340
|
*/
|
|
300
341
|
getResolvedImports(documentUri: string): Set<string> {
|
|
301
|
-
const
|
|
302
|
-
if (!
|
|
342
|
+
const importInfoList = this.documentImportInfo.get(documentUri);
|
|
343
|
+
if (!importInfoList) {
|
|
303
344
|
return new Set();
|
|
304
345
|
}
|
|
305
346
|
|
|
306
347
|
const resolved = new Set<string>();
|
|
307
|
-
for (const
|
|
348
|
+
for (const info of importInfoList) {
|
|
308
349
|
// Only include successfully resolved imports (non-empty string)
|
|
309
|
-
if (resolvedUri) {
|
|
310
|
-
resolved.add(resolvedUri);
|
|
350
|
+
if (info.resolvedUri) {
|
|
351
|
+
resolved.add(info.resolvedUri);
|
|
311
352
|
}
|
|
312
353
|
}
|
|
313
354
|
return resolved;
|
|
314
355
|
}
|
|
315
356
|
|
|
357
|
+
/**
|
|
358
|
+
* Gets the full import information (including aliases) for a document.
|
|
359
|
+
* Used by the scope provider to implement alias-prefixed name resolution.
|
|
360
|
+
*
|
|
361
|
+
* @param documentUri - The URI of the document
|
|
362
|
+
* @returns Array of ImportInfo objects, or empty array if none
|
|
363
|
+
*/
|
|
364
|
+
getImportInfo(documentUri: string): ImportInfo[] {
|
|
365
|
+
return this.documentImportInfo.get(documentUri) ?? [];
|
|
366
|
+
}
|
|
367
|
+
|
|
316
368
|
/**
|
|
317
369
|
* Gets all documents that would be affected by changes to the given URIs.
|
|
318
370
|
* This includes direct dependents and transitive dependents.
|
|
@@ -406,8 +458,8 @@ export class DomainLangIndexManager extends DefaultIndexManager {
|
|
|
406
458
|
private findDocumentsMatchingPaths(changedPaths: Set<string>): Set<string> {
|
|
407
459
|
const affected = new Set<string>();
|
|
408
460
|
|
|
409
|
-
for (const [docUri,
|
|
410
|
-
if (this.hasMatchingSpecifierOrResolvedUri(
|
|
461
|
+
for (const [docUri, importInfoList] of this.documentImportInfo) {
|
|
462
|
+
if (this.hasMatchingSpecifierOrResolvedUri(importInfoList, changedPaths)) {
|
|
411
463
|
affected.add(docUri);
|
|
412
464
|
}
|
|
413
465
|
}
|
|
@@ -425,19 +477,19 @@ export class DomainLangIndexManager extends DefaultIndexManager {
|
|
|
425
477
|
*
|
|
426
478
|
* We check both to ensure moves of aliased imports trigger revalidation.
|
|
427
479
|
*/
|
|
428
|
-
private hasMatchingSpecifierOrResolvedUri(
|
|
429
|
-
for (const
|
|
430
|
-
const normalizedSpecifier = specifier.replace(/^[.@/]+/, '');
|
|
480
|
+
private hasMatchingSpecifierOrResolvedUri(importInfoList: ImportInfo[], changedPaths: Set<string>): boolean {
|
|
481
|
+
for (const info of importInfoList) {
|
|
482
|
+
const normalizedSpecifier = info.specifier.replace(/^[.@/]+/, '');
|
|
431
483
|
|
|
432
484
|
for (const changedPath of changedPaths) {
|
|
433
485
|
// Check the raw specifier (handles relative imports)
|
|
434
|
-
if (specifier.includes(changedPath) || changedPath.endsWith(normalizedSpecifier)) {
|
|
486
|
+
if (info.specifier.includes(changedPath) || changedPath.endsWith(normalizedSpecifier)) {
|
|
435
487
|
return true;
|
|
436
488
|
}
|
|
437
489
|
|
|
438
490
|
// Check the resolved URI (handles path aliases like @domains/...)
|
|
439
491
|
// The resolved URI contains the full file path which matches moved files
|
|
440
|
-
if (resolvedUri?.includes(changedPath)) {
|
|
492
|
+
if (info.resolvedUri?.includes(changedPath)) {
|
|
441
493
|
return true;
|
|
442
494
|
}
|
|
443
495
|
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Custom NodeKindProvider — maps DomainLang AST types to VS Code SymbolKinds.
|
|
3
|
+
*
|
|
4
|
+
* Langium's DefaultNodeKindProvider returns `SymbolKind.Field` for everything.
|
|
5
|
+
* This override provides semantically meaningful icons for the Outline view,
|
|
6
|
+
* breadcrumbs, Go to Symbol, and completion items.
|
|
7
|
+
*
|
|
8
|
+
* @module lsp/domain-lang-node-kind-provider
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { DefaultNodeKindProvider } from 'langium/lsp';
|
|
12
|
+
import { CompletionItemKind, SymbolKind } from 'vscode-languageserver';
|
|
13
|
+
import type { AstNode, AstNodeDescription } from 'langium';
|
|
14
|
+
import {
|
|
15
|
+
isDomain,
|
|
16
|
+
isBoundedContext,
|
|
17
|
+
isTeam,
|
|
18
|
+
isClassification,
|
|
19
|
+
isMetadata,
|
|
20
|
+
isContextMap,
|
|
21
|
+
isDomainMap,
|
|
22
|
+
isNamespaceDeclaration,
|
|
23
|
+
isRelationship,
|
|
24
|
+
isDomainTerm,
|
|
25
|
+
isDecision,
|
|
26
|
+
isPolicy,
|
|
27
|
+
isBusinessRule,
|
|
28
|
+
isMetadataEntry,
|
|
29
|
+
} from '../generated/ast.js';
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* AST type to icon kind mapping table.
|
|
33
|
+
*/
|
|
34
|
+
type KindMapping = readonly [
|
|
35
|
+
guard: (node: AstNode) => boolean,
|
|
36
|
+
symbolKind: SymbolKind,
|
|
37
|
+
completionKind: CompletionItemKind
|
|
38
|
+
];
|
|
39
|
+
|
|
40
|
+
const KIND_MAPPINGS: readonly KindMapping[] = [
|
|
41
|
+
// Strategic design
|
|
42
|
+
[isDomain, SymbolKind.Namespace, CompletionItemKind.Folder],
|
|
43
|
+
[isBoundedContext, SymbolKind.Package, CompletionItemKind.Module],
|
|
44
|
+
|
|
45
|
+
// Tactical design
|
|
46
|
+
[isTeam, SymbolKind.Interface, CompletionItemKind.Interface],
|
|
47
|
+
[isClassification, SymbolKind.Enum, CompletionItemKind.Enum],
|
|
48
|
+
[isMetadata, SymbolKind.Enum, CompletionItemKind.Enum],
|
|
49
|
+
|
|
50
|
+
// Architecture mapping
|
|
51
|
+
[isContextMap, SymbolKind.Package, CompletionItemKind.Module],
|
|
52
|
+
[isDomainMap, SymbolKind.Package, CompletionItemKind.Module],
|
|
53
|
+
|
|
54
|
+
// Module system
|
|
55
|
+
[isNamespaceDeclaration, SymbolKind.Namespace, CompletionItemKind.Module],
|
|
56
|
+
|
|
57
|
+
// Relationships
|
|
58
|
+
[isRelationship, SymbolKind.Interface, CompletionItemKind.Interface],
|
|
59
|
+
|
|
60
|
+
// Documentation & governance
|
|
61
|
+
[isDomainTerm, SymbolKind.Field, CompletionItemKind.Field],
|
|
62
|
+
[isDecision, SymbolKind.Field, CompletionItemKind.Field],
|
|
63
|
+
[isPolicy, SymbolKind.Field, CompletionItemKind.Field],
|
|
64
|
+
[isBusinessRule, SymbolKind.Field, CompletionItemKind.Field],
|
|
65
|
+
|
|
66
|
+
// Metadata entries
|
|
67
|
+
[isMetadataEntry, SymbolKind.Field, CompletionItemKind.Field],
|
|
68
|
+
] as const;
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Maps DomainLang AST types to semantically appropriate SymbolKind values.
|
|
72
|
+
*
|
|
73
|
+
* Used by the DocumentSymbolProvider (outline/breadcrumbs), WorkspaceSymbolProvider,
|
|
74
|
+
* and the CompletionProvider.
|
|
75
|
+
*/
|
|
76
|
+
export class DomainLangNodeKindProvider extends DefaultNodeKindProvider {
|
|
77
|
+
|
|
78
|
+
override getSymbolKind(node: AstNode | AstNodeDescription): SymbolKind {
|
|
79
|
+
try {
|
|
80
|
+
const astNode = this.resolveNode(node);
|
|
81
|
+
if (!astNode) return super.getSymbolKind(node);
|
|
82
|
+
|
|
83
|
+
for (const [guard, symbolKind] of KIND_MAPPINGS) {
|
|
84
|
+
if (guard(astNode)) return symbolKind;
|
|
85
|
+
}
|
|
86
|
+
return super.getSymbolKind(node);
|
|
87
|
+
} catch (error) {
|
|
88
|
+
console.error('Error in getSymbolKind:', error);
|
|
89
|
+
return super.getSymbolKind(node);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
override getCompletionItemKind(node: AstNode | AstNodeDescription): CompletionItemKind {
|
|
94
|
+
try {
|
|
95
|
+
const astNode = this.resolveNode(node);
|
|
96
|
+
if (!astNode) return super.getCompletionItemKind(node);
|
|
97
|
+
|
|
98
|
+
for (const [guard, , completionKind] of KIND_MAPPINGS) {
|
|
99
|
+
if (guard(astNode)) return completionKind;
|
|
100
|
+
}
|
|
101
|
+
return super.getCompletionItemKind(node);
|
|
102
|
+
} catch (error) {
|
|
103
|
+
console.error('Error in getCompletionItemKind:', error);
|
|
104
|
+
return super.getCompletionItemKind(node);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Resolves an AstNode from an AstNodeDescription (which may only have a reference).
|
|
110
|
+
* Returns the node directly if it's already an AstNode.
|
|
111
|
+
*/
|
|
112
|
+
private resolveNode(node: AstNode | AstNodeDescription): AstNode | undefined {
|
|
113
|
+
if ('$type' in node) {
|
|
114
|
+
return node;
|
|
115
|
+
}
|
|
116
|
+
// AstNodeDescription — resolve if possible
|
|
117
|
+
return node.node;
|
|
118
|
+
}
|
|
119
|
+
}
|