@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.
Files changed (88) hide show
  1. package/out/domain-lang-module.d.ts +2 -0
  2. package/out/domain-lang-module.js +21 -2
  3. package/out/domain-lang-module.js.map +1 -1
  4. package/out/lsp/domain-lang-completion.d.ts +142 -1
  5. package/out/lsp/domain-lang-completion.js +620 -22
  6. package/out/lsp/domain-lang-completion.js.map +1 -1
  7. package/out/lsp/domain-lang-document-symbol-provider.d.ts +79 -0
  8. package/out/lsp/domain-lang-document-symbol-provider.js +210 -0
  9. package/out/lsp/domain-lang-document-symbol-provider.js.map +1 -0
  10. package/out/lsp/domain-lang-index-manager.d.ts +34 -5
  11. package/out/lsp/domain-lang-index-manager.js +66 -27
  12. package/out/lsp/domain-lang-index-manager.js.map +1 -1
  13. package/out/lsp/domain-lang-node-kind-provider.d.ts +27 -0
  14. package/out/lsp/domain-lang-node-kind-provider.js +87 -0
  15. package/out/lsp/domain-lang-node-kind-provider.js.map +1 -0
  16. package/out/lsp/domain-lang-scope-provider.d.ts +53 -20
  17. package/out/lsp/domain-lang-scope-provider.js +119 -44
  18. package/out/lsp/domain-lang-scope-provider.js.map +1 -1
  19. package/out/lsp/domain-lang-workspace-manager.d.ts +23 -2
  20. package/out/lsp/domain-lang-workspace-manager.js +51 -6
  21. package/out/lsp/domain-lang-workspace-manager.js.map +1 -1
  22. package/out/lsp/hover/domain-lang-hover.d.ts +16 -6
  23. package/out/lsp/hover/domain-lang-hover.js +160 -134
  24. package/out/lsp/hover/domain-lang-hover.js.map +1 -1
  25. package/out/lsp/hover/hover-builders.d.ts +57 -0
  26. package/out/lsp/hover/hover-builders.js +171 -0
  27. package/out/lsp/hover/hover-builders.js.map +1 -0
  28. package/out/main.js +2 -1
  29. package/out/main.js.map +1 -1
  30. package/out/sdk/index.d.ts +31 -11
  31. package/out/sdk/index.js +30 -11
  32. package/out/sdk/index.js.map +1 -1
  33. package/out/sdk/loader-node.d.ts +2 -0
  34. package/out/sdk/loader-node.js +3 -1
  35. package/out/sdk/loader-node.js.map +1 -1
  36. package/out/sdk/loader.d.ts +55 -2
  37. package/out/sdk/loader.js +87 -28
  38. package/out/sdk/loader.js.map +1 -1
  39. package/out/sdk/query.js +14 -11
  40. package/out/sdk/query.js.map +1 -1
  41. package/out/sdk/validator.d.ts +134 -0
  42. package/out/sdk/validator.js +249 -0
  43. package/out/sdk/validator.js.map +1 -0
  44. package/out/services/package-boundary-detector.d.ts +101 -0
  45. package/out/services/package-boundary-detector.js +211 -0
  46. package/out/services/package-boundary-detector.js.map +1 -0
  47. package/out/services/performance-optimizer.js +6 -2
  48. package/out/services/performance-optimizer.js.map +1 -1
  49. package/out/services/types.d.ts +24 -0
  50. package/out/services/types.js.map +1 -1
  51. package/out/services/workspace-manager.d.ts +73 -6
  52. package/out/services/workspace-manager.js +210 -57
  53. package/out/services/workspace-manager.js.map +1 -1
  54. package/out/utils/import-utils.d.ts +9 -6
  55. package/out/utils/import-utils.js +26 -15
  56. package/out/utils/import-utils.js.map +1 -1
  57. package/out/validation/constants.d.ts +7 -0
  58. package/out/validation/constants.js +21 -3
  59. package/out/validation/constants.js.map +1 -1
  60. package/out/validation/import.d.ts +11 -1
  61. package/out/validation/import.js +42 -14
  62. package/out/validation/import.js.map +1 -1
  63. package/out/validation/maps.js +50 -1
  64. package/out/validation/maps.js.map +1 -1
  65. package/package.json +8 -9
  66. package/src/domain-lang-module.ts +24 -3
  67. package/src/lsp/domain-lang-completion.ts +736 -27
  68. package/src/lsp/domain-lang-document-symbol-provider.ts +254 -0
  69. package/src/lsp/domain-lang-index-manager.ts +79 -27
  70. package/src/lsp/domain-lang-node-kind-provider.ts +119 -0
  71. package/src/lsp/domain-lang-scope-provider.ts +171 -55
  72. package/src/lsp/domain-lang-workspace-manager.ts +64 -6
  73. package/src/lsp/hover/domain-lang-hover.ts +189 -131
  74. package/src/lsp/hover/hover-builders.ts +208 -0
  75. package/src/main.ts +3 -1
  76. package/src/sdk/index.ts +33 -11
  77. package/src/sdk/loader-node.ts +6 -1
  78. package/src/sdk/loader.ts +125 -34
  79. package/src/sdk/query.ts +15 -11
  80. package/src/sdk/validator.ts +358 -0
  81. package/src/services/package-boundary-detector.ts +238 -0
  82. package/src/services/performance-optimizer.ts +6 -2
  83. package/src/services/types.ts +25 -0
  84. package/src/services/workspace-manager.ts +259 -62
  85. package/src/utils/import-utils.ts +27 -15
  86. package/src/validation/constants.ts +23 -6
  87. package/src/validation/import.ts +49 -14
  88. 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 specifiers and their resolved URIs.
41
- * Used to detect when file moves could affect import resolution.
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: Map of import specifier → resolved URI
46
+ * Value: Array of ImportInfo objects
44
47
  */
45
- private readonly documentImportSpecifiers = new Map<string, Map<string, string>>();
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 used (for file move detection)
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.documentImportSpecifiers.delete(importingUri);
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 specifierMap = new Map<string, string>();
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 resolveImportPath(document, imp.uri);
188
+ const resolvedUri = await this.resolveImport(document, imp.uri);
156
189
  const importedUri = resolvedUri.toString();
157
190
 
158
- // Track the specifier resolved URI mapping
159
- specifierMap.set(imp.uri, importedUri);
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
- specifierMap.set(imp.uri, '');
207
+ importInfoList.push({
208
+ specifier: imp.uri,
209
+ alias: imp.alias,
210
+ resolvedUri: ''
211
+ });
171
212
  }
172
213
  }
173
214
 
174
- if (specifierMap.size > 0) {
175
- this.documentImportSpecifiers.set(importingUri, specifierMap);
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 resolveImportPath(document, imp.uri);
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.documentImportSpecifiers.clear();
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 specifierMap = this.documentImportSpecifiers.get(documentUri);
302
- if (!specifierMap) {
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 resolvedUri of specifierMap.values()) {
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, specifierMap] of this.documentImportSpecifiers) {
410
- if (this.hasMatchingSpecifierOrResolvedUri(specifierMap, changedPaths)) {
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(specifierMap: Map<string, string>, changedPaths: Set<string>): boolean {
429
- for (const [specifier, resolvedUri] of specifierMap.entries()) {
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
+ }