@likec4/language-server 1.36.1 → 1.38.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 (94) hide show
  1. package/dist/LikeC4LanguageServices.d.ts +43 -7
  2. package/dist/LikeC4LanguageServices.js +51 -11
  3. package/dist/Rpc.js +22 -6
  4. package/dist/browser.d.ts +1 -1
  5. package/dist/browser.js +1 -1
  6. package/dist/bundled.mjs +4258 -3083
  7. package/dist/config/schema.d.ts +1 -1
  8. package/dist/config/schema.js +1 -1
  9. package/dist/empty.d.ts +2 -0
  10. package/dist/empty.js +1 -0
  11. package/dist/filesystem/ChokidarWatcher.d.ts +14 -0
  12. package/dist/filesystem/ChokidarWatcher.js +64 -0
  13. package/dist/filesystem/FileSystemWatcher.d.ts +19 -0
  14. package/dist/filesystem/FileSystemWatcher.js +11 -0
  15. package/dist/filesystem/LikeC4FileSystem.d.ts +5 -0
  16. package/dist/filesystem/LikeC4FileSystem.js +56 -0
  17. package/dist/filesystem/index.d.ts +20 -0
  18. package/dist/filesystem/index.js +16 -0
  19. package/dist/index.d.ts +18 -4
  20. package/dist/index.js +23 -10
  21. package/dist/likec4lib.d.ts +1 -1
  22. package/dist/lsp/DocumentLinkProvider.js +3 -3
  23. package/dist/lsp/DocumentSymbolProvider.js +1 -1
  24. package/dist/mcp/{sseserver/MCPServerFactory.d.ts → MCPServerFactory.d.ts} +1 -1
  25. package/dist/mcp/MCPServerFactory.js +69 -0
  26. package/dist/mcp/NoopLikeC4MCPServer.d.ts +4 -10
  27. package/dist/mcp/NoopLikeC4MCPServer.js +5 -10
  28. package/dist/mcp/interfaces.d.ts +7 -5
  29. package/dist/mcp/interfaces.js +4 -0
  30. package/dist/mcp/server/StdioLikeC4MCPServer.d.ts +16 -0
  31. package/dist/mcp/server/StdioLikeC4MCPServer.js +43 -0
  32. package/dist/mcp/{sseserver/MCPServer.d.ts → server/StreamableLikeC4MCPServer.d.ts} +3 -2
  33. package/dist/mcp/server/StreamableLikeC4MCPServer.js +156 -0
  34. package/dist/mcp/server/WithMCPServer.d.ts +2 -0
  35. package/dist/mcp/server/WithMCPServer.js +57 -0
  36. package/dist/mcp/tools/_common.d.ts +24 -5
  37. package/dist/mcp/tools/_common.js +31 -3
  38. package/dist/mcp/tools/find-relationships.d.ts +13 -0
  39. package/dist/mcp/tools/find-relationships.js +151 -0
  40. package/dist/mcp/tools/list-projects.js +42 -14
  41. package/dist/mcp/tools/open-view.d.ts +4 -3
  42. package/dist/mcp/tools/open-view.js +37 -14
  43. package/dist/mcp/tools/{read-project-elements.d.ts → read-deployment.d.ts} +6 -3
  44. package/dist/mcp/tools/read-deployment.js +130 -0
  45. package/dist/mcp/tools/read-element.d.ts +4 -3
  46. package/dist/mcp/tools/read-element.js +114 -51
  47. package/dist/mcp/tools/read-project-summary.d.ts +3 -2
  48. package/dist/mcp/tools/read-project-summary.js +141 -34
  49. package/dist/mcp/tools/read-view.d.ts +4 -3
  50. package/dist/mcp/tools/read-view.js +146 -105
  51. package/dist/mcp/tools/search-element.js +81 -30
  52. package/dist/mcp/utils.js +7 -4
  53. package/dist/model/builder/buildModel.d.ts +1 -1
  54. package/dist/model/builder/buildModel.js +4 -6
  55. package/dist/model/model-parser.d.ts +9 -9
  56. package/dist/model/model-parser.js +3 -0
  57. package/dist/model/parser/Base.d.ts +1 -1
  58. package/dist/model/parser/Base.js +1 -1
  59. package/dist/model/parser/DeploymentModelParser.d.ts +1 -1
  60. package/dist/model/parser/DeploymentViewParser.d.ts +1 -1
  61. package/dist/model/parser/DeploymentViewParser.js +2 -2
  62. package/dist/model/parser/FqnRefParser.d.ts +1 -1
  63. package/dist/model/parser/FqnRefParser.js +8 -1
  64. package/dist/model/parser/GlobalsParser.d.ts +1 -1
  65. package/dist/model/parser/ImportsParser.d.ts +1 -1
  66. package/dist/model/parser/ModelParser.d.ts +1 -1
  67. package/dist/model/parser/PredicatesParser.d.ts +1 -1
  68. package/dist/model/parser/SpecificationParser.d.ts +1 -1
  69. package/dist/model/parser/ViewsParser.d.ts +1 -1
  70. package/dist/model/parser/ViewsParser.js +3 -3
  71. package/dist/module.d.ts +13 -9
  72. package/dist/module.js +28 -30
  73. package/dist/protocol.d.ts +18 -4
  74. package/dist/protocol.js +5 -1
  75. package/dist/test/testServices.d.ts +5 -2
  76. package/dist/test/testServices.js +7 -3
  77. package/dist/validation/DocumentValidator.d.ts +11 -0
  78. package/dist/validation/DocumentValidator.js +16 -0
  79. package/dist/validation/index.d.ts +1 -1
  80. package/dist/validation/index.js +1 -0
  81. package/dist/workspace/LangiumDocuments.d.ts +1 -0
  82. package/dist/workspace/LangiumDocuments.js +10 -1
  83. package/dist/workspace/ProjectsManager.d.ts +35 -17
  84. package/dist/workspace/ProjectsManager.js +168 -54
  85. package/dist/workspace/WorkspaceManager.d.ts +9 -2
  86. package/dist/workspace/WorkspaceManager.js +31 -40
  87. package/package.json +14 -10
  88. package/dist/LikeC4FileSystem.d.ts +0 -14
  89. package/dist/LikeC4FileSystem.js +0 -39
  90. package/dist/mcp/sseserver/MCPServer.js +0 -80
  91. package/dist/mcp/sseserver/MCPServerFactory.js +0 -50
  92. package/dist/mcp/sseserver/WithMCPServer.d.ts +0 -9
  93. package/dist/mcp/sseserver/WithMCPServer.js +0 -53
  94. package/dist/mcp/tools/read-project-elements.js +0 -93
@@ -1,18 +1,33 @@
1
- import { BiMap, invariant, nonNullable } from "@likec4/core";
1
+ import { BiMap, delay, invariant, memoizeProp, nonNullable } from "@likec4/core";
2
2
  import { loggable } from "@likec4/log";
3
- import { URI, WorkspaceCache } from "langium";
4
- import picomatch from "picomatch/posix";
5
- import { hasAtLeast, isNullish, map, pipe, prop, sortBy } from "remeda";
3
+ import { deepEqual } from "fast-equals";
4
+ import {
5
+ interruptAndCheck,
6
+ URI,
7
+ WorkspaceCache
8
+ } from "langium";
9
+ import PQueue from "p-queue";
10
+ import picomatch from "picomatch";
11
+ import { hasAtLeast, isNullish, isTruthy, map, pickBy, pipe, prop, sortBy } from "remeda";
6
12
  import {
7
13
  hasProtocol,
8
14
  joinRelativeURL,
15
+ normalizeURL,
9
16
  parseFilename,
10
17
  withoutProtocol,
11
- withProtocol
18
+ withProtocol,
19
+ withTrailingSlash
12
20
  } from "ufo";
13
21
  import { parseConfigJson, validateConfig } from "../config/index.js";
14
22
  import { logger as mainLogger } from "../logger.js";
15
23
  const logger = mainLogger.getChild("ProjectsManager");
24
+ export function ProjectFolder(folder) {
25
+ if (URI.isUri(folder)) {
26
+ folder = folder.toString();
27
+ }
28
+ folder = hasProtocol(folder) ? folder : withProtocol(folder, "file://");
29
+ return withTrailingSlash(normalizeURL(folder));
30
+ }
16
31
  export class ProjectsManager {
17
32
  constructor(services) {
18
33
  this.services = services;
@@ -38,16 +53,19 @@ export class ProjectsManager {
38
53
  * This ensures that the most specific project is used for a document.
39
54
  */
40
55
  _projects = [];
56
+ excludedDocuments = /* @__PURE__ */ new WeakMap();
41
57
  defaultGlobalProject = {
42
58
  id: ProjectsManager.DefaultProjectId,
43
59
  config: {
44
60
  name: ProjectsManager.DefaultProjectId,
45
- exclude: ["**/node_modules/**/*"]
61
+ exclude: ["**/node_modules/**"]
46
62
  },
47
- exclude: picomatch("**/node_modules/**/*", {
48
- dot: true
49
- })
63
+ exclude: picomatch("**/node_modules/**", { dot: true })
50
64
  };
65
+ reloadProjectsLimiter = new PQueue({
66
+ concurrency: 1,
67
+ timeout: 2e4
68
+ });
51
69
  /**
52
70
  * Returns:
53
71
  * - the default project ID if there are no projects.
@@ -72,32 +90,32 @@ export class ProjectsManager {
72
90
  getProject(arg) {
73
91
  const id = typeof arg === "string" ? arg : arg.likec4ProjectId || this.belongsTo(arg);
74
92
  if (id === ProjectsManager.DefaultProjectId) {
75
- let folder2;
93
+ let folderUri2;
76
94
  try {
77
- folder2 = this.services.workspace.WorkspaceManager.workspaceUri;
95
+ folderUri2 = this.services.workspace.WorkspaceManager.workspaceUri;
78
96
  } catch (error) {
79
97
  logger.warn("Failed to get workspace URI, using default folder", { error });
80
- folder2 = URI.file("");
98
+ folderUri2 = URI.file("");
81
99
  }
82
100
  return {
83
101
  id: ProjectsManager.DefaultProjectId,
84
102
  config: this.defaultGlobalProject.config,
85
- folder: folder2
103
+ folderUri: folderUri2
86
104
  };
87
105
  }
88
106
  const {
89
107
  config,
90
- folder
108
+ folderUri
91
109
  } = nonNullable(this._projects.find((p) => p.id === id), `Project "${id}" not found`);
92
110
  return {
93
111
  id,
94
- folder: URI.parse(folder),
112
+ folderUri,
95
113
  config
96
114
  };
97
115
  }
98
116
  ensureProjectId(projectId) {
99
117
  if (projectId === ProjectsManager.DefaultProjectId) {
100
- return projectId;
118
+ return this.defaultProjectId ?? ProjectsManager.DefaultProjectId;
101
119
  }
102
120
  if (projectId) {
103
121
  invariant(this.projectIdToFolder.has(projectId), `Project ID ${projectId} is not registered`);
@@ -111,14 +129,34 @@ export class ProjectsManager {
111
129
  hasMultipleProjects() {
112
130
  return this._projects.length > 1;
113
131
  }
114
- checkIfExcluded(documentUri) {
115
- let docUriAsString = documentUri.toString();
116
- const project = this.findProjectForDocument(docUriAsString);
117
- return project.exclude ? project.exclude(withoutProtocol(docUriAsString)) : false;
132
+ checkIfExcluded(document) {
133
+ if (typeof document === "string" || URI.isUri(document)) {
134
+ let docUriAsString = typeof document === "string" ? document : document.toString();
135
+ const project = this.findProjectForDocument(docUriAsString);
136
+ return project.exclude ? project.exclude(withoutProtocol(docUriAsString)) : false;
137
+ }
138
+ let isExcluded = this.excludedDocuments.get(document);
139
+ if (isExcluded === void 0) {
140
+ isExcluded = this.checkIfExcluded(document.uri);
141
+ this.excludedDocuments.set(document, isExcluded);
142
+ }
143
+ return isExcluded;
118
144
  }
145
+ /**
146
+ * Checks if it is a config file and it is not excluded by default exclude pattern
147
+ *
148
+ * @param entry The file system entry to check
149
+ */
119
150
  isConfigFile(entry) {
120
- const filename = parseFilename(entry.uri.toString(), { strict: false })?.toLowerCase();
121
- return !!filename && ProjectsManager.ConfigFileNames.includes(filename);
151
+ const filename = parseFilename(entry.toString(), { strict: false })?.toLowerCase();
152
+ const isConfigFile = !!filename && ProjectsManager.ConfigFileNames.includes(filename);
153
+ if (isConfigFile) {
154
+ if (this.defaultGlobalProject.exclude(entry.path)) {
155
+ logger.debug`exclude config file ${entry.path}`;
156
+ return false;
157
+ }
158
+ }
159
+ return isConfigFile;
122
160
  }
123
161
  /**
124
162
  * Checks if the provided file system entry is a valid project config file.
@@ -129,7 +167,7 @@ export class ProjectsManager {
129
167
  if (entry.isDirectory) {
130
168
  return void 0;
131
169
  }
132
- if (this.isConfigFile(entry)) {
170
+ if (this.isConfigFile(entry.uri)) {
133
171
  try {
134
172
  return await this.registerProject(entry.uri);
135
173
  } catch (error) {
@@ -151,42 +189,58 @@ ${loggable(error)}`
151
189
  const config2 = parseConfigJson(cfg);
152
190
  const path = joinRelativeURL(configFile.path, "..");
153
191
  const folderUri2 = configFile.with({ path });
154
- return this.registerProject({ config: config2, folderUri: folderUri2 });
192
+ return await this.registerProject({ config: config2, folderUri: folderUri2 });
155
193
  }
156
- const config = validateConfig(opts.config);
194
+ const config = pickBy(validateConfig(opts.config), isTruthy);
157
195
  const { folderUri } = opts;
158
- let id = config.name;
159
- let i = 1;
160
- while (this.projectIdToFolder.has(id)) {
161
- id = `${config.name}-${i++}`;
196
+ const folder = ProjectFolder(folderUri);
197
+ let project = this._projects.find((p) => p.folder === folder);
198
+ if (project && deepEqual(project.config, config)) {
199
+ return project;
162
200
  }
163
- let folder;
164
- if (URI.isUri(folderUri)) {
165
- folder = folderUri.toString();
201
+ let mustReset = !!project && !deepEqual(project.config, config);
202
+ let id;
203
+ if (!project) {
204
+ if (this.projectIdToFolder.has(config.name)) {
205
+ logger.warn`Project "${config.name}" already exists, generating unique ID`;
206
+ }
207
+ id = this.uniqueProjectId(config.name);
208
+ project = {
209
+ id,
210
+ config,
211
+ folder,
212
+ folderUri: URI.parse(folder)
213
+ };
214
+ mustReset = this._projects.some((p) => p.folder.startsWith(folder) || folder.startsWith(p.folder));
215
+ this._projects = pipe(
216
+ [...this._projects, project],
217
+ sortBy(
218
+ [({ folder: folder2 }) => withoutProtocol(folder2).split("/").length, "desc"]
219
+ )
220
+ );
221
+ logger.info`register project ${project.id} folder: ${folder}`;
166
222
  } else {
167
- folder = hasProtocol(folderUri) ? folderUri : withProtocol(folderUri, "file://");
223
+ if (project.config.name !== config.name) {
224
+ this.projectIdToFolder.delete(project.id);
225
+ logger.info`unregister project ${project.id} folder: ${folder}`;
226
+ id = this.uniqueProjectId(config.name);
227
+ project.id = id;
228
+ logger.info`re-register project ${project.id} folder: ${folder}`;
229
+ } else {
230
+ id = project.id;
231
+ logger.info`update project ${project.id} on config change`;
232
+ }
233
+ project.config = config;
168
234
  }
169
- const project = {
170
- id,
171
- config,
172
- folder
173
- };
174
235
  if (isNullish(config.exclude)) {
175
236
  project.exclude = this.defaultGlobalProject.exclude;
176
237
  } else if (hasAtLeast(config.exclude, 1)) {
177
- project.exclude = picomatch(config.exclude, {
178
- dot: true
179
- });
180
- }
181
- this._projects = pipe(
182
- [...this._projects, project],
183
- sortBy(
184
- [({ folder: folder2 }) => withoutProtocol(folder2).split("/").length, "desc"]
185
- )
186
- );
187
- this.projectIdToFolder.set(id, folder);
188
- logger.info`register project ${id} folder: ${folder}`;
189
- this.mappingsToProject.clear();
238
+ project.exclude = picomatch(config.exclude, { dot: true });
239
+ }
240
+ this.projectIdToFolder.set(project.id, folder);
241
+ if (mustReset) {
242
+ this.resetProjectIds();
243
+ }
190
244
  return project;
191
245
  }
192
246
  belongsTo(document) {
@@ -200,6 +254,68 @@ ${loggable(error)}`
200
254
  }
201
255
  return this.findProjectForDocument(documentUri).id;
202
256
  }
257
+ async reloadProjects(token) {
258
+ const folders = this.services.workspace.WorkspaceManager.workspaceFolders;
259
+ if (!folders) {
260
+ logger.warn("No workspace folders found");
261
+ return;
262
+ }
263
+ if (this.reloadProjectsLimiter.size + this.reloadProjectsLimiter.pending > 0) {
264
+ logger.debug`reload projects is already queued`;
265
+ return;
266
+ }
267
+ this.reloadProjectsLimiter.add(async () => {
268
+ await delay(100);
269
+ });
270
+ this.reloadProjectsLimiter.add(async () => {
271
+ if (token) {
272
+ await interruptAndCheck(token);
273
+ }
274
+ logger.debug`reload projects`;
275
+ const configFiles = [];
276
+ for (const folder of folders) {
277
+ try {
278
+ const files = await this.services.workspace.FileSystemProvider.scanProjectFiles(URI.parse(folder.uri));
279
+ for (const file of files) {
280
+ if (file.isFile && this.isConfigFile(file.uri)) {
281
+ configFiles.push(file);
282
+ }
283
+ }
284
+ } catch (error) {
285
+ logger.error("Failed to load config file", { error });
286
+ }
287
+ }
288
+ if (configFiles.length === 0 && this._projects.length !== 0) {
289
+ logger.warning("No config files found, but some projects were registered before");
290
+ }
291
+ this._projects = [];
292
+ this.projectIdToFolder.clear();
293
+ for (const entry of configFiles) {
294
+ try {
295
+ await this.registerProject(entry.uri);
296
+ } catch (error) {
297
+ logger.error("Failed to load config file", { error });
298
+ }
299
+ }
300
+ this.resetProjectIds();
301
+ const docs = this.services.workspace.LangiumDocuments.all.map((d) => d.uri).toArray();
302
+ logger.info("invalidate and rebuild documents {docs}", { docs: docs.length });
303
+ await this.services.workspace.DocumentBuilder.update(docs, []);
304
+ });
305
+ }
306
+ uniqueProjectId(name) {
307
+ let id = name;
308
+ let i = 1;
309
+ while (this.projectIdToFolder.has(id)) {
310
+ id = `${name}-${i++}`;
311
+ }
312
+ return id;
313
+ }
314
+ resetProjectIds() {
315
+ this.mappingsToProject.clear();
316
+ this.excludedDocuments = /* @__PURE__ */ new WeakMap();
317
+ this.services.workspace.LangiumDocuments.resetProjectIds();
318
+ }
203
319
  findProjectForDocument(documentUri) {
204
320
  return this.mappingsToProject.get(documentUri, () => {
205
321
  const project = this._projects.find(({ folder }) => documentUri.startsWith(folder));
@@ -208,9 +324,7 @@ ${loggable(error)}`
208
324
  }
209
325
  // The mapping between document URIs and their corresponding project ID
210
326
  // Lazy-created due to initialization order of the LanguageServer
211
- _mappingsToProject;
212
327
  get mappingsToProject() {
213
- this._mappingsToProject ??= new WorkspaceCache(this.services);
214
- return this._mappingsToProject;
328
+ return memoizeProp(this, "_mappingsToProject", () => new WorkspaceCache(this.services));
215
329
  }
216
330
  }
@@ -1,12 +1,19 @@
1
- import type { FileSelector, FileSystemNode, LangiumDocument } from 'langium';
1
+ import type { BuildOptions, FileSelector, FileSystemNode, LangiumDocument, LangiumDocumentFactory } from 'langium';
2
2
  import { DefaultWorkspaceManager } from 'langium';
3
3
  import type { WorkspaceFolder } from 'vscode-languageserver';
4
4
  import { URI } from 'vscode-uri';
5
+ import type { FileSystemProvider } from '../filesystem';
5
6
  import type { LikeC4SharedServices } from '../module';
6
7
  export declare class LikeC4WorkspaceManager extends DefaultWorkspaceManager {
7
8
  private services;
8
- private documentFactory;
9
+ protected readonly documentFactory: LangiumDocumentFactory;
10
+ protected readonly fileSystemProvider: FileSystemProvider;
11
+ initialBuildOptions: BuildOptions;
9
12
  constructor(services: LikeC4SharedServices);
13
+ /**
14
+ * First load all project config files, then load all documents in the workspace.
15
+ */
16
+ protected performStartup(folders: WorkspaceFolder[]): Promise<LangiumDocument[]>;
10
17
  /**
11
18
  * Load all additional documents that shall be visible in the context of the given workspace
12
19
  * folders and add them to the collector. This can be used to include built-in libraries of
@@ -8,63 +8,54 @@ export class LikeC4WorkspaceManager extends DefaultWorkspaceManager {
8
8
  super(services);
9
9
  this.services = services;
10
10
  this.documentFactory = services.workspace.LangiumDocumentFactory;
11
+ this.fileSystemProvider = services.workspace.FileSystemProvider;
11
12
  }
12
13
  documentFactory;
14
+ fileSystemProvider;
15
+ initialBuildOptions = {
16
+ eagerLinking: true,
17
+ validation: true
18
+ };
13
19
  /**
14
- * Load all additional documents that shall be visible in the context of the given workspace
15
- * folders and add them to the collector. This can be used to include built-in libraries of
16
- * your language, which can be either loaded from provided files or constructed in memory.
20
+ * First load all project config files, then load all documents in the workspace.
17
21
  */
18
- async loadAdditionalDocuments(folders, collector) {
19
- const projects = this.services.workspace.ProjectsManager;
22
+ async performStartup(folders) {
23
+ this.folders ??= folders;
24
+ const configFiles = [];
20
25
  for (const folder of folders) {
21
26
  try {
22
- const content = await this.fileSystemProvider.readDirectory(URI.parse(folder.uri));
23
- for (const entry of content) {
24
- try {
25
- await projects.loadConfigFile(entry);
26
- } catch (error) {
27
- logError(error);
28
- }
29
- }
27
+ const uri = URI.parse(folder.uri);
28
+ const found = await this.fileSystemProvider.scanProjectFiles(uri);
29
+ configFiles.push(...found);
30
+ this.services.workspace.FileSystemWatcher.watch(uri.fsPath);
31
+ } catch (error) {
32
+ logError(error);
33
+ }
34
+ }
35
+ const projects = this.services.workspace.ProjectsManager;
36
+ for (const entry of configFiles) {
37
+ try {
38
+ await projects.loadConfigFile(entry);
30
39
  } catch (error) {
31
40
  logError(error);
32
41
  }
33
42
  }
43
+ return await super.performStartup(folders);
44
+ }
45
+ /**
46
+ * Load all additional documents that shall be visible in the context of the given workspace
47
+ * folders and add them to the collector. This can be used to include built-in libraries of
48
+ * your language, which can be either loaded from provided files or constructed in memory.
49
+ */
50
+ async loadAdditionalDocuments(folders, collector) {
34
51
  collector(this.documentFactory.fromString(BuiltIn.Content, URI.parse(BuiltIn.Uri)));
35
52
  await super.loadAdditionalDocuments(folders, collector);
36
53
  }
37
- // /**
38
- // * We override the default implementation to process project config files during the traversal.
39
- // * This is necessary to ensure that the project config files are loaded and processed correctly.
40
- // */
41
- // protected override async traverseFolder(
42
- // workspaceFolder: WorkspaceFolder,
43
- // folderPath: URI,
44
- // fileExtensions: string[],
45
- // collector: (document: LangiumDocument) => void,
46
- // ): Promise<void> {
47
- // // Then load other files
48
- // for (const entry of nonConfigFiles) {
49
- // try {
50
- // if (this.includeEntry(workspaceFolder, entry, fileExtensions)) {
51
- // if (entry.isDirectory) {
52
- // await this.traverseFolder(workspaceFolder, entry.uri, fileExtensions, collector)
53
- // } else if (entry.isFile) {
54
- // const document = await this.langiumDocuments.getOrCreateDocument(entry.uri)
55
- // collector(document)
56
- // }
57
- // }
58
- // } catch (error) {
59
- // logError(error)
60
- // }
61
- // }
62
- // }
63
54
  /**
64
55
  * Determine whether the given folder entry shall be included while indexing the workspace.
65
56
  */
66
57
  includeEntry(_workspaceFolder, entry, selector) {
67
- if (this.services.workspace.ProjectsManager.isConfigFile(entry)) {
58
+ if (this.services.workspace.ProjectsManager.isConfigFile(entry.uri)) {
68
59
  return false;
69
60
  }
70
61
  if (entry.isFile) {
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.36.1",
4
+ "version": "1.38.0",
5
5
  "license": "MIT",
6
6
  "bugs": "https://github.com/likec4/likec4/issues",
7
7
  "homepage": "https://likec4.dev",
@@ -89,21 +89,24 @@
89
89
  "access": "public"
90
90
  },
91
91
  "dependencies": {
92
- "@hpcc-js/wasm-graphviz": "1.10.0"
92
+ "@hpcc-js/wasm-graphviz": "1.11.0"
93
93
  },
94
94
  "devDependencies": {
95
95
  "@types/chroma-js": "^3.1.1",
96
96
  "@types/natural-compare-lite": "^1.4.2",
97
- "@modelcontextprotocol/sdk": "^1.13.2",
97
+ "@types/vscode": "^1.84.0",
98
+ "@modelcontextprotocol/sdk": "^1.17.2",
98
99
  "@msgpack/msgpack": "^3.1.2",
99
100
  "@smithy/util-base64": "^4.0.0",
100
- "@types/express": "^5.0.3",
101
+ "@hono/node-server": "^1.14.4",
101
102
  "@types/node": "~20.19.2",
102
103
  "@types/picomatch": "^4.0.2",
103
104
  "@types/which": "^3.0.4",
105
+ "chokidar": "^4.0.3",
104
106
  "chroma-js": "^3.1.2",
107
+ "defu": "^6.1.4",
105
108
  "esm-env": "^1.2.2",
106
- "express": "^5.1.0",
109
+ "hono": "^4.9.0",
107
110
  "fast-equals": "^5.2.2",
108
111
  "fdir": "6.4.6",
109
112
  "indent-string": "^5.0.0",
@@ -116,6 +119,7 @@
116
119
  "p-timeout": "6.1.4",
117
120
  "picomatch": "^4.0.3",
118
121
  "pretty-ms": "^9.2.0",
122
+ "fetch-to-node": "^2.1.0",
119
123
  "remeda": "^2.23.1",
120
124
  "strip-indent": "^4.0.0",
121
125
  "tsx": "4.20.3",
@@ -133,11 +137,11 @@
133
137
  "vscode-uri": "3.1.0",
134
138
  "which": "^5.0.0",
135
139
  "zod": "3.25.67",
136
- "@likec4/layouts": "1.36.1",
137
- "@likec4/core": "1.36.1",
138
- "@likec4/icons": "1.36.1",
139
- "@likec4/log": "1.36.1",
140
- "@likec4/tsconfig": "1.36.1"
140
+ "@likec4/log": "1.38.0",
141
+ "@likec4/icons": "1.38.0",
142
+ "@likec4/core": "1.38.0",
143
+ "@likec4/tsconfig": "1.38.0",
144
+ "@likec4/layouts": "1.38.0"
141
145
  },
142
146
  "scripts": {
143
147
  "typecheck": "tsc -b --verbose",
@@ -1,14 +0,0 @@
1
- import { type FileSystemNode, URI } from 'langium';
2
- import { NodeFileSystemProvider } from 'langium/node';
3
- export declare const LikeC4FileSystem: {
4
- fileSystemProvider: () => SymLinkTraversingFileSystemProvider;
5
- };
6
- /**
7
- * A file system provider that follows symbolic links.
8
- * @see https://github.com/likec4/likec4/pull/1213
9
- */
10
- declare class SymLinkTraversingFileSystemProvider extends NodeFileSystemProvider {
11
- readFile(uri: URI): Promise<string>;
12
- readDirectory(folderPath: URI): Promise<FileSystemNode[]>;
13
- }
14
- export {};
@@ -1,39 +0,0 @@
1
- import { fdir } from "fdir";
2
- import { URI } from "langium";
3
- import { NodeFileSystemProvider } from "langium/node";
4
- import { LikeC4LanguageMetaData } from "./generated/module.js";
5
- import { Content, isLikeC4Builtin } from "./likec4lib.js";
6
- import { logError } from "./logger.js";
7
- import { ProjectsManager } from "./workspace/ProjectsManager.js";
8
- export const LikeC4FileSystem = {
9
- fileSystemProvider: () => new SymLinkTraversingFileSystemProvider()
10
- };
11
- const SearchExtension = [
12
- ...LikeC4LanguageMetaData.fileExtensions,
13
- ...ProjectsManager.ConfigFileNames
14
- ];
15
- const hasExtension = (path) => SearchExtension.some((ext) => path.endsWith(ext));
16
- class SymLinkTraversingFileSystemProvider extends NodeFileSystemProvider {
17
- async readFile(uri) {
18
- if (isLikeC4Builtin(uri)) {
19
- return Promise.resolve(Content);
20
- }
21
- return await super.readFile(uri);
22
- }
23
- async readDirectory(folderPath) {
24
- const entries = [];
25
- try {
26
- const crawled = await new fdir().withSymlinks({ resolvePaths: false }).withFullPaths().filter(hasExtension).crawl(folderPath.fsPath).withPromise();
27
- for (const path of crawled) {
28
- entries.push({
29
- isFile: true,
30
- isDirectory: false,
31
- uri: URI.file(path)
32
- });
33
- }
34
- } catch (error) {
35
- logError(error);
36
- }
37
- return entries;
38
- }
39
- }
@@ -1,80 +0,0 @@
1
- import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
2
- import express from "express";
3
- import { logger } from "../utils.js";
4
- export class SSELikeC4MCPServer {
5
- constructor(services) {
6
- this.services = services;
7
- }
8
- // Store transports by session ID to send notifications
9
- transports = {};
10
- server = void 0;
11
- _port = 33335;
12
- get isStarted() {
13
- return this.server?.listening === true;
14
- }
15
- get port() {
16
- return this._port;
17
- }
18
- async dispose() {
19
- await this.stop();
20
- }
21
- async start(port = 33335) {
22
- if (this.server) {
23
- if (this.port === port) {
24
- return;
25
- }
26
- await this.stop();
27
- }
28
- logger.info("Starting MCP server on port {port}", { port });
29
- this._port = port;
30
- const mcp = this.services.mcp.ServerFactory.create();
31
- const app = express();
32
- app.get("/sse", async (_, res) => {
33
- const transport = new SSEServerTransport("/messages", res);
34
- this.transports[transport.sessionId] = transport;
35
- logger.debug`SSE connection established, sessionId: ${transport.sessionId}`;
36
- res.on("close", () => {
37
- delete this.transports[transport.sessionId];
38
- });
39
- await mcp.connect(transport);
40
- });
41
- app.post("/messages", async (req, res) => {
42
- const sessionId = req.query["sessionId"];
43
- const transport = this.transports[sessionId];
44
- if (transport) {
45
- logger.debug`SSE message received, sessionId: ${sessionId}`;
46
- await transport.handlePostMessage(req, res);
47
- } else {
48
- res.status(400).send("No transport found for sessionId");
49
- }
50
- });
51
- return new Promise((resolve, reject) => {
52
- this.server = app.listen(this._port, (err) => {
53
- if (err) {
54
- reject(err);
55
- return;
56
- }
57
- logger.info("MCP server listening on port {port}", { port: this._port });
58
- resolve();
59
- });
60
- });
61
- }
62
- async stop() {
63
- this.transports = {};
64
- const server = this.server;
65
- if (!server) {
66
- return;
67
- }
68
- logger.info("Stopping MCP server");
69
- this.server = void 0;
70
- return new Promise((resolve) => {
71
- server.close((err) => {
72
- if (err) {
73
- logger.error("Failed to stop MCP server", { err });
74
- }
75
- logger.info("MCP server stopped");
76
- resolve();
77
- });
78
- });
79
- }
80
- }