@domainlang/language 0.5.2 → 0.7.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 (116) hide show
  1. package/README.md +1 -1
  2. package/out/domain-lang-module.js +5 -1
  3. package/out/domain-lang-module.js.map +1 -1
  4. package/out/generated/ast.d.ts +24 -0
  5. package/out/generated/ast.js.map +1 -1
  6. package/out/generated/grammar.js +22 -32
  7. package/out/generated/grammar.js.map +1 -1
  8. package/out/index.d.ts +2 -5
  9. package/out/index.js +10 -6
  10. package/out/index.js.map +1 -1
  11. package/out/lsp/domain-lang-code-actions.js +14 -8
  12. package/out/lsp/domain-lang-code-actions.js.map +1 -1
  13. package/out/lsp/domain-lang-completion.d.ts +3 -0
  14. package/out/lsp/domain-lang-completion.js +41 -13
  15. package/out/lsp/domain-lang-completion.js.map +1 -1
  16. package/out/lsp/domain-lang-formatter.js +24 -18
  17. package/out/lsp/domain-lang-formatter.js.map +1 -1
  18. package/out/lsp/domain-lang-index-manager.d.ts +170 -0
  19. package/out/lsp/domain-lang-index-manager.js +389 -0
  20. package/out/lsp/domain-lang-index-manager.js.map +1 -0
  21. package/out/lsp/domain-lang-scope-provider.d.ts +67 -0
  22. package/out/lsp/domain-lang-scope-provider.js +95 -0
  23. package/out/lsp/domain-lang-scope-provider.js.map +1 -0
  24. package/out/lsp/domain-lang-scope.js +31 -17
  25. package/out/lsp/domain-lang-scope.js.map +1 -1
  26. package/out/lsp/domain-lang-workspace-manager.d.ts +76 -9
  27. package/out/lsp/domain-lang-workspace-manager.js +176 -54
  28. package/out/lsp/domain-lang-workspace-manager.js.map +1 -1
  29. package/out/lsp/hover/domain-lang-hover.d.ts +45 -1
  30. package/out/lsp/hover/domain-lang-hover.js +308 -232
  31. package/out/lsp/hover/domain-lang-hover.js.map +1 -1
  32. package/out/lsp/hover/domain-lang-keywords.d.ts +3 -7
  33. package/out/lsp/hover/domain-lang-keywords.js +115 -38
  34. package/out/lsp/hover/domain-lang-keywords.js.map +1 -1
  35. package/out/lsp/manifest-diagnostics.js +95 -50
  36. package/out/lsp/manifest-diagnostics.js.map +1 -1
  37. package/out/main.js +204 -17
  38. package/out/main.js.map +1 -1
  39. package/out/services/import-resolver.d.ts +39 -2
  40. package/out/services/import-resolver.js +77 -12
  41. package/out/services/import-resolver.js.map +1 -1
  42. package/out/services/types.d.ts +2 -2
  43. package/out/services/workspace-manager.d.ts +33 -31
  44. package/out/services/workspace-manager.js +92 -148
  45. package/out/services/workspace-manager.js.map +1 -1
  46. package/out/utils/document-utils.d.ts +41 -0
  47. package/out/utils/document-utils.js +64 -0
  48. package/out/utils/document-utils.js.map +1 -0
  49. package/out/utils/import-utils.d.ts +0 -17
  50. package/out/utils/import-utils.js +2 -32
  51. package/out/utils/import-utils.js.map +1 -1
  52. package/out/utils/manifest-utils.d.ts +56 -0
  53. package/out/utils/manifest-utils.js +119 -0
  54. package/out/utils/manifest-utils.js.map +1 -0
  55. package/out/validation/constants.d.ts +13 -0
  56. package/out/validation/constants.js +18 -0
  57. package/out/validation/constants.js.map +1 -1
  58. package/out/validation/import.d.ts +12 -2
  59. package/out/validation/import.js +95 -22
  60. package/out/validation/import.js.map +1 -1
  61. package/out/validation/maps.js +51 -2
  62. package/out/validation/maps.js.map +1 -1
  63. package/package.json +1 -1
  64. package/src/domain-lang-module.ts +6 -1
  65. package/src/domain-lang.langium +37 -13
  66. package/src/generated/ast.ts +24 -0
  67. package/src/generated/grammar.ts +22 -32
  68. package/src/index.ts +12 -6
  69. package/src/lsp/domain-lang-code-actions.ts +13 -8
  70. package/src/lsp/domain-lang-completion.ts +61 -13
  71. package/src/lsp/domain-lang-formatter.ts +28 -23
  72. package/src/lsp/domain-lang-index-manager.ts +447 -0
  73. package/src/lsp/domain-lang-scope-provider.ts +134 -0
  74. package/src/lsp/domain-lang-scope.ts +29 -17
  75. package/src/lsp/domain-lang-workspace-manager.ts +201 -53
  76. package/src/lsp/hover/domain-lang-hover.ts +332 -226
  77. package/src/lsp/hover/domain-lang-keywords.ts +129 -43
  78. package/src/lsp/manifest-diagnostics.ts +100 -59
  79. package/src/main.ts +258 -16
  80. package/src/services/import-resolver.ts +91 -12
  81. package/src/services/types.ts +2 -2
  82. package/src/services/workspace-manager.ts +101 -175
  83. package/src/utils/document-utils.ts +80 -0
  84. package/src/utils/import-utils.ts +2 -40
  85. package/src/utils/manifest-utils.ts +132 -0
  86. package/src/validation/constants.ts +24 -0
  87. package/src/validation/import.ts +107 -24
  88. package/src/validation/maps.ts +59 -2
  89. package/out/lsp/hover/ddd-pattern-explanations.d.ts +0 -50
  90. package/out/lsp/hover/ddd-pattern-explanations.js +0 -196
  91. package/out/lsp/hover/ddd-pattern-explanations.js.map +0 -1
  92. package/out/services/dependency-analyzer.d.ts +0 -58
  93. package/out/services/dependency-analyzer.js +0 -254
  94. package/out/services/dependency-analyzer.js.map +0 -1
  95. package/out/services/dependency-resolver.d.ts +0 -146
  96. package/out/services/dependency-resolver.js +0 -452
  97. package/out/services/dependency-resolver.js.map +0 -1
  98. package/out/services/git-url-resolver.browser.d.ts +0 -10
  99. package/out/services/git-url-resolver.browser.js +0 -19
  100. package/out/services/git-url-resolver.browser.js.map +0 -1
  101. package/out/services/git-url-resolver.d.ts +0 -158
  102. package/out/services/git-url-resolver.js +0 -416
  103. package/out/services/git-url-resolver.js.map +0 -1
  104. package/out/services/governance-validator.d.ts +0 -44
  105. package/out/services/governance-validator.js +0 -153
  106. package/out/services/governance-validator.js.map +0 -1
  107. package/out/services/semver.d.ts +0 -98
  108. package/out/services/semver.js +0 -195
  109. package/out/services/semver.js.map +0 -1
  110. package/src/lsp/hover/ddd-pattern-explanations.ts +0 -237
  111. package/src/services/dependency-analyzer.ts +0 -321
  112. package/src/services/dependency-resolver.ts +0 -551
  113. package/src/services/git-url-resolver.browser.ts +0 -26
  114. package/src/services/git-url-resolver.ts +0 -517
  115. package/src/services/governance-validator.ts +0 -177
  116. package/src/services/semver.ts +0 -213
@@ -8,6 +8,7 @@
8
8
  * - Maintainable: Clear mapping from grammar to completions
9
9
  */
10
10
 
11
+ import type { AstNode } from 'langium';
11
12
  import { CompletionAcceptor, CompletionContext, DefaultCompletionProvider, NextFeature } from 'langium/lsp';
12
13
  import { CompletionItemKind, InsertTextFormat } from 'vscode-languageserver';
13
14
  import * as ast from '../generated/ast.js';
@@ -121,6 +122,20 @@ export class DomainLangCompletionProvider extends DefaultCompletionProvider {
121
122
  context: CompletionContext,
122
123
  next: NextFeature,
123
124
  acceptor: CompletionAcceptor
125
+ ): void {
126
+ try {
127
+ this.safeCompletionFor(context, next, acceptor);
128
+ } catch (error) {
129
+ console.error('Error in completionFor:', error);
130
+ // Fall back to default completion on error
131
+ super.completionFor(context, next, acceptor);
132
+ }
133
+ }
134
+
135
+ private safeCompletionFor(
136
+ context: CompletionContext,
137
+ next: NextFeature,
138
+ acceptor: CompletionAcceptor
124
139
  ): void {
125
140
  const node = context.node;
126
141
  if (!node) {
@@ -143,84 +158,117 @@ export class DomainLangCompletionProvider extends DefaultCompletionProvider {
143
158
  return;
144
159
  }
145
160
 
161
+ // Handle node-level completions
162
+ if (this.handleNodeCompletions(node, acceptor, context, next)) {
163
+ return;
164
+ }
165
+
166
+ // Handle container-level completions
167
+ const container = node.$container;
168
+ if (this.handleContainerCompletions(container, node, acceptor, context, next)) {
169
+ return;
170
+ }
171
+
172
+ // Let Langium handle default completions
173
+ super.completionFor(context, next, acceptor);
174
+ }
175
+
176
+ private handleNodeCompletions(
177
+ node: AstNode,
178
+ acceptor: CompletionAcceptor,
179
+ context: CompletionContext,
180
+ next: NextFeature
181
+ ): boolean {
146
182
  // If we're AT a BoundedContext node: only BC documentation blocks
147
183
  if (ast.isBoundedContext(node)) {
148
184
  this.addBoundedContextCompletions(node, acceptor, context);
149
185
  super.completionFor(context, next, acceptor);
150
- return;
186
+ return true;
151
187
  }
152
188
 
153
189
  // If we're AT a Domain node: only Domain documentation blocks
154
190
  if (ast.isDomain(node)) {
155
191
  this.addDomainCompletions(node, acceptor, context);
156
192
  super.completionFor(context, next, acceptor);
157
- return;
193
+ return true;
158
194
  }
159
195
 
160
196
  // If we're AT a ContextMap node: relationships and contains
161
197
  if (ast.isContextMap(node)) {
162
198
  this.addContextMapCompletions(node, acceptor, context);
163
199
  super.completionFor(context, next, acceptor);
164
- return;
200
+ return true;
165
201
  }
166
202
 
167
203
  // If we're AT a DomainMap node: contains
168
204
  if (ast.isDomainMap(node)) {
169
205
  this.addDomainMapCompletions(node, acceptor, context);
170
206
  super.completionFor(context, next, acceptor);
171
- return;
207
+ return true;
172
208
  }
173
209
 
174
210
  // If we're AT the Model or NamespaceDeclaration level: all top-level constructs
175
211
  if (ast.isModel(node) || ast.isNamespaceDeclaration(node)) {
176
212
  this.addTopLevelSnippets(acceptor, context);
177
213
  super.completionFor(context, next, acceptor);
178
- return;
214
+ return true;
179
215
  }
180
216
 
181
- const container = node.$container;
217
+ return false;
218
+ }
219
+
220
+ private handleContainerCompletions(
221
+ container: AstNode | undefined,
222
+ node: AstNode,
223
+ acceptor: CompletionAcceptor,
224
+ context: CompletionContext,
225
+ next: NextFeature
226
+ ): boolean {
227
+ if (!container) {
228
+ return false;
229
+ }
182
230
 
183
231
  // Inside BoundedContext body: suggest missing scalar properties and collections
184
232
  if (ast.isBoundedContext(container)) {
185
233
  this.addBoundedContextCompletions(container, acceptor, context);
186
234
  super.completionFor(context, next, acceptor);
187
- return;
235
+ return true;
188
236
  }
189
237
 
190
238
  // Inside Domain body: suggest missing scalar properties
191
239
  if (ast.isDomain(container)) {
192
240
  this.addDomainCompletions(container, acceptor, context);
193
241
  super.completionFor(context, next, acceptor);
194
- return;
242
+ return true;
195
243
  }
196
244
 
197
245
  // Inside ContextMap body: relationships and contains
198
246
  if (ast.isContextMap(container)) {
199
247
  this.addContextMapCompletions(container, acceptor, context);
200
248
  super.completionFor(context, next, acceptor);
201
- return;
249
+ return true;
202
250
  }
203
251
 
204
252
  // Inside DomainMap body: contains
205
253
  if (ast.isDomainMap(container)) {
206
254
  this.addDomainMapCompletions(container, acceptor, context);
207
255
  super.completionFor(context, next, acceptor);
208
- return;
256
+ return true;
209
257
  }
210
258
 
211
259
  if (ast.isRelationship(node) || ast.isRelationship(container)) {
212
260
  this.addRelationshipCompletions(acceptor, context);
213
261
  super.completionFor(context, next, acceptor);
214
- return;
262
+ return true;
215
263
  }
216
264
 
217
265
  // Top level container (Model or NamespaceDeclaration): all top-level constructs
218
266
  if (ast.isModel(container) || ast.isNamespaceDeclaration(container)) {
219
267
  this.addTopLevelSnippets(acceptor, context);
268
+ return true;
220
269
  }
221
270
 
222
- // Let Langium handle default completions
223
- super.completionFor(context, next, acceptor);
271
+ return false;
224
272
  }
225
273
 
226
274
  private addTopLevelSnippets(acceptor: CompletionAcceptor, context: CompletionContext): void {
@@ -9,29 +9,34 @@ import * as ast from '../generated/ast.js';
9
9
  export class DomainLangFormatter extends AbstractFormatter {
10
10
 
11
11
  protected format(node: AstNode): void {
12
- // Namespace declarations
13
- if (ast.isNamespaceDeclaration(node)) {
14
- this.formatBlock(node);
15
- }
16
-
17
- // Domain declarations
18
- if (ast.isDomain(node)) {
19
- this.formatBlock(node);
20
- }
21
-
22
- // Bounded contexts
23
- if (ast.isBoundedContext(node)) {
24
- this.formatBlock(node);
25
- }
26
-
27
- // Context maps
28
- if (ast.isContextMap(node)) {
29
- this.formatBlock(node);
30
- }
31
-
32
- // Domain maps
33
- if (ast.isDomainMap(node)) {
34
- this.formatBlock(node);
12
+ try {
13
+ // Namespace declarations
14
+ if (ast.isNamespaceDeclaration(node)) {
15
+ this.formatBlock(node);
16
+ }
17
+
18
+ // Domain declarations
19
+ if (ast.isDomain(node)) {
20
+ this.formatBlock(node);
21
+ }
22
+
23
+ // Bounded contexts
24
+ if (ast.isBoundedContext(node)) {
25
+ this.formatBlock(node);
26
+ }
27
+
28
+ // Context maps
29
+ if (ast.isContextMap(node)) {
30
+ this.formatBlock(node);
31
+ }
32
+
33
+ // Domain maps
34
+ if (ast.isDomainMap(node)) {
35
+ this.formatBlock(node);
36
+ }
37
+ } catch (error) {
38
+ console.error('Error in format:', error);
39
+ // Continue - don't crash formatting
35
40
  }
36
41
  }
37
42
 
@@ -0,0 +1,447 @@
1
+ import type { LangiumDocument, LangiumSharedCoreServices, URI } from 'langium';
2
+ import { DefaultIndexManager, DocumentState } from 'langium';
3
+ import { CancellationToken } from 'vscode-jsonrpc';
4
+ import { resolveImportPath } from '../utils/import-utils.js';
5
+ import type { Model } from '../generated/ast.js';
6
+
7
+ /**
8
+ * Custom IndexManager that extends Langium's default to:
9
+ * 1. Automatically load imported documents during indexing
10
+ * 2. Track import dependencies for cross-file revalidation
11
+ *
12
+ * **Why this exists:**
13
+ * Langium's `DefaultIndexManager.isAffected()` only checks cross-references
14
+ * (elements declared with `[Type]` grammar syntax). DomainLang's imports use
15
+ * string literals (`import "path"`), which are not cross-references.
16
+ *
17
+ * **How it works:**
18
+ * - When a document is indexed, we ensure all its imports are also loaded
19
+ * - Maintains a reverse dependency graph: importedUri → Set<importingUri>
20
+ * - Also tracks import specifiers to detect when file moves affect resolution
21
+ * - Overrides `isAffected()` to also check this graph
22
+ * - This integrates with Langium's native `DocumentBuilder.update()` flow
23
+ *
24
+ * **Integration with Langium:**
25
+ * This approach is idiomatic because:
26
+ * 1. `updateContent()` is called for EVERY document during build
27
+ * 2. We load imports during indexing, BEFORE linking/validation
28
+ * 3. `DocumentBuilder.shouldRelink()` calls `IndexManager.isAffected()`
29
+ * 4. No need for separate lifecycle service - this IS the central place
30
+ */
31
+ export class DomainLangIndexManager extends DefaultIndexManager {
32
+ /**
33
+ * Reverse dependency graph: maps a document URI to all documents that import it.
34
+ * Key: imported document URI (string)
35
+ * Value: Set of URIs of documents that import the key document
36
+ */
37
+ private readonly importDependencies = new Map<string, Set<string>>();
38
+
39
+ /**
40
+ * Maps document URI to its import specifiers and their resolved URIs.
41
+ * Used to detect when file moves could affect import resolution.
42
+ * Key: importing document URI
43
+ * Value: Map of import specifier → resolved URI
44
+ */
45
+ private readonly documentImportSpecifiers = new Map<string, Map<string, string>>();
46
+
47
+ /**
48
+ * Tracks documents that have had their imports loaded to avoid redundant work.
49
+ * Cleared on workspace config changes.
50
+ */
51
+ private readonly importsLoaded = new Set<string>();
52
+
53
+ /**
54
+ * Reference to shared services for accessing LangiumDocuments.
55
+ */
56
+ private readonly sharedServices: LangiumSharedCoreServices;
57
+
58
+ constructor(services: LangiumSharedCoreServices) {
59
+ super(services);
60
+ this.sharedServices = services;
61
+ }
62
+
63
+ /**
64
+ * Extends the default content update to:
65
+ * 1. Ensure all imported documents are loaded
66
+ * 2. Track import dependencies for change propagation
67
+ *
68
+ * Called by Langium during the IndexedContent build phase.
69
+ * This is BEFORE linking/validation, so imports are available for resolution.
70
+ */
71
+ override async updateContent(document: LangiumDocument, cancelToken = CancellationToken.None): Promise<void> {
72
+ // First, do the standard content indexing
73
+ await super.updateContent(document, cancelToken);
74
+
75
+ // Then, ensure imports are loaded and track dependencies
76
+ await this.ensureImportsLoaded(document);
77
+ await this.trackImportDependencies(document);
78
+ }
79
+
80
+ /**
81
+ * Extends the default remove to also clean up import dependencies.
82
+ */
83
+ override remove(uri: URI): void {
84
+ super.remove(uri);
85
+ const uriString = uri.toString();
86
+ this.removeImportDependencies(uriString);
87
+ this.importsLoaded.delete(uriString);
88
+ }
89
+
90
+ /**
91
+ * Extends the default content removal to also clean up import dependencies.
92
+ */
93
+ override removeContent(uri: URI): void {
94
+ super.removeContent(uri);
95
+ const uriString = uri.toString();
96
+ this.removeImportDependencies(uriString);
97
+ this.importsLoaded.delete(uriString);
98
+ }
99
+
100
+ /**
101
+ * Extends `isAffected` to also check import dependencies.
102
+ *
103
+ * A document is affected if:
104
+ * 1. It has cross-references to any changed document (default Langium behavior)
105
+ * 2. It imports any of the changed documents (our extension)
106
+ */
107
+ override isAffected(document: LangiumDocument, changedUris: Set<string>): boolean {
108
+ // First check Langium's default: cross-references
109
+ if (super.isAffected(document, changedUris)) {
110
+ return true;
111
+ }
112
+
113
+ // Then check our import dependencies
114
+ const docUri = document.uri.toString();
115
+ for (const changedUri of changedUris) {
116
+ const dependents = this.importDependencies.get(changedUri);
117
+ if (dependents?.has(docUri)) {
118
+ return true;
119
+ }
120
+ }
121
+
122
+ return false;
123
+ }
124
+
125
+ /**
126
+ * Tracks import dependencies for a document.
127
+ * For each import in the document, records:
128
+ * 1. That the imported URI is depended upon (for direct change detection)
129
+ * 2. The import specifier used (for file move detection)
130
+ */
131
+ private async trackImportDependencies(document: LangiumDocument): Promise<void> {
132
+ const importingUri = document.uri.toString();
133
+
134
+ // First, remove old dependencies from this document
135
+ // (in case imports changed)
136
+ this.removeDocumentFromDependencies(importingUri);
137
+ this.documentImportSpecifiers.delete(importingUri);
138
+
139
+ // Skip if document isn't ready (no parse result)
140
+ if (document.state < DocumentState.Parsed) {
141
+ return;
142
+ }
143
+
144
+ const model = document.parseResult.value as unknown as Model;
145
+ if (!model.imports) {
146
+ return;
147
+ }
148
+
149
+ const specifierMap = new Map<string, string>();
150
+
151
+ for (const imp of model.imports) {
152
+ if (!imp.uri) continue;
153
+
154
+ try {
155
+ const resolvedUri = await resolveImportPath(document, imp.uri);
156
+ const importedUri = resolvedUri.toString();
157
+
158
+ // Track the specifier → resolved URI mapping
159
+ specifierMap.set(imp.uri, importedUri);
160
+
161
+ // Add to reverse dependency graph: importedUri → importingUri
162
+ let dependents = this.importDependencies.get(importedUri);
163
+ if (!dependents) {
164
+ dependents = new Set();
165
+ this.importDependencies.set(importedUri, dependents);
166
+ }
167
+ dependents.add(importingUri);
168
+ } catch {
169
+ // Import resolution failed - still track the specifier with empty resolution
170
+ specifierMap.set(imp.uri, '');
171
+ }
172
+ }
173
+
174
+ if (specifierMap.size > 0) {
175
+ this.documentImportSpecifiers.set(importingUri, specifierMap);
176
+ }
177
+ }
178
+
179
+ /**
180
+ * Ensures all imported documents are loaded and available.
181
+ * This is called during indexing, BEFORE linking/validation,
182
+ * so that cross-file references can be resolved.
183
+ *
184
+ * Works for both workspace files and standalone files.
185
+ */
186
+ private async ensureImportsLoaded(document: LangiumDocument): Promise<void> {
187
+ const uriString = document.uri.toString();
188
+
189
+ // Skip if already processed (avoid redundant work and infinite loops)
190
+ if (this.importsLoaded.has(uriString)) {
191
+ return;
192
+ }
193
+ this.importsLoaded.add(uriString);
194
+
195
+ // Skip if document isn't ready (no parse result)
196
+ if (document.state < DocumentState.Parsed) {
197
+ return;
198
+ }
199
+
200
+ const model = document.parseResult.value as unknown as Model;
201
+ if (!model.imports || model.imports.length === 0) {
202
+ return;
203
+ }
204
+
205
+ const langiumDocuments = this.sharedServices.workspace.LangiumDocuments;
206
+ const documentBuilder = this.sharedServices.workspace.DocumentBuilder;
207
+ const newDocs: LangiumDocument[] = [];
208
+
209
+ for (const imp of model.imports) {
210
+ if (!imp.uri) continue;
211
+
212
+ try {
213
+ const resolvedUri = await resolveImportPath(document, imp.uri);
214
+ const importedUriString = resolvedUri.toString();
215
+
216
+ // Skip if already loaded
217
+ if (this.importsLoaded.has(importedUriString)) {
218
+ continue;
219
+ }
220
+
221
+ // Load or create the imported document
222
+ const importedDoc = await langiumDocuments.getOrCreateDocument(resolvedUri);
223
+
224
+ // If document is not yet validated, add to batch for building
225
+ // This ensures all imported documents reach Validated state,
226
+ // preventing "workspace state is already Validated" errors
227
+ if (importedDoc.state < DocumentState.Validated) {
228
+ newDocs.push(importedDoc);
229
+ }
230
+ } catch {
231
+ // Import resolution failed - validation will report the error
232
+ }
233
+ }
234
+
235
+ // Build any newly discovered documents to Validated state
236
+ // This triggers indexing which will recursively load their imports
237
+ if (newDocs.length > 0) {
238
+ await documentBuilder.build(newDocs, { validation: true });
239
+ }
240
+ }
241
+
242
+ /**
243
+ * Removes a document from the import dependencies graph entirely.
244
+ * Called when a document is deleted.
245
+ */
246
+ private removeImportDependencies(uri: string): void {
247
+ // Remove as an imported document
248
+ this.importDependencies.delete(uri);
249
+
250
+ // Remove from all dependency sets (as an importer)
251
+ this.removeDocumentFromDependencies(uri);
252
+ }
253
+
254
+ /**
255
+ * Removes a document from all dependency sets.
256
+ * Called when a document's imports change or it's deleted.
257
+ */
258
+ private removeDocumentFromDependencies(uri: string): void {
259
+ for (const deps of this.importDependencies.values()) {
260
+ deps.delete(uri);
261
+ }
262
+ }
263
+
264
+ /**
265
+ * Clears all import-related caches.
266
+ * Call this when workspace configuration changes.
267
+ */
268
+ clearImportDependencies(): void {
269
+ this.importDependencies.clear();
270
+ this.documentImportSpecifiers.clear();
271
+ this.importsLoaded.clear();
272
+ }
273
+
274
+ /**
275
+ * Marks a document as needing import re-loading.
276
+ * Called when a document's content changes.
277
+ */
278
+ markForReprocessing(uri: string): void {
279
+ this.importsLoaded.delete(uri);
280
+ }
281
+
282
+ /**
283
+ * Gets all documents that import the given URI.
284
+ * Used to find documents that need rebuilding when a file changes.
285
+ *
286
+ * @param uri - The URI of the changed/deleted file
287
+ * @returns Set of URIs (as strings) of documents that import this file
288
+ */
289
+ getDependentDocuments(uri: string): Set<string> {
290
+ return this.importDependencies.get(uri) ?? new Set();
291
+ }
292
+
293
+ /**
294
+ * Gets the resolved import URIs for a document.
295
+ * Returns only URIs where import resolution succeeded (non-empty resolved URI).
296
+ *
297
+ * @param documentUri - The URI of the document
298
+ * @returns Set of resolved import URIs, or empty set if none
299
+ */
300
+ getResolvedImports(documentUri: string): Set<string> {
301
+ const specifierMap = this.documentImportSpecifiers.get(documentUri);
302
+ if (!specifierMap) {
303
+ return new Set();
304
+ }
305
+
306
+ const resolved = new Set<string>();
307
+ for (const resolvedUri of specifierMap.values()) {
308
+ // Only include successfully resolved imports (non-empty string)
309
+ if (resolvedUri) {
310
+ resolved.add(resolvedUri);
311
+ }
312
+ }
313
+ return resolved;
314
+ }
315
+
316
+ /**
317
+ * Gets all documents that would be affected by changes to the given URIs.
318
+ * This includes direct dependents and transitive dependents.
319
+ *
320
+ * @param changedUris - URIs of changed/deleted files
321
+ * @returns Set of all affected document URIs
322
+ */
323
+ getAllAffectedDocuments(changedUris: Iterable<string>): Set<string> {
324
+ const affected = new Set<string>();
325
+ const toProcess = [...changedUris];
326
+
327
+ while (toProcess.length > 0) {
328
+ const uri = toProcess.pop();
329
+ if (!uri) {
330
+ continue;
331
+ }
332
+ const dependents = this.importDependencies.get(uri);
333
+ if (dependents) {
334
+ for (const dep of dependents) {
335
+ if (!affected.has(dep)) {
336
+ affected.add(dep);
337
+ // Also check transitive dependents
338
+ toProcess.push(dep);
339
+ }
340
+ }
341
+ }
342
+ }
343
+
344
+ return affected;
345
+ }
346
+
347
+ /**
348
+ * Gets documents that have import specifiers which might be affected by file moves.
349
+ *
350
+ * When a file is moved/renamed, import specifiers that previously resolved to it
351
+ * (or could now resolve to it) need to be re-evaluated. This method finds documents
352
+ * whose imports might resolve differently after the file system change.
353
+ *
354
+ * @param changedUris - URIs of changed/deleted/created files
355
+ * @returns Set of document URIs that should be rebuilt
356
+ */
357
+ getDocumentsWithPotentiallyAffectedImports(changedUris: Iterable<string>): Set<string> {
358
+ const changedPaths = this.extractPathSegments(changedUris);
359
+ return this.findDocumentsMatchingPaths(changedPaths);
360
+ }
361
+
362
+ /**
363
+ * Extracts path segments from URIs for fuzzy matching.
364
+ */
365
+ private extractPathSegments(uris: Iterable<string>): Set<string> {
366
+ const paths = new Set<string>();
367
+
368
+ for (const uri of uris) {
369
+ this.addPathSegmentsFromUri(uri, paths);
370
+ }
371
+
372
+ return paths;
373
+ }
374
+
375
+ /**
376
+ * Adds path segments from a single URI to the set.
377
+ */
378
+ private addPathSegmentsFromUri(uri: string, paths: Set<string>): void {
379
+ try {
380
+ const url = new URL(uri);
381
+ const pathParts = url.pathname.split('/').filter(p => p.length > 0);
382
+
383
+ // Add filename
384
+ const fileName = pathParts.at(-1);
385
+ if (fileName) {
386
+ paths.add(fileName);
387
+ }
388
+
389
+ // Add parent/filename combination
390
+ if (pathParts.length >= 2) {
391
+ paths.add(pathParts.slice(-2).join('/'));
392
+ }
393
+
394
+ // Add grandparent/parent/filename combination
395
+ if (pathParts.length >= 3) {
396
+ paths.add(pathParts.slice(-3).join('/'));
397
+ }
398
+ } catch {
399
+ // Invalid URI, skip
400
+ }
401
+ }
402
+
403
+ /**
404
+ * Finds documents with import specifiers matching any of the given paths.
405
+ */
406
+ private findDocumentsMatchingPaths(changedPaths: Set<string>): Set<string> {
407
+ const affected = new Set<string>();
408
+
409
+ for (const [docUri, specifierMap] of this.documentImportSpecifiers) {
410
+ if (this.hasMatchingSpecifierOrResolvedUri(specifierMap, changedPaths)) {
411
+ affected.add(docUri);
412
+ }
413
+ }
414
+
415
+ return affected;
416
+ }
417
+
418
+ /**
419
+ * Checks if any specifier OR its resolved URI matches the changed paths.
420
+ *
421
+ * This handles both regular imports and path aliases:
422
+ * - Regular: `./domains/sales.dlang` matches path `sales.dlang`
423
+ * - Aliased: `@domains/sales.dlang` resolves to `/full/path/domains/sales.dlang`
424
+ * When the file moves, the resolved URI matches but the specifier doesn't
425
+ *
426
+ * We check both to ensure moves of aliased imports trigger revalidation.
427
+ */
428
+ private hasMatchingSpecifierOrResolvedUri(specifierMap: Map<string, string>, changedPaths: Set<string>): boolean {
429
+ for (const [specifier, resolvedUri] of specifierMap.entries()) {
430
+ const normalizedSpecifier = specifier.replace(/^[.@/]+/, '');
431
+
432
+ for (const changedPath of changedPaths) {
433
+ // Check the raw specifier (handles relative imports)
434
+ if (specifier.includes(changedPath) || changedPath.endsWith(normalizedSpecifier)) {
435
+ return true;
436
+ }
437
+
438
+ // Check the resolved URI (handles path aliases like @domains/...)
439
+ // The resolved URI contains the full file path which matches moved files
440
+ if (resolvedUri?.includes(changedPath)) {
441
+ return true;
442
+ }
443
+ }
444
+ }
445
+ return false;
446
+ }
447
+ }