@domainlang/language 0.6.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (88) hide show
  1. package/README.md +1 -1
  2. package/out/domain-lang-module.d.ts +2 -0
  3. package/out/domain-lang-module.js +23 -2
  4. package/out/domain-lang-module.js.map +1 -1
  5. package/out/lsp/domain-lang-completion.d.ts +142 -1
  6. package/out/lsp/domain-lang-completion.js +620 -22
  7. package/out/lsp/domain-lang-completion.js.map +1 -1
  8. package/out/lsp/domain-lang-document-symbol-provider.d.ts +79 -0
  9. package/out/lsp/domain-lang-document-symbol-provider.js +210 -0
  10. package/out/lsp/domain-lang-document-symbol-provider.js.map +1 -0
  11. package/out/lsp/domain-lang-index-manager.d.ts +98 -1
  12. package/out/lsp/domain-lang-index-manager.js +214 -7
  13. package/out/lsp/domain-lang-index-manager.js.map +1 -1
  14. package/out/lsp/domain-lang-node-kind-provider.d.ts +27 -0
  15. package/out/lsp/domain-lang-node-kind-provider.js +87 -0
  16. package/out/lsp/domain-lang-node-kind-provider.js.map +1 -0
  17. package/out/lsp/domain-lang-scope-provider.d.ts +100 -0
  18. package/out/lsp/domain-lang-scope-provider.js +170 -0
  19. package/out/lsp/domain-lang-scope-provider.js.map +1 -0
  20. package/out/lsp/domain-lang-workspace-manager.d.ts +46 -0
  21. package/out/lsp/domain-lang-workspace-manager.js +148 -4
  22. package/out/lsp/domain-lang-workspace-manager.js.map +1 -1
  23. package/out/lsp/hover/domain-lang-hover.d.ts +16 -6
  24. package/out/lsp/hover/domain-lang-hover.js +160 -134
  25. package/out/lsp/hover/domain-lang-hover.js.map +1 -1
  26. package/out/lsp/hover/hover-builders.d.ts +57 -0
  27. package/out/lsp/hover/hover-builders.js +171 -0
  28. package/out/lsp/hover/hover-builders.js.map +1 -0
  29. package/out/main.js +116 -20
  30. package/out/main.js.map +1 -1
  31. package/out/sdk/index.d.ts +2 -1
  32. package/out/sdk/index.js +1 -1
  33. package/out/sdk/index.js.map +1 -1
  34. package/out/sdk/loader-node.js +1 -1
  35. package/out/sdk/loader-node.js.map +1 -1
  36. package/out/sdk/loader.d.ts +55 -2
  37. package/out/sdk/loader.js +87 -28
  38. package/out/sdk/loader.js.map +1 -1
  39. package/out/sdk/query.js +14 -11
  40. package/out/sdk/query.js.map +1 -1
  41. package/out/services/import-resolver.d.ts +29 -6
  42. package/out/services/import-resolver.js +48 -9
  43. package/out/services/import-resolver.js.map +1 -1
  44. package/out/services/package-boundary-detector.d.ts +101 -0
  45. package/out/services/package-boundary-detector.js +211 -0
  46. package/out/services/package-boundary-detector.js.map +1 -0
  47. package/out/services/performance-optimizer.js +6 -2
  48. package/out/services/performance-optimizer.js.map +1 -1
  49. package/out/services/types.d.ts +24 -0
  50. package/out/services/types.js.map +1 -1
  51. package/out/services/workspace-manager.d.ts +73 -6
  52. package/out/services/workspace-manager.js +210 -57
  53. package/out/services/workspace-manager.js.map +1 -1
  54. package/out/utils/import-utils.d.ts +9 -6
  55. package/out/utils/import-utils.js +26 -15
  56. package/out/utils/import-utils.js.map +1 -1
  57. package/out/validation/constants.d.ts +20 -0
  58. package/out/validation/constants.js +39 -3
  59. package/out/validation/constants.js.map +1 -1
  60. package/out/validation/import.d.ts +22 -1
  61. package/out/validation/import.js +104 -16
  62. package/out/validation/import.js.map +1 -1
  63. package/out/validation/maps.js +101 -3
  64. package/out/validation/maps.js.map +1 -1
  65. package/package.json +5 -5
  66. package/src/domain-lang-module.ts +26 -3
  67. package/src/lsp/domain-lang-completion.ts +736 -27
  68. package/src/lsp/domain-lang-document-symbol-provider.ts +254 -0
  69. package/src/lsp/domain-lang-index-manager.ts +250 -7
  70. package/src/lsp/domain-lang-node-kind-provider.ts +119 -0
  71. package/src/lsp/domain-lang-scope-provider.ts +250 -0
  72. package/src/lsp/domain-lang-workspace-manager.ts +187 -4
  73. package/src/lsp/hover/domain-lang-hover.ts +189 -131
  74. package/src/lsp/hover/hover-builders.ts +208 -0
  75. package/src/main.ts +156 -23
  76. package/src/sdk/index.ts +2 -1
  77. package/src/sdk/loader-node.ts +2 -1
  78. package/src/sdk/loader.ts +125 -34
  79. package/src/sdk/query.ts +15 -11
  80. package/src/services/import-resolver.ts +60 -9
  81. package/src/services/package-boundary-detector.ts +238 -0
  82. package/src/services/performance-optimizer.ts +6 -2
  83. package/src/services/types.ts +25 -0
  84. package/src/services/workspace-manager.ts +259 -62
  85. package/src/utils/import-utils.ts +27 -15
  86. package/src/validation/constants.ts +47 -6
  87. package/src/validation/import.ts +124 -16
  88. package/src/validation/maps.ts +118 -4
@@ -0,0 +1,119 @@
1
+ /**
2
+ * Custom NodeKindProvider — maps DomainLang AST types to VS Code SymbolKinds.
3
+ *
4
+ * Langium's DefaultNodeKindProvider returns `SymbolKind.Field` for everything.
5
+ * This override provides semantically meaningful icons for the Outline view,
6
+ * breadcrumbs, Go to Symbol, and completion items.
7
+ *
8
+ * @module lsp/domain-lang-node-kind-provider
9
+ */
10
+
11
+ import { DefaultNodeKindProvider } from 'langium/lsp';
12
+ import { CompletionItemKind, SymbolKind } from 'vscode-languageserver';
13
+ import type { AstNode, AstNodeDescription } from 'langium';
14
+ import {
15
+ isDomain,
16
+ isBoundedContext,
17
+ isTeam,
18
+ isClassification,
19
+ isMetadata,
20
+ isContextMap,
21
+ isDomainMap,
22
+ isNamespaceDeclaration,
23
+ isRelationship,
24
+ isDomainTerm,
25
+ isDecision,
26
+ isPolicy,
27
+ isBusinessRule,
28
+ isMetadataEntry,
29
+ } from '../generated/ast.js';
30
+
31
+ /**
32
+ * AST type to icon kind mapping table.
33
+ */
34
+ type KindMapping = readonly [
35
+ guard: (node: AstNode) => boolean,
36
+ symbolKind: SymbolKind,
37
+ completionKind: CompletionItemKind
38
+ ];
39
+
40
+ const KIND_MAPPINGS: readonly KindMapping[] = [
41
+ // Strategic design
42
+ [isDomain, SymbolKind.Namespace, CompletionItemKind.Folder],
43
+ [isBoundedContext, SymbolKind.Package, CompletionItemKind.Module],
44
+
45
+ // Tactical design
46
+ [isTeam, SymbolKind.Interface, CompletionItemKind.Interface],
47
+ [isClassification, SymbolKind.Enum, CompletionItemKind.Enum],
48
+ [isMetadata, SymbolKind.Enum, CompletionItemKind.Enum],
49
+
50
+ // Architecture mapping
51
+ [isContextMap, SymbolKind.Package, CompletionItemKind.Module],
52
+ [isDomainMap, SymbolKind.Package, CompletionItemKind.Module],
53
+
54
+ // Module system
55
+ [isNamespaceDeclaration, SymbolKind.Namespace, CompletionItemKind.Module],
56
+
57
+ // Relationships
58
+ [isRelationship, SymbolKind.Interface, CompletionItemKind.Interface],
59
+
60
+ // Documentation & governance
61
+ [isDomainTerm, SymbolKind.Field, CompletionItemKind.Field],
62
+ [isDecision, SymbolKind.Field, CompletionItemKind.Field],
63
+ [isPolicy, SymbolKind.Field, CompletionItemKind.Field],
64
+ [isBusinessRule, SymbolKind.Field, CompletionItemKind.Field],
65
+
66
+ // Metadata entries
67
+ [isMetadataEntry, SymbolKind.Field, CompletionItemKind.Field],
68
+ ] as const;
69
+
70
+ /**
71
+ * Maps DomainLang AST types to semantically appropriate SymbolKind values.
72
+ *
73
+ * Used by the DocumentSymbolProvider (outline/breadcrumbs), WorkspaceSymbolProvider,
74
+ * and the CompletionProvider.
75
+ */
76
+ export class DomainLangNodeKindProvider extends DefaultNodeKindProvider {
77
+
78
+ override getSymbolKind(node: AstNode | AstNodeDescription): SymbolKind {
79
+ try {
80
+ const astNode = this.resolveNode(node);
81
+ if (!astNode) return super.getSymbolKind(node);
82
+
83
+ for (const [guard, symbolKind] of KIND_MAPPINGS) {
84
+ if (guard(astNode)) return symbolKind;
85
+ }
86
+ return super.getSymbolKind(node);
87
+ } catch (error) {
88
+ console.error('Error in getSymbolKind:', error);
89
+ return super.getSymbolKind(node);
90
+ }
91
+ }
92
+
93
+ override getCompletionItemKind(node: AstNode | AstNodeDescription): CompletionItemKind {
94
+ try {
95
+ const astNode = this.resolveNode(node);
96
+ if (!astNode) return super.getCompletionItemKind(node);
97
+
98
+ for (const [guard, , completionKind] of KIND_MAPPINGS) {
99
+ if (guard(astNode)) return completionKind;
100
+ }
101
+ return super.getCompletionItemKind(node);
102
+ } catch (error) {
103
+ console.error('Error in getCompletionItemKind:', error);
104
+ return super.getCompletionItemKind(node);
105
+ }
106
+ }
107
+
108
+ /**
109
+ * Resolves an AstNode from an AstNodeDescription (which may only have a reference).
110
+ * Returns the node directly if it's already an AstNode.
111
+ */
112
+ private resolveNode(node: AstNode | AstNodeDescription): AstNode | undefined {
113
+ if ('$type' in node) {
114
+ return node;
115
+ }
116
+ // AstNodeDescription — resolve if possible
117
+ return node.node;
118
+ }
119
+ }
@@ -0,0 +1,250 @@
1
+ /**
2
+ * DomainLang Scope Provider
3
+ *
4
+ * Implements import-based scoping with alias support and package-boundary transitive imports.
5
+ *
6
+ * **Key Concepts (per ADR-003):**
7
+ * - Elements are only visible if defined in current document OR explicitly imported
8
+ * - Import aliases control visibility: `import "pkg" as ddd` makes types visible as `ddd.*` only
9
+ * - Package-boundary transitive imports: External packages (.dlang/packages/) can re-export
10
+ * - Local file imports remain non-transitive (explicit dependencies only)
11
+ *
12
+ * **Why this matters:**
13
+ * Without this, Langium's DefaultScopeProvider would make ALL indexed documents visible
14
+ * in the global scope, which would:
15
+ * 1. Allow referencing elements that haven't been imported
16
+ * 2. Make the import system meaningless
17
+ * 3. Create confusion about dependencies between files
18
+ *
19
+ * @see https://langium.org/docs/recipes/scoping/ for Langium scoping patterns
20
+ * @see ADR-003 for alias and package-boundary design decisions
21
+ */
22
+
23
+ import type {
24
+ AstNodeDescription,
25
+ LangiumDocument,
26
+ ReferenceInfo,
27
+ Scope,
28
+ Stream
29
+ } from 'langium';
30
+ import {
31
+ AstUtils,
32
+ DefaultScopeProvider,
33
+ EMPTY_SCOPE,
34
+ MapScope,
35
+ stream
36
+ } from 'langium';
37
+ import type { DomainLangServices } from '../domain-lang-module.js';
38
+ import type { DomainLangIndexManager } from './domain-lang-index-manager.js';
39
+ import type { PackageBoundaryDetector } from '../services/package-boundary-detector.js';
40
+ import type { ImportInfo } from '../services/types.js';
41
+
42
+ /**
43
+ * Custom scope provider that restricts cross-file references to imported documents only.
44
+ *
45
+ * Extends Langium's DefaultScopeProvider to override the global scope computation.
46
+ */
47
+ export class DomainLangScopeProvider extends DefaultScopeProvider {
48
+ /**
49
+ * Reference to IndexManager for getting resolved imports with aliases.
50
+ */
51
+ private readonly domainLangIndexManager: DomainLangIndexManager;
52
+
53
+ /**
54
+ * Detects package boundaries for transitive import resolution.
55
+ */
56
+ private readonly packageBoundaryDetector: PackageBoundaryDetector;
57
+
58
+ constructor(services: DomainLangServices) {
59
+ super(services);
60
+ this.domainLangIndexManager = services.shared.workspace.IndexManager as DomainLangIndexManager;
61
+ this.packageBoundaryDetector = services.imports.PackageBoundaryDetector;
62
+ }
63
+
64
+ /**
65
+ * Override getGlobalScope to implement alias-scoped and package-boundary transitive imports.
66
+ *
67
+ * The default Langium behavior includes ALL documents in the workspace.
68
+ * We restrict and transform scope to:
69
+ * 1. The current document's own exported symbols
70
+ * 2. Symbols from directly imported documents (with alias prefixing)
71
+ * 3. Symbols from package-boundary transitive imports (external packages only)
72
+ *
73
+ * @param referenceType - The AST type being referenced
74
+ * @param context - Information about the reference
75
+ * @returns A scope containing only visible elements
76
+ */
77
+ protected override getGlobalScope(referenceType: string, context: ReferenceInfo): Scope {
78
+ try {
79
+ const document = AstUtils.getDocument(context.container);
80
+ if (!document) {
81
+ return EMPTY_SCOPE;
82
+ }
83
+
84
+ const descriptions = this.computeVisibleDescriptions(referenceType, document);
85
+ return new MapScope(descriptions);
86
+ } catch (error) {
87
+ console.error('Error in getGlobalScope:', error);
88
+ return EMPTY_SCOPE;
89
+ }
90
+ }
91
+
92
+ /**
93
+ * Computes all visible descriptions for a document, including:
94
+ * - Current document's own symbols
95
+ * - Direct imports (with alias prefixing)
96
+ * - Package-boundary transitive imports
97
+ *
98
+ * @param referenceType - The AST type being referenced
99
+ * @param document - The document making the reference
100
+ * @returns Stream of visible descriptions
101
+ */
102
+ private computeVisibleDescriptions(
103
+ referenceType: string,
104
+ document: LangiumDocument
105
+ ): Stream<AstNodeDescription> {
106
+ const docUri = document.uri.toString();
107
+ const allVisibleDescriptions: AstNodeDescription[] = [];
108
+
109
+ // 1. Always include current document's own symbols
110
+ const ownDescriptions = this.indexManager.allElements(referenceType)
111
+ .filter(desc => desc.documentUri.toString() === docUri);
112
+ allVisibleDescriptions.push(...ownDescriptions.toArray());
113
+
114
+ // 2. Get import info (with aliases)
115
+ const importInfo = this.domainLangIndexManager.getImportInfo(docUri);
116
+
117
+ // Track which documents we've already included to avoid duplicates
118
+ const processedUris = new Set<string>([docUri]);
119
+
120
+ // 3. Process each direct import
121
+ for (const imp of importInfo) {
122
+ if (!imp.resolvedUri || processedUris.has(imp.resolvedUri)) {
123
+ continue;
124
+ }
125
+
126
+ // Add descriptions from the directly imported document
127
+ this.addDescriptionsFromImport(
128
+ imp,
129
+ referenceType,
130
+ processedUris,
131
+ allVisibleDescriptions
132
+ );
133
+
134
+ // 4. Check for package-boundary transitive imports
135
+ this.addPackageBoundaryTransitiveImports(
136
+ imp,
137
+ referenceType,
138
+ document,
139
+ processedUris,
140
+ allVisibleDescriptions
141
+ );
142
+ }
143
+
144
+ return stream(allVisibleDescriptions);
145
+ }
146
+
147
+ /**
148
+ * Adds descriptions from a single import, applying alias prefixing if needed.
149
+ *
150
+ * @param imp - Import information (specifier, alias, resolved URI)
151
+ * @param referenceType - The AST type being referenced
152
+ * @param processedUris - Set of already-processed URIs to avoid duplicates
153
+ * @param output - Array to append visible descriptions to
154
+ */
155
+ private addDescriptionsFromImport(
156
+ imp: ImportInfo,
157
+ referenceType: string,
158
+ processedUris: Set<string>,
159
+ output: AstNodeDescription[]
160
+ ): void {
161
+ const descriptions = this.indexManager.allElements(referenceType)
162
+ .filter(desc => desc.documentUri.toString() === imp.resolvedUri);
163
+
164
+ if (imp.alias) {
165
+ // With alias: prefix all names with alias
166
+ // Example: CoreDomain → ddd.CoreDomain
167
+ for (const desc of descriptions) {
168
+ output.push(this.createAliasedDescription(desc, imp.alias));
169
+ }
170
+ } else {
171
+ // Without alias: use original names
172
+ output.push(...descriptions.toArray());
173
+ }
174
+
175
+ processedUris.add(imp.resolvedUri);
176
+ }
177
+
178
+ /**
179
+ * Adds package-boundary transitive imports for external packages.
180
+ *
181
+ * When document A imports package document B (e.g., index.dlang),
182
+ * and B imports internal package files C, D, etc. (same package root),
183
+ * then A can see types from C, D, etc. (package re-exports).
184
+ *
185
+ * Local file imports remain non-transitive.
186
+ *
187
+ * @param imp - Import information for the direct import
188
+ * @param referenceType - The AST type being referenced
189
+ * @param currentDocument - The document making the reference
190
+ * @param processedUris - Set of already-processed URIs to avoid duplicates
191
+ * @param output - Array to append visible descriptions to
192
+ */
193
+ private addPackageBoundaryTransitiveImports(
194
+ imp: ImportInfo,
195
+ referenceType: string,
196
+ currentDocument: LangiumDocument,
197
+ processedUris: Set<string>,
198
+ output: AstNodeDescription[]
199
+ ): void {
200
+ // Get the imports of the imported document (B's imports)
201
+ const transitiveImports = this.domainLangIndexManager.getImportInfo(imp.resolvedUri);
202
+
203
+ for (const transitiveImp of transitiveImports) {
204
+ if (!transitiveImp.resolvedUri || processedUris.has(transitiveImp.resolvedUri)) {
205
+ continue;
206
+ }
207
+
208
+ // Check if both documents are in the same external package
209
+ // (package boundary = same commit directory within .dlang/packages/)
210
+ const samePackage = this.packageBoundaryDetector.areInSamePackageSync(
211
+ imp.resolvedUri,
212
+ transitiveImp.resolvedUri
213
+ );
214
+
215
+ if (samePackage) {
216
+ // Within package boundary: include transitive imports
217
+ // Apply the top-level import's alias (if any)
218
+ this.addDescriptionsFromImport(
219
+ {
220
+ specifier: transitiveImp.specifier,
221
+ alias: imp.alias, // Use the top-level import's alias
222
+ resolvedUri: transitiveImp.resolvedUri
223
+ },
224
+ referenceType,
225
+ processedUris,
226
+ output
227
+ );
228
+ }
229
+ }
230
+ }
231
+
232
+ /**
233
+ * Creates an alias-prefixed version of a description.
234
+ *
235
+ * Example: CoreDomain with alias "ddd" → ddd.CoreDomain
236
+ *
237
+ * @param original - Original description
238
+ * @param alias - Import alias to prefix with
239
+ * @returns New description with prefixed name
240
+ */
241
+ private createAliasedDescription(
242
+ original: AstNodeDescription,
243
+ alias: string
244
+ ): AstNodeDescription {
245
+ return {
246
+ ...original,
247
+ name: `${alias}.${original.name}`
248
+ };
249
+ }
250
+ }
@@ -1,7 +1,11 @@
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
1
3
  import { DefaultWorkspaceManager, URI, UriUtils, type FileSystemNode, type LangiumDocument, type LangiumSharedCoreServices, type WorkspaceFolder } from 'langium';
2
4
  import type { CancellationToken } from 'vscode-languageserver-protocol';
3
5
  import { ensureImportGraphFromDocument } from '../utils/import-utils.js';
4
6
  import { findManifestsInDirectories } from '../utils/manifest-utils.js';
7
+ import type { ImportResolver } from '../services/import-resolver.js';
8
+ import type { DomainLangServices } from '../domain-lang-module.js';
5
9
 
6
10
  /**
7
11
  * Langium WorkspaceManager override implementing manifest-centric import loading per PRS-010.
@@ -52,11 +56,26 @@ import { findManifestsInDirectories } from '../utils/manifest-utils.js';
52
56
  export class DomainLangWorkspaceManager extends DefaultWorkspaceManager {
53
57
  private readonly sharedServices: LangiumSharedCoreServices;
54
58
 
59
+ /**
60
+ * DI-injected import resolver. Set via late-binding because
61
+ * WorkspaceManager (shared module) is created before ImportResolver (language module).
62
+ * Falls back to standalone ensureImportGraphFromDocument when not set.
63
+ */
64
+ private importResolver: ImportResolver | undefined;
65
+
55
66
  constructor(services: LangiumSharedCoreServices) {
56
67
  super(services);
57
68
  this.sharedServices = services;
58
69
  }
59
70
 
71
+ /**
72
+ * Late-binds the language-specific services after DI initialization.
73
+ * Called from `createDomainLangServices()` after the language module is created.
74
+ */
75
+ setLanguageServices(services: DomainLangServices): void {
76
+ this.importResolver = services.imports.ImportResolver;
77
+ }
78
+
60
79
  override shouldIncludeEntry(entry: FileSystemNode): boolean {
61
80
  // Prevent auto-including .dlang files; we'll load via entry/import graph
62
81
  const name = UriUtils.basename(entry.uri);
@@ -74,9 +93,10 @@ export class DomainLangWorkspaceManager extends DefaultWorkspaceManager {
74
93
  // Find ALL model.yaml files in workspace (supports mixed mode)
75
94
  const manifestInfos = await this.findAllManifestsInFolders(folders);
76
95
 
77
- if (manifestInfos.length === 0) {
78
- return; // Pure Mode B: no manifests, all files loaded on-demand
79
- }
96
+ // Track directories covered by manifests to avoid loading their files as standalone
97
+ const moduleDirectories = new Set(
98
+ manifestInfos.map(m => path.dirname(m.manifestPath))
99
+ );
80
100
 
81
101
  // Mode A or Mode C: Load each module's entry + import graph
82
102
  for (const manifestInfo of manifestInfos) {
@@ -90,7 +110,7 @@ export class DomainLangWorkspaceManager extends DefaultWorkspaceManager {
90
110
  validation: true
91
111
  });
92
112
 
93
- const uris = await ensureImportGraphFromDocument(entryDoc, this.langiumDocuments);
113
+ const uris = await this.loadImportGraph(entryDoc);
94
114
  const importedDocs: LangiumDocument[] = [];
95
115
  for (const uriString of uris) {
96
116
  const uri = URI.parse(uriString);
@@ -111,6 +131,128 @@ export class DomainLangWorkspaceManager extends DefaultWorkspaceManager {
111
131
  // Continue with other modules - partial failure is acceptable
112
132
  }
113
133
  }
134
+
135
+ // Load standalone .dlang files in workspace root folders
136
+ // These are files NOT covered by any module's import graph
137
+ await this.loadStandaloneFiles(folders, moduleDirectories, collector);
138
+ }
139
+
140
+ /**
141
+ * Loads standalone .dlang files from workspace folders recursively.
142
+ *
143
+ * Skips:
144
+ * - Module directories (directories with model.yaml) - loaded via import graph
145
+ * - `.dlang/packages` directory - package cache managed by CLI
146
+ *
147
+ * @param folders - Workspace folders to scan
148
+ * @param moduleDirectories - Set of directories containing model.yaml (to skip)
149
+ * @param collector - Document collector callback
150
+ */
151
+ private async loadStandaloneFiles(
152
+ folders: WorkspaceFolder[],
153
+ moduleDirectories: Set<string>,
154
+ collector: (document: LangiumDocument) => void
155
+ ): Promise<void> {
156
+ const standaloneDocs: LangiumDocument[] = [];
157
+
158
+ for (const folder of folders) {
159
+ const folderPath = URI.parse(folder.uri).fsPath;
160
+ const docs = await this.loadDlangFilesRecursively(folderPath, moduleDirectories, collector);
161
+ standaloneDocs.push(...docs);
162
+ }
163
+
164
+ // Build all standalone documents in batch for performance
165
+ if (standaloneDocs.length > 0) {
166
+ await this.sharedServices.workspace.DocumentBuilder.build(standaloneDocs, {
167
+ validation: true
168
+ });
169
+ }
170
+ }
171
+
172
+ /**
173
+ * Recursively loads .dlang files from a directory.
174
+ * Skips module directories and the .dlang/packages cache.
175
+ */
176
+ private async loadDlangFilesRecursively(
177
+ dirPath: string,
178
+ moduleDirectories: Set<string>,
179
+ collector: (document: LangiumDocument) => void
180
+ ): Promise<LangiumDocument[]> {
181
+ // Skip module directories - they're loaded via import graph
182
+ if (moduleDirectories.has(dirPath)) {
183
+ return [];
184
+ }
185
+
186
+ // Skip .dlang/packages - package cache managed by CLI
187
+ const baseName = path.basename(dirPath);
188
+ const parentName = path.basename(path.dirname(dirPath));
189
+ if (baseName === 'packages' && parentName === '.dlang') {
190
+ return [];
191
+ }
192
+ // Also skip the .dlang directory itself (contains packages cache)
193
+ if (baseName === '.dlang') {
194
+ return [];
195
+ }
196
+
197
+ const docs: LangiumDocument[] = [];
198
+
199
+ try {
200
+ const entries = await fs.readdir(dirPath, { withFileTypes: true });
201
+
202
+ for (const entry of entries) {
203
+ const entryPath = path.join(dirPath, entry.name);
204
+
205
+ if (entry.isDirectory()) {
206
+ // Recurse into subdirectories
207
+ const subDocs = await this.loadDlangFilesRecursively(entryPath, moduleDirectories, collector);
208
+ docs.push(...subDocs);
209
+ } else if (this.isDlangFile(entry)) {
210
+ const doc = await this.tryLoadDocument(dirPath, entry.name, collector);
211
+ if (doc) {
212
+ docs.push(doc);
213
+ }
214
+ }
215
+ }
216
+ } catch (error) {
217
+ const message = error instanceof Error ? error.message : String(error);
218
+ console.warn(`Failed to read directory ${dirPath}: ${message}`);
219
+ }
220
+
221
+ return docs;
222
+ }
223
+
224
+ /**
225
+ * Checks if a directory entry is a .dlang file.
226
+ */
227
+ private isDlangFile(entry: { isFile(): boolean; name: string }): boolean {
228
+ return entry.isFile() && entry.name.toLowerCase().endsWith('.dlang');
229
+ }
230
+
231
+ /**
232
+ * Attempts to load a document, returning undefined on failure.
233
+ */
234
+ private async tryLoadDocument(
235
+ folderPath: string,
236
+ fileName: string,
237
+ collector: (document: LangiumDocument) => void
238
+ ): Promise<LangiumDocument | undefined> {
239
+ const filePath = path.join(folderPath, fileName);
240
+ const uri = URI.file(filePath);
241
+
242
+ // Skip if already loaded (e.g., through imports)
243
+ if (this.langiumDocuments.hasDocument(uri)) {
244
+ return undefined;
245
+ }
246
+
247
+ try {
248
+ const doc = await this.langiumDocuments.getOrCreateDocument(uri);
249
+ collector(doc);
250
+ return doc;
251
+ } catch (error) {
252
+ const message = error instanceof Error ? error.message : String(error);
253
+ console.warn(`Failed to load standalone file ${filePath}: ${message}`);
254
+ return undefined;
255
+ }
114
256
  }
115
257
 
116
258
  /**
@@ -124,4 +266,45 @@ export class DomainLangWorkspaceManager extends DefaultWorkspaceManager {
124
266
  const directories = folders.map(f => URI.parse(f.uri).fsPath);
125
267
  return findManifestsInDirectories(directories);
126
268
  }
269
+
270
+ /**
271
+ * Recursively builds the import graph from a document.
272
+ * Uses the DI-injected ImportResolver when available,
273
+ * falling back to the standalone utility.
274
+ *
275
+ * @param document - The starting document
276
+ * @returns Set of URIs (as strings) for all documents in the import graph
277
+ */
278
+ private async loadImportGraph(document: LangiumDocument): Promise<Set<string>> {
279
+ if (!this.importResolver) {
280
+ // Fallback to standalone utility when DI isn't wired
281
+ return ensureImportGraphFromDocument(document, this.langiumDocuments);
282
+ }
283
+
284
+ const resolver = this.importResolver;
285
+ const langiumDocuments = this.langiumDocuments;
286
+ const visited = new Set<string>();
287
+
288
+ async function visit(doc: LangiumDocument): Promise<void> {
289
+ const uriString = doc.uri.toString();
290
+ if (visited.has(uriString)) return;
291
+ visited.add(uriString);
292
+
293
+ const model = doc.parseResult.value as { imports?: Array<{ uri?: string }> };
294
+ for (const imp of model.imports ?? []) {
295
+ if (!imp.uri) continue;
296
+
297
+ try {
298
+ const resolvedUri = await resolver.resolveForDocument(doc, imp.uri);
299
+ const childDoc = await langiumDocuments.getOrCreateDocument(resolvedUri);
300
+ await visit(childDoc);
301
+ } catch {
302
+ // Import resolution failed — validation will report the error
303
+ }
304
+ }
305
+ }
306
+
307
+ await visit(document);
308
+ return visited;
309
+ }
127
310
  }