@domainlang/language 0.5.2 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (116) hide show
  1. package/README.md +1 -1
  2. package/out/domain-lang-module.js +5 -1
  3. package/out/domain-lang-module.js.map +1 -1
  4. package/out/generated/ast.d.ts +24 -0
  5. package/out/generated/ast.js.map +1 -1
  6. package/out/generated/grammar.js +22 -32
  7. package/out/generated/grammar.js.map +1 -1
  8. package/out/index.d.ts +2 -5
  9. package/out/index.js +10 -6
  10. package/out/index.js.map +1 -1
  11. package/out/lsp/domain-lang-code-actions.js +14 -8
  12. package/out/lsp/domain-lang-code-actions.js.map +1 -1
  13. package/out/lsp/domain-lang-completion.d.ts +3 -0
  14. package/out/lsp/domain-lang-completion.js +41 -13
  15. package/out/lsp/domain-lang-completion.js.map +1 -1
  16. package/out/lsp/domain-lang-formatter.js +24 -18
  17. package/out/lsp/domain-lang-formatter.js.map +1 -1
  18. package/out/lsp/domain-lang-index-manager.d.ts +170 -0
  19. package/out/lsp/domain-lang-index-manager.js +389 -0
  20. package/out/lsp/domain-lang-index-manager.js.map +1 -0
  21. package/out/lsp/domain-lang-scope-provider.d.ts +67 -0
  22. package/out/lsp/domain-lang-scope-provider.js +95 -0
  23. package/out/lsp/domain-lang-scope-provider.js.map +1 -0
  24. package/out/lsp/domain-lang-scope.js +31 -17
  25. package/out/lsp/domain-lang-scope.js.map +1 -1
  26. package/out/lsp/domain-lang-workspace-manager.d.ts +76 -9
  27. package/out/lsp/domain-lang-workspace-manager.js +176 -54
  28. package/out/lsp/domain-lang-workspace-manager.js.map +1 -1
  29. package/out/lsp/hover/domain-lang-hover.d.ts +45 -1
  30. package/out/lsp/hover/domain-lang-hover.js +308 -232
  31. package/out/lsp/hover/domain-lang-hover.js.map +1 -1
  32. package/out/lsp/hover/domain-lang-keywords.d.ts +3 -7
  33. package/out/lsp/hover/domain-lang-keywords.js +115 -38
  34. package/out/lsp/hover/domain-lang-keywords.js.map +1 -1
  35. package/out/lsp/manifest-diagnostics.js +95 -50
  36. package/out/lsp/manifest-diagnostics.js.map +1 -1
  37. package/out/main.js +204 -17
  38. package/out/main.js.map +1 -1
  39. package/out/services/import-resolver.d.ts +39 -2
  40. package/out/services/import-resolver.js +77 -12
  41. package/out/services/import-resolver.js.map +1 -1
  42. package/out/services/types.d.ts +2 -2
  43. package/out/services/workspace-manager.d.ts +33 -31
  44. package/out/services/workspace-manager.js +92 -148
  45. package/out/services/workspace-manager.js.map +1 -1
  46. package/out/utils/document-utils.d.ts +41 -0
  47. package/out/utils/document-utils.js +64 -0
  48. package/out/utils/document-utils.js.map +1 -0
  49. package/out/utils/import-utils.d.ts +0 -17
  50. package/out/utils/import-utils.js +2 -32
  51. package/out/utils/import-utils.js.map +1 -1
  52. package/out/utils/manifest-utils.d.ts +56 -0
  53. package/out/utils/manifest-utils.js +119 -0
  54. package/out/utils/manifest-utils.js.map +1 -0
  55. package/out/validation/constants.d.ts +13 -0
  56. package/out/validation/constants.js +18 -0
  57. package/out/validation/constants.js.map +1 -1
  58. package/out/validation/import.d.ts +12 -2
  59. package/out/validation/import.js +95 -22
  60. package/out/validation/import.js.map +1 -1
  61. package/out/validation/maps.js +51 -2
  62. package/out/validation/maps.js.map +1 -1
  63. package/package.json +1 -1
  64. package/src/domain-lang-module.ts +6 -1
  65. package/src/domain-lang.langium +37 -13
  66. package/src/generated/ast.ts +24 -0
  67. package/src/generated/grammar.ts +22 -32
  68. package/src/index.ts +12 -6
  69. package/src/lsp/domain-lang-code-actions.ts +13 -8
  70. package/src/lsp/domain-lang-completion.ts +61 -13
  71. package/src/lsp/domain-lang-formatter.ts +28 -23
  72. package/src/lsp/domain-lang-index-manager.ts +447 -0
  73. package/src/lsp/domain-lang-scope-provider.ts +134 -0
  74. package/src/lsp/domain-lang-scope.ts +29 -17
  75. package/src/lsp/domain-lang-workspace-manager.ts +201 -53
  76. package/src/lsp/hover/domain-lang-hover.ts +332 -226
  77. package/src/lsp/hover/domain-lang-keywords.ts +129 -43
  78. package/src/lsp/manifest-diagnostics.ts +100 -59
  79. package/src/main.ts +258 -16
  80. package/src/services/import-resolver.ts +91 -12
  81. package/src/services/types.ts +2 -2
  82. package/src/services/workspace-manager.ts +101 -175
  83. package/src/utils/document-utils.ts +80 -0
  84. package/src/utils/import-utils.ts +2 -40
  85. package/src/utils/manifest-utils.ts +132 -0
  86. package/src/validation/constants.ts +24 -0
  87. package/src/validation/import.ts +107 -24
  88. package/src/validation/maps.ts +59 -2
  89. package/out/lsp/hover/ddd-pattern-explanations.d.ts +0 -50
  90. package/out/lsp/hover/ddd-pattern-explanations.js +0 -196
  91. package/out/lsp/hover/ddd-pattern-explanations.js.map +0 -1
  92. package/out/services/dependency-analyzer.d.ts +0 -58
  93. package/out/services/dependency-analyzer.js +0 -254
  94. package/out/services/dependency-analyzer.js.map +0 -1
  95. package/out/services/dependency-resolver.d.ts +0 -146
  96. package/out/services/dependency-resolver.js +0 -452
  97. package/out/services/dependency-resolver.js.map +0 -1
  98. package/out/services/git-url-resolver.browser.d.ts +0 -10
  99. package/out/services/git-url-resolver.browser.js +0 -19
  100. package/out/services/git-url-resolver.browser.js.map +0 -1
  101. package/out/services/git-url-resolver.d.ts +0 -158
  102. package/out/services/git-url-resolver.js +0 -416
  103. package/out/services/git-url-resolver.js.map +0 -1
  104. package/out/services/governance-validator.d.ts +0 -44
  105. package/out/services/governance-validator.js +0 -153
  106. package/out/services/governance-validator.js.map +0 -1
  107. package/out/services/semver.d.ts +0 -98
  108. package/out/services/semver.js +0 -195
  109. package/out/services/semver.js.map +0 -1
  110. package/src/lsp/hover/ddd-pattern-explanations.ts +0 -237
  111. package/src/services/dependency-analyzer.ts +0 -321
  112. package/src/services/dependency-resolver.ts +0 -551
  113. package/src/services/git-url-resolver.browser.ts +0 -26
  114. package/src/services/git-url-resolver.ts +0 -517
  115. package/src/services/governance-validator.ts +0 -177
  116. package/src/services/semver.ts +0 -213
@@ -0,0 +1,134 @@
1
+ /**
2
+ * DomainLang Scope Provider
3
+ *
4
+ * Implements import-based scoping for the DomainLang DSL.
5
+ *
6
+ * **Key Concept:**
7
+ * Unlike languages with global namespaces, DomainLang enforces strict import-based scoping:
8
+ * - Elements are only visible if they are defined in the current document OR explicitly imported
9
+ * - The global scope is restricted to imported documents only
10
+ * - Transitive imports do NOT provide scope (only direct imports)
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
+ */
21
+
22
+ import type {
23
+ AstNodeDescription,
24
+ LangiumDocument,
25
+ ReferenceInfo,
26
+ Scope,
27
+ Stream
28
+ } from 'langium';
29
+ import {
30
+ AstUtils,
31
+ DefaultScopeProvider,
32
+ EMPTY_SCOPE,
33
+ MapScope
34
+ } from 'langium';
35
+ import type { DomainLangServices } from '../domain-lang-module.js';
36
+ import type { DomainLangIndexManager } from './domain-lang-index-manager.js';
37
+
38
+ /**
39
+ * Custom scope provider that restricts cross-file references to imported documents only.
40
+ *
41
+ * Extends Langium's DefaultScopeProvider to override the global scope computation.
42
+ */
43
+ export class DomainLangScopeProvider extends DefaultScopeProvider {
44
+ /**
45
+ * Reference to IndexManager for getting resolved imports.
46
+ */
47
+ private readonly domainLangIndexManager: DomainLangIndexManager;
48
+
49
+ constructor(services: DomainLangServices) {
50
+ super(services);
51
+ this.domainLangIndexManager = services.shared.workspace.IndexManager as DomainLangIndexManager;
52
+ }
53
+
54
+ /**
55
+ * Override getGlobalScope to restrict it to imported documents only.
56
+ *
57
+ * The default Langium behavior includes ALL documents in the workspace.
58
+ * We restrict this to:
59
+ * 1. The current document's own exported symbols
60
+ * 2. Exported symbols from directly imported documents
61
+ *
62
+ * @param referenceType - The AST type being referenced
63
+ * @param context - Information about the reference
64
+ * @returns A scope containing only visible elements
65
+ */
66
+ protected override getGlobalScope(referenceType: string, context: ReferenceInfo): Scope {
67
+ const document = AstUtils.getDocument(context.container);
68
+ if (!document) {
69
+ return EMPTY_SCOPE;
70
+ }
71
+
72
+ // Get the set of URIs that are in scope for this document
73
+ const importedUris = this.getImportedDocumentUris(document);
74
+
75
+ // Filter the global index to only include descriptions from imported documents
76
+ const filteredDescriptions = this.filterDescriptionsByImports(
77
+ referenceType,
78
+ document,
79
+ importedUris
80
+ );
81
+
82
+ // Create a scope from the filtered descriptions
83
+ return new MapScope(filteredDescriptions);
84
+ }
85
+
86
+ /**
87
+ * Gets the set of document URIs that are directly imported by the given document.
88
+ *
89
+ * Uses the resolved imports tracked by DomainLangIndexManager during indexing.
90
+ * This ensures accurate resolution including path aliases.
91
+ *
92
+ * @param document - The document to get imports for
93
+ * @returns Set of imported document URIs (as strings)
94
+ */
95
+ private getImportedDocumentUris(document: LangiumDocument): Set<string> {
96
+ const docUri = document.uri.toString();
97
+
98
+ // Get resolved imports from the index manager (tracked during indexing)
99
+ const resolvedImports = this.domainLangIndexManager.getResolvedImports(docUri);
100
+
101
+ // Always include the current document itself
102
+ const importedUris = new Set<string>([docUri]);
103
+
104
+ // Add all resolved import URIs
105
+ for (const resolvedUri of resolvedImports) {
106
+ importedUris.add(resolvedUri);
107
+ }
108
+
109
+ return importedUris;
110
+ }
111
+
112
+ /**
113
+ * Filters the global index to only include descriptions from imported documents.
114
+ *
115
+ * @param referenceType - The AST type being referenced
116
+ * @param currentDocument - The document making the reference
117
+ * @param importedUris - Set of URIs that are in scope
118
+ * @returns Stream of filtered descriptions
119
+ */
120
+ private filterDescriptionsByImports(
121
+ referenceType: string,
122
+ currentDocument: LangiumDocument,
123
+ importedUris: Set<string>
124
+ ): Stream<AstNodeDescription> {
125
+ // Get all descriptions of the reference type from the index
126
+ const allDescriptions = this.indexManager.allElements(referenceType);
127
+
128
+ // Filter to only those from imported documents
129
+ return allDescriptions.filter(desc => {
130
+ const descDocUri = desc.documentUri.toString();
131
+ return importedUris.has(descDocUri);
132
+ });
133
+ }
134
+ }
@@ -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,62 @@
1
+ import fs from 'node:fs/promises';
1
2
  import path from 'node:path';
2
- import YAML from 'yaml';
3
3
  import { DefaultWorkspaceManager, URI, UriUtils, type FileSystemNode, type LangiumDocument, type LangiumSharedCoreServices, type WorkspaceFolder } from 'langium';
4
4
  import type { CancellationToken } from 'vscode-languageserver-protocol';
5
5
  import { ensureImportGraphFromDocument } from '../utils/import-utils.js';
6
+ import { findManifestsInDirectories } from '../utils/manifest-utils.js';
6
7
 
7
8
  /**
8
9
  * Langium WorkspaceManager override implementing manifest-centric import loading per PRS-010.
9
10
  *
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.
11
+ * **Three Operational Modes:**
12
+ *
13
+ * **Mode A (Pure Workspace with model.yaml):**
14
+ * - model.yaml exists at workspace root
15
+ * - Loads entry file (default: index.dlang, or custom via model.entry)
16
+ * - Pre-builds entry and follows import graph
17
+ * - All imported documents built to Validated state before workspace ready
18
+ * - LSP features have immediate access to complete reference information
19
+ *
20
+ * **Mode B (Pure Standalone files):**
21
+ * - No model.yaml anywhere in workspace
22
+ * - No pre-loading of .dlang files during workspace scan
23
+ * - Documents loaded on-demand when user opens them
24
+ * - Imports resolved lazily via ImportResolver
25
+ * - Each document built individually when opened
26
+ * - Works with relative imports only (no path aliases or external deps)
27
+ *
28
+ * **Mode C (Mixed - Standalone + Module folders):**
29
+ * - Workspace contains both standalone .dlang files AND folders with model.yaml
30
+ * - Each model.yaml folder treated as a module/package:
31
+ * - Module entry + import graph pre-loaded
32
+ * - Path aliases and external deps work within module
33
+ * - Standalone files outside modules loaded on-demand
34
+ * - Example structure:
35
+ * ```
36
+ * workspace/
37
+ * ├── standalone.dlang ← Mode B (on-demand)
38
+ * ├── core/
39
+ * │ ├── model.yaml ← Module root
40
+ * │ ├── index.dlang ← Pre-loaded
41
+ * │ └── domains/
42
+ * │ └── sales.dlang ← Pre-loaded via imports
43
+ * └── util.dlang ← Mode B (on-demand)
44
+ * ```
45
+ *
46
+ * **Performance Characteristics:**
47
+ * - Mode A/C modules: Slower initial load, instant LSP features afterward
48
+ * - Mode B/C standalone: Instant workspace init, per-file build on open
49
+ * - All modes cache import resolution for subsequent access
50
+ *
51
+ * **Never performs network fetches** - relies on cached dependencies/lock files.
52
+ * Missing cache produces diagnostics upstream via ImportValidator.
15
53
  */
16
54
  export class DomainLangWorkspaceManager extends DefaultWorkspaceManager {
55
+ private readonly sharedServices: LangiumSharedCoreServices;
56
+
17
57
  constructor(services: LangiumSharedCoreServices) {
18
58
  super(services);
59
+ this.sharedServices = services;
19
60
  }
20
61
 
21
62
  override shouldIncludeEntry(entry: FileSystemNode): boolean {
@@ -32,73 +73,180 @@ export class DomainLangWorkspaceManager extends DefaultWorkspaceManager {
32
73
  }
33
74
 
34
75
  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
- }
76
+ // Find ALL model.yaml files in workspace (supports mixed mode)
77
+ const manifestInfos = await this.findAllManifestsInFolders(folders);
78
+
79
+ // Track directories covered by manifests to avoid loading their files as standalone
80
+ const moduleDirectories = new Set(
81
+ manifestInfos.map(m => path.dirname(m.manifestPath))
82
+ );
39
83
 
40
- const entryUri = URI.file(manifestInfo.entryPath);
41
- const entryDoc = await this.langiumDocuments.getOrCreateDocument(entryUri);
42
- collector(entryDoc);
84
+ // Mode A or Mode C: Load each module's entry + import graph
85
+ for (const manifestInfo of manifestInfos) {
86
+ try {
87
+ const entryUri = URI.file(manifestInfo.entryPath);
88
+ const entryDoc = await this.langiumDocuments.getOrCreateDocument(entryUri);
89
+ collector(entryDoc);
43
90
 
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);
91
+ // Build entry document first to ensure it's ready for import resolution
92
+ await this.sharedServices.workspace.DocumentBuilder.build([entryDoc], {
93
+ validation: true
94
+ });
95
+
96
+ const uris = await ensureImportGraphFromDocument(entryDoc, this.langiumDocuments);
97
+ const importedDocs: LangiumDocument[] = [];
98
+ for (const uriString of uris) {
99
+ const uri = URI.parse(uriString);
100
+ const doc = await this.langiumDocuments.getOrCreateDocument(uri);
101
+ collector(doc);
102
+ importedDocs.push(doc);
103
+ }
104
+
105
+ // Build all imported documents in batch for performance
106
+ if (importedDocs.length > 0) {
107
+ await this.sharedServices.workspace.DocumentBuilder.build(importedDocs, {
108
+ validation: true
109
+ });
110
+ }
111
+ } catch (error) {
112
+ const message = error instanceof Error ? error.message : String(error);
113
+ console.error(`Failed to load import graph from ${manifestInfo.manifestPath}: ${message}`);
114
+ // Continue with other modules - partial failure is acceptable
115
+ }
49
116
  }
117
+
118
+ // Load standalone .dlang files in workspace root folders
119
+ // These are files NOT covered by any module's import graph
120
+ await this.loadStandaloneFiles(folders, moduleDirectories, collector);
50
121
  }
51
122
 
52
- private async findManifestInFolders(folders: WorkspaceFolder[]): Promise<{ manifestPath: string; entryPath: string } | undefined> {
123
+ /**
124
+ * Loads standalone .dlang files from workspace folders recursively.
125
+ *
126
+ * Skips:
127
+ * - Module directories (directories with model.yaml) - loaded via import graph
128
+ * - `.dlang/cache` directory - package cache managed by CLI
129
+ *
130
+ * @param folders - Workspace folders to scan
131
+ * @param moduleDirectories - Set of directories containing model.yaml (to skip)
132
+ * @param collector - Document collector callback
133
+ */
134
+ private async loadStandaloneFiles(
135
+ folders: WorkspaceFolder[],
136
+ moduleDirectories: Set<string>,
137
+ collector: (document: LangiumDocument) => void
138
+ ): Promise<void> {
139
+ const standaloneDocs: LangiumDocument[] = [];
140
+
53
141
  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
- }
142
+ const folderPath = URI.parse(folder.uri).fsPath;
143
+ const docs = await this.loadDlangFilesRecursively(folderPath, moduleDirectories, collector);
144
+ standaloneDocs.push(...docs);
145
+ }
146
+
147
+ // Build all standalone documents in batch for performance
148
+ if (standaloneDocs.length > 0) {
149
+ await this.sharedServices.workspace.DocumentBuilder.build(standaloneDocs, {
150
+ validation: true
151
+ });
60
152
  }
61
- return undefined;
62
153
  }
63
154
 
64
- private async findNearestManifest(startUri: string): Promise<string | undefined> {
65
- let current = path.resolve(URI.parse(startUri).fsPath);
66
- const { root } = path.parse(current);
155
+ /**
156
+ * Recursively loads .dlang files from a directory.
157
+ * Skips module directories and the .dlang/cache package cache.
158
+ */
159
+ private async loadDlangFilesRecursively(
160
+ dirPath: string,
161
+ moduleDirectories: Set<string>,
162
+ collector: (document: LangiumDocument) => void
163
+ ): Promise<LangiumDocument[]> {
164
+ // Skip module directories - they're loaded via import graph
165
+ if (moduleDirectories.has(dirPath)) {
166
+ return [];
167
+ }
67
168
 
68
- while (true) {
69
- const candidate = path.join(current, 'model.yaml');
70
- if (await this.pathExists(candidate)) {
71
- return candidate;
72
- }
169
+ // Skip .dlang/cache - package cache managed by CLI
170
+ const baseName = path.basename(dirPath);
171
+ const parentName = path.basename(path.dirname(dirPath));
172
+ if (baseName === 'cache' && parentName === '.dlang') {
173
+ return [];
174
+ }
175
+ // Also skip the .dlang directory itself if it contains cache
176
+ if (baseName === '.dlang') {
177
+ return [];
178
+ }
73
179
 
74
- if (current === root) {
75
- return undefined;
76
- }
180
+ const docs: LangiumDocument[] = [];
181
+
182
+ try {
183
+ const entries = await fs.readdir(dirPath, { withFileTypes: true });
184
+
185
+ for (const entry of entries) {
186
+ const entryPath = path.join(dirPath, entry.name);
77
187
 
78
- const parent = path.dirname(current);
79
- if (parent === current) {
80
- return undefined;
188
+ if (entry.isDirectory()) {
189
+ // Recurse into subdirectories
190
+ const subDocs = await this.loadDlangFilesRecursively(entryPath, moduleDirectories, collector);
191
+ docs.push(...subDocs);
192
+ } else if (this.isDlangFile(entry)) {
193
+ const doc = await this.tryLoadDocument(dirPath, entry.name, collector);
194
+ if (doc) {
195
+ docs.push(doc);
196
+ }
197
+ }
81
198
  }
82
- current = parent;
199
+ } catch (error) {
200
+ const message = error instanceof Error ? error.message : String(error);
201
+ console.warn(`Failed to read directory ${dirPath}: ${message}`);
83
202
  }
203
+
204
+ return docs;
84
205
  }
85
206
 
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 {
207
+ /**
208
+ * Checks if a directory entry is a .dlang file.
209
+ */
210
+ private isDlangFile(entry: { isFile(): boolean; name: string }): boolean {
211
+ return entry.isFile() && entry.name.toLowerCase().endsWith('.dlang');
212
+ }
213
+
214
+ /**
215
+ * Attempts to load a document, returning undefined on failure.
216
+ */
217
+ private async tryLoadDocument(
218
+ folderPath: string,
219
+ fileName: string,
220
+ collector: (document: LangiumDocument) => void
221
+ ): Promise<LangiumDocument | undefined> {
222
+ const filePath = path.join(folderPath, fileName);
223
+ const uri = URI.file(filePath);
224
+
225
+ // Skip if already loaded (e.g., through imports)
226
+ if (this.langiumDocuments.hasDocument(uri)) {
92
227
  return undefined;
93
228
  }
94
- }
95
229
 
96
- private async pathExists(target: string): Promise<boolean> {
97
230
  try {
98
- await this.fileSystemProvider.stat(URI.file(target));
99
- return true;
100
- } catch {
101
- return false;
231
+ const doc = await this.langiumDocuments.getOrCreateDocument(uri);
232
+ collector(doc);
233
+ return doc;
234
+ } catch (error) {
235
+ const message = error instanceof Error ? error.message : String(error);
236
+ console.warn(`Failed to load standalone file ${filePath}: ${message}`);
237
+ return undefined;
102
238
  }
103
239
  }
240
+
241
+ /**
242
+ * Finds ALL model.yaml files in the workspace.
243
+ * Delegates to shared manifest utilities.
244
+ *
245
+ * @param folders - Workspace folders to search
246
+ * @returns Array of manifest info (one per model.yaml found)
247
+ */
248
+ private async findAllManifestsInFolders(folders: WorkspaceFolder[]): Promise<Array<{ manifestPath: string; entryPath: string }>> {
249
+ const directories = folders.map(f => URI.parse(f.uri).fsPath);
250
+ return findManifestsInDirectories(directories);
251
+ }
104
252
  }