@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.
- package/dist/LikeC4LanguageServices.d.ts +43 -7
- package/dist/LikeC4LanguageServices.js +51 -11
- package/dist/Rpc.js +22 -6
- package/dist/browser.d.ts +1 -1
- package/dist/browser.js +1 -1
- package/dist/bundled.mjs +4258 -3083
- package/dist/config/schema.d.ts +1 -1
- package/dist/config/schema.js +1 -1
- package/dist/empty.d.ts +2 -0
- package/dist/empty.js +1 -0
- package/dist/filesystem/ChokidarWatcher.d.ts +14 -0
- package/dist/filesystem/ChokidarWatcher.js +64 -0
- package/dist/filesystem/FileSystemWatcher.d.ts +19 -0
- package/dist/filesystem/FileSystemWatcher.js +11 -0
- package/dist/filesystem/LikeC4FileSystem.d.ts +5 -0
- package/dist/filesystem/LikeC4FileSystem.js +56 -0
- package/dist/filesystem/index.d.ts +20 -0
- package/dist/filesystem/index.js +16 -0
- package/dist/index.d.ts +18 -4
- package/dist/index.js +23 -10
- package/dist/likec4lib.d.ts +1 -1
- package/dist/lsp/DocumentLinkProvider.js +3 -3
- package/dist/lsp/DocumentSymbolProvider.js +1 -1
- package/dist/mcp/{sseserver/MCPServerFactory.d.ts → MCPServerFactory.d.ts} +1 -1
- package/dist/mcp/MCPServerFactory.js +69 -0
- package/dist/mcp/NoopLikeC4MCPServer.d.ts +4 -10
- package/dist/mcp/NoopLikeC4MCPServer.js +5 -10
- package/dist/mcp/interfaces.d.ts +7 -5
- package/dist/mcp/interfaces.js +4 -0
- package/dist/mcp/server/StdioLikeC4MCPServer.d.ts +16 -0
- package/dist/mcp/server/StdioLikeC4MCPServer.js +43 -0
- package/dist/mcp/{sseserver/MCPServer.d.ts → server/StreamableLikeC4MCPServer.d.ts} +3 -2
- package/dist/mcp/server/StreamableLikeC4MCPServer.js +156 -0
- package/dist/mcp/server/WithMCPServer.d.ts +2 -0
- package/dist/mcp/server/WithMCPServer.js +57 -0
- package/dist/mcp/tools/_common.d.ts +24 -5
- package/dist/mcp/tools/_common.js +31 -3
- package/dist/mcp/tools/find-relationships.d.ts +13 -0
- package/dist/mcp/tools/find-relationships.js +151 -0
- package/dist/mcp/tools/list-projects.js +42 -14
- package/dist/mcp/tools/open-view.d.ts +4 -3
- package/dist/mcp/tools/open-view.js +37 -14
- package/dist/mcp/tools/{read-project-elements.d.ts → read-deployment.d.ts} +6 -3
- package/dist/mcp/tools/read-deployment.js +130 -0
- package/dist/mcp/tools/read-element.d.ts +4 -3
- package/dist/mcp/tools/read-element.js +114 -51
- package/dist/mcp/tools/read-project-summary.d.ts +3 -2
- package/dist/mcp/tools/read-project-summary.js +141 -34
- package/dist/mcp/tools/read-view.d.ts +4 -3
- package/dist/mcp/tools/read-view.js +146 -105
- package/dist/mcp/tools/search-element.js +81 -30
- package/dist/mcp/utils.js +7 -4
- package/dist/model/builder/buildModel.d.ts +1 -1
- package/dist/model/builder/buildModel.js +4 -6
- package/dist/model/model-parser.d.ts +9 -9
- package/dist/model/model-parser.js +3 -0
- package/dist/model/parser/Base.d.ts +1 -1
- package/dist/model/parser/Base.js +1 -1
- package/dist/model/parser/DeploymentModelParser.d.ts +1 -1
- package/dist/model/parser/DeploymentViewParser.d.ts +1 -1
- package/dist/model/parser/DeploymentViewParser.js +2 -2
- package/dist/model/parser/FqnRefParser.d.ts +1 -1
- package/dist/model/parser/FqnRefParser.js +8 -1
- package/dist/model/parser/GlobalsParser.d.ts +1 -1
- package/dist/model/parser/ImportsParser.d.ts +1 -1
- package/dist/model/parser/ModelParser.d.ts +1 -1
- package/dist/model/parser/PredicatesParser.d.ts +1 -1
- package/dist/model/parser/SpecificationParser.d.ts +1 -1
- package/dist/model/parser/ViewsParser.d.ts +1 -1
- package/dist/model/parser/ViewsParser.js +3 -3
- package/dist/module.d.ts +13 -9
- package/dist/module.js +28 -30
- package/dist/protocol.d.ts +18 -4
- package/dist/protocol.js +5 -1
- package/dist/test/testServices.d.ts +5 -2
- package/dist/test/testServices.js +7 -3
- package/dist/validation/DocumentValidator.d.ts +11 -0
- package/dist/validation/DocumentValidator.js +16 -0
- package/dist/validation/index.d.ts +1 -1
- package/dist/validation/index.js +1 -0
- package/dist/workspace/LangiumDocuments.d.ts +1 -0
- package/dist/workspace/LangiumDocuments.js +10 -1
- package/dist/workspace/ProjectsManager.d.ts +35 -17
- package/dist/workspace/ProjectsManager.js +168 -54
- package/dist/workspace/WorkspaceManager.d.ts +9 -2
- package/dist/workspace/WorkspaceManager.js +31 -40
- package/package.json +14 -10
- package/dist/LikeC4FileSystem.d.ts +0 -14
- package/dist/LikeC4FileSystem.js +0 -39
- package/dist/mcp/sseserver/MCPServer.js +0 -80
- package/dist/mcp/sseserver/MCPServerFactory.js +0 -50
- package/dist/mcp/sseserver/WithMCPServer.d.ts +0 -9
- package/dist/mcp/sseserver/WithMCPServer.js +0 -53
- 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 {
|
|
4
|
-
import
|
|
5
|
-
|
|
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
|
|
93
|
+
let folderUri2;
|
|
76
94
|
try {
|
|
77
|
-
|
|
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
|
-
|
|
98
|
+
folderUri2 = URI.file("");
|
|
81
99
|
}
|
|
82
100
|
return {
|
|
83
101
|
id: ProjectsManager.DefaultProjectId,
|
|
84
102
|
config: this.defaultGlobalProject.config,
|
|
85
|
-
|
|
103
|
+
folderUri: folderUri2
|
|
86
104
|
};
|
|
87
105
|
}
|
|
88
106
|
const {
|
|
89
107
|
config,
|
|
90
|
-
|
|
108
|
+
folderUri
|
|
91
109
|
} = nonNullable(this._projects.find((p) => p.id === id), `Project "${id}" not found`);
|
|
92
110
|
return {
|
|
93
111
|
id,
|
|
94
|
-
|
|
112
|
+
folderUri,
|
|
95
113
|
config
|
|
96
114
|
};
|
|
97
115
|
}
|
|
98
116
|
ensureProjectId(projectId) {
|
|
99
117
|
if (projectId === ProjectsManager.DefaultProjectId) {
|
|
100
|
-
return
|
|
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(
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
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.
|
|
121
|
-
|
|
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
|
-
|
|
159
|
-
let
|
|
160
|
-
|
|
161
|
-
|
|
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
|
|
164
|
-
|
|
165
|
-
|
|
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
|
-
|
|
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
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
*
|
|
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
|
|
19
|
-
|
|
22
|
+
async performStartup(folders) {
|
|
23
|
+
this.folders ??= folders;
|
|
24
|
+
const configFiles = [];
|
|
20
25
|
for (const folder of folders) {
|
|
21
26
|
try {
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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.
|
|
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.
|
|
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
|
-
"@
|
|
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
|
-
"@
|
|
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
|
-
"
|
|
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/
|
|
137
|
-
"@likec4/
|
|
138
|
-
"@likec4/
|
|
139
|
-
"@likec4/
|
|
140
|
-
"@likec4/
|
|
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 {};
|
package/dist/LikeC4FileSystem.js
DELETED
|
@@ -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
|
-
}
|