@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.
Files changed (88) hide show
  1. package/README.md +1 -1
  2. package/out/domain-lang-module.d.ts +2 -0
  3. package/out/domain-lang-module.js +23 -2
  4. package/out/domain-lang-module.js.map +1 -1
  5. package/out/lsp/domain-lang-completion.d.ts +142 -1
  6. package/out/lsp/domain-lang-completion.js +620 -22
  7. package/out/lsp/domain-lang-completion.js.map +1 -1
  8. package/out/lsp/domain-lang-document-symbol-provider.d.ts +79 -0
  9. package/out/lsp/domain-lang-document-symbol-provider.js +210 -0
  10. package/out/lsp/domain-lang-document-symbol-provider.js.map +1 -0
  11. package/out/lsp/domain-lang-index-manager.d.ts +98 -1
  12. package/out/lsp/domain-lang-index-manager.js +214 -7
  13. package/out/lsp/domain-lang-index-manager.js.map +1 -1
  14. package/out/lsp/domain-lang-node-kind-provider.d.ts +27 -0
  15. package/out/lsp/domain-lang-node-kind-provider.js +87 -0
  16. package/out/lsp/domain-lang-node-kind-provider.js.map +1 -0
  17. package/out/lsp/domain-lang-scope-provider.d.ts +100 -0
  18. package/out/lsp/domain-lang-scope-provider.js +170 -0
  19. package/out/lsp/domain-lang-scope-provider.js.map +1 -0
  20. package/out/lsp/domain-lang-workspace-manager.d.ts +46 -0
  21. package/out/lsp/domain-lang-workspace-manager.js +148 -4
  22. package/out/lsp/domain-lang-workspace-manager.js.map +1 -1
  23. package/out/lsp/hover/domain-lang-hover.d.ts +16 -6
  24. package/out/lsp/hover/domain-lang-hover.js +160 -134
  25. package/out/lsp/hover/domain-lang-hover.js.map +1 -1
  26. package/out/lsp/hover/hover-builders.d.ts +57 -0
  27. package/out/lsp/hover/hover-builders.js +171 -0
  28. package/out/lsp/hover/hover-builders.js.map +1 -0
  29. package/out/main.js +116 -20
  30. package/out/main.js.map +1 -1
  31. package/out/sdk/index.d.ts +2 -1
  32. package/out/sdk/index.js +1 -1
  33. package/out/sdk/index.js.map +1 -1
  34. package/out/sdk/loader-node.js +1 -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/services/import-resolver.d.ts +29 -6
  42. package/out/services/import-resolver.js +48 -9
  43. package/out/services/import-resolver.js.map +1 -1
  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 +20 -0
  58. package/out/validation/constants.js +39 -3
  59. package/out/validation/constants.js.map +1 -1
  60. package/out/validation/import.d.ts +22 -1
  61. package/out/validation/import.js +104 -16
  62. package/out/validation/import.js.map +1 -1
  63. package/out/validation/maps.js +101 -3
  64. package/out/validation/maps.js.map +1 -1
  65. package/package.json +5 -5
  66. package/src/domain-lang-module.ts +26 -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 +250 -7
  70. package/src/lsp/domain-lang-node-kind-provider.ts +119 -0
  71. package/src/lsp/domain-lang-scope-provider.ts +250 -0
  72. package/src/lsp/domain-lang-workspace-manager.ts +187 -4
  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 +156 -23
  76. package/src/sdk/index.ts +2 -1
  77. package/src/sdk/loader-node.ts +2 -1
  78. package/src/sdk/loader.ts +125 -34
  79. package/src/sdk/query.ts +15 -11
  80. package/src/services/import-resolver.ts +60 -9
  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 +47 -6
  87. package/src/validation/import.ts +124 -16
  88. package/src/validation/maps.ts +118 -4
@@ -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:
@@ -17,6 +20,7 @@ import type { Model } from '../generated/ast.js';
17
20
  * **How it works:**
18
21
  * - When a document is indexed, we ensure all its imports are also loaded
19
22
  * - Maintains a reverse dependency graph: importedUri → Set<importingUri>
23
+ * - Also tracks import specifiers to detect when file moves affect resolution
20
24
  * - Overrides `isAffected()` to also check this graph
21
25
  * - This integrates with Langium's native `DocumentBuilder.update()` flow
22
26
  *
@@ -35,6 +39,14 @@ export class DomainLangIndexManager extends DefaultIndexManager {
35
39
  */
36
40
  private readonly importDependencies = new Map<string, Set<string>>();
37
41
 
42
+ /**
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.
45
+ * Key: importing document URI
46
+ * Value: Array of ImportInfo objects
47
+ */
48
+ private readonly documentImportInfo = new Map<string, ImportInfo[]>();
49
+
38
50
  /**
39
51
  * Tracks documents that have had their imports loaded to avoid redundant work.
40
52
  * Cleared on workspace config changes.
@@ -46,11 +58,41 @@ export class DomainLangIndexManager extends DefaultIndexManager {
46
58
  */
47
59
  private readonly sharedServices: LangiumSharedCoreServices;
48
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
+
49
68
  constructor(services: LangiumSharedCoreServices) {
50
69
  super(services);
51
70
  this.sharedServices = services;
52
71
  }
53
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
+
54
96
  /**
55
97
  * Extends the default content update to:
56
98
  * 1. Ensure all imported documents are loaded
@@ -115,7 +157,9 @@ export class DomainLangIndexManager extends DefaultIndexManager {
115
157
 
116
158
  /**
117
159
  * Tracks import dependencies for a document.
118
- * For each import in the document, records that the imported URI is depended upon.
160
+ * For each import in the document, records:
161
+ * 1. That the imported URI is depended upon (for direct change detection)
162
+ * 2. The import specifier and alias (for scope resolution)
119
163
  */
120
164
  private async trackImportDependencies(document: LangiumDocument): Promise<void> {
121
165
  const importingUri = document.uri.toString();
@@ -123,6 +167,7 @@ export class DomainLangIndexManager extends DefaultIndexManager {
123
167
  // First, remove old dependencies from this document
124
168
  // (in case imports changed)
125
169
  this.removeDocumentFromDependencies(importingUri);
170
+ this.documentImportInfo.delete(importingUri);
126
171
 
127
172
  // Skip if document isn't ready (no parse result)
128
173
  if (document.state < DocumentState.Parsed) {
@@ -134,13 +179,22 @@ export class DomainLangIndexManager extends DefaultIndexManager {
134
179
  return;
135
180
  }
136
181
 
182
+ const importInfoList: ImportInfo[] = [];
183
+
137
184
  for (const imp of model.imports) {
138
185
  if (!imp.uri) continue;
139
186
 
140
187
  try {
141
- const resolvedUri = await resolveImportPath(document, imp.uri);
188
+ const resolvedUri = await this.resolveImport(document, imp.uri);
142
189
  const importedUri = resolvedUri.toString();
143
190
 
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
+ });
197
+
144
198
  // Add to reverse dependency graph: importedUri → importingUri
145
199
  let dependents = this.importDependencies.get(importedUri);
146
200
  if (!dependents) {
@@ -149,9 +203,18 @@ export class DomainLangIndexManager extends DefaultIndexManager {
149
203
  }
150
204
  dependents.add(importingUri);
151
205
  } catch {
152
- // Import resolution failed - validation will report the error
206
+ // Import resolution failed - still track the specifier with empty resolution
207
+ importInfoList.push({
208
+ specifier: imp.uri,
209
+ alias: imp.alias,
210
+ resolvedUri: ''
211
+ });
153
212
  }
154
213
  }
214
+
215
+ if (importInfoList.length > 0) {
216
+ this.documentImportInfo.set(importingUri, importInfoList);
217
+ }
155
218
  }
156
219
 
157
220
  /**
@@ -188,7 +251,7 @@ export class DomainLangIndexManager extends DefaultIndexManager {
188
251
  if (!imp.uri) continue;
189
252
 
190
253
  try {
191
- const resolvedUri = await resolveImportPath(document, imp.uri);
254
+ const resolvedUri = await this.resolveImport(document, imp.uri);
192
255
  const importedUriString = resolvedUri.toString();
193
256
 
194
257
  // Skip if already loaded
@@ -199,8 +262,10 @@ export class DomainLangIndexManager extends DefaultIndexManager {
199
262
  // Load or create the imported document
200
263
  const importedDoc = await langiumDocuments.getOrCreateDocument(resolvedUri);
201
264
 
202
- // If document is new (not yet indexed), add to batch
203
- if (importedDoc.state < DocumentState.IndexedContent) {
265
+ // If document is not yet validated, add to batch for building
266
+ // This ensures all imported documents reach Validated state,
267
+ // preventing "workspace state is already Validated" errors
268
+ if (importedDoc.state < DocumentState.Validated) {
204
269
  newDocs.push(importedDoc);
205
270
  }
206
271
  } catch {
@@ -208,7 +273,7 @@ export class DomainLangIndexManager extends DefaultIndexManager {
208
273
  }
209
274
  }
210
275
 
211
- // Build any newly discovered documents
276
+ // Build any newly discovered documents to Validated state
212
277
  // This triggers indexing which will recursively load their imports
213
278
  if (newDocs.length > 0) {
214
279
  await documentBuilder.build(newDocs, { validation: true });
@@ -243,6 +308,7 @@ export class DomainLangIndexManager extends DefaultIndexManager {
243
308
  */
244
309
  clearImportDependencies(): void {
245
310
  this.importDependencies.clear();
311
+ this.documentImportInfo.clear();
246
312
  this.importsLoaded.clear();
247
313
  }
248
314
 
@@ -253,4 +319,181 @@ export class DomainLangIndexManager extends DefaultIndexManager {
253
319
  markForReprocessing(uri: string): void {
254
320
  this.importsLoaded.delete(uri);
255
321
  }
322
+
323
+ /**
324
+ * Gets all documents that import the given URI.
325
+ * Used to find documents that need rebuilding when a file changes.
326
+ *
327
+ * @param uri - The URI of the changed/deleted file
328
+ * @returns Set of URIs (as strings) of documents that import this file
329
+ */
330
+ getDependentDocuments(uri: string): Set<string> {
331
+ return this.importDependencies.get(uri) ?? new Set();
332
+ }
333
+
334
+ /**
335
+ * Gets the resolved import URIs for a document.
336
+ * Returns only URIs where import resolution succeeded (non-empty resolved URI).
337
+ *
338
+ * @param documentUri - The URI of the document
339
+ * @returns Set of resolved import URIs, or empty set if none
340
+ */
341
+ getResolvedImports(documentUri: string): Set<string> {
342
+ const importInfoList = this.documentImportInfo.get(documentUri);
343
+ if (!importInfoList) {
344
+ return new Set();
345
+ }
346
+
347
+ const resolved = new Set<string>();
348
+ for (const info of importInfoList) {
349
+ // Only include successfully resolved imports (non-empty string)
350
+ if (info.resolvedUri) {
351
+ resolved.add(info.resolvedUri);
352
+ }
353
+ }
354
+ return resolved;
355
+ }
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
+
368
+ /**
369
+ * Gets all documents that would be affected by changes to the given URIs.
370
+ * This includes direct dependents and transitive dependents.
371
+ *
372
+ * @param changedUris - URIs of changed/deleted files
373
+ * @returns Set of all affected document URIs
374
+ */
375
+ getAllAffectedDocuments(changedUris: Iterable<string>): Set<string> {
376
+ const affected = new Set<string>();
377
+ const toProcess = [...changedUris];
378
+
379
+ while (toProcess.length > 0) {
380
+ const uri = toProcess.pop();
381
+ if (!uri) {
382
+ continue;
383
+ }
384
+ const dependents = this.importDependencies.get(uri);
385
+ if (dependents) {
386
+ for (const dep of dependents) {
387
+ if (!affected.has(dep)) {
388
+ affected.add(dep);
389
+ // Also check transitive dependents
390
+ toProcess.push(dep);
391
+ }
392
+ }
393
+ }
394
+ }
395
+
396
+ return affected;
397
+ }
398
+
399
+ /**
400
+ * Gets documents that have import specifiers which might be affected by file moves.
401
+ *
402
+ * When a file is moved/renamed, import specifiers that previously resolved to it
403
+ * (or could now resolve to it) need to be re-evaluated. This method finds documents
404
+ * whose imports might resolve differently after the file system change.
405
+ *
406
+ * @param changedUris - URIs of changed/deleted/created files
407
+ * @returns Set of document URIs that should be rebuilt
408
+ */
409
+ getDocumentsWithPotentiallyAffectedImports(changedUris: Iterable<string>): Set<string> {
410
+ const changedPaths = this.extractPathSegments(changedUris);
411
+ return this.findDocumentsMatchingPaths(changedPaths);
412
+ }
413
+
414
+ /**
415
+ * Extracts path segments from URIs for fuzzy matching.
416
+ */
417
+ private extractPathSegments(uris: Iterable<string>): Set<string> {
418
+ const paths = new Set<string>();
419
+
420
+ for (const uri of uris) {
421
+ this.addPathSegmentsFromUri(uri, paths);
422
+ }
423
+
424
+ return paths;
425
+ }
426
+
427
+ /**
428
+ * Adds path segments from a single URI to the set.
429
+ */
430
+ private addPathSegmentsFromUri(uri: string, paths: Set<string>): void {
431
+ try {
432
+ const url = new URL(uri);
433
+ const pathParts = url.pathname.split('/').filter(p => p.length > 0);
434
+
435
+ // Add filename
436
+ const fileName = pathParts.at(-1);
437
+ if (fileName) {
438
+ paths.add(fileName);
439
+ }
440
+
441
+ // Add parent/filename combination
442
+ if (pathParts.length >= 2) {
443
+ paths.add(pathParts.slice(-2).join('/'));
444
+ }
445
+
446
+ // Add grandparent/parent/filename combination
447
+ if (pathParts.length >= 3) {
448
+ paths.add(pathParts.slice(-3).join('/'));
449
+ }
450
+ } catch {
451
+ // Invalid URI, skip
452
+ }
453
+ }
454
+
455
+ /**
456
+ * Finds documents with import specifiers matching any of the given paths.
457
+ */
458
+ private findDocumentsMatchingPaths(changedPaths: Set<string>): Set<string> {
459
+ const affected = new Set<string>();
460
+
461
+ for (const [docUri, importInfoList] of this.documentImportInfo) {
462
+ if (this.hasMatchingSpecifierOrResolvedUri(importInfoList, changedPaths)) {
463
+ affected.add(docUri);
464
+ }
465
+ }
466
+
467
+ return affected;
468
+ }
469
+
470
+ /**
471
+ * Checks if any specifier OR its resolved URI matches the changed paths.
472
+ *
473
+ * This handles both regular imports and path aliases:
474
+ * - Regular: `./domains/sales.dlang` matches path `sales.dlang`
475
+ * - Aliased: `@domains/sales.dlang` resolves to `/full/path/domains/sales.dlang`
476
+ * When the file moves, the resolved URI matches but the specifier doesn't
477
+ *
478
+ * We check both to ensure moves of aliased imports trigger revalidation.
479
+ */
480
+ private hasMatchingSpecifierOrResolvedUri(importInfoList: ImportInfo[], changedPaths: Set<string>): boolean {
481
+ for (const info of importInfoList) {
482
+ const normalizedSpecifier = info.specifier.replace(/^[.@/]+/, '');
483
+
484
+ for (const changedPath of changedPaths) {
485
+ // Check the raw specifier (handles relative imports)
486
+ if (info.specifier.includes(changedPath) || changedPath.endsWith(normalizedSpecifier)) {
487
+ return true;
488
+ }
489
+
490
+ // Check the resolved URI (handles path aliases like @domains/...)
491
+ // The resolved URI contains the full file path which matches moved files
492
+ if (info.resolvedUri?.includes(changedPath)) {
493
+ return true;
494
+ }
495
+ }
496
+ }
497
+ return false;
498
+ }
256
499
  }