@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.
- package/dist/LikeC4LanguageServices.d.ts +1 -15
- package/dist/LikeC4LanguageServices.js +2 -32
- package/dist/Rpc.js +32 -20
- package/dist/ast.js +6 -2
- package/dist/browser.js +2 -2
- package/dist/bundled.js +2 -0
- package/dist/bundled.mjs +3184 -3162
- package/dist/filesystem/ChokidarWatcher.d.ts +2 -0
- package/dist/filesystem/ChokidarWatcher.js +27 -16
- package/dist/filesystem/LikeC4FileSystem.js +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.js +5 -3
- package/dist/mcp/server/StdioLikeC4MCPServer.js +10 -6
- package/dist/mcp/server/StreamableLikeC4MCPServer.js +97 -97
- package/dist/mcp/server/WithMCPServer.js +5 -5
- package/dist/mcp/tools/search-element.js +5 -5
- package/dist/mcp/utils.js +1 -1
- package/dist/model/deployments-index.js +2 -2
- package/dist/model/fqn-index.d.ts +1 -2
- package/dist/model/fqn-index.js +13 -16
- package/dist/model/model-builder.js +0 -2
- package/dist/model/model-parser.js +34 -27
- package/dist/model/parser/SpecificationParser.js +4 -0
- package/dist/model/parser/ViewsParser.js +3 -1
- package/dist/model-change/ModelChanges.d.ts +2 -2
- package/dist/model-change/ModelChanges.js +36 -9
- package/dist/protocol.d.ts +33 -10
- package/dist/protocol.js +13 -4
- package/dist/view-utils/manual-layout.js +2 -4
- package/dist/views/LikeC4ManualLayouts.d.ts +16 -2
- package/dist/views/LikeC4ManualLayouts.js +99 -22
- package/dist/views/LikeC4Views.d.ts +26 -5
- package/dist/views/LikeC4Views.js +49 -33
- package/dist/workspace/AstNodeDescriptionProvider.js +6 -3
- package/dist/workspace/IndexManager.js +1 -1
- package/dist/workspace/LangiumDocuments.d.ts +3 -2
- package/dist/workspace/LangiumDocuments.js +29 -15
- package/dist/workspace/ProjectsManager.d.ts +19 -15
- package/dist/workspace/ProjectsManager.js +137 -41
- package/dist/workspace/WorkspaceManager.js +5 -0
- 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
|
-
*
|
|
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 (
|
|
197
|
-
|
|
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
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
|
276
|
-
|
|
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
|
-
|
|
282
|
-
|
|
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
|
-
|
|
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
|
|
363
|
+
for (const uri of configFiles) {
|
|
331
364
|
try {
|
|
332
|
-
await this.registerConfigFile(
|
|
365
|
+
await this.registerConfigFile(uri);
|
|
333
366
|
}
|
|
334
367
|
catch (error) {
|
|
335
|
-
logger.error('Failed to load config file {uri}', { uri:
|
|
368
|
+
logger.error('Failed to load config file {uri}', { uri: uri.fsPath, error });
|
|
336
369
|
}
|
|
337
370
|
}
|
|
338
|
-
this.
|
|
339
|
-
await this.
|
|
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
|
-
|
|
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
|
|
359
|
-
|
|
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 ??
|
|
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.
|
|
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.
|
|
81
|
+
"esbuild": "0.27.0"
|
|
82
82
|
},
|
|
83
83
|
"devDependencies": {
|
|
84
|
-
"@hono/node-server": "^1.19.
|
|
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.
|
|
96
|
+
"fast-equals": "^5.3.3",
|
|
97
97
|
"fdir": "6.4.0",
|
|
98
98
|
"fetch-to-node": "^2.1.0",
|
|
99
|
-
"hono": "^4.10.
|
|
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.
|
|
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.
|
|
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.
|
|
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/
|
|
129
|
-
"@likec4/
|
|
130
|
-
"@likec4/
|
|
131
|
-
"@likec4/
|
|
132
|
-
"@likec4/
|
|
133
|
-
"@likec4/
|
|
134
|
-
"@likec4/
|
|
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",
|