@likec4/language-server 1.44.0 → 1.45.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 (41) hide show
  1. package/dist/LikeC4LanguageServices.d.ts +1 -15
  2. package/dist/LikeC4LanguageServices.js +2 -32
  3. package/dist/Rpc.js +32 -20
  4. package/dist/ast.js +6 -2
  5. package/dist/browser.js +2 -2
  6. package/dist/bundled.js +2 -0
  7. package/dist/bundled.mjs +3184 -3162
  8. package/dist/filesystem/ChokidarWatcher.d.ts +2 -0
  9. package/dist/filesystem/ChokidarWatcher.js +27 -16
  10. package/dist/filesystem/LikeC4FileSystem.js +1 -1
  11. package/dist/index.d.ts +3 -1
  12. package/dist/index.js +5 -3
  13. package/dist/mcp/server/StdioLikeC4MCPServer.js +10 -6
  14. package/dist/mcp/server/StreamableLikeC4MCPServer.js +97 -97
  15. package/dist/mcp/server/WithMCPServer.js +5 -5
  16. package/dist/mcp/tools/search-element.js +5 -5
  17. package/dist/mcp/utils.js +1 -1
  18. package/dist/model/deployments-index.js +2 -2
  19. package/dist/model/fqn-index.d.ts +1 -2
  20. package/dist/model/fqn-index.js +13 -16
  21. package/dist/model/model-builder.js +0 -2
  22. package/dist/model/model-parser.js +34 -27
  23. package/dist/model/parser/SpecificationParser.js +4 -0
  24. package/dist/model/parser/ViewsParser.js +3 -1
  25. package/dist/model-change/ModelChanges.d.ts +2 -2
  26. package/dist/model-change/ModelChanges.js +36 -9
  27. package/dist/protocol.d.ts +33 -10
  28. package/dist/protocol.js +13 -4
  29. package/dist/view-utils/manual-layout.js +2 -4
  30. package/dist/views/LikeC4ManualLayouts.d.ts +16 -2
  31. package/dist/views/LikeC4ManualLayouts.js +99 -22
  32. package/dist/views/LikeC4Views.d.ts +26 -5
  33. package/dist/views/LikeC4Views.js +49 -33
  34. package/dist/workspace/AstNodeDescriptionProvider.js +6 -3
  35. package/dist/workspace/IndexManager.js +1 -1
  36. package/dist/workspace/LangiumDocuments.d.ts +3 -2
  37. package/dist/workspace/LangiumDocuments.js +29 -15
  38. package/dist/workspace/ProjectsManager.d.ts +19 -15
  39. package/dist/workspace/ProjectsManager.js +137 -41
  40. package/dist/workspace/WorkspaceManager.js +5 -0
  41. package/package.json +16 -15
@@ -1,10 +1,12 @@
1
1
  import { isLikeC4Config, validateProjectConfig, } from '@likec4/config';
2
2
  import { BiMap, delay, invariant, memoizeProp, nonNullable } from '@likec4/core/utils';
3
+ import { wrapError } from '@likec4/log';
3
4
  import { deepEqual } from 'fast-equals';
4
5
  import { interruptAndCheck, URI, WorkspaceCache, } from 'langium';
5
6
  import picomatch from 'picomatch';
6
7
  import { hasAtLeast, isNullish, map, pipe, prop, sortBy } from 'remeda';
7
8
  import { joinRelativeURL, parseFilename, withoutProtocol, withTrailingSlash, } from 'ufo';
9
+ import { isLikeC4Builtin } from '../likec4lib';
8
10
  import { logger as mainLogger } from '../logger';
9
11
  const logger = mainLogger.getChild('ProjectsManager');
10
12
  function normalizeUri(uri) {
@@ -43,6 +45,10 @@ export class ProjectsManager {
43
45
  * (it is used in CLI and Vite plugin)
44
46
  */
45
47
  #defaultProjectId = undefined;
48
+ /**
49
+ * Cached default project.
50
+ */
51
+ #defaultProject = undefined;
46
52
  /**
47
53
  * The mapping between project config files and project IDs.
48
54
  */
@@ -78,6 +84,7 @@ export class ProjectsManager {
78
84
  if (id === this.#defaultProjectId) {
79
85
  return;
80
86
  }
87
+ this.#defaultProject = undefined;
81
88
  if (!id || id === ProjectsManager.DefaultProjectId) {
82
89
  logger.debug `reset default project ID`;
83
90
  this.#defaultProjectId = undefined;
@@ -87,6 +94,24 @@ export class ProjectsManager {
87
94
  logger.debug `set default project ID to ${id}`;
88
95
  this.#defaultProjectId = id;
89
96
  }
97
+ get default() {
98
+ if (!this.#defaultProject) {
99
+ const id = this.defaultProjectId ?? ProjectsManager.DefaultProjectId;
100
+ let project = this.#projects.find(p => p.id === id);
101
+ if (!project) {
102
+ const folderUri = this.getWorkspaceFolder();
103
+ project = {
104
+ id,
105
+ config: DefaultProject.config,
106
+ folder: ProjectFolder(folderUri),
107
+ folderUri,
108
+ exclude: DefaultProject.exclude,
109
+ };
110
+ }
111
+ this.#defaultProject = project;
112
+ }
113
+ return this.#defaultProject;
114
+ }
90
115
  get all() {
91
116
  if (hasAtLeast(this.#projects, 1)) {
92
117
  const ids = [
@@ -114,7 +139,7 @@ export class ProjectsManager {
114
139
  }
115
140
  catch (error) {
116
141
  logger.warn('Failed to get workspace URI, using default folder', { error });
117
- folderUri = URI.file('');
142
+ folderUri = URI.file('/');
118
143
  // ignore - workspace not initialized
119
144
  }
120
145
  return {
@@ -188,42 +213,31 @@ export class ProjectsManager {
188
213
  return isConfigFile;
189
214
  }
190
215
  /**
191
- * Checks if the provided file system entry is a valid project config file.
192
- *
193
- * @param entry The file system entry to check
216
+ * Registers likec4 project by config file.
194
217
  */
195
218
  async registerConfigFile(configFile) {
196
- if (this.isConfigFile(configFile)) {
197
- try {
198
- return await this.registerProject(configFile);
199
- }
200
- catch (error) {
201
- this.services.lsp.Connection?.window.showErrorMessage(`LikeC4: Failed to register project at ${configFile.fsPath}`);
202
- logger.warn('Failed to register project at {uri}', { uri: configFile.fsPath, error });
203
- return undefined;
204
- }
219
+ if (DefaultProject.exclude(configFile.path)) {
220
+ throw new Error(`Path to ${configFile.fsPath} is excluded by: ${DefaultProject.config.exclude.join(', ')}`);
205
221
  }
206
- return undefined;
207
- }
208
- /**
209
- * Registers (or reloads) likec4 project by config file or config object.
210
- * If there is some project registered at same folder, it will be reloaded.
211
- */
212
- async registerProject(opts) {
213
- if (URI.isUri(opts)) {
214
- const configFile = opts;
222
+ if (!this.isConfigFile(configFile)) {
223
+ throw new Error(`${configFile.fsPath} is not a valid LikeC4 config filename.`);
224
+ }
225
+ try {
215
226
  const config = await this.services.workspace.FileSystemProvider.loadProjectConfig(configFile);
216
227
  const path = joinRelativeURL(configFile.path, '..');
217
228
  const folderUri = configFile.with({ path });
218
- return this._registerProject({ config, folderUri });
229
+ return await this.registerProject({ config, folderUri });
230
+ }
231
+ catch (error) {
232
+ this.services.lsp.Connection?.window.showErrorMessage(`LikeC4: Failed to register project at ${configFile.fsPath}`);
233
+ throw wrapError(error, `Failed to register project config ${configFile.fsPath}:\n`);
219
234
  }
220
- return this._registerProject(opts);
221
235
  }
222
236
  /**
223
237
  * Registers (or reloads) likec4 project by config file or config object.
224
238
  * If there is some project registered at same folder, it will be reloaded.
225
239
  */
226
- _registerProject(opts) {
240
+ async registerProject(opts) {
227
241
  const config = validateProjectConfig(opts.config);
228
242
  const folder = ProjectFolder(opts.folderUri);
229
243
  let project = this.#projects.find(p => p.folder === folder);
@@ -265,6 +279,8 @@ export class ProjectsManager {
265
279
  }
266
280
  project.config = config;
267
281
  }
282
+ // Reset cached default project
283
+ this.#defaultProject = undefined;
268
284
  if (isNullish(config.exclude)) {
269
285
  project.exclude = DefaultProject.exclude;
270
286
  }
@@ -272,14 +288,29 @@ export class ProjectsManager {
272
288
  project.exclude = picomatch(config.exclude, { dot: true });
273
289
  }
274
290
  this.#projectIdToFolder.set(project.id, folder);
275
- if (mustReset) {
276
- this.resetProjectIds();
291
+ // Reset assigned project IDs if no projects reload is active
292
+ if (mustReset && !this.#activeReload) {
293
+ await this.rebuidProject(project.id).catch(error => {
294
+ logger.warn('Failed to rebuild project {projectId} after config change', {
295
+ projectId: project.id,
296
+ error,
297
+ });
298
+ });
277
299
  }
278
300
  return project;
279
301
  }
302
+ /**
303
+ * Determines which project the given document belongs to.
304
+ * If the document does not belong to any project, returns the default project ID.
305
+ */
280
306
  belongsTo(document) {
281
- const documentUri = normalizeUri(document);
282
- return this.findProjectForDocument(documentUri).id;
307
+ if (URI.isUri(document) || typeof document === 'string') {
308
+ const documentUri = normalizeUri(document);
309
+ return this.findProjectForDocument(documentUri).id;
310
+ }
311
+ return this.documentBelongsTo.get(document, () => {
312
+ return this.findProjectForDocument(normalizeUri(document.uri));
313
+ }).id;
283
314
  }
284
315
  #activeReload = null;
285
316
  async reloadProjects() {
@@ -306,13 +337,16 @@ export class ProjectsManager {
306
337
  return;
307
338
  }
308
339
  await this.services.workspace.WorkspaceLock.write(async (cancelToken) => {
340
+ logger.debug `start reload projects`;
309
341
  const configFiles = [];
310
342
  for (const folder of folders) {
311
343
  try {
344
+ logger.debug `scan projects in folder ${folder.uri}`;
312
345
  const files = await this.services.workspace.FileSystemProvider.scanProjectFiles(URI.parse(folder.uri));
313
346
  for (const file of files) {
314
347
  if (file.isFile && this.isConfigFile(file.uri)) {
315
- configFiles.push(file);
348
+ logger.debug `found config ${file.uri.fsPath}`;
349
+ configFiles.push(file.uri);
316
350
  }
317
351
  }
318
352
  }
@@ -324,19 +358,18 @@ export class ProjectsManager {
324
358
  logger.warning('No config files found, but some projects were registered before');
325
359
  }
326
360
  await interruptAndCheck(cancelToken);
327
- logger.debug `start reload projects`;
328
361
  this.#projects = [];
329
362
  this.#projectIdToFolder.clear();
330
- for (const entry of configFiles) {
363
+ for (const uri of configFiles) {
331
364
  try {
332
- await this.registerConfigFile(entry.uri);
365
+ await this.registerConfigFile(uri);
333
366
  }
334
367
  catch (error) {
335
- logger.error('Failed to load config file {uri}', { uri: entry.uri.toString(), error });
368
+ logger.error('Failed to load config file {uri}', { uri: uri.fsPath, error });
336
369
  }
337
370
  }
338
- this.resetProjectIds();
339
- await this.rebuidDocuments(cancelToken);
371
+ this.reset();
372
+ await this.services.workspace.WorkspaceManager.rebuildAll(cancelToken);
340
373
  });
341
374
  }
342
375
  uniqueProjectId(name) {
@@ -347,22 +380,68 @@ export class ProjectsManager {
347
380
  }
348
381
  return id;
349
382
  }
350
- resetProjectIds() {
383
+ reset() {
384
+ this.#defaultProject = undefined;
351
385
  if (this.#defaultProjectId && !this.#projectIdToFolder.has(this.#defaultProjectId)) {
352
386
  this.#defaultProjectId = undefined;
353
387
  }
388
+ this.services.workspace.LangiumDocuments.all.forEach(doc => {
389
+ if (isLikeC4Builtin(doc.uri)) {
390
+ return;
391
+ }
392
+ // Remove assigned project ID to force re-calculation
393
+ delete doc.likec4ProjectId;
394
+ });
395
+ this.documentBelongsTo.clear();
354
396
  this.mappingsToProject.clear();
355
397
  this.#excludedDocuments = new WeakMap();
356
- this.services.workspace.LangiumDocuments.resetProjectIds();
357
398
  }
358
- async rebuidDocuments(cancelToken) {
359
- await this.services.workspace.WorkspaceManager.rebuildAll(cancelToken);
399
+ async rebuidProject(projectId, cancelToken) {
400
+ if (!cancelToken) {
401
+ return await this.services.workspace.WorkspaceLock.write(async (ct) => {
402
+ await this.rebuidProject(projectId, ct);
403
+ });
404
+ }
405
+ // reset default project cache
406
+ this.#defaultProject = undefined;
407
+ const project = this.#projects.find(p => p.id === projectId) ?? this.default;
408
+ const folder = project.folder;
409
+ const docs = this.services.workspace.LangiumDocuments
410
+ .all
411
+ .filter(doc => {
412
+ if (project.exclude?.(doc.uri.path)) {
413
+ return false;
414
+ }
415
+ if (doc.uri.toString().startsWith(folder)) {
416
+ return true;
417
+ }
418
+ const docdir = withTrailingSlash(joinRelativeURL(doc.uri.toString(), '..'));
419
+ return docdir.startsWith(folder) || folder.startsWith(docdir);
420
+ })
421
+ .map(d => d.uri)
422
+ .toArray();
423
+ if (docs.length > 0) {
424
+ this.reset();
425
+ const projectId = project.id;
426
+ logger.info('rebuild documents of project {projectId}: {docs}', {
427
+ projectId,
428
+ docs: docs.length,
429
+ });
430
+ await this.services.workspace.DocumentBuilder
431
+ .update(docs, [], cancelToken)
432
+ .catch(error => {
433
+ logger.warn('Failed to rebuild project {projectId}', {
434
+ projectId,
435
+ error,
436
+ });
437
+ });
438
+ }
360
439
  }
361
440
  findProjectForDocument(documentUri) {
362
441
  return this.mappingsToProject.get(documentUri, () => {
363
442
  const project = this.#projects.find(({ folder }) => documentUri.startsWith(folder));
364
443
  // If the document is not part of any project, assign it to the global project ID
365
- return project ?? DefaultProject;
444
+ return project ?? this.default;
366
445
  });
367
446
  }
368
447
  // The mapping between document URIs and their corresponding project ID
@@ -370,4 +449,21 @@ export class ProjectsManager {
370
449
  get mappingsToProject() {
371
450
  return memoizeProp(this, '_mappingsToProject', () => new WorkspaceCache(this.services));
372
451
  }
452
+ /**
453
+ * The mapping between documents and projects they belong to.
454
+ * Lazy-created due to initialization order of the LanguageServer
455
+ */
456
+ get documentBelongsTo() {
457
+ return memoizeProp(this, '_documentBelongsTo', () => new WorkspaceCache(this.services));
458
+ }
459
+ getWorkspaceFolder() {
460
+ try {
461
+ return this.services.workspace.WorkspaceManager.workspaceUri;
462
+ }
463
+ catch (error) {
464
+ logger.warn('Failed to get workspace URI, using default folder', { error });
465
+ return URI.file('/');
466
+ // ignore - workspace not initialized
467
+ }
468
+ }
373
469
  }
@@ -75,6 +75,11 @@ export class LikeC4WorkspaceManager extends DefaultWorkspaceManager {
75
75
  return null;
76
76
  }
77
77
  async rebuildAll(cancelToken) {
78
+ if (!cancelToken) {
79
+ return await this.services.workspace.WorkspaceLock.write(async (ct) => {
80
+ await this.rebuildAll(ct);
81
+ });
82
+ }
78
83
  const docs = this.services.workspace.LangiumDocuments.all.map(d => d.uri).toArray();
79
84
  logger.info('invalidate and rebuild all {docs} documents', { docs: docs.length });
80
85
  this.services.workspace.Cache.clear();
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@likec4/language-server",
3
3
  "description": "LikeC4 Language Server",
4
- "version": "1.44.0",
4
+ "version": "1.45.0",
5
5
  "license": "MIT",
6
6
  "bugs": "https://github.com/likec4/likec4/issues",
7
7
  "homepage": "https://likec4.dev",
@@ -78,10 +78,10 @@
78
78
  "dependencies": {
79
79
  "@hpcc-js/wasm-graphviz": "1.15.0",
80
80
  "bundle-require": "^5.1.0",
81
- "esbuild": "0.25.11"
81
+ "esbuild": "0.27.0"
82
82
  },
83
83
  "devDependencies": {
84
- "@hono/node-server": "^1.19.5",
84
+ "@hono/node-server": "^1.19.6",
85
85
  "@modelcontextprotocol/sdk": "^1.20.1",
86
86
  "@msgpack/msgpack": "^3.1.2",
87
87
  "@smithy/util-base64": "^4.3.0",
@@ -93,17 +93,18 @@
93
93
  "chokidar": "^4.0.3",
94
94
  "defu": "^6.1.4",
95
95
  "esm-env": "^1.2.2",
96
- "fast-equals": "^5.3.2",
96
+ "fast-equals": "^5.3.3",
97
97
  "fdir": "6.4.0",
98
98
  "fetch-to-node": "^2.1.0",
99
- "hono": "^4.10.2",
99
+ "hono": "^4.10.6",
100
+ "immer": "^10.2.0",
100
101
  "indent-string": "^5.0.0",
101
102
  "json5": "^2.2.3",
102
103
  "langium": "3.5.0",
103
104
  "langium-cli": "3.5.2",
104
105
  "nano-spawn": "^1.0.3",
105
106
  "natural-compare-lite": "^1.4.0",
106
- "oxlint": "1.28.0",
107
+ "oxlint": "1.29.0",
107
108
  "p-debounce": "4.0.0",
108
109
  "p-queue": "8.1.1",
109
110
  "p-timeout": "6.1.4",
@@ -112,12 +113,12 @@
112
113
  "remeda": "^2.32.0",
113
114
  "strip-indent": "^4.1.1",
114
115
  "tsx": "4.20.6",
115
- "turbo": "2.6.0",
116
+ "turbo": "2.6.1",
116
117
  "type-fest": "^4.41.0",
117
118
  "typescript": "5.9.3",
118
119
  "ufo": "1.6.1",
119
120
  "unbuild": "3.5.0",
120
- "vitest": "4.0.8",
121
+ "vitest": "4.0.13",
121
122
  "vscode-jsonrpc": "8.2.1",
122
123
  "vscode-languageserver": "9.0.1",
123
124
  "vscode-languageserver-protocol": "3.17.5",
@@ -125,13 +126,13 @@
125
126
  "vscode-uri": "3.1.0",
126
127
  "which": "^5.0.0",
127
128
  "zod": "^3.25.76",
128
- "@likec4/core": "1.44.0",
129
- "@likec4/config": "1.44.0",
130
- "@likec4/layouts": "1.44.0",
131
- "@likec4/log": "1.44.0",
132
- "@likec4/icons": "1.44.0",
133
- "@likec4/tsconfig": "1.44.0",
134
- "@likec4/devops": "1.42.0"
129
+ "@likec4/config": "1.45.0",
130
+ "@likec4/core": "1.45.0",
131
+ "@likec4/devops": "1.42.0",
132
+ "@likec4/icons": "1.45.0",
133
+ "@likec4/layouts": "1.45.0",
134
+ "@likec4/log": "1.45.0",
135
+ "@likec4/tsconfig": "1.45.0"
135
136
  },
136
137
  "scripts": {
137
138
  "typecheck": "tsc -b --verbose",