@domainlang/language 0.10.0 → 0.11.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 (65) hide show
  1. package/README.md +44 -102
  2. package/out/domain-lang-module.d.ts +2 -2
  3. package/out/domain-lang-module.js +2 -2
  4. package/out/domain-lang-module.js.map +1 -1
  5. package/out/index.d.ts +3 -0
  6. package/out/index.js +3 -0
  7. package/out/index.js.map +1 -1
  8. package/out/lsp/domain-lang-completion.js +1 -1
  9. package/out/lsp/domain-lang-completion.js.map +1 -1
  10. package/out/lsp/domain-lang-index-manager.d.ts +149 -5
  11. package/out/lsp/domain-lang-index-manager.js +388 -52
  12. package/out/lsp/domain-lang-index-manager.js.map +1 -1
  13. package/out/lsp/domain-lang-refresh.d.ts +35 -0
  14. package/out/lsp/domain-lang-refresh.js +129 -0
  15. package/out/lsp/domain-lang-refresh.js.map +1 -0
  16. package/out/lsp/domain-lang-workspace-manager.d.ts +10 -0
  17. package/out/lsp/domain-lang-workspace-manager.js +35 -0
  18. package/out/lsp/domain-lang-workspace-manager.js.map +1 -1
  19. package/out/main.js +30 -190
  20. package/out/main.js.map +1 -1
  21. package/out/sdk/loader-node.js +1 -1
  22. package/out/sdk/loader-node.js.map +1 -1
  23. package/out/sdk/validator.js +17 -14
  24. package/out/sdk/validator.js.map +1 -1
  25. package/out/services/import-resolver.d.ts +67 -17
  26. package/out/services/import-resolver.js +146 -65
  27. package/out/services/import-resolver.js.map +1 -1
  28. package/out/services/lsp-logger.d.ts +42 -0
  29. package/out/services/lsp-logger.js +50 -0
  30. package/out/services/lsp-logger.js.map +1 -0
  31. package/out/services/lsp-runtime-settings.d.ts +20 -0
  32. package/out/services/lsp-runtime-settings.js +20 -0
  33. package/out/services/lsp-runtime-settings.js.map +1 -0
  34. package/out/services/performance-optimizer.d.ts +9 -9
  35. package/out/services/performance-optimizer.js +17 -41
  36. package/out/services/performance-optimizer.js.map +1 -1
  37. package/out/services/workspace-manager.d.ts +22 -1
  38. package/out/services/workspace-manager.js +57 -9
  39. package/out/services/workspace-manager.js.map +1 -1
  40. package/out/utils/import-utils.js +6 -6
  41. package/out/utils/import-utils.js.map +1 -1
  42. package/out/validation/constants.d.ts +6 -0
  43. package/out/validation/constants.js +7 -0
  44. package/out/validation/constants.js.map +1 -1
  45. package/out/validation/import.d.ts +13 -3
  46. package/out/validation/import.js +54 -10
  47. package/out/validation/import.js.map +1 -1
  48. package/package.json +1 -1
  49. package/src/domain-lang-module.ts +3 -3
  50. package/src/index.ts +3 -0
  51. package/src/lsp/domain-lang-completion.ts +3 -3
  52. package/src/lsp/domain-lang-index-manager.ts +438 -56
  53. package/src/lsp/domain-lang-refresh.ts +205 -0
  54. package/src/lsp/domain-lang-workspace-manager.ts +45 -0
  55. package/src/main.ts +36 -244
  56. package/src/sdk/loader-node.ts +1 -1
  57. package/src/sdk/validator.ts +17 -13
  58. package/src/services/import-resolver.ts +196 -89
  59. package/src/services/lsp-logger.ts +89 -0
  60. package/src/services/lsp-runtime-settings.ts +34 -0
  61. package/src/services/performance-optimizer.ts +18 -57
  62. package/src/services/workspace-manager.ts +62 -10
  63. package/src/utils/import-utils.ts +6 -6
  64. package/src/validation/constants.ts +9 -0
  65. package/src/validation/import.ts +67 -12
@@ -0,0 +1,205 @@
1
+ import path from 'node:path';
2
+ import { URI } from 'langium';
3
+ import type { LangiumSharedServices } from 'langium/lsp';
4
+ import { FileChangeType, type DidChangeWatchedFilesParams, type FileEvent } from 'vscode-languageserver-protocol';
5
+ import type { DomainLangServices } from '../domain-lang-module.js';
6
+ import { DomainLangIndexManager } from './domain-lang-index-manager.js';
7
+
8
+ /**
9
+ * Categorized file change events.
10
+ * Only config files need explicit handling — .dlang changes are handled
11
+ * by Langium's built-in `DocumentBuilder.update()` → `isAffected()` pipeline,
12
+ * which DomainLangIndexManager enhances with transitive import dependency tracking.
13
+ */
14
+ interface CategorizedChanges {
15
+ readonly manifestChanges: Array<{ uri: string; type: FileChangeType }>;
16
+ readonly lockFileChanges: Array<{ uri: string; type: FileChangeType }>;
17
+ }
18
+
19
+ export interface DomainLangRefreshHooks {
20
+ onManifestChanged?: (change: { uri: string; type: FileChangeType }) => Promise<void> | void;
21
+ onManifestDeleted?: (uri: string) => Promise<void> | void;
22
+ }
23
+
24
+ export interface DomainLangRefreshOptions {
25
+ dedupeWindowMs?: number;
26
+ }
27
+
28
+ export interface RefreshOutcome {
29
+ readonly configChanged: boolean;
30
+ readonly fullRebuildTriggered: boolean;
31
+ }
32
+
33
+ class RecentChangeDeduper {
34
+ private readonly dedupeWindowMs: number;
35
+ private readonly seen = new Map<string, number>();
36
+
37
+ constructor(dedupeWindowMs = 300) {
38
+ this.dedupeWindowMs = dedupeWindowMs;
39
+ }
40
+
41
+ dedupe(changes: FileEvent[]): FileEvent[] {
42
+ const now = Date.now();
43
+ const filtered: FileEvent[] = [];
44
+
45
+ for (const [key, timestamp] of this.seen.entries()) {
46
+ if (now - timestamp > this.dedupeWindowMs * 4) {
47
+ this.seen.delete(key);
48
+ }
49
+ }
50
+
51
+ for (const change of changes) {
52
+ const key = `${change.uri}|${change.type}`;
53
+ const previous = this.seen.get(key);
54
+ if (previous !== undefined && now - previous < this.dedupeWindowMs) {
55
+ continue;
56
+ }
57
+ this.seen.set(key, now);
58
+ filtered.push(change);
59
+ }
60
+
61
+ return filtered;
62
+ }
63
+ }
64
+
65
+ export function registerDomainLangRefresh(
66
+ shared: LangiumSharedServices,
67
+ domainLang: DomainLangServices,
68
+ hooks: DomainLangRefreshHooks = {},
69
+ options: DomainLangRefreshOptions = {}
70
+ ): { dispose(): void } {
71
+ const deduper = new RecentChangeDeduper(options.dedupeWindowMs);
72
+
73
+ return shared.lsp.DocumentUpdateHandler.onWatchedFilesChange(async (params: DidChangeWatchedFilesParams) => {
74
+ try {
75
+ const dedupedChanges = deduper.dedupe(params.changes);
76
+ if (dedupedChanges.length === 0) {
77
+ return;
78
+ }
79
+
80
+ await processWatchedFileChanges(
81
+ { changes: dedupedChanges },
82
+ shared,
83
+ domainLang,
84
+ hooks,
85
+ );
86
+ } catch (error) {
87
+ const message = error instanceof Error ? error.message : String(error);
88
+ console.error(`Error handling watched file changes: ${message}`);
89
+ }
90
+ });
91
+ }
92
+
93
+ /**
94
+ * Processes watched file change events.
95
+ *
96
+ * **Architecture:**
97
+ * - `.dlang` changes are handled entirely by Langium's own `DocumentBuilder.update()`
98
+ * pipeline. `DomainLangIndexManager.isAffected()` provides transitive import
99
+ * dependency tracking and specifier-sensitive matching, so Langium's single
100
+ * `update()` call propagates changes correctly through the import graph.
101
+ *
102
+ * - Config changes (model.yaml, model.lock) need explicit handling because Langium
103
+ * ignores non-language files (they have no LangiumDocument). Config changes
104
+ * invalidate caches and trigger a full rebuild of all loaded documents, routed
105
+ * through the workspace lock to serialize with Langium's own updates.
106
+ */
107
+ export async function processWatchedFileChanges(
108
+ params: DidChangeWatchedFilesParams,
109
+ shared: LangiumSharedServices,
110
+ domainLang: DomainLangServices,
111
+ hooks: DomainLangRefreshHooks = {}
112
+ ): Promise<RefreshOutcome> {
113
+ const categorized = categorizeChanges(params);
114
+ const hasConfigChanges = categorized.manifestChanges.length > 0
115
+ || categorized.lockFileChanges.length > 0;
116
+
117
+ if (!hasConfigChanges) {
118
+ // .dlang changes handled by Langium's update() → isAffected() pipeline.
119
+ // DomainLangIndexManager.isAffected() checks transitive import deps
120
+ // and specifier-sensitive matching — no second update() needed.
121
+ return { configChanged: false, fullRebuildTriggered: false };
122
+ }
123
+
124
+ // Config changes need explicit handling: invalidate caches, then rebuild.
125
+ // Route through the workspace lock to serialize with Langium's own updates.
126
+ const indexManager = shared.workspace.IndexManager as DomainLangIndexManager;
127
+
128
+ await shared.workspace.WorkspaceLock.write(async (token) => {
129
+ // 1. Invalidate caches
130
+ await processManifestChanges(categorized.manifestChanges, domainLang, hooks);
131
+ await processLockFileChanges(categorized.lockFileChanges, domainLang);
132
+ domainLang.imports.ImportResolver.clearCache();
133
+ indexManager.clearImportDependencies();
134
+
135
+ // 2. Rebuild ALL loaded documents — config changes affect all imports
136
+ const allDocUris = shared.workspace.LangiumDocuments.all
137
+ .map(doc => doc.uri)
138
+ .toArray();
139
+
140
+ if (allDocUris.length > 0) {
141
+ await shared.workspace.DocumentBuilder.update(allDocUris, [], token);
142
+ }
143
+ });
144
+
145
+ return { configChanged: true, fullRebuildTriggered: true };
146
+ }
147
+
148
+ function categorizeChanges(params: DidChangeWatchedFilesParams): CategorizedChanges {
149
+ const manifestChanges: Array<{ uri: string; type: FileChangeType }> = [];
150
+ const lockFileChanges: Array<{ uri: string; type: FileChangeType }> = [];
151
+
152
+ for (const change of params.changes) {
153
+ const uri = URI.parse(change.uri);
154
+ const fileName = path.basename(uri.path).toLowerCase();
155
+
156
+ if (fileName === 'model.yaml') {
157
+ manifestChanges.push({ uri: change.uri, type: change.type });
158
+ } else if (fileName === 'model.lock') {
159
+ lockFileChanges.push({ uri: change.uri, type: change.type });
160
+ }
161
+ }
162
+
163
+ return { manifestChanges, lockFileChanges };
164
+ }
165
+
166
+ async function processManifestChanges(
167
+ manifestChanges: Array<{ uri: string; type: FileChangeType }>,
168
+ domainLang: DomainLangServices,
169
+ hooks: DomainLangRefreshHooks,
170
+ ): Promise<void> {
171
+ for (const change of manifestChanges) {
172
+ domainLang.imports.ManifestManager.invalidateManifestCache();
173
+
174
+ // R11: Update workspace layout cache for the manifest's directory
175
+ const manifestDir = path.dirname(URI.parse(change.uri).fsPath);
176
+ domainLang.imports.ManifestManager.onManifestEvent(
177
+ manifestDir,
178
+ change.type !== FileChangeType.Deleted,
179
+ );
180
+
181
+ if (change.type === FileChangeType.Deleted) {
182
+ if (hooks.onManifestDeleted) {
183
+ await hooks.onManifestDeleted(change.uri);
184
+ }
185
+ continue;
186
+ }
187
+
188
+ if (hooks.onManifestChanged) {
189
+ await hooks.onManifestChanged(change);
190
+ }
191
+ }
192
+ }
193
+
194
+ async function processLockFileChanges(
195
+ lockFileChanges: Array<{ uri: string; type: FileChangeType }>,
196
+ domainLang: DomainLangServices,
197
+ ): Promise<void> {
198
+ for (const change of lockFileChanges) {
199
+ if (change.type === FileChangeType.Deleted) {
200
+ domainLang.imports.ManifestManager.invalidateLockCache();
201
+ continue;
202
+ }
203
+ await domainLang.imports.ManifestManager.refreshLockFile();
204
+ }
205
+ }
@@ -2,6 +2,7 @@ import fs from 'node:fs/promises';
2
2
  import path from 'node:path';
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
+ import type { Connection } from 'vscode-languageserver';
5
6
  import { ensureImportGraphFromDocument } from '../utils/import-utils.js';
6
7
  import { findManifestsInDirectories } from '../utils/manifest-utils.js';
7
8
  import type { ImportResolver } from '../services/import-resolver.js';
@@ -56,6 +57,12 @@ import type { DomainLangServices } from '../domain-lang-module.js';
56
57
  export class DomainLangWorkspaceManager extends DefaultWorkspaceManager {
57
58
  private readonly sharedServices: LangiumSharedCoreServices;
58
59
 
60
+ /**
61
+ * LSP connection for progress reporting (PRS-017 R7).
62
+ * Optional because the workspace manager can run in non-LSP contexts.
63
+ */
64
+ private readonly connection: Connection | undefined;
65
+
59
66
  /**
60
67
  * DI-injected import resolver. Set via late-binding because
61
68
  * WorkspaceManager (shared module) is created before ImportResolver (language module).
@@ -66,6 +73,9 @@ export class DomainLangWorkspaceManager extends DefaultWorkspaceManager {
66
73
  constructor(services: LangiumSharedCoreServices) {
67
74
  super(services);
68
75
  this.sharedServices = services;
76
+ // Attempt to access connection from LSP services (cast to full shared services)
77
+ const lspServices = services as { lsp?: { Connection?: Connection } };
78
+ this.connection = lspServices.lsp?.Connection;
69
79
  }
70
80
 
71
81
  /**
@@ -90,6 +100,8 @@ export class DomainLangWorkspaceManager extends DefaultWorkspaceManager {
90
100
  }
91
101
 
92
102
  protected override async loadAdditionalDocuments(folders: WorkspaceFolder[], collector: (document: LangiumDocument) => void): Promise<void> {
103
+ const progress = await this.createProgress('DomainLang: Indexing workspace');
104
+
93
105
  // Find ALL model.yaml files in workspace (supports mixed mode)
94
106
  const manifestInfos = await this.findAllManifestsInFolders(folders);
95
107
 
@@ -98,9 +110,14 @@ export class DomainLangWorkspaceManager extends DefaultWorkspaceManager {
98
110
  manifestInfos.map(m => path.dirname(m.manifestPath))
99
111
  );
100
112
 
113
+ progress?.report(`Found ${manifestInfos.length} module(s)`);
114
+
101
115
  // Mode A or Mode C: Load each module's entry + import graph
116
+ let moduleIdx = 0;
102
117
  for (const manifestInfo of manifestInfos) {
118
+ moduleIdx++;
103
119
  try {
120
+ progress?.report(`Loading module ${moduleIdx}/${manifestInfos.length}`);
104
121
  const entryUri = URI.file(manifestInfo.entryPath);
105
122
  const entryDoc = await this.langiumDocuments.getOrCreateDocument(entryUri);
106
123
  collector(entryDoc);
@@ -134,7 +151,9 @@ export class DomainLangWorkspaceManager extends DefaultWorkspaceManager {
134
151
 
135
152
  // Load standalone .dlang files in workspace root folders
136
153
  // These are files NOT covered by any module's import graph
154
+ progress?.report('Loading standalone files');
137
155
  await this.loadStandaloneFiles(folders, moduleDirectories, collector);
156
+ progress?.done();
138
157
  }
139
158
 
140
159
  /**
@@ -307,4 +326,30 @@ export class DomainLangWorkspaceManager extends DefaultWorkspaceManager {
307
326
  await visit(document);
308
327
  return visited;
309
328
  }
329
+
330
+ // --- PRS-017 R7: Progress reporting ---
331
+
332
+ /**
333
+ * Creates an LSP work-done progress reporter.
334
+ * Returns undefined in non-LSP contexts (no connection).
335
+ */
336
+ private async createProgress(title: string): Promise<{ report(message: string): void; done(): void } | undefined> {
337
+ if (!this.connection) return undefined;
338
+
339
+ try {
340
+ const reporter = await this.connection.window.createWorkDoneProgress();
341
+ reporter.begin(title);
342
+ return {
343
+ report: (message: string) => {
344
+ reporter.report(message);
345
+ },
346
+ done: () => {
347
+ reporter.done();
348
+ }
349
+ };
350
+ } catch {
351
+ // Client may not support progress — degrade gracefully
352
+ return undefined;
353
+ }
354
+ }
310
355
  }
package/src/main.ts CHANGED
@@ -1,11 +1,12 @@
1
1
  import { startLanguageServer } from 'langium/lsp';
2
2
  import { NodeFileSystem } from 'langium/node';
3
- import { createConnection, ProposedFeatures, FileChangeType } from 'vscode-languageserver/node.js';
3
+ import { createConnection, ProposedFeatures } from 'vscode-languageserver/node.js';
4
4
  import { createDomainLangServices } from './domain-lang-module.js';
5
5
  import { ensureImportGraphFromEntryFile } from './utils/import-utils.js';
6
- import { DomainLangIndexManager } from './lsp/domain-lang-index-manager.js';
6
+ import { registerDomainLangRefresh } from './lsp/domain-lang-refresh.js';
7
7
  import { registerToolHandlers } from './lsp/tool-handlers.js';
8
8
  import { URI } from 'langium';
9
+ import { setLspRuntimeSettings } from './services/lsp-runtime-settings.js';
9
10
 
10
11
  // Create a connection to the client
11
12
  const connection = createConnection(ProposedFeatures.all);
@@ -16,6 +17,14 @@ const { shared, DomainLang } = createDomainLangServices({ connection, ...NodeFil
16
17
  // Register custom LSP request handlers for VS Code Language Model Tools (PRS-015)
17
18
  registerToolHandlers(connection, shared);
18
19
 
20
+ shared.lsp.LanguageServer.onInitialize((params) => {
21
+ applyLspSettings(params.initializationOptions);
22
+ });
23
+
24
+ connection.onDidChangeConfiguration((params) => {
25
+ applyLspSettings(params.settings);
26
+ });
27
+
19
28
  // Initialize workspace manager when language server initializes
20
29
  // Uses Langium's LanguageServer.onInitialize hook (not raw connection handler)
21
30
  // This integrates properly with Langium's initialization flow
@@ -29,7 +38,7 @@ shared.lsp.LanguageServer.onInitialize((params) => {
29
38
  if (workspaceRoot) {
30
39
  // Initialize workspace manager synchronously (just sets root path)
31
40
  // Heavy work happens in initializeWorkspace() called by Langium later
32
- const workspaceManager = DomainLang.imports.WorkspaceManager;
41
+ const workspaceManager = DomainLang.imports.ManifestManager;
33
42
  workspaceManager.initialize(workspaceRoot).catch(error => {
34
43
  const message = error instanceof Error ? error.message : String(error);
35
44
  console.warn(`Failed to initialize workspace: ${message}`);
@@ -39,247 +48,7 @@ shared.lsp.LanguageServer.onInitialize((params) => {
39
48
  }
40
49
  });
41
50
 
42
- // Handle file changes for model.yaml and model.lock (PRS-010)
43
- // Uses Langium's built-in file watcher which already watches **/* in workspace
44
- // This invalidates caches when config files change externally
45
- shared.lsp.DocumentUpdateHandler?.onWatchedFilesChange(async (params) => {
46
- try {
47
- await handleFileChanges(params, DomainLang.imports.WorkspaceManager, shared, DomainLang);
48
- } catch (error) {
49
- const message = error instanceof Error ? error.message : String(error);
50
- console.error(`Error handling file change notification: ${message}`);
51
- // Continue - don't crash the server
52
- }
53
- });
54
-
55
- /** Categorized file changes */
56
- interface CategorizedChanges {
57
- manifestChanged: boolean;
58
- lockFileChanged: boolean;
59
- changedDlangUris: Set<string>;
60
- deletedDlangUris: Set<string>;
61
- createdDlangUris: Set<string>;
62
- }
63
-
64
- /**
65
- * Categorizes file changes by type.
66
- */
67
- function categorizeChanges(
68
- params: { changes: Array<{ uri: string; type: number }> },
69
- workspaceManager: typeof DomainLang.imports.WorkspaceManager,
70
- langServices: typeof DomainLang,
71
- indexManager: DomainLangIndexManager
72
- ): CategorizedChanges {
73
- const result: CategorizedChanges = {
74
- manifestChanged: false,
75
- lockFileChanged: false,
76
- changedDlangUris: new Set(),
77
- deletedDlangUris: new Set(),
78
- createdDlangUris: new Set()
79
- };
80
-
81
- for (const change of params.changes) {
82
- const uri = URI.parse(change.uri);
83
- const fileName = uri.path.split('/').pop() ?? '';
84
- const uriString = change.uri;
85
-
86
- if (fileName === 'model.yaml') {
87
- console.warn(`model.yaml changed: ${uriString}`);
88
- workspaceManager.invalidateManifestCache();
89
- langServices.imports.ImportResolver.clearCache();
90
- indexManager.clearImportDependencies();
91
- result.manifestChanged = true;
92
- } else if (fileName === 'model.lock') {
93
- console.warn(`model.lock changed: ${uriString}`);
94
- langServices.imports.ImportResolver.clearCache();
95
- indexManager.clearImportDependencies();
96
- result.lockFileChanged = true;
97
- } else if (fileName.endsWith('.dlang')) {
98
- if (change.type === FileChangeType.Deleted) {
99
- result.deletedDlangUris.add(uriString);
100
- console.warn(`DomainLang file deleted: ${uriString}`);
101
- } else if (change.type === FileChangeType.Created) {
102
- result.createdDlangUris.add(uriString);
103
- console.warn(`DomainLang file created: ${uriString}`);
104
- } else {
105
- result.changedDlangUris.add(uriString);
106
- console.warn(`DomainLang file changed: ${uriString}`);
107
- }
108
- }
109
- }
110
-
111
- return result;
112
- }
113
-
114
- /**
115
- * Rebuilds documents that depend on changed/deleted/created .dlang files.
116
- */
117
- async function rebuildAffectedDocuments(
118
- changes: CategorizedChanges,
119
- indexManager: DomainLangIndexManager,
120
- sharedServices: typeof shared,
121
- langServices: typeof DomainLang
122
- ): Promise<void> {
123
- const hasChanges = changes.changedDlangUris.size > 0 ||
124
- changes.deletedDlangUris.size > 0 ||
125
- changes.createdDlangUris.size > 0;
126
- if (!hasChanges) {
127
- return;
128
- }
129
-
130
- // CRITICAL: Clear ImportResolver cache BEFORE rebuilding.
131
- // The WorkspaceCache only clears AFTER linking, but resolution happens
132
- // DURING linking. Without this, stale cached resolutions would be used.
133
- langServices.imports.ImportResolver.clearCache();
134
-
135
- const affectedUris = collectAffectedDocuments(changes, indexManager);
136
-
137
- if (affectedUris.size === 0) {
138
- return;
139
- }
140
-
141
- console.warn(`Rebuilding ${affectedUris.size} documents affected by file changes`);
142
-
143
- const langiumDocuments = sharedServices.workspace.LangiumDocuments;
144
- const affectedDocs: URI[] = [];
145
-
146
- for (const uriString of affectedUris) {
147
- const uri = URI.parse(uriString);
148
- if (langiumDocuments.hasDocument(uri)) {
149
- affectedDocs.push(uri);
150
- indexManager.markForReprocessing(uriString);
151
- }
152
- }
153
-
154
- const deletedUriObjects = [...changes.deletedDlangUris].map(u => URI.parse(u));
155
- if (affectedDocs.length > 0 || deletedUriObjects.length > 0) {
156
- await sharedServices.workspace.DocumentBuilder.update(affectedDocs, deletedUriObjects);
157
- }
158
- }
159
-
160
- /**
161
- * Collects all document URIs that should be rebuilt based on the changes.
162
- *
163
- * Uses targeted matching to avoid expensive full rebuilds:
164
- * - For edits: rebuild documents that import the changed file (by resolved URI)
165
- * - For all changes: rebuild documents whose import specifiers match the path
166
- *
167
- * The specifier matching handles renamed/moved/created files by comparing
168
- * import specifiers against path segments (filename, parent/filename, etc.).
169
- */
170
- function collectAffectedDocuments(
171
- changes: CategorizedChanges,
172
- indexManager: DomainLangIndexManager
173
- ): Set<string> {
174
- const allChangedUris = new Set([
175
- ...changes.changedDlangUris,
176
- ...changes.deletedDlangUris,
177
- ...changes.createdDlangUris
178
- ]);
179
-
180
- // Get documents affected by resolved URI changes (edits to imported files)
181
- const affectedByUri = indexManager.getAllAffectedDocuments(allChangedUris);
182
-
183
- // Get documents with import specifiers that match changed paths
184
- // This catches:
185
- // - File moves/renames: specifiers that previously resolved but now won't
186
- // - File creations: specifiers that previously failed but might now resolve
187
- // Uses fuzzy matching on path segments rather than rebuilding all imports
188
- const affectedBySpecifier = indexManager.getDocumentsWithPotentiallyAffectedImports(allChangedUris);
189
-
190
- return new Set([...affectedByUri, ...affectedBySpecifier]);
191
- }
192
-
193
- /**
194
- * Handles all file changes including .dlang files, model.yaml, and model.lock.
195
- *
196
- * For .dlang files: rebuilds all documents that import the changed file.
197
- * For config files: invalidates caches and rebuilds workspace as needed.
198
- */
199
- async function handleFileChanges(
200
- params: { changes: Array<{ uri: string; type: number }> },
201
- workspaceManager: typeof DomainLang.imports.WorkspaceManager,
202
- sharedServices: typeof shared,
203
- langServices: typeof DomainLang
204
- ): Promise<void> {
205
- const indexManager = sharedServices.workspace.IndexManager as DomainLangIndexManager;
206
-
207
- // Categorize and process changes
208
- const changes = categorizeChanges(params, workspaceManager, langServices, indexManager);
209
-
210
- // Handle lock file changes
211
- if (changes.lockFileChanged) {
212
- const lockChange = params.changes.find(c => c.uri.endsWith('model.lock'));
213
- if (lockChange) {
214
- await handleLockFileChange(lockChange, workspaceManager);
215
- }
216
- }
217
-
218
- // Rebuild documents affected by .dlang file changes
219
- await rebuildAffectedDocuments(changes, indexManager, sharedServices, langServices);
220
-
221
- // Handle config file changes
222
- if (changes.manifestChanged || changes.lockFileChanged) {
223
- await rebuildWorkspace(sharedServices, workspaceManager, changes.manifestChanged);
224
- }
225
- }
226
-
227
- /**
228
- * Handles lock file creation, change, or deletion.
229
- */
230
- async function handleLockFileChange(
231
- change: { uri: string; type: number },
232
- workspaceManager: typeof DomainLang.imports.WorkspaceManager
233
- ): Promise<void> {
234
- if (change.type === FileChangeType.Changed || change.type === FileChangeType.Created) {
235
- await workspaceManager.refreshLockFile();
236
- } else if (change.type === FileChangeType.Deleted) {
237
- workspaceManager.invalidateLockCache();
238
- }
239
- }
240
-
241
- /**
242
- * Rebuilds the workspace after config file changes.
243
- * Uses incremental strategy: only full rebuild if dependencies changed.
244
- *
245
- * @param sharedServices - Shared Langium services
246
- * @param workspaceManager - Workspace manager for manifest access
247
- * @param manifestChanged - Whether model.yaml changed (vs just model.lock)
248
- */
249
- async function rebuildWorkspace(
250
- sharedServices: typeof shared,
251
- workspaceManager: typeof DomainLang.imports.WorkspaceManager,
252
- manifestChanged: boolean
253
- ): Promise<void> {
254
- try {
255
- // If only lock file changed, caches are already invalidated - no rebuild needed
256
- // Lock file changes mean resolved versions changed, but import resolver cache is cleared
257
- // Documents will re-resolve imports on next access
258
- if (!manifestChanged) {
259
- console.warn('Lock file changed - caches invalidated, no rebuild needed');
260
- return;
261
- }
262
-
263
- // For manifest changes, check if dependencies section actually changed
264
- // If only metadata changed (name, version, etc.), no rebuild needed
265
- const manifest = await workspaceManager.getManifest();
266
- const hasDependencies = manifest?.dependencies && Object.keys(manifest.dependencies).length > 0;
267
-
268
- if (!hasDependencies) {
269
- console.warn('Manifest changed but has no dependencies - skipping rebuild');
270
- return;
271
- }
272
-
273
- // Dependencies exist and manifest changed - do full rebuild
274
- const documents = sharedServices.workspace.LangiumDocuments.all.toArray();
275
- const uris = documents.map(doc => doc.uri);
276
- await sharedServices.workspace.DocumentBuilder.update([], uris);
277
- console.warn(`Workspace rebuilt: ${documents.length} documents revalidated`);
278
- } catch (error) {
279
- const message = error instanceof Error ? error.message : String(error);
280
- console.error(`Failed to rebuild workspace: ${message}`);
281
- }
282
- }
51
+ registerDomainLangRefresh(shared, DomainLang);
283
52
 
284
53
  // Optionally start from a single entry file and follow imports.
285
54
  // Configure via env DOMAINLANG_ENTRY (absolute or workspace-relative path)
@@ -331,3 +100,26 @@ if (entryFile) {
331
100
  // No entry file configured: start normally
332
101
  startLanguageServer(shared);
333
102
  }
103
+
104
+ function applyLspSettings(settings: unknown): void {
105
+ const lsp = extractLspSettings(settings);
106
+ setLspRuntimeSettings({
107
+ traceImports: lsp.traceImports,
108
+ infoLogs: lsp.infoLogs,
109
+ });
110
+ }
111
+
112
+ function extractLspSettings(source: unknown): { traceImports: boolean; infoLogs: boolean } {
113
+ const root = isRecord(source) ? source : {};
114
+ const domainlang = isRecord(root.domainlang) ? root.domainlang : root;
115
+ const lsp = isRecord(domainlang.lsp) ? domainlang.lsp : {};
116
+
117
+ return {
118
+ traceImports: typeof lsp.traceImports === 'boolean' ? lsp.traceImports : false,
119
+ infoLogs: typeof lsp.infoLogs === 'boolean' ? lsp.infoLogs : false,
120
+ };
121
+ }
122
+
123
+ function isRecord(value: unknown): value is Record<string, unknown> {
124
+ return typeof value === 'object' && value !== null;
125
+ }
@@ -77,7 +77,7 @@ export async function loadModel(
77
77
 
78
78
  // Initialize workspace if directory provided
79
79
  if (options?.workspaceDir) {
80
- const workspaceManager = services.imports.WorkspaceManager;
80
+ const workspaceManager = services.imports.ManifestManager;
81
81
  await workspaceManager.initialize(options.workspaceDir);
82
82
  }
83
83
 
@@ -130,8 +130,8 @@ export async function validateFile(
130
130
 
131
131
  // Initialize workspace with the specified directory or file's directory
132
132
  const workspaceDir = options.workspaceDir ?? dirname(absolutePath);
133
- const workspaceManager = services.imports.WorkspaceManager;
134
- await workspaceManager.initialize(workspaceDir);
133
+ const manifestManager = services.imports.ManifestManager;
134
+ await manifestManager.initialize(workspaceDir);
135
135
 
136
136
  // Load and parse the document
137
137
  const uri = URI.file(absolutePath);
@@ -152,17 +152,21 @@ export async function validateFile(
152
152
  const allDocuments = Array.from(shared.workspace.LangiumDocuments.all);
153
153
  await shared.workspace.DocumentBuilder.build(allDocuments, { validation: true });
154
154
 
155
- // Collect diagnostics from the entry document
156
- const diagnostics = document.diagnostics ?? [];
155
+ // Collect diagnostics from all loaded documents (entry + imports)
157
156
  const errors: ValidationDiagnostic[] = [];
158
157
  const warnings: ValidationDiagnostic[] = [];
159
158
 
160
- for (const diagnostic of diagnostics) {
161
- const validationDiag = toValidationDiagnostic(diagnostic, absolutePath);
162
- if (diagnostic.severity === 1) {
163
- errors.push(validationDiag);
164
- } else if (diagnostic.severity === 2) {
165
- warnings.push(validationDiag);
159
+ for (const doc of allDocuments) {
160
+ const diagnostics = doc.diagnostics ?? [];
161
+ const diagnosticFile = doc.uri.fsPath;
162
+
163
+ for (const diagnostic of diagnostics) {
164
+ const validationDiag = toValidationDiagnostic(diagnostic, diagnosticFile);
165
+ if (diagnostic.severity === 1) {
166
+ errors.push(validationDiag);
167
+ } else if (diagnostic.severity === 2) {
168
+ warnings.push(validationDiag);
169
+ }
166
170
  }
167
171
  }
168
172
 
@@ -262,18 +266,18 @@ export async function validateWorkspace(
262
266
  const servicesObj = createDomainLangServices(NodeFileSystem);
263
267
  const shared = servicesObj.shared;
264
268
  const services = servicesObj.DomainLang;
265
- const workspaceManager = services.imports.WorkspaceManager;
269
+ const manifestManager = services.imports.ManifestManager;
266
270
 
267
271
  try {
268
272
  // Initialize workspace - this will find and load model.yaml
269
- await workspaceManager.initialize(absolutePath);
273
+ await manifestManager.initialize(absolutePath);
270
274
  } catch (error) {
271
275
  const message = error instanceof Error ? error.message : String(error);
272
276
  throw new Error(`Failed to initialize workspace at ${workspaceDir}: ${message}`);
273
277
  }
274
278
 
275
279
  // Get the manifest to find the entry file
276
- const manifest = await workspaceManager.getManifest();
280
+ const manifest = await manifestManager.getManifest();
277
281
  let entryFile = 'index.dlang';
278
282
 
279
283
  if (manifest?.model?.entry) {