@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.
- package/README.md +1 -1
- package/out/domain-lang-module.js +5 -1
- package/out/domain-lang-module.js.map +1 -1
- package/out/generated/ast.d.ts +24 -0
- package/out/generated/ast.js.map +1 -1
- package/out/generated/grammar.js +22 -32
- package/out/generated/grammar.js.map +1 -1
- package/out/index.d.ts +2 -5
- package/out/index.js +10 -6
- package/out/index.js.map +1 -1
- package/out/lsp/domain-lang-code-actions.js +14 -8
- package/out/lsp/domain-lang-code-actions.js.map +1 -1
- package/out/lsp/domain-lang-completion.d.ts +3 -0
- package/out/lsp/domain-lang-completion.js +41 -13
- package/out/lsp/domain-lang-completion.js.map +1 -1
- package/out/lsp/domain-lang-formatter.js +24 -18
- package/out/lsp/domain-lang-formatter.js.map +1 -1
- package/out/lsp/domain-lang-index-manager.d.ts +170 -0
- package/out/lsp/domain-lang-index-manager.js +389 -0
- package/out/lsp/domain-lang-index-manager.js.map +1 -0
- package/out/lsp/domain-lang-scope-provider.d.ts +67 -0
- package/out/lsp/domain-lang-scope-provider.js +95 -0
- package/out/lsp/domain-lang-scope-provider.js.map +1 -0
- package/out/lsp/domain-lang-scope.js +31 -17
- package/out/lsp/domain-lang-scope.js.map +1 -1
- package/out/lsp/domain-lang-workspace-manager.d.ts +76 -9
- package/out/lsp/domain-lang-workspace-manager.js +176 -54
- package/out/lsp/domain-lang-workspace-manager.js.map +1 -1
- package/out/lsp/hover/domain-lang-hover.d.ts +45 -1
- package/out/lsp/hover/domain-lang-hover.js +308 -232
- package/out/lsp/hover/domain-lang-hover.js.map +1 -1
- package/out/lsp/hover/domain-lang-keywords.d.ts +3 -7
- package/out/lsp/hover/domain-lang-keywords.js +115 -38
- package/out/lsp/hover/domain-lang-keywords.js.map +1 -1
- package/out/lsp/manifest-diagnostics.js +95 -50
- package/out/lsp/manifest-diagnostics.js.map +1 -1
- package/out/main.js +204 -17
- package/out/main.js.map +1 -1
- package/out/services/import-resolver.d.ts +39 -2
- package/out/services/import-resolver.js +77 -12
- package/out/services/import-resolver.js.map +1 -1
- package/out/services/types.d.ts +2 -2
- package/out/services/workspace-manager.d.ts +33 -31
- package/out/services/workspace-manager.js +92 -148
- package/out/services/workspace-manager.js.map +1 -1
- package/out/utils/document-utils.d.ts +41 -0
- package/out/utils/document-utils.js +64 -0
- package/out/utils/document-utils.js.map +1 -0
- package/out/utils/import-utils.d.ts +0 -17
- package/out/utils/import-utils.js +2 -32
- package/out/utils/import-utils.js.map +1 -1
- package/out/utils/manifest-utils.d.ts +56 -0
- package/out/utils/manifest-utils.js +119 -0
- package/out/utils/manifest-utils.js.map +1 -0
- package/out/validation/constants.d.ts +13 -0
- package/out/validation/constants.js +18 -0
- package/out/validation/constants.js.map +1 -1
- package/out/validation/import.d.ts +12 -2
- package/out/validation/import.js +95 -22
- package/out/validation/import.js.map +1 -1
- package/out/validation/maps.js +51 -2
- package/out/validation/maps.js.map +1 -1
- package/package.json +1 -1
- package/src/domain-lang-module.ts +6 -1
- package/src/domain-lang.langium +37 -13
- package/src/generated/ast.ts +24 -0
- package/src/generated/grammar.ts +22 -32
- package/src/index.ts +12 -6
- package/src/lsp/domain-lang-code-actions.ts +13 -8
- package/src/lsp/domain-lang-completion.ts +61 -13
- package/src/lsp/domain-lang-formatter.ts +28 -23
- package/src/lsp/domain-lang-index-manager.ts +447 -0
- package/src/lsp/domain-lang-scope-provider.ts +134 -0
- package/src/lsp/domain-lang-scope.ts +29 -17
- package/src/lsp/domain-lang-workspace-manager.ts +201 -53
- package/src/lsp/hover/domain-lang-hover.ts +332 -226
- package/src/lsp/hover/domain-lang-keywords.ts +129 -43
- package/src/lsp/manifest-diagnostics.ts +100 -59
- package/src/main.ts +258 -16
- package/src/services/import-resolver.ts +91 -12
- package/src/services/types.ts +2 -2
- package/src/services/workspace-manager.ts +101 -175
- package/src/utils/document-utils.ts +80 -0
- package/src/utils/import-utils.ts +2 -40
- package/src/utils/manifest-utils.ts +132 -0
- package/src/validation/constants.ts +24 -0
- package/src/validation/import.ts +107 -24
- package/src/validation/maps.ts +59 -2
- package/out/lsp/hover/ddd-pattern-explanations.d.ts +0 -50
- package/out/lsp/hover/ddd-pattern-explanations.js +0 -196
- package/out/lsp/hover/ddd-pattern-explanations.js.map +0 -1
- package/out/services/dependency-analyzer.d.ts +0 -58
- package/out/services/dependency-analyzer.js +0 -254
- package/out/services/dependency-analyzer.js.map +0 -1
- package/out/services/dependency-resolver.d.ts +0 -146
- package/out/services/dependency-resolver.js +0 -452
- package/out/services/dependency-resolver.js.map +0 -1
- package/out/services/git-url-resolver.browser.d.ts +0 -10
- package/out/services/git-url-resolver.browser.js +0 -19
- package/out/services/git-url-resolver.browser.js.map +0 -1
- package/out/services/git-url-resolver.d.ts +0 -158
- package/out/services/git-url-resolver.js +0 -416
- package/out/services/git-url-resolver.js.map +0 -1
- package/out/services/governance-validator.d.ts +0 -44
- package/out/services/governance-validator.js +0 -153
- package/out/services/governance-validator.js.map +0 -1
- package/out/services/semver.d.ts +0 -98
- package/out/services/semver.js +0 -195
- package/out/services/semver.js.map +0 -1
- package/src/lsp/hover/ddd-pattern-explanations.ts +0 -237
- package/src/services/dependency-analyzer.ts +0 -321
- package/src/services/dependency-resolver.ts +0 -551
- package/src/services/git-url-resolver.browser.ts +0 -26
- package/src/services/git-url-resolver.ts +0 -517
- package/src/services/governance-validator.ts +0 -177
- 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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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
|
+
}
|