@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
|
@@ -32,9 +32,29 @@ interface LoadedLockFile {
|
|
|
32
32
|
readonly filePath: string;
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
+
/**
|
|
36
|
+
* Cached context for a single workspace (directory containing model.yaml).
|
|
37
|
+
* Each workspace root has its own independent state.
|
|
38
|
+
*/
|
|
39
|
+
interface WorkspaceContext {
|
|
40
|
+
/** The resolved workspace root path */
|
|
41
|
+
readonly root: string;
|
|
42
|
+
/** Cached lock file for this workspace */
|
|
43
|
+
lockFile: LockFile | undefined;
|
|
44
|
+
/** Cached manifest for this workspace */
|
|
45
|
+
manifestCache: ManifestCache | undefined;
|
|
46
|
+
/** Initialization promise for this context */
|
|
47
|
+
initPromise: Promise<void> | undefined;
|
|
48
|
+
}
|
|
49
|
+
|
|
35
50
|
/**
|
|
36
51
|
* Coordinates workspace discovery and manifest/lock file reading.
|
|
37
52
|
*
|
|
53
|
+
* **Multi-Root Support:**
|
|
54
|
+
* Maintains separate contexts for each workspace root (directory with model.yaml).
|
|
55
|
+
* This enables correct resolution in multi-project setups where sub-projects
|
|
56
|
+
* have their own model.yaml files.
|
|
57
|
+
*
|
|
38
58
|
* This is a read-only service for the LSP - it does NOT:
|
|
39
59
|
* - Generate lock files (use CLI: `dlang install`)
|
|
40
60
|
* - Download packages (use CLI: `dlang install`)
|
|
@@ -48,23 +68,88 @@ interface LoadedLockFile {
|
|
|
48
68
|
export class WorkspaceManager {
|
|
49
69
|
private readonly manifestFiles: readonly string[];
|
|
50
70
|
private readonly lockFiles: readonly string[];
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Cache of workspace contexts by resolved workspace root path.
|
|
74
|
+
* Supports multiple independent workspaces in a single session.
|
|
75
|
+
*/
|
|
76
|
+
private readonly workspaceContexts = new Map<string, WorkspaceContext>();
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Cache mapping start paths to their resolved workspace roots.
|
|
80
|
+
* Avoids repeated directory tree walking for the same paths.
|
|
81
|
+
*/
|
|
82
|
+
private readonly pathToRootCache = new Map<string, string>();
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* The currently active workspace root (set by last initialize() call).
|
|
86
|
+
* Used by methods like getWorkspaceRoot(), getManifest(), etc.
|
|
87
|
+
*/
|
|
88
|
+
private activeRoot: string | undefined;
|
|
55
89
|
|
|
56
90
|
constructor(options: WorkspaceManagerOptions = {}) {
|
|
57
91
|
this.manifestFiles = options.manifestFiles ?? [...DEFAULT_MANIFEST_FILES];
|
|
58
92
|
this.lockFiles = options.lockFiles ?? [...DEFAULT_LOCK_FILES];
|
|
59
93
|
}
|
|
60
94
|
|
|
95
|
+
/**
|
|
96
|
+
* Returns the active workspace context, or undefined if not initialized.
|
|
97
|
+
* All methods that need context should call this after ensureInitialized().
|
|
98
|
+
*/
|
|
99
|
+
private getActiveContext(): WorkspaceContext | undefined {
|
|
100
|
+
if (!this.activeRoot) return undefined;
|
|
101
|
+
return this.workspaceContexts.get(this.activeRoot);
|
|
102
|
+
}
|
|
103
|
+
|
|
61
104
|
/**
|
|
62
105
|
* Finds the workspace root and loads any existing lock file.
|
|
63
|
-
*
|
|
106
|
+
*
|
|
107
|
+
* **Multi-Root Support:**
|
|
108
|
+
* Each call may switch to a different workspace context based on the startPath.
|
|
109
|
+
* The workspace root is the nearest ancestor directory containing model.yaml.
|
|
110
|
+
*
|
|
111
|
+
* @param startPath - Directory to start searching from (usually document directory)
|
|
64
112
|
*/
|
|
65
113
|
async initialize(startPath: string): Promise<void> {
|
|
66
|
-
|
|
67
|
-
|
|
114
|
+
const normalizedStart = path.resolve(startPath);
|
|
115
|
+
|
|
116
|
+
// Fast path: check if we've already resolved this path
|
|
117
|
+
let workspaceRoot = this.pathToRootCache.get(normalizedStart);
|
|
118
|
+
|
|
119
|
+
if (!workspaceRoot) {
|
|
120
|
+
// Find workspace root by walking up directory tree
|
|
121
|
+
workspaceRoot = await this.findWorkspaceRoot(normalizedStart) ?? normalizedStart;
|
|
122
|
+
this.pathToRootCache.set(normalizedStart, workspaceRoot);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Switch to this workspace's context
|
|
126
|
+
this.activeRoot = workspaceRoot;
|
|
127
|
+
|
|
128
|
+
// Get or create context for this workspace
|
|
129
|
+
let context = this.workspaceContexts.get(workspaceRoot);
|
|
130
|
+
if (!context) {
|
|
131
|
+
context = {
|
|
132
|
+
root: workspaceRoot,
|
|
133
|
+
lockFile: undefined,
|
|
134
|
+
manifestCache: undefined,
|
|
135
|
+
initPromise: undefined
|
|
136
|
+
};
|
|
137
|
+
this.workspaceContexts.set(workspaceRoot, context);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Initialize this context (lazy, once per context)
|
|
141
|
+
context.initPromise ??= this.initializeContext(context);
|
|
142
|
+
await context.initPromise;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Initializes a workspace context by loading its lock file.
|
|
147
|
+
*/
|
|
148
|
+
private async initializeContext(context: WorkspaceContext): Promise<void> {
|
|
149
|
+
const loaded = await this.loadLockFileFromDisk(context.root);
|
|
150
|
+
if (loaded) {
|
|
151
|
+
context.lockFile = loaded.lockFile;
|
|
152
|
+
}
|
|
68
153
|
}
|
|
69
154
|
|
|
70
155
|
/**
|
|
@@ -72,21 +157,53 @@ export class WorkspaceManager {
|
|
|
72
157
|
* @throws Error if {@link initialize} has not completed successfully.
|
|
73
158
|
*/
|
|
74
159
|
getWorkspaceRoot(): string {
|
|
75
|
-
if (!this.
|
|
160
|
+
if (!this.activeRoot) {
|
|
76
161
|
throw new Error('WorkspaceManager not initialized. Call initialize() first.');
|
|
77
162
|
}
|
|
78
|
-
return this.
|
|
163
|
+
return this.activeRoot;
|
|
79
164
|
}
|
|
80
165
|
|
|
81
166
|
/**
|
|
82
167
|
* Returns the project-local package cache directory.
|
|
83
168
|
* Per PRS-010: .dlang/packages/
|
|
169
|
+
*
|
|
170
|
+
* If the current workspace root is inside a cached package,
|
|
171
|
+
* walks up to find the actual project root's cache directory.
|
|
84
172
|
*/
|
|
85
173
|
getCacheDir(): string {
|
|
86
|
-
if (!this.
|
|
174
|
+
if (!this.activeRoot) {
|
|
87
175
|
throw new Error('WorkspaceManager not initialized. Call initialize() first.');
|
|
88
176
|
}
|
|
89
|
-
|
|
177
|
+
|
|
178
|
+
// If workspace root is inside .dlang/packages, find the project root
|
|
179
|
+
const projectRoot = this.findProjectRootFromCache(this.activeRoot);
|
|
180
|
+
return path.join(projectRoot, '.dlang', 'packages');
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Finds the actual project root when inside a cached package.
|
|
185
|
+
*
|
|
186
|
+
* Cached packages are stored in: <project>/.dlang/packages/<owner>/<repo>/<commit>/
|
|
187
|
+
* If workspaceRoot is inside this structure, returns <project>
|
|
188
|
+
* Otherwise returns workspaceRoot unchanged.
|
|
189
|
+
*/
|
|
190
|
+
private findProjectRootFromCache(currentRoot: string): string {
|
|
191
|
+
// Normalize path for cross-platform compatibility
|
|
192
|
+
const normalized = currentRoot.split(path.sep);
|
|
193
|
+
|
|
194
|
+
// Find last occurrence of .dlang in the path
|
|
195
|
+
const dlangIndex = normalized.lastIndexOf('.dlang');
|
|
196
|
+
|
|
197
|
+
// Check if we're inside .dlang/packages/...
|
|
198
|
+
if (dlangIndex !== -1 &&
|
|
199
|
+
dlangIndex + 1 < normalized.length &&
|
|
200
|
+
normalized[dlangIndex + 1] === 'packages') {
|
|
201
|
+
// Return the directory containing .dlang (the project root)
|
|
202
|
+
return normalized.slice(0, dlangIndex).join(path.sep);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Not in a cached package, return as-is
|
|
206
|
+
return currentRoot;
|
|
90
207
|
}
|
|
91
208
|
|
|
92
209
|
/**
|
|
@@ -94,7 +211,7 @@ export class WorkspaceManager {
|
|
|
94
211
|
*/
|
|
95
212
|
async getManifestPath(): Promise<string | undefined> {
|
|
96
213
|
await this.ensureInitialized();
|
|
97
|
-
const root = this.
|
|
214
|
+
const root = this.activeRoot;
|
|
98
215
|
if (!root) {
|
|
99
216
|
return undefined;
|
|
100
217
|
}
|
|
@@ -118,13 +235,39 @@ export class WorkspaceManager {
|
|
|
118
235
|
return this.loadManifest();
|
|
119
236
|
}
|
|
120
237
|
|
|
238
|
+
/**
|
|
239
|
+
* Returns the cached manifest synchronously (if available).
|
|
240
|
+
* Used by LSP features that need synchronous access (like completion).
|
|
241
|
+
* Returns undefined if manifest hasn't been loaded yet.
|
|
242
|
+
*/
|
|
243
|
+
getCachedManifest(): ModelManifest | undefined {
|
|
244
|
+
return this.getActiveContext()?.manifestCache?.manifest;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Ensures the manifest is loaded and returns it.
|
|
249
|
+
* Use this over getCachedManifest() when you need to guarantee the manifest
|
|
250
|
+
* is available (e.g., in async LSP operations like completions).
|
|
251
|
+
*
|
|
252
|
+
* @returns The manifest or undefined if no model.yaml exists
|
|
253
|
+
*/
|
|
254
|
+
async ensureManifestLoaded(): Promise<ModelManifest | undefined> {
|
|
255
|
+
// If we already have a cached manifest, return it immediately
|
|
256
|
+
const context = this.getActiveContext();
|
|
257
|
+
if (context?.manifestCache?.manifest) {
|
|
258
|
+
return context.manifestCache.manifest;
|
|
259
|
+
}
|
|
260
|
+
// Otherwise load it (this also populates the cache)
|
|
261
|
+
return this.getManifest();
|
|
262
|
+
}
|
|
263
|
+
|
|
121
264
|
/**
|
|
122
265
|
* Gets the currently cached lock file.
|
|
123
266
|
* Returns undefined if no lock file exists (run `dlang install` to create one).
|
|
124
267
|
*/
|
|
125
268
|
async getLockFile(): Promise<LockFile | undefined> {
|
|
126
269
|
await this.ensureInitialized();
|
|
127
|
-
return this.lockFile;
|
|
270
|
+
return this.getActiveContext()?.lockFile;
|
|
128
271
|
}
|
|
129
272
|
|
|
130
273
|
/**
|
|
@@ -132,13 +275,12 @@ export class WorkspaceManager {
|
|
|
132
275
|
*/
|
|
133
276
|
async refreshLockFile(): Promise<LockFile | undefined> {
|
|
134
277
|
await this.ensureInitialized();
|
|
278
|
+
const context = this.getActiveContext();
|
|
135
279
|
const loaded = await this.loadLockFileFromDisk();
|
|
136
|
-
if (
|
|
137
|
-
|
|
138
|
-
} else {
|
|
139
|
-
this.lockFile = undefined;
|
|
280
|
+
if (context) {
|
|
281
|
+
context.lockFile = loaded?.lockFile;
|
|
140
282
|
}
|
|
141
|
-
return
|
|
283
|
+
return loaded?.lockFile;
|
|
142
284
|
}
|
|
143
285
|
|
|
144
286
|
/**
|
|
@@ -149,8 +291,11 @@ export class WorkspaceManager {
|
|
|
149
291
|
* will re-read from disk.
|
|
150
292
|
*/
|
|
151
293
|
invalidateCache(): void {
|
|
152
|
-
|
|
153
|
-
|
|
294
|
+
const context = this.getActiveContext();
|
|
295
|
+
if (context) {
|
|
296
|
+
context.manifestCache = undefined;
|
|
297
|
+
context.lockFile = undefined;
|
|
298
|
+
}
|
|
154
299
|
}
|
|
155
300
|
|
|
156
301
|
/**
|
|
@@ -158,7 +303,10 @@ export class WorkspaceManager {
|
|
|
158
303
|
* Call this when model.yaml changes.
|
|
159
304
|
*/
|
|
160
305
|
invalidateManifestCache(): void {
|
|
161
|
-
|
|
306
|
+
const context = this.getActiveContext();
|
|
307
|
+
if (context) {
|
|
308
|
+
context.manifestCache = undefined;
|
|
309
|
+
}
|
|
162
310
|
}
|
|
163
311
|
|
|
164
312
|
/**
|
|
@@ -166,7 +314,10 @@ export class WorkspaceManager {
|
|
|
166
314
|
* Call this when model.lock changes.
|
|
167
315
|
*/
|
|
168
316
|
invalidateLockCache(): void {
|
|
169
|
-
|
|
317
|
+
const context = this.getActiveContext();
|
|
318
|
+
if (context) {
|
|
319
|
+
context.lockFile = undefined;
|
|
320
|
+
}
|
|
170
321
|
}
|
|
171
322
|
|
|
172
323
|
/**
|
|
@@ -209,7 +360,8 @@ export class WorkspaceManager {
|
|
|
209
360
|
async resolveDependencyPath(specifier: string): Promise<string | undefined> {
|
|
210
361
|
await this.ensureInitialized();
|
|
211
362
|
|
|
212
|
-
|
|
363
|
+
const context = this.getActiveContext();
|
|
364
|
+
if (!context?.lockFile) {
|
|
213
365
|
return undefined;
|
|
214
366
|
}
|
|
215
367
|
|
|
@@ -236,7 +388,7 @@ export class WorkspaceManager {
|
|
|
236
388
|
// Match if specifier equals key or starts with key/
|
|
237
389
|
if (specifier === key || specifier.startsWith(`${key}/`)) {
|
|
238
390
|
// Find in lock file
|
|
239
|
-
const locked =
|
|
391
|
+
const locked = context.lockFile.dependencies[normalized.source];
|
|
240
392
|
if (!locked) {
|
|
241
393
|
return undefined;
|
|
242
394
|
}
|
|
@@ -275,36 +427,34 @@ export class WorkspaceManager {
|
|
|
275
427
|
}
|
|
276
428
|
}
|
|
277
429
|
|
|
278
|
-
private async performInitialization(startPath: string): Promise<void> {
|
|
279
|
-
this.workspaceRoot = await this.findWorkspaceRoot(startPath) ?? path.resolve(startPath);
|
|
280
|
-
const loaded = await this.loadLockFileFromDisk();
|
|
281
|
-
if (loaded) {
|
|
282
|
-
this.lockFile = loaded.lockFile;
|
|
283
|
-
}
|
|
284
|
-
}
|
|
285
|
-
|
|
286
430
|
private async ensureInitialized(): Promise<void> {
|
|
287
|
-
if
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
431
|
+
// Check if we have an active workspace context
|
|
432
|
+
if (this.activeRoot) {
|
|
433
|
+
const context = this.workspaceContexts.get(this.activeRoot);
|
|
434
|
+
if (context?.initPromise) {
|
|
435
|
+
await context.initPromise;
|
|
436
|
+
return;
|
|
437
|
+
}
|
|
291
438
|
}
|
|
439
|
+
|
|
440
|
+
throw new Error('WorkspaceManager not initialized. Call initialize() first.');
|
|
292
441
|
}
|
|
293
442
|
|
|
294
|
-
private async loadLockFileFromDisk(): Promise<LoadedLockFile | undefined> {
|
|
295
|
-
|
|
443
|
+
private async loadLockFileFromDisk(root?: string): Promise<LoadedLockFile | undefined> {
|
|
444
|
+
const workspaceRoot = root ?? this.activeRoot;
|
|
445
|
+
if (!workspaceRoot) {
|
|
296
446
|
return undefined;
|
|
297
447
|
}
|
|
298
448
|
|
|
299
449
|
// Try performance optimizer cache first
|
|
300
450
|
const optimizer = getGlobalOptimizer();
|
|
301
|
-
const cached = await optimizer.getCachedLockFile(
|
|
451
|
+
const cached = await optimizer.getCachedLockFile(workspaceRoot);
|
|
302
452
|
if (cached) {
|
|
303
|
-
return { lockFile: cached, filePath: path.join(
|
|
453
|
+
return { lockFile: cached, filePath: path.join(workspaceRoot, 'model.lock') };
|
|
304
454
|
}
|
|
305
455
|
|
|
306
456
|
for (const filename of this.lockFiles) {
|
|
307
|
-
const filePath = path.join(
|
|
457
|
+
const filePath = path.join(workspaceRoot, filename);
|
|
308
458
|
const lockFile = await this.tryReadLockFile(filePath);
|
|
309
459
|
if (lockFile) {
|
|
310
460
|
return { lockFile, filePath };
|
|
@@ -327,37 +477,84 @@ export class WorkspaceManager {
|
|
|
327
477
|
}
|
|
328
478
|
|
|
329
479
|
private async loadManifest(): Promise<ModelManifest | undefined> {
|
|
480
|
+
const context = this.getActiveContext();
|
|
330
481
|
const manifestPath = await this.getManifestPath();
|
|
331
482
|
if (!manifestPath) {
|
|
332
|
-
this.
|
|
483
|
+
this.clearManifestCache(context);
|
|
333
484
|
return undefined;
|
|
334
485
|
}
|
|
335
486
|
|
|
336
487
|
try {
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
488
|
+
return await this.readAndCacheManifest(manifestPath, context);
|
|
489
|
+
} catch (error) {
|
|
490
|
+
return this.handleManifestError(error, manifestPath, context);
|
|
491
|
+
}
|
|
492
|
+
}
|
|
342
493
|
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
494
|
+
/**
|
|
495
|
+
* Reads, validates, and caches a manifest file.
|
|
496
|
+
*/
|
|
497
|
+
private async readAndCacheManifest(
|
|
498
|
+
manifestPath: string,
|
|
499
|
+
context: WorkspaceContext | undefined
|
|
500
|
+
): Promise<ModelManifest> {
|
|
501
|
+
const stat = await fs.stat(manifestPath);
|
|
502
|
+
if (context?.manifestCache?.path === manifestPath &&
|
|
503
|
+
context.manifestCache.mtimeMs === stat.mtimeMs) {
|
|
504
|
+
return context.manifestCache.manifest;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
const content = await fs.readFile(manifestPath, 'utf-8');
|
|
508
|
+
const manifest = (YAML.parse(content) ?? {}) as ModelManifest;
|
|
509
|
+
|
|
510
|
+
// Validate manifest structure
|
|
511
|
+
this.validateManifest(manifest, manifestPath);
|
|
512
|
+
|
|
513
|
+
if (context) {
|
|
514
|
+
context.manifestCache = {
|
|
350
515
|
manifest,
|
|
351
516
|
path: manifestPath,
|
|
352
517
|
mtimeMs: stat.mtimeMs,
|
|
353
518
|
};
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
519
|
+
}
|
|
520
|
+
return manifest;
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
/**
|
|
524
|
+
* Handles errors from manifest loading, distinguishing recoverable
|
|
525
|
+
* errors (missing file, parse errors) from unexpected ones.
|
|
526
|
+
*/
|
|
527
|
+
private handleManifestError(
|
|
528
|
+
error: unknown,
|
|
529
|
+
manifestPath: string,
|
|
530
|
+
context: WorkspaceContext | undefined
|
|
531
|
+
): ModelManifest | undefined {
|
|
532
|
+
if ((error as NodeJS.ErrnoException)?.code === 'ENOENT') {
|
|
533
|
+
this.clearManifestCache(context);
|
|
534
|
+
return undefined;
|
|
535
|
+
}
|
|
536
|
+
// YAML parse errors should not crash the LSP
|
|
537
|
+
if (error instanceof Error &&
|
|
538
|
+
(error.name === 'YAMLParseError' || error.name === 'YAMLSyntaxError')) {
|
|
539
|
+
console.error(`Invalid model.yaml at ${manifestPath}: ${error.message}`);
|
|
540
|
+
this.clearManifestCache(context);
|
|
541
|
+
return undefined;
|
|
542
|
+
}
|
|
543
|
+
// Validation errors from validateManifest should not crash the LSP
|
|
544
|
+
if (error instanceof Error) {
|
|
545
|
+
console.error(`Manifest validation error at ${manifestPath}: ${error.message}`);
|
|
546
|
+
this.clearManifestCache(context);
|
|
547
|
+
return undefined;
|
|
548
|
+
}
|
|
549
|
+
throw error;
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
/**
|
|
553
|
+
* Clears the manifest cache on the given context, if available.
|
|
554
|
+
*/
|
|
555
|
+
private clearManifestCache(context: WorkspaceContext | undefined): void {
|
|
556
|
+
if (context) {
|
|
557
|
+
context.manifestCache = undefined;
|
|
361
558
|
}
|
|
362
559
|
}
|
|
363
560
|
|
|
@@ -451,7 +648,7 @@ export class WorkspaceManager {
|
|
|
451
648
|
// Resolve path relative to manifest directory
|
|
452
649
|
const manifestDir = path.dirname(manifestPath);
|
|
453
650
|
const resolvedPath = path.resolve(manifestDir, localPath);
|
|
454
|
-
const workspaceRoot = this.
|
|
651
|
+
const workspaceRoot = this.activeRoot || manifestDir;
|
|
455
652
|
|
|
456
653
|
// Check if resolved path is within workspace
|
|
457
654
|
const relativePath = path.relative(workspaceRoot, resolvedPath);
|
|
@@ -7,7 +7,11 @@ import type { DomainLangServices } from '../domain-lang-module.js';
|
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
9
|
* Lazily initialized workspace manager for standalone (non-LSP) usage.
|
|
10
|
-
* Used by import graph building when
|
|
10
|
+
* Used by import graph building when no DI-injected ImportResolver is available.
|
|
11
|
+
*
|
|
12
|
+
* @deprecated Prefer passing an ImportResolver from the DI container.
|
|
13
|
+
* These singletons exist only for backwards compatibility with callers
|
|
14
|
+
* that haven't been updated to pass through DI services.
|
|
11
15
|
*/
|
|
12
16
|
let standaloneWorkspaceManager: WorkspaceManager | undefined;
|
|
13
17
|
let standaloneImportResolver: ImportResolver | undefined;
|
|
@@ -17,9 +21,7 @@ let lastInitializedDir: string | undefined;
|
|
|
17
21
|
* Gets or creates a standalone import resolver for non-LSP contexts.
|
|
18
22
|
* Creates its own WorkspaceManager if not previously initialized for this directory.
|
|
19
23
|
*
|
|
20
|
-
*
|
|
21
|
-
* This function exists for utilities that don't have access to the service container.
|
|
22
|
-
*
|
|
24
|
+
* @deprecated Prefer using services.imports.ImportResolver directly.
|
|
23
25
|
* @param startDir - Directory to start workspace search from
|
|
24
26
|
* @returns Promise resolving to the import resolver
|
|
25
27
|
*/
|
|
@@ -44,10 +46,8 @@ async function getStandaloneImportResolver(startDir: string): Promise<ImportReso
|
|
|
44
46
|
/**
|
|
45
47
|
* Resolves an import path to an absolute file URI.
|
|
46
48
|
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
* - Module imports (no extension): Requires model.yaml in directory
|
|
50
|
-
* - External dependencies: Resolved via manifest and lock file
|
|
49
|
+
* @deprecated Prefer using ImportResolver.resolveForDocument() from the DI container.
|
|
50
|
+
* This function creates standalone instances outside the DI system.
|
|
51
51
|
*
|
|
52
52
|
* @param importingDoc - The document containing the import statement
|
|
53
53
|
* @param rawImportPath - The raw import path from the import statement
|
|
@@ -68,16 +68,19 @@ export async function resolveImportPath(
|
|
|
68
68
|
*
|
|
69
69
|
* @param entryFilePath - Absolute or workspace-relative path to entry file
|
|
70
70
|
* @param langiumDocuments - The Langium documents manager
|
|
71
|
+
* @param importResolver - Optional DI-injected ImportResolver. When provided,
|
|
72
|
+
* uses it instead of creating standalone instances. Recommended for LSP contexts.
|
|
71
73
|
* @returns Set of URIs (as strings) for all documents in the import graph
|
|
72
74
|
* @throws {Error} If entry file cannot be resolved or loaded
|
|
73
75
|
*/
|
|
74
76
|
export async function ensureImportGraphFromEntryFile(
|
|
75
77
|
entryFilePath: string,
|
|
76
|
-
langiumDocuments: LangiumDocuments
|
|
78
|
+
langiumDocuments: LangiumDocuments,
|
|
79
|
+
importResolver?: ImportResolver
|
|
77
80
|
): Promise<Set<string>> {
|
|
78
81
|
const entryUri = URI.file(path.resolve(entryFilePath));
|
|
79
82
|
const entryDoc = await langiumDocuments.getOrCreateDocument(entryUri);
|
|
80
|
-
return ensureImportGraphFromDocument(entryDoc, langiumDocuments);
|
|
83
|
+
return ensureImportGraphFromDocument(entryDoc, langiumDocuments, importResolver);
|
|
81
84
|
}
|
|
82
85
|
|
|
83
86
|
/**
|
|
@@ -85,11 +88,14 @@ export async function ensureImportGraphFromEntryFile(
|
|
|
85
88
|
*
|
|
86
89
|
* @param document - The starting document
|
|
87
90
|
* @param langiumDocuments - The Langium documents manager
|
|
91
|
+
* @param importResolver - Optional DI-injected ImportResolver. When provided,
|
|
92
|
+
* uses it instead of creating standalone instances. Recommended for LSP contexts.
|
|
88
93
|
* @returns Set of URIs (as strings) for all documents in the import graph
|
|
89
94
|
*/
|
|
90
95
|
export async function ensureImportGraphFromDocument(
|
|
91
96
|
document: LangiumDocument,
|
|
92
|
-
langiumDocuments: LangiumDocuments
|
|
97
|
+
langiumDocuments: LangiumDocuments,
|
|
98
|
+
importResolver?: ImportResolver
|
|
93
99
|
): Promise<Set<string>> {
|
|
94
100
|
const visited = new Set<string>();
|
|
95
101
|
|
|
@@ -102,10 +108,16 @@ export async function ensureImportGraphFromDocument(
|
|
|
102
108
|
for (const imp of model.imports ?? []) {
|
|
103
109
|
if (!imp.uri) continue;
|
|
104
110
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
111
|
+
try {
|
|
112
|
+
// Use DI-injected resolver when available, falling back to standalone
|
|
113
|
+
const resolvedUri = importResolver
|
|
114
|
+
? await importResolver.resolveForDocument(doc, imp.uri)
|
|
115
|
+
: await resolveImportPath(doc, imp.uri);
|
|
116
|
+
const childDoc = await langiumDocuments.getOrCreateDocument(resolvedUri);
|
|
117
|
+
await visit(childDoc);
|
|
118
|
+
} catch {
|
|
119
|
+
// Import resolution failed — validation will report the error
|
|
120
|
+
}
|
|
109
121
|
}
|
|
110
122
|
}
|
|
111
123
|
|
|
@@ -34,6 +34,7 @@ export const IssueCodes = {
|
|
|
34
34
|
ImportMissingRef: 'import-missing-ref',
|
|
35
35
|
ImportAbsolutePath: 'import-absolute-path',
|
|
36
36
|
ImportEscapesWorkspace: 'import-escapes-workspace',
|
|
37
|
+
ImportUnresolved: 'import-unresolved',
|
|
37
38
|
|
|
38
39
|
// Domain Issues
|
|
39
40
|
DomainNoVision: 'domain-no-vision',
|
|
@@ -54,8 +55,12 @@ export const IssueCodes = {
|
|
|
54
55
|
// Context/Domain Map Issues
|
|
55
56
|
ContextMapNoContexts: 'context-map-no-contexts',
|
|
56
57
|
ContextMapNoRelationships: 'context-map-no-relationships',
|
|
58
|
+
ContextMapDuplicateRelationship: 'context-map-duplicate-relationship',
|
|
57
59
|
DomainMapNoDomains: 'domain-map-no-domains',
|
|
58
60
|
|
|
61
|
+
// Reference Issues
|
|
62
|
+
UnresolvedReference: 'unresolved-reference',
|
|
63
|
+
|
|
59
64
|
// Metadata Issues
|
|
60
65
|
MetadataMissingName: 'metadata-missing-name',
|
|
61
66
|
|
|
@@ -77,8 +82,10 @@ const DOCS_BASE = `${REPO_BASE}/dsl/domain-lang/docs`;
|
|
|
77
82
|
* @param docPath - Relative path from docs/ folder
|
|
78
83
|
* @param anchor - Optional section anchor (without #)
|
|
79
84
|
*/
|
|
80
|
-
const buildDocLink = (docPath: string, anchor?: string): string =>
|
|
81
|
-
|
|
85
|
+
const buildDocLink = (docPath: string, anchor?: string): string => {
|
|
86
|
+
const anchorPart = anchor ? `#${anchor}` : '';
|
|
87
|
+
return `${DOCS_BASE}/${docPath}${anchorPart}`;
|
|
88
|
+
};
|
|
82
89
|
|
|
83
90
|
/**
|
|
84
91
|
* Creates a CodeDescription for clickable documentation links in VS Code.
|
|
@@ -129,8 +136,11 @@ export const ValidationMessages = {
|
|
|
129
136
|
* @param inlineClassification - The inline classification name (from 'as')
|
|
130
137
|
* @param blockClassification - The block classification name (from 'classification:')
|
|
131
138
|
*/
|
|
132
|
-
BOUNDED_CONTEXT_CLASSIFICATION_CONFLICT: (bcName: string, inlineClassification?: string, blockClassification?: string) =>
|
|
133
|
-
|
|
139
|
+
BOUNDED_CONTEXT_CLASSIFICATION_CONFLICT: (bcName: string, inlineClassification?: string, blockClassification?: string) => {
|
|
140
|
+
const inlinePart = inlineClassification ? ` ('as ${inlineClassification}')` : '';
|
|
141
|
+
const blockPart = blockClassification ? ` ('classification: ${blockClassification}')` : '';
|
|
142
|
+
return `Classification specified both inline${inlinePart} and in block${blockPart}. Inline value takes precedence.`;
|
|
143
|
+
},
|
|
134
144
|
|
|
135
145
|
/**
|
|
136
146
|
* Warning when team is specified both inline and in a block.
|
|
@@ -139,8 +149,11 @@ export const ValidationMessages = {
|
|
|
139
149
|
* @param inlineTeam - The inline team name (from 'by')
|
|
140
150
|
* @param blockTeam - The block team name (from 'team:')
|
|
141
151
|
*/
|
|
142
|
-
BOUNDED_CONTEXT_TEAM_CONFLICT: (bcName: string, inlineTeam?: string, blockTeam?: string) =>
|
|
143
|
-
|
|
152
|
+
BOUNDED_CONTEXT_TEAM_CONFLICT: (bcName: string, inlineTeam?: string, blockTeam?: string) => {
|
|
153
|
+
const inlinePart = inlineTeam ? ` ('by ${inlineTeam}')` : '';
|
|
154
|
+
const blockPart = blockTeam ? ` ('team: ${blockTeam}')` : '';
|
|
155
|
+
return `Team specified both inline${inlinePart} and in block${blockPart}. Inline value takes precedence.`;
|
|
156
|
+
},
|
|
144
157
|
|
|
145
158
|
/**
|
|
146
159
|
* Error message when an element is defined multiple times.
|
|
@@ -262,6 +275,14 @@ export const ValidationMessages = {
|
|
|
262
275
|
`Local path dependency '${alias}' escapes workspace boundary.\n` +
|
|
263
276
|
`Hint: Local dependencies must be within the workspace. Consider moving the dependency or using a git-based source.`,
|
|
264
277
|
|
|
278
|
+
/**
|
|
279
|
+
* Error when import path cannot be resolved to a file.
|
|
280
|
+
* @param uri - The import URI that couldn't be resolved
|
|
281
|
+
*/
|
|
282
|
+
IMPORT_UNRESOLVED: (uri: string) =>
|
|
283
|
+
`Cannot resolve import '${uri}'.\n` +
|
|
284
|
+
`Hint: Check that the file exists and the path is correct.`,
|
|
285
|
+
|
|
265
286
|
// ========================================================================
|
|
266
287
|
// Context Map & Domain Map Validation
|
|
267
288
|
// ========================================================================
|
|
@@ -283,6 +304,14 @@ export const ValidationMessages = {
|
|
|
283
304
|
`Context Map '${name}' contains ${count} contexts but no documented relationships.\n` +
|
|
284
305
|
`Hint: Add relationships to show how contexts integrate (e.g., '[OHS] A -> [CF] B').`,
|
|
285
306
|
|
|
307
|
+
/**
|
|
308
|
+
* Warning when a context map contains duplicate relationships.
|
|
309
|
+
* @param leftContext - Name of the left context
|
|
310
|
+
* @param rightContext - Name of the right context
|
|
311
|
+
*/
|
|
312
|
+
CONTEXT_MAP_DUPLICATE_RELATIONSHIP: (leftContext: string, rightContext: string) =>
|
|
313
|
+
`Duplicate relationship between '${leftContext}' and '${rightContext}' in context map.`,
|
|
314
|
+
|
|
286
315
|
/**
|
|
287
316
|
* Warning when domain map contains no domains.
|
|
288
317
|
* @param name - The domain map name
|
|
@@ -291,6 +320,18 @@ export const ValidationMessages = {
|
|
|
291
320
|
`Domain Map '${name}' contains no domains.\n` +
|
|
292
321
|
`Hint: Use 'contains DomainA, DomainB' to specify which domains are in the map.`,
|
|
293
322
|
|
|
323
|
+
// ========================================================================
|
|
324
|
+
// Reference Resolution Validation
|
|
325
|
+
// ========================================================================
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* Error when a reference cannot be resolved (for MultiReferences).
|
|
329
|
+
* @param type - The type being referenced (e.g., 'BoundedContext')
|
|
330
|
+
* @param name - The unresolved name
|
|
331
|
+
*/
|
|
332
|
+
UNRESOLVED_REFERENCE: (type: string, name: string) =>
|
|
333
|
+
`Could not resolve reference to ${type} named '${name}'.`,
|
|
334
|
+
|
|
294
335
|
// ========================================================================
|
|
295
336
|
// Metadata Validation
|
|
296
337
|
// ========================================================================
|