@domainlang/language 0.5.2 → 0.6.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 (104) hide show
  1. package/out/domain-lang-module.js +3 -1
  2. package/out/domain-lang-module.js.map +1 -1
  3. package/out/generated/ast.d.ts +24 -0
  4. package/out/generated/ast.js.map +1 -1
  5. package/out/generated/grammar.js +22 -32
  6. package/out/generated/grammar.js.map +1 -1
  7. package/out/index.d.ts +2 -5
  8. package/out/index.js +10 -6
  9. package/out/index.js.map +1 -1
  10. package/out/lsp/domain-lang-code-actions.js +14 -8
  11. package/out/lsp/domain-lang-code-actions.js.map +1 -1
  12. package/out/lsp/domain-lang-completion.d.ts +3 -0
  13. package/out/lsp/domain-lang-completion.js +41 -13
  14. package/out/lsp/domain-lang-completion.js.map +1 -1
  15. package/out/lsp/domain-lang-formatter.js +24 -18
  16. package/out/lsp/domain-lang-formatter.js.map +1 -1
  17. package/out/lsp/domain-lang-index-manager.d.ts +102 -0
  18. package/out/lsp/domain-lang-index-manager.js +221 -0
  19. package/out/lsp/domain-lang-index-manager.js.map +1 -0
  20. package/out/lsp/domain-lang-scope.js +31 -17
  21. package/out/lsp/domain-lang-scope.js.map +1 -1
  22. package/out/lsp/domain-lang-workspace-manager.d.ts +51 -9
  23. package/out/lsp/domain-lang-workspace-manager.js +86 -63
  24. package/out/lsp/domain-lang-workspace-manager.js.map +1 -1
  25. package/out/lsp/hover/domain-lang-hover.d.ts +45 -1
  26. package/out/lsp/hover/domain-lang-hover.js +308 -232
  27. package/out/lsp/hover/domain-lang-hover.js.map +1 -1
  28. package/out/lsp/hover/domain-lang-keywords.d.ts +3 -7
  29. package/out/lsp/hover/domain-lang-keywords.js +115 -38
  30. package/out/lsp/hover/domain-lang-keywords.js.map +1 -1
  31. package/out/lsp/manifest-diagnostics.js +95 -50
  32. package/out/lsp/manifest-diagnostics.js.map +1 -1
  33. package/out/main.js +109 -17
  34. package/out/main.js.map +1 -1
  35. package/out/services/import-resolver.d.ts +16 -2
  36. package/out/services/import-resolver.js +37 -11
  37. package/out/services/import-resolver.js.map +1 -1
  38. package/out/services/types.d.ts +2 -2
  39. package/out/services/workspace-manager.d.ts +33 -31
  40. package/out/services/workspace-manager.js +92 -148
  41. package/out/services/workspace-manager.js.map +1 -1
  42. package/out/utils/document-utils.d.ts +41 -0
  43. package/out/utils/document-utils.js +64 -0
  44. package/out/utils/document-utils.js.map +1 -0
  45. package/out/utils/import-utils.d.ts +0 -17
  46. package/out/utils/import-utils.js +2 -32
  47. package/out/utils/import-utils.js.map +1 -1
  48. package/out/utils/manifest-utils.d.ts +56 -0
  49. package/out/utils/manifest-utils.js +119 -0
  50. package/out/utils/manifest-utils.js.map +1 -0
  51. package/out/validation/import.d.ts +1 -2
  52. package/out/validation/import.js +33 -20
  53. package/out/validation/import.js.map +1 -1
  54. package/package.json +1 -1
  55. package/src/domain-lang-module.ts +4 -1
  56. package/src/domain-lang.langium +37 -13
  57. package/src/generated/ast.ts +24 -0
  58. package/src/generated/grammar.ts +22 -32
  59. package/src/index.ts +12 -6
  60. package/src/lsp/domain-lang-code-actions.ts +13 -8
  61. package/src/lsp/domain-lang-completion.ts +61 -13
  62. package/src/lsp/domain-lang-formatter.ts +28 -23
  63. package/src/lsp/domain-lang-index-manager.ts +256 -0
  64. package/src/lsp/domain-lang-scope.ts +29 -17
  65. package/src/lsp/domain-lang-workspace-manager.ts +89 -66
  66. package/src/lsp/hover/domain-lang-hover.ts +332 -226
  67. package/src/lsp/hover/domain-lang-keywords.ts +129 -43
  68. package/src/lsp/manifest-diagnostics.ts +100 -59
  69. package/src/main.ts +127 -16
  70. package/src/services/import-resolver.ts +39 -11
  71. package/src/services/types.ts +2 -2
  72. package/src/services/workspace-manager.ts +101 -175
  73. package/src/utils/document-utils.ts +80 -0
  74. package/src/utils/import-utils.ts +2 -40
  75. package/src/utils/manifest-utils.ts +132 -0
  76. package/src/validation/import.ts +32 -22
  77. package/out/lsp/hover/ddd-pattern-explanations.d.ts +0 -50
  78. package/out/lsp/hover/ddd-pattern-explanations.js +0 -196
  79. package/out/lsp/hover/ddd-pattern-explanations.js.map +0 -1
  80. package/out/services/dependency-analyzer.d.ts +0 -58
  81. package/out/services/dependency-analyzer.js +0 -254
  82. package/out/services/dependency-analyzer.js.map +0 -1
  83. package/out/services/dependency-resolver.d.ts +0 -146
  84. package/out/services/dependency-resolver.js +0 -452
  85. package/out/services/dependency-resolver.js.map +0 -1
  86. package/out/services/git-url-resolver.browser.d.ts +0 -10
  87. package/out/services/git-url-resolver.browser.js +0 -19
  88. package/out/services/git-url-resolver.browser.js.map +0 -1
  89. package/out/services/git-url-resolver.d.ts +0 -158
  90. package/out/services/git-url-resolver.js +0 -416
  91. package/out/services/git-url-resolver.js.map +0 -1
  92. package/out/services/governance-validator.d.ts +0 -44
  93. package/out/services/governance-validator.js +0 -153
  94. package/out/services/governance-validator.js.map +0 -1
  95. package/out/services/semver.d.ts +0 -98
  96. package/out/services/semver.js +0 -195
  97. package/out/services/semver.js.map +0 -1
  98. package/src/lsp/hover/ddd-pattern-explanations.ts +0 -237
  99. package/src/services/dependency-analyzer.ts +0 -321
  100. package/src/services/dependency-resolver.ts +0 -551
  101. package/src/services/git-url-resolver.browser.ts +0 -26
  102. package/src/services/git-url-resolver.ts +0 -517
  103. package/src/services/governance-validator.ts +0 -177
  104. package/src/services/semver.ts +0 -213
@@ -0,0 +1,256 @@
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
+ * - Overrides `isAffected()` to also check this graph
21
+ * - This integrates with Langium's native `DocumentBuilder.update()` flow
22
+ *
23
+ * **Integration with Langium:**
24
+ * This approach is idiomatic because:
25
+ * 1. `updateContent()` is called for EVERY document during build
26
+ * 2. We load imports during indexing, BEFORE linking/validation
27
+ * 3. `DocumentBuilder.shouldRelink()` calls `IndexManager.isAffected()`
28
+ * 4. No need for separate lifecycle service - this IS the central place
29
+ */
30
+ export class DomainLangIndexManager extends DefaultIndexManager {
31
+ /**
32
+ * Reverse dependency graph: maps a document URI to all documents that import it.
33
+ * Key: imported document URI (string)
34
+ * Value: Set of URIs of documents that import the key document
35
+ */
36
+ private readonly importDependencies = new Map<string, Set<string>>();
37
+
38
+ /**
39
+ * Tracks documents that have had their imports loaded to avoid redundant work.
40
+ * Cleared on workspace config changes.
41
+ */
42
+ private readonly importsLoaded = new Set<string>();
43
+
44
+ /**
45
+ * Reference to shared services for accessing LangiumDocuments.
46
+ */
47
+ private readonly sharedServices: LangiumSharedCoreServices;
48
+
49
+ constructor(services: LangiumSharedCoreServices) {
50
+ super(services);
51
+ this.sharedServices = services;
52
+ }
53
+
54
+ /**
55
+ * Extends the default content update to:
56
+ * 1. Ensure all imported documents are loaded
57
+ * 2. Track import dependencies for change propagation
58
+ *
59
+ * Called by Langium during the IndexedContent build phase.
60
+ * This is BEFORE linking/validation, so imports are available for resolution.
61
+ */
62
+ override async updateContent(document: LangiumDocument, cancelToken = CancellationToken.None): Promise<void> {
63
+ // First, do the standard content indexing
64
+ await super.updateContent(document, cancelToken);
65
+
66
+ // Then, ensure imports are loaded and track dependencies
67
+ await this.ensureImportsLoaded(document);
68
+ await this.trackImportDependencies(document);
69
+ }
70
+
71
+ /**
72
+ * Extends the default remove to also clean up import dependencies.
73
+ */
74
+ override remove(uri: URI): void {
75
+ super.remove(uri);
76
+ const uriString = uri.toString();
77
+ this.removeImportDependencies(uriString);
78
+ this.importsLoaded.delete(uriString);
79
+ }
80
+
81
+ /**
82
+ * Extends the default content removal to also clean up import dependencies.
83
+ */
84
+ override removeContent(uri: URI): void {
85
+ super.removeContent(uri);
86
+ const uriString = uri.toString();
87
+ this.removeImportDependencies(uriString);
88
+ this.importsLoaded.delete(uriString);
89
+ }
90
+
91
+ /**
92
+ * Extends `isAffected` to also check import dependencies.
93
+ *
94
+ * A document is affected if:
95
+ * 1. It has cross-references to any changed document (default Langium behavior)
96
+ * 2. It imports any of the changed documents (our extension)
97
+ */
98
+ override isAffected(document: LangiumDocument, changedUris: Set<string>): boolean {
99
+ // First check Langium's default: cross-references
100
+ if (super.isAffected(document, changedUris)) {
101
+ return true;
102
+ }
103
+
104
+ // Then check our import dependencies
105
+ const docUri = document.uri.toString();
106
+ for (const changedUri of changedUris) {
107
+ const dependents = this.importDependencies.get(changedUri);
108
+ if (dependents?.has(docUri)) {
109
+ return true;
110
+ }
111
+ }
112
+
113
+ return false;
114
+ }
115
+
116
+ /**
117
+ * Tracks import dependencies for a document.
118
+ * For each import in the document, records that the imported URI is depended upon.
119
+ */
120
+ private async trackImportDependencies(document: LangiumDocument): Promise<void> {
121
+ const importingUri = document.uri.toString();
122
+
123
+ // First, remove old dependencies from this document
124
+ // (in case imports changed)
125
+ this.removeDocumentFromDependencies(importingUri);
126
+
127
+ // Skip if document isn't ready (no parse result)
128
+ if (document.state < DocumentState.Parsed) {
129
+ return;
130
+ }
131
+
132
+ const model = document.parseResult.value as unknown as Model;
133
+ if (!model.imports) {
134
+ return;
135
+ }
136
+
137
+ for (const imp of model.imports) {
138
+ if (!imp.uri) continue;
139
+
140
+ try {
141
+ const resolvedUri = await resolveImportPath(document, imp.uri);
142
+ const importedUri = resolvedUri.toString();
143
+
144
+ // Add to reverse dependency graph: importedUri → importingUri
145
+ let dependents = this.importDependencies.get(importedUri);
146
+ if (!dependents) {
147
+ dependents = new Set();
148
+ this.importDependencies.set(importedUri, dependents);
149
+ }
150
+ dependents.add(importingUri);
151
+ } catch {
152
+ // Import resolution failed - validation will report the error
153
+ }
154
+ }
155
+ }
156
+
157
+ /**
158
+ * Ensures all imported documents are loaded and available.
159
+ * This is called during indexing, BEFORE linking/validation,
160
+ * so that cross-file references can be resolved.
161
+ *
162
+ * Works for both workspace files and standalone files.
163
+ */
164
+ private async ensureImportsLoaded(document: LangiumDocument): Promise<void> {
165
+ const uriString = document.uri.toString();
166
+
167
+ // Skip if already processed (avoid redundant work and infinite loops)
168
+ if (this.importsLoaded.has(uriString)) {
169
+ return;
170
+ }
171
+ this.importsLoaded.add(uriString);
172
+
173
+ // Skip if document isn't ready (no parse result)
174
+ if (document.state < DocumentState.Parsed) {
175
+ return;
176
+ }
177
+
178
+ const model = document.parseResult.value as unknown as Model;
179
+ if (!model.imports || model.imports.length === 0) {
180
+ return;
181
+ }
182
+
183
+ const langiumDocuments = this.sharedServices.workspace.LangiumDocuments;
184
+ const documentBuilder = this.sharedServices.workspace.DocumentBuilder;
185
+ const newDocs: LangiumDocument[] = [];
186
+
187
+ for (const imp of model.imports) {
188
+ if (!imp.uri) continue;
189
+
190
+ try {
191
+ const resolvedUri = await resolveImportPath(document, imp.uri);
192
+ const importedUriString = resolvedUri.toString();
193
+
194
+ // Skip if already loaded
195
+ if (this.importsLoaded.has(importedUriString)) {
196
+ continue;
197
+ }
198
+
199
+ // Load or create the imported document
200
+ const importedDoc = await langiumDocuments.getOrCreateDocument(resolvedUri);
201
+
202
+ // If document is new (not yet indexed), add to batch
203
+ if (importedDoc.state < DocumentState.IndexedContent) {
204
+ newDocs.push(importedDoc);
205
+ }
206
+ } catch {
207
+ // Import resolution failed - validation will report the error
208
+ }
209
+ }
210
+
211
+ // Build any newly discovered documents
212
+ // This triggers indexing which will recursively load their imports
213
+ if (newDocs.length > 0) {
214
+ await documentBuilder.build(newDocs, { validation: true });
215
+ }
216
+ }
217
+
218
+ /**
219
+ * Removes a document from the import dependencies graph entirely.
220
+ * Called when a document is deleted.
221
+ */
222
+ private removeImportDependencies(uri: string): void {
223
+ // Remove as an imported document
224
+ this.importDependencies.delete(uri);
225
+
226
+ // Remove from all dependency sets (as an importer)
227
+ this.removeDocumentFromDependencies(uri);
228
+ }
229
+
230
+ /**
231
+ * Removes a document from all dependency sets.
232
+ * Called when a document's imports change or it's deleted.
233
+ */
234
+ private removeDocumentFromDependencies(uri: string): void {
235
+ for (const deps of this.importDependencies.values()) {
236
+ deps.delete(uri);
237
+ }
238
+ }
239
+
240
+ /**
241
+ * Clears all import-related caches.
242
+ * Call this when workspace configuration changes.
243
+ */
244
+ clearImportDependencies(): void {
245
+ this.importDependencies.clear();
246
+ this.importsLoaded.clear();
247
+ }
248
+
249
+ /**
250
+ * Marks a document as needing import re-loading.
251
+ * Called when a document's content changes.
252
+ */
253
+ markForReprocessing(uri: string): void {
254
+ this.importsLoaded.delete(uri);
255
+ }
256
+ }
@@ -48,22 +48,28 @@ export class DomainLangScopeComputation extends DefaultScopeComputation {
48
48
  * @returns A promise resolving to an array of AstNodeDescription
49
49
  */
50
50
  override async collectExportedSymbols(document: LangiumDocument, cancelToken = CancellationToken.None): Promise<AstNodeDescription[]> {
51
- const descr: AstNodeDescription[] = [];
52
- for (const modelNode of AstUtils.streamAllContents(document.parseResult.value)) {
53
- await interruptAndCheck(cancelToken);
54
- if (isType(modelNode)) {
55
- let name = this.nameProvider.getName(modelNode);
56
- if (!name) {
57
- // Defensive: skip unnamed types
58
- continue;
59
- }
60
- if (isNamespaceDeclaration(modelNode.$container)) {
61
- name = this.qualifiedNameProvider.getQualifiedName(modelNode.$container as NamespaceDeclaration, name);
51
+ try {
52
+ const descr: AstNodeDescription[] = [];
53
+ for (const modelNode of AstUtils.streamAllContents(document.parseResult.value)) {
54
+ await interruptAndCheck(cancelToken);
55
+ if (isType(modelNode)) {
56
+ let name = this.nameProvider.getName(modelNode);
57
+ if (!name) {
58
+ // Defensive: skip unnamed types
59
+ continue;
60
+ }
61
+ if (isNamespaceDeclaration(modelNode.$container)) {
62
+ name = this.qualifiedNameProvider.getQualifiedName(modelNode.$container, name);
63
+ }
64
+ descr.push(this.descriptions.createDescription(modelNode, name, document));
62
65
  }
63
- descr.push(this.descriptions.createDescription(modelNode, name, document));
64
66
  }
67
+ return descr;
68
+ } catch (error) {
69
+ console.error('Error in collectExportedSymbols:', error);
70
+ // Return empty array on error to prevent crash
71
+ return [];
65
72
  }
66
- return descr;
67
73
  }
68
74
 
69
75
  /**
@@ -73,10 +79,16 @@ export class DomainLangScopeComputation extends DefaultScopeComputation {
73
79
  * @returns A promise resolving to a LocalSymbols map
74
80
  */
75
81
  override async collectLocalSymbols(document: LangiumDocument, cancelToken = CancellationToken.None): Promise<LocalSymbols> {
76
- const model = document.parseResult.value as Model;
77
- const scopes = new MultiMap<AstNode, AstNodeDescription>();
78
- await this.processContainer(model, scopes, document, cancelToken);
79
- return scopes;
82
+ try {
83
+ const model = document.parseResult.value as Model;
84
+ const scopes = new MultiMap<AstNode, AstNodeDescription>();
85
+ await this.processContainer(model, scopes, document, cancelToken);
86
+ return scopes;
87
+ } catch (error) {
88
+ console.error('Error in collectLocalSymbols:', error);
89
+ // Return empty multimap on error to prevent crash
90
+ return new MultiMap<AstNode, AstNodeDescription>();
91
+ }
80
92
  }
81
93
 
82
94
  /**
@@ -1,21 +1,60 @@
1
- import path from 'node:path';
2
- import YAML from 'yaml';
3
1
  import { DefaultWorkspaceManager, URI, UriUtils, type FileSystemNode, type LangiumDocument, type LangiumSharedCoreServices, type WorkspaceFolder } from 'langium';
4
2
  import type { CancellationToken } from 'vscode-languageserver-protocol';
5
3
  import { ensureImportGraphFromDocument } from '../utils/import-utils.js';
4
+ import { findManifestsInDirectories } from '../utils/manifest-utils.js';
6
5
 
7
6
  /**
8
7
  * Langium WorkspaceManager override implementing manifest-centric import loading per PRS-010.
9
8
  *
10
- * Behavior:
11
- * - Skips pre-loading *.dlang during workspace scan (only entry graph is loaded when manifest exists).
12
- * - Mode A (with manifest): find nearest model.yaml in folder, load entry (default index.dlang) and its import graph.
13
- * - Mode B (no manifest): no pre-loading; imports resolved on-demand when a document is opened.
14
- * - Never performs network fetches; relies on cached dependencies/lock files. Missing cache produces diagnostics upstream.
9
+ * **Three Operational Modes:**
10
+ *
11
+ * **Mode A (Pure Workspace with model.yaml):**
12
+ * - model.yaml exists at workspace root
13
+ * - Loads entry file (default: index.dlang, or custom via model.entry)
14
+ * - Pre-builds entry and follows import graph
15
+ * - All imported documents built to Validated state before workspace ready
16
+ * - LSP features have immediate access to complete reference information
17
+ *
18
+ * **Mode B (Pure Standalone files):**
19
+ * - No model.yaml anywhere in workspace
20
+ * - No pre-loading of .dlang files during workspace scan
21
+ * - Documents loaded on-demand when user opens them
22
+ * - Imports resolved lazily via ImportResolver
23
+ * - Each document built individually when opened
24
+ * - Works with relative imports only (no path aliases or external deps)
25
+ *
26
+ * **Mode C (Mixed - Standalone + Module folders):**
27
+ * - Workspace contains both standalone .dlang files AND folders with model.yaml
28
+ * - Each model.yaml folder treated as a module/package:
29
+ * - Module entry + import graph pre-loaded
30
+ * - Path aliases and external deps work within module
31
+ * - Standalone files outside modules loaded on-demand
32
+ * - Example structure:
33
+ * ```
34
+ * workspace/
35
+ * ├── standalone.dlang ← Mode B (on-demand)
36
+ * ├── core/
37
+ * │ ├── model.yaml ← Module root
38
+ * │ ├── index.dlang ← Pre-loaded
39
+ * │ └── domains/
40
+ * │ └── sales.dlang ← Pre-loaded via imports
41
+ * └── util.dlang ← Mode B (on-demand)
42
+ * ```
43
+ *
44
+ * **Performance Characteristics:**
45
+ * - Mode A/C modules: Slower initial load, instant LSP features afterward
46
+ * - Mode B/C standalone: Instant workspace init, per-file build on open
47
+ * - All modes cache import resolution for subsequent access
48
+ *
49
+ * **Never performs network fetches** - relies on cached dependencies/lock files.
50
+ * Missing cache produces diagnostics upstream via ImportValidator.
15
51
  */
16
52
  export class DomainLangWorkspaceManager extends DefaultWorkspaceManager {
53
+ private readonly sharedServices: LangiumSharedCoreServices;
54
+
17
55
  constructor(services: LangiumSharedCoreServices) {
18
56
  super(services);
57
+ this.sharedServices = services;
19
58
  }
20
59
 
21
60
  override shouldIncludeEntry(entry: FileSystemNode): boolean {
@@ -32,73 +71,57 @@ export class DomainLangWorkspaceManager extends DefaultWorkspaceManager {
32
71
  }
33
72
 
34
73
  protected override async loadAdditionalDocuments(folders: WorkspaceFolder[], collector: (document: LangiumDocument) => void): Promise<void> {
35
- const manifestInfo = await this.findManifestInFolders(folders);
36
- if (!manifestInfo) {
37
- return; // Mode B: no manifest
38
- }
39
-
40
- const entryUri = URI.file(manifestInfo.entryPath);
41
- const entryDoc = await this.langiumDocuments.getOrCreateDocument(entryUri);
42
- collector(entryDoc);
43
-
44
- const uris = await ensureImportGraphFromDocument(entryDoc, this.langiumDocuments);
45
- for (const uriString of uris) {
46
- const uri = URI.parse(uriString);
47
- const doc = await this.langiumDocuments.getOrCreateDocument(uri);
48
- collector(doc);
49
- }
50
- }
51
-
52
- private async findManifestInFolders(folders: WorkspaceFolder[]): Promise<{ manifestPath: string; entryPath: string } | undefined> {
53
- for (const folder of folders) {
54
- const manifestPath = await this.findNearestManifest(folder.uri);
55
- if (manifestPath) {
56
- const entry = await this.readEntryFromManifest(manifestPath) ?? 'index.dlang';
57
- const entryPath = path.resolve(path.dirname(manifestPath), entry);
58
- return { manifestPath, entryPath };
59
- }
74
+ // Find ALL model.yaml files in workspace (supports mixed mode)
75
+ const manifestInfos = await this.findAllManifestsInFolders(folders);
76
+
77
+ if (manifestInfos.length === 0) {
78
+ return; // Pure Mode B: no manifests, all files loaded on-demand
60
79
  }
61
- return undefined;
62
- }
63
80
 
64
- private async findNearestManifest(startUri: string): Promise<string | undefined> {
65
- let current = path.resolve(URI.parse(startUri).fsPath);
66
- const { root } = path.parse(current);
81
+ // Mode A or Mode C: Load each module's entry + import graph
82
+ for (const manifestInfo of manifestInfos) {
83
+ try {
84
+ const entryUri = URI.file(manifestInfo.entryPath);
85
+ const entryDoc = await this.langiumDocuments.getOrCreateDocument(entryUri);
86
+ collector(entryDoc);
67
87
 
68
- while (true) {
69
- const candidate = path.join(current, 'model.yaml');
70
- if (await this.pathExists(candidate)) {
71
- return candidate;
72
- }
88
+ // Build entry document first to ensure it's ready for import resolution
89
+ await this.sharedServices.workspace.DocumentBuilder.build([entryDoc], {
90
+ validation: true
91
+ });
73
92
 
74
- if (current === root) {
75
- return undefined;
76
- }
93
+ const uris = await ensureImportGraphFromDocument(entryDoc, this.langiumDocuments);
94
+ const importedDocs: LangiumDocument[] = [];
95
+ for (const uriString of uris) {
96
+ const uri = URI.parse(uriString);
97
+ const doc = await this.langiumDocuments.getOrCreateDocument(uri);
98
+ collector(doc);
99
+ importedDocs.push(doc);
100
+ }
77
101
 
78
- const parent = path.dirname(current);
79
- if (parent === current) {
80
- return undefined;
102
+ // Build all imported documents in batch for performance
103
+ if (importedDocs.length > 0) {
104
+ await this.sharedServices.workspace.DocumentBuilder.build(importedDocs, {
105
+ validation: true
106
+ });
107
+ }
108
+ } catch (error) {
109
+ const message = error instanceof Error ? error.message : String(error);
110
+ console.error(`Failed to load import graph from ${manifestInfo.manifestPath}: ${message}`);
111
+ // Continue with other modules - partial failure is acceptable
81
112
  }
82
- current = parent;
83
- }
84
- }
85
-
86
- private async readEntryFromManifest(manifestPath: string): Promise<string | undefined> {
87
- try {
88
- const content = await this.fileSystemProvider.readFile(URI.file(manifestPath));
89
- const manifest = (YAML.parse(content) ?? {}) as { model?: { entry?: string } };
90
- return manifest.model?.entry;
91
- } catch {
92
- return undefined;
93
113
  }
94
114
  }
95
115
 
96
- private async pathExists(target: string): Promise<boolean> {
97
- try {
98
- await this.fileSystemProvider.stat(URI.file(target));
99
- return true;
100
- } catch {
101
- return false;
102
- }
116
+ /**
117
+ * Finds ALL model.yaml files in the workspace.
118
+ * Delegates to shared manifest utilities.
119
+ *
120
+ * @param folders - Workspace folders to search
121
+ * @returns Array of manifest info (one per model.yaml found)
122
+ */
123
+ private async findAllManifestsInFolders(folders: WorkspaceFolder[]): Promise<Array<{ manifestPath: string; entryPath: string }>> {
124
+ const directories = folders.map(f => URI.parse(f.uri).fsPath);
125
+ return findManifestsInDirectories(directories);
103
126
  }
104
127
  }