@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.
- package/README.md +1 -1
- package/out/domain-lang-module.d.ts +2 -0
- package/out/domain-lang-module.js +23 -2
- package/out/domain-lang-module.js.map +1 -1
- package/out/lsp/domain-lang-completion.d.ts +142 -1
- package/out/lsp/domain-lang-completion.js +620 -22
- package/out/lsp/domain-lang-completion.js.map +1 -1
- package/out/lsp/domain-lang-document-symbol-provider.d.ts +79 -0
- package/out/lsp/domain-lang-document-symbol-provider.js +210 -0
- package/out/lsp/domain-lang-document-symbol-provider.js.map +1 -0
- package/out/lsp/domain-lang-index-manager.d.ts +98 -1
- package/out/lsp/domain-lang-index-manager.js +214 -7
- package/out/lsp/domain-lang-index-manager.js.map +1 -1
- package/out/lsp/domain-lang-node-kind-provider.d.ts +27 -0
- package/out/lsp/domain-lang-node-kind-provider.js +87 -0
- package/out/lsp/domain-lang-node-kind-provider.js.map +1 -0
- package/out/lsp/domain-lang-scope-provider.d.ts +100 -0
- package/out/lsp/domain-lang-scope-provider.js +170 -0
- package/out/lsp/domain-lang-scope-provider.js.map +1 -0
- package/out/lsp/domain-lang-workspace-manager.d.ts +46 -0
- package/out/lsp/domain-lang-workspace-manager.js +148 -4
- package/out/lsp/domain-lang-workspace-manager.js.map +1 -1
- package/out/lsp/hover/domain-lang-hover.d.ts +16 -6
- package/out/lsp/hover/domain-lang-hover.js +160 -134
- package/out/lsp/hover/domain-lang-hover.js.map +1 -1
- package/out/lsp/hover/hover-builders.d.ts +57 -0
- package/out/lsp/hover/hover-builders.js +171 -0
- package/out/lsp/hover/hover-builders.js.map +1 -0
- package/out/main.js +116 -20
- package/out/main.js.map +1 -1
- package/out/sdk/index.d.ts +2 -1
- package/out/sdk/index.js +1 -1
- package/out/sdk/index.js.map +1 -1
- package/out/sdk/loader-node.js +1 -1
- package/out/sdk/loader-node.js.map +1 -1
- package/out/sdk/loader.d.ts +55 -2
- package/out/sdk/loader.js +87 -28
- package/out/sdk/loader.js.map +1 -1
- package/out/sdk/query.js +14 -11
- package/out/sdk/query.js.map +1 -1
- package/out/services/import-resolver.d.ts +29 -6
- package/out/services/import-resolver.js +48 -9
- package/out/services/import-resolver.js.map +1 -1
- package/out/services/package-boundary-detector.d.ts +101 -0
- package/out/services/package-boundary-detector.js +211 -0
- package/out/services/package-boundary-detector.js.map +1 -0
- package/out/services/performance-optimizer.js +6 -2
- package/out/services/performance-optimizer.js.map +1 -1
- package/out/services/types.d.ts +24 -0
- package/out/services/types.js.map +1 -1
- package/out/services/workspace-manager.d.ts +73 -6
- package/out/services/workspace-manager.js +210 -57
- package/out/services/workspace-manager.js.map +1 -1
- package/out/utils/import-utils.d.ts +9 -6
- package/out/utils/import-utils.js +26 -15
- package/out/utils/import-utils.js.map +1 -1
- package/out/validation/constants.d.ts +20 -0
- package/out/validation/constants.js +39 -3
- package/out/validation/constants.js.map +1 -1
- package/out/validation/import.d.ts +22 -1
- package/out/validation/import.js +104 -16
- package/out/validation/import.js.map +1 -1
- package/out/validation/maps.js +101 -3
- package/out/validation/maps.js.map +1 -1
- package/package.json +5 -5
- package/src/domain-lang-module.ts +26 -3
- package/src/lsp/domain-lang-completion.ts +736 -27
- package/src/lsp/domain-lang-document-symbol-provider.ts +254 -0
- package/src/lsp/domain-lang-index-manager.ts +250 -7
- package/src/lsp/domain-lang-node-kind-provider.ts +119 -0
- package/src/lsp/domain-lang-scope-provider.ts +250 -0
- package/src/lsp/domain-lang-workspace-manager.ts +187 -4
- package/src/lsp/hover/domain-lang-hover.ts +189 -131
- package/src/lsp/hover/hover-builders.ts +208 -0
- package/src/main.ts +156 -23
- package/src/sdk/index.ts +2 -1
- package/src/sdk/loader-node.ts +2 -1
- package/src/sdk/loader.ts +125 -34
- package/src/sdk/query.ts +15 -11
- package/src/services/import-resolver.ts +60 -9
- package/src/services/package-boundary-detector.ts +238 -0
- package/src/services/performance-optimizer.ts +6 -2
- package/src/services/types.ts +25 -0
- package/src/services/workspace-manager.ts +259 -62
- package/src/utils/import-utils.ts +27 -15
- package/src/validation/constants.ts +47 -6
- package/src/validation/import.ts +124 -16
- 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
|
|
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
|
|
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 -
|
|
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
|
|
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
|
|
203
|
-
|
|
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
|
}
|