@domainlang/language 0.10.0 → 0.12.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 (87) hide show
  1. package/README.md +44 -102
  2. package/out/diagram/context-map-diagram-generator.d.ts +65 -0
  3. package/out/diagram/context-map-diagram-generator.js +356 -0
  4. package/out/diagram/context-map-diagram-generator.js.map +1 -0
  5. package/out/diagram/context-map-layout-configurator.d.ts +15 -0
  6. package/out/diagram/context-map-layout-configurator.js +39 -0
  7. package/out/diagram/context-map-layout-configurator.js.map +1 -0
  8. package/out/diagram/elk-layout-factory.d.ts +43 -0
  9. package/out/diagram/elk-layout-factory.js +64 -0
  10. package/out/diagram/elk-layout-factory.js.map +1 -0
  11. package/out/domain-lang-module.d.ts +9 -2
  12. package/out/domain-lang-module.js +13 -4
  13. package/out/domain-lang-module.js.map +1 -1
  14. package/out/index.d.ts +6 -0
  15. package/out/index.js +7 -0
  16. package/out/index.js.map +1 -1
  17. package/out/lsp/domain-lang-code-lens-provider.d.ts +8 -0
  18. package/out/lsp/domain-lang-code-lens-provider.js +48 -0
  19. package/out/lsp/domain-lang-code-lens-provider.js.map +1 -0
  20. package/out/lsp/domain-lang-completion.js +1 -1
  21. package/out/lsp/domain-lang-completion.js.map +1 -1
  22. package/out/lsp/domain-lang-document-symbol-provider.js +5 -5
  23. package/out/lsp/domain-lang-document-symbol-provider.js.map +1 -1
  24. package/out/lsp/domain-lang-index-manager.d.ts +149 -5
  25. package/out/lsp/domain-lang-index-manager.js +388 -52
  26. package/out/lsp/domain-lang-index-manager.js.map +1 -1
  27. package/out/lsp/domain-lang-refresh.d.ts +35 -0
  28. package/out/lsp/domain-lang-refresh.js +129 -0
  29. package/out/lsp/domain-lang-refresh.js.map +1 -0
  30. package/out/lsp/domain-lang-workspace-manager.d.ts +10 -0
  31. package/out/lsp/domain-lang-workspace-manager.js +35 -0
  32. package/out/lsp/domain-lang-workspace-manager.js.map +1 -1
  33. package/out/lsp/tool-handlers.js +63 -57
  34. package/out/lsp/tool-handlers.js.map +1 -1
  35. package/out/main.js +30 -190
  36. package/out/main.js.map +1 -1
  37. package/out/sdk/loader-node.js +1 -1
  38. package/out/sdk/loader-node.js.map +1 -1
  39. package/out/sdk/validator.js +17 -14
  40. package/out/sdk/validator.js.map +1 -1
  41. package/out/services/import-resolver.d.ts +67 -17
  42. package/out/services/import-resolver.js +146 -65
  43. package/out/services/import-resolver.js.map +1 -1
  44. package/out/services/lsp-logger.d.ts +42 -0
  45. package/out/services/lsp-logger.js +50 -0
  46. package/out/services/lsp-logger.js.map +1 -0
  47. package/out/services/lsp-runtime-settings.d.ts +20 -0
  48. package/out/services/lsp-runtime-settings.js +20 -0
  49. package/out/services/lsp-runtime-settings.js.map +1 -0
  50. package/out/services/performance-optimizer.d.ts +9 -9
  51. package/out/services/performance-optimizer.js +17 -41
  52. package/out/services/performance-optimizer.js.map +1 -1
  53. package/out/services/workspace-manager.d.ts +22 -1
  54. package/out/services/workspace-manager.js +57 -9
  55. package/out/services/workspace-manager.js.map +1 -1
  56. package/out/utils/import-utils.js +6 -6
  57. package/out/utils/import-utils.js.map +1 -1
  58. package/out/validation/constants.d.ts +6 -0
  59. package/out/validation/constants.js +7 -0
  60. package/out/validation/constants.js.map +1 -1
  61. package/out/validation/import.d.ts +13 -3
  62. package/out/validation/import.js +54 -10
  63. package/out/validation/import.js.map +1 -1
  64. package/package.json +5 -2
  65. package/src/diagram/context-map-diagram-generator.ts +451 -0
  66. package/src/diagram/context-map-layout-configurator.ts +43 -0
  67. package/src/diagram/elk-layout-factory.ts +83 -0
  68. package/src/domain-lang-module.ts +22 -5
  69. package/src/index.ts +8 -0
  70. package/src/lsp/domain-lang-code-lens-provider.ts +54 -0
  71. package/src/lsp/domain-lang-completion.ts +3 -3
  72. package/src/lsp/domain-lang-document-symbol-provider.ts +5 -5
  73. package/src/lsp/domain-lang-index-manager.ts +438 -56
  74. package/src/lsp/domain-lang-refresh.ts +205 -0
  75. package/src/lsp/domain-lang-workspace-manager.ts +45 -0
  76. package/src/lsp/tool-handlers.ts +61 -47
  77. package/src/main.ts +36 -244
  78. package/src/sdk/loader-node.ts +1 -1
  79. package/src/sdk/validator.ts +17 -13
  80. package/src/services/import-resolver.ts +196 -89
  81. package/src/services/lsp-logger.ts +89 -0
  82. package/src/services/lsp-runtime-settings.ts +34 -0
  83. package/src/services/performance-optimizer.ts +18 -57
  84. package/src/services/workspace-manager.ts +62 -10
  85. package/src/utils/import-utils.ts +6 -6
  86. package/src/validation/constants.ts +9 -0
  87. 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
  }
@@ -337,54 +337,68 @@ function executeListQuery(
337
337
  entityType: QueryEntityType,
338
338
  filters: QueryFilters
339
339
  ): Record<string, unknown>[] {
340
- switch (entityType) {
341
- case 'domains': {
342
- let builder = query.domains();
343
- if (filters.name) builder = builder.withName(filters.name);
344
- if (filters.fqn) builder = builder.withFqn(filters.fqn);
345
- return builder.toArray().map((d) => serializeNode(d, query));
346
- }
347
- case 'bcs': {
348
- let builder = query.boundedContexts();
349
- if (filters.domain) builder = builder.inDomain(filters.domain);
350
- if (filters.team) builder = builder.withTeam(filters.team);
351
- if (filters.classification)
352
- builder = builder.withClassification(filters.classification);
353
- if (filters.metadata) {
354
- const [key, value] = filters.metadata.split('=');
355
- builder = builder.withMetadata(key, value);
356
- }
357
- if (filters.name) builder = builder.withName(filters.name) as ReturnType<Query['boundedContexts']>;
358
- if (filters.fqn) builder = builder.withFqn(filters.fqn) as ReturnType<Query['boundedContexts']>;
359
- return builder.toArray().map((bc) => serializeNode(bc, query));
360
- }
361
- case 'teams': {
362
- let builder = query.teams();
363
- if (filters.name) builder = builder.withName(filters.name);
364
- return builder.toArray().map((t) => serializeNode(t, query));
365
- }
366
- case 'classifications': {
367
- let builder = query.classifications();
368
- if (filters.name) builder = builder.withName(filters.name);
369
- return builder.toArray().map((c) => serializeNode(c, query));
370
- }
371
- case 'relationships': {
372
- const rels = query.relationships().toArray();
373
- return rels.map((r) => serializeRelationship(r));
374
- }
375
- case 'context-maps': {
376
- let builder = query.contextMaps();
377
- if (filters.name) builder = builder.withName(filters.name);
378
- return builder.toArray().map((cm) => serializeNode(cm, query));
379
- }
380
- case 'domain-maps': {
381
- let builder = query.domainMaps();
382
- if (filters.name) builder = builder.withName(filters.name);
383
- return builder.toArray().map((dm) => serializeNode(dm, query));
384
- }
385
- default:
386
- return [];
340
+ const handlers: Record<QueryEntityType, (activeQuery: Query, activeFilters: QueryFilters) => Record<string, unknown>[]> = {
341
+ domains: listDomains,
342
+ bcs: listBoundedContexts,
343
+ teams: listTeams,
344
+ classifications: listClassifications,
345
+ relationships: listRelationships,
346
+ 'context-maps': listContextMaps,
347
+ 'domain-maps': listDomainMaps,
348
+ };
349
+
350
+ return handlers[entityType](query, filters);
351
+ }
352
+
353
+ function listDomains(query: Query, filters: QueryFilters): Record<string, unknown>[] {
354
+ let builder = query.domains();
355
+ if (filters.name) builder = builder.withName(filters.name);
356
+ if (filters.fqn) builder = builder.withFqn(filters.fqn);
357
+ return builder.toArray().map((domain) => serializeNode(domain, query));
358
+ }
359
+
360
+ function listBoundedContexts(query: Query, filters: QueryFilters): Record<string, unknown>[] {
361
+ let builder = query.boundedContexts();
362
+ if (filters.domain) builder = builder.inDomain(filters.domain);
363
+ if (filters.team) builder = builder.withTeam(filters.team);
364
+ if (filters.classification) {
365
+ builder = builder.withClassification(filters.classification);
387
366
  }
367
+ if (filters.metadata) {
368
+ const [key, value] = filters.metadata.split('=');
369
+ builder = builder.withMetadata(key, value);
370
+ }
371
+ if (filters.name) builder = builder.withName(filters.name) as ReturnType<Query['boundedContexts']>;
372
+ if (filters.fqn) builder = builder.withFqn(filters.fqn) as ReturnType<Query['boundedContexts']>;
373
+ return builder.toArray().map((boundedContext) => serializeNode(boundedContext, query));
374
+ }
375
+
376
+ function listTeams(query: Query, filters: QueryFilters): Record<string, unknown>[] {
377
+ let builder = query.teams();
378
+ if (filters.name) builder = builder.withName(filters.name);
379
+ return builder.toArray().map((team) => serializeNode(team, query));
380
+ }
381
+
382
+ function listClassifications(query: Query, filters: QueryFilters): Record<string, unknown>[] {
383
+ let builder = query.classifications();
384
+ if (filters.name) builder = builder.withName(filters.name);
385
+ return builder.toArray().map((classification) => serializeNode(classification, query));
386
+ }
387
+
388
+ function listRelationships(query: Query, _filters: QueryFilters): Record<string, unknown>[] {
389
+ return query.relationships().toArray().map((relationship) => serializeRelationship(relationship));
390
+ }
391
+
392
+ function listContextMaps(query: Query, filters: QueryFilters): Record<string, unknown>[] {
393
+ let builder = query.contextMaps();
394
+ if (filters.name) builder = builder.withName(filters.name);
395
+ return builder.toArray().map((contextMap) => serializeNode(contextMap, query));
396
+ }
397
+
398
+ function listDomainMaps(query: Query, filters: QueryFilters): Record<string, unknown>[] {
399
+ let builder = query.domainMaps();
400
+ if (filters.name) builder = builder.withName(filters.name);
401
+ return builder.toArray().map((domainMap) => serializeNode(domainMap, query));
388
402
  }
389
403
 
390
404
  /**