@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
package/src/main.ts CHANGED
@@ -1,8 +1,9 @@
1
1
  import { startLanguageServer } from 'langium/lsp';
2
2
  import { NodeFileSystem } from 'langium/node';
3
- import { createConnection, ProposedFeatures } from 'vscode-languageserver/node.js';
3
+ import { createConnection, ProposedFeatures, FileChangeType } 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
7
  import { URI } from 'langium';
7
8
 
8
9
  // Create a connection to the client
@@ -11,29 +12,269 @@ const connection = createConnection(ProposedFeatures.all);
11
12
  // Inject the shared services and language-specific services
12
13
  const { shared, DomainLang } = createDomainLangServices({ connection, ...NodeFileSystem });
13
14
 
14
- // Initialize workspace on connection
15
- connection.onInitialize(async (params) => {
16
- const workspaceRoot = params.rootUri ? URI.parse(params.rootUri).fsPath : undefined;
15
+ // Initialize workspace manager when language server initializes
16
+ // Uses Langium's LanguageServer.onInitialize hook (not raw connection handler)
17
+ // This integrates properly with Langium's initialization flow
18
+ shared.lsp.LanguageServer.onInitialize((params) => {
19
+ // Use workspaceFolders (preferred) over deprecated rootUri
20
+ const folders = params.workspaceFolders;
21
+ const workspaceRoot = folders?.[0]?.uri
22
+ ? URI.parse(folders[0].uri).fsPath
23
+ : undefined;
17
24
 
18
25
  if (workspaceRoot) {
19
- try {
20
- // Initialize workspace manager
21
- const workspaceManager = DomainLang.imports.WorkspaceManager;
22
- await workspaceManager.initialize(workspaceRoot);
23
- console.warn(`DomainLang workspace initialized: ${workspaceRoot}`);
24
- } catch (error) {
26
+ // Initialize workspace manager synchronously (just sets root path)
27
+ // Heavy work happens in initializeWorkspace() called by Langium later
28
+ const workspaceManager = DomainLang.imports.WorkspaceManager;
29
+ workspaceManager.initialize(workspaceRoot).catch(error => {
25
30
  const message = error instanceof Error ? error.message : String(error);
26
31
  console.warn(`Failed to initialize workspace: ${message}`);
27
32
  // Continue without workspace - local imports will still work
33
+ });
34
+ console.warn(`DomainLang workspace root: ${workspaceRoot}`);
35
+ }
36
+ });
37
+
38
+ // Handle file changes for model.yaml and model.lock (PRS-010)
39
+ // Uses Langium's built-in file watcher which already watches **/* in workspace
40
+ // This invalidates caches when config files change externally
41
+ shared.lsp.DocumentUpdateHandler?.onWatchedFilesChange(async (params) => {
42
+ try {
43
+ await handleFileChanges(params, DomainLang.imports.WorkspaceManager, shared, DomainLang);
44
+ } catch (error) {
45
+ const message = error instanceof Error ? error.message : String(error);
46
+ console.error(`Error handling file change notification: ${message}`);
47
+ // Continue - don't crash the server
48
+ }
49
+ });
50
+
51
+ /** Categorized file changes */
52
+ interface CategorizedChanges {
53
+ manifestChanged: boolean;
54
+ lockFileChanged: boolean;
55
+ changedDlangUris: Set<string>;
56
+ deletedDlangUris: Set<string>;
57
+ createdDlangUris: Set<string>;
58
+ }
59
+
60
+ /**
61
+ * Categorizes file changes by type.
62
+ */
63
+ function categorizeChanges(
64
+ params: { changes: Array<{ uri: string; type: number }> },
65
+ workspaceManager: typeof DomainLang.imports.WorkspaceManager,
66
+ langServices: typeof DomainLang,
67
+ indexManager: DomainLangIndexManager
68
+ ): CategorizedChanges {
69
+ const result: CategorizedChanges = {
70
+ manifestChanged: false,
71
+ lockFileChanged: false,
72
+ changedDlangUris: new Set(),
73
+ deletedDlangUris: new Set(),
74
+ createdDlangUris: new Set()
75
+ };
76
+
77
+ for (const change of params.changes) {
78
+ const uri = URI.parse(change.uri);
79
+ const fileName = uri.path.split('/').pop() ?? '';
80
+ const uriString = change.uri;
81
+
82
+ if (fileName === 'model.yaml') {
83
+ console.warn(`model.yaml changed: ${uriString}`);
84
+ workspaceManager.invalidateManifestCache();
85
+ langServices.imports.ImportResolver.clearCache();
86
+ indexManager.clearImportDependencies();
87
+ result.manifestChanged = true;
88
+ } else if (fileName === 'model.lock') {
89
+ console.warn(`model.lock changed: ${uriString}`);
90
+ langServices.imports.ImportResolver.clearCache();
91
+ result.lockFileChanged = true;
92
+ } else if (fileName.endsWith('.dlang')) {
93
+ if (change.type === FileChangeType.Deleted) {
94
+ result.deletedDlangUris.add(uriString);
95
+ console.warn(`DomainLang file deleted: ${uriString}`);
96
+ } else if (change.type === FileChangeType.Created) {
97
+ result.createdDlangUris.add(uriString);
98
+ console.warn(`DomainLang file created: ${uriString}`);
99
+ } else {
100
+ result.changedDlangUris.add(uriString);
101
+ console.warn(`DomainLang file changed: ${uriString}`);
102
+ }
28
103
  }
29
104
  }
105
+
106
+ return result;
107
+ }
108
+
109
+ /**
110
+ * Rebuilds documents that depend on changed/deleted/created .dlang files.
111
+ */
112
+ async function rebuildAffectedDocuments(
113
+ changes: CategorizedChanges,
114
+ indexManager: DomainLangIndexManager,
115
+ sharedServices: typeof shared,
116
+ langServices: typeof DomainLang
117
+ ): Promise<void> {
118
+ const hasChanges = changes.changedDlangUris.size > 0 ||
119
+ changes.deletedDlangUris.size > 0 ||
120
+ changes.createdDlangUris.size > 0;
121
+ if (!hasChanges) {
122
+ return;
123
+ }
124
+
125
+ // CRITICAL: Clear ImportResolver cache BEFORE rebuilding.
126
+ // The WorkspaceCache only clears AFTER linking, but resolution happens
127
+ // DURING linking. Without this, stale cached resolutions would be used.
128
+ langServices.imports.ImportResolver.clearCache();
129
+
130
+ const affectedUris = collectAffectedDocuments(changes, indexManager);
131
+
132
+ if (affectedUris.size === 0) {
133
+ return;
134
+ }
135
+
136
+ console.warn(`Rebuilding ${affectedUris.size} documents affected by file changes`);
137
+
138
+ const langiumDocuments = sharedServices.workspace.LangiumDocuments;
139
+ const affectedDocs: URI[] = [];
140
+
141
+ for (const uriString of affectedUris) {
142
+ const uri = URI.parse(uriString);
143
+ if (langiumDocuments.hasDocument(uri)) {
144
+ affectedDocs.push(uri);
145
+ indexManager.markForReprocessing(uriString);
146
+ }
147
+ }
148
+
149
+ const deletedUriObjects = [...changes.deletedDlangUris].map(u => URI.parse(u));
150
+ if (affectedDocs.length > 0 || deletedUriObjects.length > 0) {
151
+ await sharedServices.workspace.DocumentBuilder.update(affectedDocs, deletedUriObjects);
152
+ }
153
+ }
154
+
155
+ /**
156
+ * Collects all document URIs that should be rebuilt based on the changes.
157
+ *
158
+ * Uses targeted matching to avoid expensive full rebuilds:
159
+ * - For edits: rebuild documents that import the changed file (by resolved URI)
160
+ * - For all changes: rebuild documents whose import specifiers match the path
161
+ *
162
+ * The specifier matching handles renamed/moved/created files by comparing
163
+ * import specifiers against path segments (filename, parent/filename, etc.).
164
+ */
165
+ function collectAffectedDocuments(
166
+ changes: CategorizedChanges,
167
+ indexManager: DomainLangIndexManager
168
+ ): Set<string> {
169
+ const allChangedUris = new Set([
170
+ ...changes.changedDlangUris,
171
+ ...changes.deletedDlangUris,
172
+ ...changes.createdDlangUris
173
+ ]);
174
+
175
+ // Get documents affected by resolved URI changes (edits to imported files)
176
+ const affectedByUri = indexManager.getAllAffectedDocuments(allChangedUris);
30
177
 
31
- return {
32
- capabilities: {
33
- // Language server capabilities are configured by Langium
178
+ // Get documents with import specifiers that match changed paths
179
+ // This catches:
180
+ // - File moves/renames: specifiers that previously resolved but now won't
181
+ // - File creations: specifiers that previously failed but might now resolve
182
+ // Uses fuzzy matching on path segments rather than rebuilding all imports
183
+ const affectedBySpecifier = indexManager.getDocumentsWithPotentiallyAffectedImports(allChangedUris);
184
+
185
+ return new Set([...affectedByUri, ...affectedBySpecifier]);
186
+ }
187
+
188
+ /**
189
+ * Handles all file changes including .dlang files, model.yaml, and model.lock.
190
+ *
191
+ * For .dlang files: rebuilds all documents that import the changed file.
192
+ * For config files: invalidates caches and rebuilds workspace as needed.
193
+ */
194
+ async function handleFileChanges(
195
+ params: { changes: Array<{ uri: string; type: number }> },
196
+ workspaceManager: typeof DomainLang.imports.WorkspaceManager,
197
+ sharedServices: typeof shared,
198
+ langServices: typeof DomainLang
199
+ ): Promise<void> {
200
+ const indexManager = sharedServices.workspace.IndexManager as DomainLangIndexManager;
201
+
202
+ // Categorize and process changes
203
+ const changes = categorizeChanges(params, workspaceManager, langServices, indexManager);
204
+
205
+ // Handle lock file changes
206
+ if (changes.lockFileChanged) {
207
+ const lockChange = params.changes.find(c => c.uri.endsWith('model.lock'));
208
+ if (lockChange) {
209
+ await handleLockFileChange(lockChange, workspaceManager);
34
210
  }
35
- };
36
- });
211
+ }
212
+
213
+ // Rebuild documents affected by .dlang file changes
214
+ await rebuildAffectedDocuments(changes, indexManager, sharedServices, langServices);
215
+
216
+ // Handle config file changes
217
+ if (changes.manifestChanged || changes.lockFileChanged) {
218
+ await rebuildWorkspace(sharedServices, workspaceManager, changes.manifestChanged);
219
+ }
220
+ }
221
+
222
+ /**
223
+ * Handles lock file creation, change, or deletion.
224
+ */
225
+ async function handleLockFileChange(
226
+ change: { uri: string; type: number },
227
+ workspaceManager: typeof DomainLang.imports.WorkspaceManager
228
+ ): Promise<void> {
229
+ if (change.type === FileChangeType.Changed || change.type === FileChangeType.Created) {
230
+ await workspaceManager.refreshLockFile();
231
+ } else if (change.type === FileChangeType.Deleted) {
232
+ workspaceManager.invalidateLockCache();
233
+ }
234
+ }
235
+
236
+ /**
237
+ * Rebuilds the workspace after config file changes.
238
+ * Uses incremental strategy: only full rebuild if dependencies changed.
239
+ *
240
+ * @param sharedServices - Shared Langium services
241
+ * @param workspaceManager - Workspace manager for manifest access
242
+ * @param manifestChanged - Whether model.yaml changed (vs just model.lock)
243
+ */
244
+ async function rebuildWorkspace(
245
+ sharedServices: typeof shared,
246
+ workspaceManager: typeof DomainLang.imports.WorkspaceManager,
247
+ manifestChanged: boolean
248
+ ): Promise<void> {
249
+ try {
250
+ // If only lock file changed, caches are already invalidated - no rebuild needed
251
+ // Lock file changes mean resolved versions changed, but import resolver cache is cleared
252
+ // Documents will re-resolve imports on next access
253
+ if (!manifestChanged) {
254
+ console.warn('Lock file changed - caches invalidated, no rebuild needed');
255
+ return;
256
+ }
257
+
258
+ // For manifest changes, check if dependencies section actually changed
259
+ // If only metadata changed (name, version, etc.), no rebuild needed
260
+ const manifest = await workspaceManager.getManifest();
261
+ const hasDependencies = manifest?.dependencies && Object.keys(manifest.dependencies).length > 0;
262
+
263
+ if (!hasDependencies) {
264
+ console.warn('Manifest changed but has no dependencies - skipping rebuild');
265
+ return;
266
+ }
267
+
268
+ // Dependencies exist and manifest changed - do full rebuild
269
+ const documents = sharedServices.workspace.LangiumDocuments.all.toArray();
270
+ const uris = documents.map(doc => doc.uri);
271
+ await sharedServices.workspace.DocumentBuilder.update([], uris);
272
+ console.warn(`Workspace rebuilt: ${documents.length} documents revalidated`);
273
+ } catch (error) {
274
+ const message = error instanceof Error ? error.message : String(error);
275
+ console.error(`Failed to rebuild workspace: ${message}`);
276
+ }
277
+ }
37
278
 
38
279
  // Optionally start from a single entry file and follow imports.
39
280
  // Configure via env DOMAINLANG_ENTRY (absolute or workspace-relative path)
@@ -63,7 +304,8 @@ if (entryFile) {
63
304
  };
64
305
 
65
306
  // Initial load from entry file, then start the server
66
- reloadFromEntry().finally(() => startLanguageServer(shared));
307
+ await reloadFromEntry();
308
+ startLanguageServer(shared);
67
309
 
68
310
  // Any change within the loaded graph should trigger a reload from the entry
69
311
  shared.workspace.TextDocuments.onDidChangeContent(async (event) => {
@@ -1,35 +1,110 @@
1
1
  import fs from 'node:fs/promises';
2
2
  import path from 'node:path';
3
- import { URI, type LangiumDocument } from 'langium';
3
+ import { DocumentState, SimpleCache, WorkspaceCache, URI, type LangiumDocument, type LangiumSharedCoreServices } from 'langium';
4
4
  import { WorkspaceManager } from './workspace-manager.js';
5
5
  import type { DomainLangServices } from '../domain-lang-module.js';
6
6
  import type { LockFile } from './types.js';
7
7
 
8
+ /**
9
+ * Cache interface for import resolution.
10
+ * Uses WorkspaceCache in LSP mode (clears on ANY document change) or SimpleCache in standalone mode.
11
+ */
12
+ type ResolverCache = WorkspaceCache<string, URI> | SimpleCache<string, URI>;
13
+
8
14
  /**
9
15
  * ImportResolver resolves import statements using manifest-centric rules (PRS-010).
10
16
  *
11
17
  * Import Types (PRS-010):
12
18
  * - Local relative: ./path, ../path → Directory-first resolution
13
19
  * - Path aliases: @/path, @alias/path → Configurable in model.yaml paths section
14
- * - External: owner/package Manifest dependencies
20
+ * - External: dependency key → Manifest dependencies (key can be owner/package or an alias that maps to source)
15
21
  *
16
22
  * Directory-First Resolution:
17
23
  * - ./types → ./types/index.dlang → ./types.dlang
18
24
  * - Module entry defaults to index.dlang (no model.yaml required)
25
+ *
26
+ * Caching Strategy (uses Langium standard infrastructure):
27
+ * - LSP mode: Uses `WorkspaceCache` - clears on ANY document change in workspace
28
+ * This is necessary because file moves/deletes affect resolution of OTHER documents
29
+ * - Standalone mode: Uses `SimpleCache` - manual invalidation via clearCache()
30
+ *
31
+ * Why WorkspaceCache (not DocumentCache)?
32
+ * - DocumentCache only invalidates when the KEYED document changes
33
+ * - But import resolution can break when IMPORTED files are moved/deleted
34
+ * - Example: index.dlang imports @domains → domains/index.dlang
35
+ * If domains/index.dlang is moved, index.dlang's cache entry must be cleared
36
+ * DocumentCache wouldn't clear it (index.dlang didn't change)
37
+ * WorkspaceCache clears on ANY change, ensuring correct re-resolution
38
+ *
39
+ * @see https://langium.org/docs/recipes/caching/ for Langium caching patterns
19
40
  */
20
41
  export class ImportResolver {
21
42
  private readonly workspaceManager: WorkspaceManager;
43
+ /**
44
+ * Workspace-level cache for resolved import URIs.
45
+ * In LSP mode: WorkspaceCache - clears when ANY document changes (correct for imports)
46
+ * In standalone mode: SimpleCache - manual invalidation via clearCache()
47
+ */
48
+ private readonly resolverCache: ResolverCache;
22
49
 
50
+ /**
51
+ * Creates an ImportResolver.
52
+ *
53
+ * @param services - DomainLang services. If `services.shared` is present, uses WorkspaceCache
54
+ * for automatic invalidation. Otherwise uses SimpleCache for standalone mode.
55
+ */
23
56
  constructor(services: DomainLangServices) {
24
57
  this.workspaceManager = services.imports.WorkspaceManager;
58
+
59
+ // Use Langium's WorkspaceCache when shared services are available (LSP mode)
60
+ // Fall back to SimpleCache for standalone utilities (SDK, CLI)
61
+ const shared = (services as DomainLangServices & { shared?: LangiumSharedCoreServices }).shared;
62
+ if (shared) {
63
+ // LSP mode: WorkspaceCache with DocumentState.Linked
64
+ //
65
+ // This follows the standard pattern used by TypeScript, rust-analyzer, gopls:
66
+ // - Cache is valid for a "workspace snapshot"
67
+ // - Invalidates after a batch of changes completes linking (debounced ~300ms)
68
+ // - Invalidates immediately on file deletion
69
+ // - Does NOT invalidate during typing (would be too expensive)
70
+ //
71
+ // DocumentState.Linked is the right phase because:
72
+ // - Import resolution is needed during linking
73
+ // - By the time linking completes, we know which files exist
74
+ // - File renames appear as delete+create, triggering immediate invalidation
75
+ this.resolverCache = new WorkspaceCache(shared, DocumentState.Linked);
76
+ } else {
77
+ // Standalone mode: simple key-value cache, manual invalidation
78
+ this.resolverCache = new SimpleCache<string, URI>();
79
+ }
80
+ }
81
+
82
+ /**
83
+ * Clears the entire import resolution cache.
84
+ * In LSP mode, this is also triggered automatically by WorkspaceCache on any document change.
85
+ * Call explicitly when model.yaml or model.lock changes.
86
+ */
87
+ clearCache(): void {
88
+ this.resolverCache.clear();
25
89
  }
26
90
 
27
91
  /**
28
92
  * Resolve an import specifier relative to a Langium document.
93
+ * Results are cached using WorkspaceCache (clears on any workspace change).
29
94
  */
30
95
  async resolveForDocument(document: LangiumDocument, specifier: string): Promise<URI> {
96
+ // Cache key combines document URI + specifier for uniqueness
97
+ const cacheKey = `${document.uri.toString()}|${specifier}`;
98
+ const cached = this.resolverCache.get(cacheKey);
99
+ if (cached) {
100
+ return cached;
101
+ }
102
+
103
+ // Resolve and cache
31
104
  const baseDir = path.dirname(document.uri.fsPath);
32
- return this.resolveFrom(baseDir, specifier);
105
+ const result = await this.resolveFrom(baseDir, specifier);
106
+ this.resolverCache.set(cacheKey, result);
107
+ return result;
33
108
  }
34
109
 
35
110
  /**
@@ -121,7 +196,10 @@ export class ImportResolver {
121
196
  /**
122
197
  * Resolves an external dependency via manifest.
123
198
  *
124
- * NEW FORMAT (PRS-010): Import specifier is owner/package format.
199
+ * Import specifier is a dependency key from model.yaml.
200
+ * - Recommended: key is owner/package.
201
+ * - Optional: key is an alias with an explicit source.
202
+ * The LSP only resolves to cached packages - no network calls.
125
203
  */
126
204
  private async resolveExternalDependency(specifier: string): Promise<URI> {
127
205
  const manifest = await this.workspaceManager.getManifest();
@@ -143,19 +221,20 @@ export class ImportResolver {
143
221
  );
144
222
  }
145
223
 
146
- const mapped = await this.workspaceManager.resolveDependencyImport(specifier);
147
- if (!mapped) {
224
+ // Use WorkspaceManager to resolve from cache (read-only, no network)
225
+ const resolved = await this.workspaceManager.resolveDependencyPath(specifier);
226
+ if (!resolved) {
148
227
  throw new Error(
149
- `Dependency '${specifier}' not found in model.yaml.\n` +
228
+ `Dependency '${specifier}' not found in model.yaml or not installed.\n` +
150
229
  `Hint: Add it to your dependencies:\n` +
151
230
  ` dependencies:\n` +
152
231
  ` ${specifier}:\n` +
153
- ` ref: v1.0.0`
232
+ ` ref: v1.0.0\n` +
233
+ `Then run 'dlang install' to fetch it.`
154
234
  );
155
235
  }
156
236
 
157
- const git = await this.workspaceManager.getGitResolver();
158
- return git.resolve(mapped, { allowNetwork: false });
237
+ return URI.file(resolved);
159
238
  }
160
239
 
161
240
  /**
@@ -284,8 +363,8 @@ async function assertFileExists(filePath: string, original: string): Promise<voi
284
363
  await fs.access(filePath);
285
364
  } catch {
286
365
  throw new Error(
287
- `Import file not found: '${original}'.\\n` +
288
- `Resolved path: ${filePath}\\n` +
366
+ `Import file not found: '${original}'.\n` +
367
+ `Resolved path: ${filePath}\n` +
289
368
  `Hint: Check that the file exists and the path is correct.`
290
369
  );
291
370
  }
@@ -187,8 +187,8 @@ export interface LockedDependency {
187
187
  * @example
188
188
  * ```yaml
189
189
  * paths:
190
- * "@/": "./src/"
191
- * "@shared/": "./libs/shared/"
190
+ * "@": "./src"
191
+ * "@shared": "./libs/shared"
192
192
  * ```
193
193
  */
194
194
  export type PathAliases = Readonly<Record<string, string>>;