umple-lsp-server 0.2.1 → 0.2.4
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/completions.scm +21 -6
- package/definitions.scm +4 -0
- package/out/completionAnalysis.d.ts +44 -0
- package/out/completionAnalysis.js +391 -0
- package/out/completionAnalysis.js.map +1 -0
- package/out/completionBuilder.d.ts +28 -0
- package/out/completionBuilder.js +251 -0
- package/out/completionBuilder.js.map +1 -0
- package/out/documentSymbolBuilder.d.ts +13 -0
- package/out/documentSymbolBuilder.js +95 -0
- package/out/documentSymbolBuilder.js.map +1 -0
- package/out/formatRules.d.ts +27 -0
- package/out/formatRules.js +114 -0
- package/out/formatRules.js.map +1 -0
- package/out/formatter.d.ts +53 -0
- package/out/formatter.js +380 -0
- package/out/formatter.js.map +1 -0
- package/out/hoverBuilder.d.ts +21 -0
- package/out/hoverBuilder.js +308 -0
- package/out/hoverBuilder.js.map +1 -0
- package/out/importGraph.d.ts +28 -0
- package/out/importGraph.js +91 -0
- package/out/importGraph.js.map +1 -0
- package/out/referenceSearch.d.ts +22 -0
- package/out/referenceSearch.js +271 -0
- package/out/referenceSearch.js.map +1 -0
- package/out/resolver.d.ts +21 -0
- package/out/resolver.js +174 -0
- package/out/resolver.js.map +1 -0
- package/out/server.js +560 -327
- package/out/server.js.map +1 -1
- package/out/symbolIndex.d.ts +100 -94
- package/out/symbolIndex.js +392 -399
- package/out/symbolIndex.js.map +1 -1
- package/out/symbolTypes.d.ts +34 -0
- package/out/symbolTypes.js +9 -0
- package/out/symbolTypes.js.map +1 -0
- package/out/tokenAnalysis.d.ts +24 -0
- package/out/tokenAnalysis.js +195 -0
- package/out/tokenAnalysis.js.map +1 -0
- package/out/tokenTypes.d.ts +46 -0
- package/out/tokenTypes.js +28 -0
- package/out/tokenTypes.js.map +1 -0
- package/out/treeUtils.d.ts +32 -0
- package/out/treeUtils.js +89 -0
- package/out/treeUtils.js.map +1 -0
- package/package.json +4 -2
- package/references.scm +78 -10
- package/tree-sitter-umple.wasm +0 -0
package/out/server.js
CHANGED
|
@@ -2,18 +2,106 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
const child_process_1 = require("child_process");
|
|
4
4
|
const fs = require("fs");
|
|
5
|
-
const net = require("net");
|
|
6
5
|
const os = require("os");
|
|
7
6
|
const path = require("path");
|
|
8
7
|
const url_1 = require("url");
|
|
9
8
|
const node_1 = require("vscode-languageserver/node");
|
|
10
9
|
const vscode_languageserver_textdocument_1 = require("vscode-languageserver-textdocument");
|
|
11
|
-
const keywords_1 = require("./keywords");
|
|
12
10
|
const symbolIndex_1 = require("./symbolIndex");
|
|
11
|
+
const resolver_1 = require("./resolver");
|
|
12
|
+
const completionBuilder_1 = require("./completionBuilder");
|
|
13
|
+
const hoverBuilder_1 = require("./hoverBuilder");
|
|
14
|
+
const documentSymbolBuilder_1 = require("./documentSymbolBuilder");
|
|
15
|
+
const formatter_1 = require("./formatter");
|
|
13
16
|
const connection = (0, node_1.createConnection)(node_1.ProposedFeatures.all);
|
|
14
17
|
const documents = new Map();
|
|
15
18
|
const pendingValidations = new Map();
|
|
16
19
|
let workspaceRoots = [];
|
|
20
|
+
const rootScanStates = new Map();
|
|
21
|
+
/**
|
|
22
|
+
* Check if the workspace use-graph is ready for the root containing a file.
|
|
23
|
+
* Returns true if the root's scan is complete, false if scanning or idle.
|
|
24
|
+
* Files outside all workspace roots return true (local-only scope is acceptable).
|
|
25
|
+
*/
|
|
26
|
+
function isUseGraphReadyForFile(filePath) {
|
|
27
|
+
const normalized = path.normalize(filePath);
|
|
28
|
+
for (const [root, state] of rootScanStates) {
|
|
29
|
+
if (normalized.startsWith(root))
|
|
30
|
+
return state === "ready";
|
|
31
|
+
}
|
|
32
|
+
// File outside all workspace roots — no graph needed, local scope is fine
|
|
33
|
+
return true;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Async cooperative directory scanner. Discovers .ump files without blocking
|
|
37
|
+
* the event loop. Uses fs.promises.readdir with yielding every 100 directories.
|
|
38
|
+
*/
|
|
39
|
+
async function discoverUmpFilesAsync(root) {
|
|
40
|
+
const results = [];
|
|
41
|
+
const queue = [root];
|
|
42
|
+
const skipDirs = new Set(["node_modules", ".git", "out", ".test-out", "build", "dist"]);
|
|
43
|
+
let processed = 0;
|
|
44
|
+
while (queue.length > 0) {
|
|
45
|
+
const dir = queue.pop();
|
|
46
|
+
let entries;
|
|
47
|
+
try {
|
|
48
|
+
entries = await fs.promises.readdir(dir, { withFileTypes: true });
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
continue; // permission denied, etc.
|
|
52
|
+
}
|
|
53
|
+
for (const entry of entries) {
|
|
54
|
+
const full = path.join(dir, entry.name);
|
|
55
|
+
if (entry.isDirectory()) {
|
|
56
|
+
if (!skipDirs.has(entry.name)) {
|
|
57
|
+
queue.push(full);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
else if (entry.name.endsWith(".ump")) {
|
|
61
|
+
results.push(path.normalize(full));
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
// Yield every 100 directories to keep the server responsive
|
|
65
|
+
if (++processed % 100 === 0) {
|
|
66
|
+
await new Promise((r) => setTimeout(r, 0));
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return results;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Async cooperative use-graph population for a workspace root.
|
|
73
|
+
* Discovers .ump files, extracts use statements (tree-sitter parse, no symbol extraction),
|
|
74
|
+
* and populates the import graph. Skips already-indexed files.
|
|
75
|
+
*/
|
|
76
|
+
async function scanWorkspaceRootAsync(root, getOpenDocContent) {
|
|
77
|
+
rootScanStates.set(root, "scanning");
|
|
78
|
+
try {
|
|
79
|
+
const files = await discoverUmpFilesAsync(root);
|
|
80
|
+
for (let i = 0; i < files.length; i++) {
|
|
81
|
+
const filePath = files[i];
|
|
82
|
+
// Skip already-indexed files (edges are fresh from didOpen/didChange)
|
|
83
|
+
if (symbolIndex_1.symbolIndex.isFileIndexed(filePath))
|
|
84
|
+
continue;
|
|
85
|
+
// Prefer open document content over disk
|
|
86
|
+
const content = getOpenDocContent(filePath) ?? readFileSafe(filePath);
|
|
87
|
+
if (!content)
|
|
88
|
+
continue;
|
|
89
|
+
// Extract use statements and update import graph edges only
|
|
90
|
+
const uses = symbolIndex_1.symbolIndex.extractUseStatements(filePath, content);
|
|
91
|
+
symbolIndex_1.symbolIndex.updateUseGraphEdges(filePath, uses);
|
|
92
|
+
// Yield every 50 files
|
|
93
|
+
if (i % 50 === 0) {
|
|
94
|
+
await new Promise((r) => setTimeout(r, 0));
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
rootScanStates.set(root, "ready");
|
|
98
|
+
connection.console.info(`Workspace use-graph ready for: ${root}`);
|
|
99
|
+
}
|
|
100
|
+
catch (err) {
|
|
101
|
+
rootScanStates.set(root, "idle");
|
|
102
|
+
connection.console.warn(`Workspace use-graph scan failed for ${root}: ${err}`);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
17
105
|
/**
|
|
18
106
|
* Normalize a file URI to a consistent key for the documents map.
|
|
19
107
|
* Converts URI to file path and back to ensure consistent encoding.
|
|
@@ -72,29 +160,17 @@ function findFile(candidates) {
|
|
|
72
160
|
return undefined;
|
|
73
161
|
}
|
|
74
162
|
let umpleSyncJarPath;
|
|
75
|
-
let
|
|
76
|
-
let umpleSyncPort = 5555;
|
|
77
|
-
let umpleSyncTimeoutMs = 50000;
|
|
163
|
+
let umpleSyncTimeoutMs = 30000;
|
|
78
164
|
let jarWarningShown = false;
|
|
79
|
-
let serverProcess;
|
|
80
165
|
let treeSitterWasmPath;
|
|
81
166
|
let symbolIndexReady = false;
|
|
82
|
-
const DEFAULT_UMPLESYNC_TIMEOUT_MS =
|
|
167
|
+
const DEFAULT_UMPLESYNC_TIMEOUT_MS = 30000;
|
|
168
|
+
// Track in-flight validations so we can abort stale ones
|
|
169
|
+
const inFlightValidations = new Map();
|
|
83
170
|
connection.onInitialize((params) => {
|
|
84
171
|
const initOptions = params.initializationOptions;
|
|
85
172
|
umpleSyncJarPath =
|
|
86
173
|
initOptions?.umpleSyncJarPath || process.env.UMPLESYNC_JAR_PATH;
|
|
87
|
-
umpleSyncHost =
|
|
88
|
-
initOptions?.umpleSyncHost || process.env.UMPLESYNC_HOST || "localhost";
|
|
89
|
-
if (typeof initOptions?.umpleSyncPort === "number") {
|
|
90
|
-
umpleSyncPort = initOptions.umpleSyncPort;
|
|
91
|
-
}
|
|
92
|
-
else if (process.env.UMPLESYNC_PORT) {
|
|
93
|
-
const parsed = Number(process.env.UMPLESYNC_PORT);
|
|
94
|
-
if (!Number.isNaN(parsed)) {
|
|
95
|
-
umpleSyncPort = parsed;
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
174
|
if (typeof initOptions?.umpleSyncTimeoutMs === "number") {
|
|
99
175
|
umpleSyncTimeoutMs = initOptions.umpleSyncTimeoutMs;
|
|
100
176
|
}
|
|
@@ -113,9 +189,16 @@ connection.onInitialize((params) => {
|
|
|
113
189
|
textDocumentSync: node_1.TextDocumentSyncKind.Incremental,
|
|
114
190
|
completionProvider: {
|
|
115
191
|
resolveProvider: false,
|
|
116
|
-
triggerCharacters: ["/"],
|
|
192
|
+
triggerCharacters: ["/", "."],
|
|
117
193
|
},
|
|
118
194
|
definitionProvider: true,
|
|
195
|
+
referencesProvider: true,
|
|
196
|
+
renameProvider: {
|
|
197
|
+
prepareProvider: true,
|
|
198
|
+
},
|
|
199
|
+
hoverProvider: true,
|
|
200
|
+
documentSymbolProvider: true,
|
|
201
|
+
documentFormattingProvider: true,
|
|
119
202
|
},
|
|
120
203
|
};
|
|
121
204
|
});
|
|
@@ -134,6 +217,20 @@ connection.onInitialized(async () => {
|
|
|
134
217
|
symbolIndexReady = await symbolIndex_1.symbolIndex.initialize(treeSitterWasmPath);
|
|
135
218
|
if (symbolIndexReady) {
|
|
136
219
|
connection.console.info("Symbol index initialized with tree-sitter.");
|
|
220
|
+
// Start async workspace use-graph scan (non-blocking, cooperative)
|
|
221
|
+
if (workspaceRoots.length > 0) {
|
|
222
|
+
for (const root of workspaceRoots) {
|
|
223
|
+
rootScanStates.set(root, "idle");
|
|
224
|
+
scanWorkspaceRootAsync(root, (filePath) => {
|
|
225
|
+
const uri = (0, url_1.pathToFileURL)(filePath).toString();
|
|
226
|
+
return getDocument(uri)?.getText();
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
// Register file watcher for .ump files to keep the use-graph fresh
|
|
230
|
+
connection.client.register(node_1.DidChangeWatchedFilesNotification.type, {
|
|
231
|
+
watchers: [{ globPattern: "**/*.ump" }],
|
|
232
|
+
});
|
|
233
|
+
}
|
|
137
234
|
}
|
|
138
235
|
}
|
|
139
236
|
catch (err) {
|
|
@@ -154,7 +251,7 @@ connection.onDidOpenTextDocument((params) => {
|
|
|
154
251
|
if (symbolIndexReady) {
|
|
155
252
|
try {
|
|
156
253
|
const filePath = (0, url_1.fileURLToPath)(params.textDocument.uri);
|
|
157
|
-
symbolIndex_1.symbolIndex.
|
|
254
|
+
symbolIndex_1.symbolIndex.indexFile(filePath, params.textDocument.text);
|
|
158
255
|
}
|
|
159
256
|
catch {
|
|
160
257
|
// Ignore errors for non-file URIs
|
|
@@ -190,6 +287,14 @@ connection.onDidChangeTextDocument((params) => {
|
|
|
190
287
|
}
|
|
191
288
|
const updated = vscode_languageserver_textdocument_1.TextDocument.update(document, params.contentChanges, params.textDocument.version);
|
|
192
289
|
setDocument(params.textDocument.uri, updated);
|
|
290
|
+
// Keep the symbol index current so the clean baseline stays fresh.
|
|
291
|
+
// Without this, state symbols added during clean edits would be lost
|
|
292
|
+
// when the file later enters an errored state (error preservation
|
|
293
|
+
// would use a stale clean snapshot).
|
|
294
|
+
const changedPath = getDocumentFilePath(updated);
|
|
295
|
+
if (changedPath && symbolIndexReady) {
|
|
296
|
+
symbolIndex_1.symbolIndex.updateFile(changedPath, updated.getText());
|
|
297
|
+
}
|
|
193
298
|
scheduleValidation(updated);
|
|
194
299
|
// Re-validate other open documents that might depend on this file
|
|
195
300
|
scheduleDependentValidation(params.textDocument.uri);
|
|
@@ -202,8 +307,43 @@ connection.onDidCloseTextDocument((params) => {
|
|
|
202
307
|
clearTimeout(pendingValidation);
|
|
203
308
|
pendingValidations.delete(normalizedUri);
|
|
204
309
|
}
|
|
310
|
+
// Abort any in-flight validation so stale results aren't published after close
|
|
311
|
+
const inFlight = inFlightValidations.get(normalizedUri);
|
|
312
|
+
if (inFlight) {
|
|
313
|
+
inFlight.abort();
|
|
314
|
+
inFlightValidations.delete(normalizedUri);
|
|
315
|
+
}
|
|
205
316
|
connection.sendDiagnostics({ uri: params.textDocument.uri, diagnostics: [] });
|
|
206
317
|
});
|
|
318
|
+
// ── File watcher: keep workspace use-graph fresh for unopened files ──────────
|
|
319
|
+
connection.onDidChangeWatchedFiles((params) => {
|
|
320
|
+
if (!symbolIndexReady)
|
|
321
|
+
return;
|
|
322
|
+
for (const change of params.changes) {
|
|
323
|
+
let filePath;
|
|
324
|
+
try {
|
|
325
|
+
filePath = path.normalize((0, url_1.fileURLToPath)(change.uri));
|
|
326
|
+
}
|
|
327
|
+
catch {
|
|
328
|
+
continue;
|
|
329
|
+
}
|
|
330
|
+
// Skip files that are open in the editor — their edges are managed by didOpen/didChange
|
|
331
|
+
if (getDocument(change.uri))
|
|
332
|
+
continue;
|
|
333
|
+
if (change.type === node_1.FileChangeType.Deleted) {
|
|
334
|
+
// File deleted — remove its import graph edges
|
|
335
|
+
symbolIndex_1.symbolIndex.removeImportEdges(filePath);
|
|
336
|
+
}
|
|
337
|
+
else {
|
|
338
|
+
// Created or Changed — update use-graph edges from disk content
|
|
339
|
+
const content = readFileSafe(filePath);
|
|
340
|
+
if (content) {
|
|
341
|
+
const uses = symbolIndex_1.symbolIndex.extractUseStatements(filePath, content);
|
|
342
|
+
symbolIndex_1.symbolIndex.updateUseGraphEdges(filePath, uses);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
});
|
|
207
347
|
connection.onCompletion(async (params) => {
|
|
208
348
|
const document = getDocument(params.textDocument.uri);
|
|
209
349
|
if (!document) {
|
|
@@ -245,170 +385,344 @@ connection.onCompletion(async (params) => {
|
|
|
245
385
|
else if (params.context?.triggerCharacter === "/") {
|
|
246
386
|
return [];
|
|
247
387
|
}
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
kind: node_1.CompletionItemKind.TypeParameter,
|
|
271
|
-
detail: "type",
|
|
272
|
-
});
|
|
273
|
-
}
|
|
274
|
-
}
|
|
275
|
-
}
|
|
276
|
-
// 5d. Constraint scope: only own attributes (Umple E28)
|
|
277
|
-
if (symbolKinds === "own_attribute" && info.enclosingClass) {
|
|
278
|
-
const symbols = symbolIndex_1.symbolIndex
|
|
279
|
-
.getSymbols({ container: info.enclosingClass, kind: "attribute" })
|
|
280
|
-
.filter((s) => reachableFiles.has(path.normalize(s.file)));
|
|
281
|
-
for (const sym of symbols) {
|
|
282
|
-
if (!seen.has(sym.name)) {
|
|
283
|
-
seen.add(sym.name);
|
|
284
|
-
items.push({
|
|
285
|
-
label: sym.name,
|
|
286
|
-
kind: symbolKindToCompletionKind("attribute"),
|
|
287
|
-
detail: "attribute",
|
|
288
|
-
});
|
|
289
|
-
}
|
|
290
|
-
}
|
|
291
|
-
return items;
|
|
292
|
-
}
|
|
293
|
-
// 5e. Symbol completions from index (scoped to reachable files)
|
|
294
|
-
if (Array.isArray(symbolKinds)) {
|
|
295
|
-
for (const symKind of symbolKinds) {
|
|
296
|
-
let symbols;
|
|
297
|
-
// Scoped lookups for container-aware kinds
|
|
298
|
-
if (symKind === "attribute" && info.enclosingClass) {
|
|
299
|
-
symbols = symbolIndex_1.symbolIndex
|
|
300
|
-
.getSymbols({
|
|
301
|
-
container: info.enclosingClass,
|
|
302
|
-
kind: "attribute",
|
|
303
|
-
inherited: true,
|
|
304
|
-
})
|
|
305
|
-
.filter((s) => reachableFiles.has(path.normalize(s.file)));
|
|
306
|
-
}
|
|
307
|
-
else if (symKind === "state" && info.enclosingStateMachine) {
|
|
308
|
-
symbols = symbolIndex_1.symbolIndex
|
|
309
|
-
.getSymbols({ container: info.enclosingStateMachine, kind: "state" })
|
|
310
|
-
.filter((s) => reachableFiles.has(path.normalize(s.file)));
|
|
311
|
-
}
|
|
312
|
-
else if (symKind === "template" && info.enclosingClass) {
|
|
313
|
-
symbols = symbolIndex_1.symbolIndex
|
|
314
|
-
.getSymbols({ container: info.enclosingClass, kind: "template" })
|
|
315
|
-
.filter((s) => reachableFiles.has(path.normalize(s.file)));
|
|
316
|
-
}
|
|
317
|
-
else {
|
|
318
|
-
symbols = symbolIndex_1.symbolIndex
|
|
319
|
-
.getSymbols({ kind: symKind })
|
|
320
|
-
.filter((s) => reachableFiles.has(path.normalize(s.file)));
|
|
321
|
-
}
|
|
322
|
-
for (const sym of symbols) {
|
|
323
|
-
if (!seen.has(sym.name)) {
|
|
324
|
-
seen.add(sym.name);
|
|
325
|
-
items.push({
|
|
326
|
-
label: sym.name,
|
|
327
|
-
kind: symbolKindToCompletionKind(symKind),
|
|
328
|
-
detail: symKind,
|
|
329
|
-
});
|
|
330
|
-
}
|
|
331
|
-
}
|
|
388
|
+
else if (params.context?.triggerCharacter === ".") {
|
|
389
|
+
if (!info.dottedStatePrefix)
|
|
390
|
+
return [];
|
|
391
|
+
// Dot-state completion: return only child state names, skip all
|
|
392
|
+
// generic phases (keywords, operators, types, other symbol kinds).
|
|
393
|
+
const childNames = info.enclosingStateMachine
|
|
394
|
+
? symbolIndex_1.symbolIndex.getChildStateNames(info.dottedStatePrefix, info.enclosingStateMachine, reachableFiles)
|
|
395
|
+
: [];
|
|
396
|
+
return childNames.map((name) => ({
|
|
397
|
+
label: name,
|
|
398
|
+
kind: (0, completionBuilder_1.symbolKindToCompletionKind)("state"),
|
|
399
|
+
detail: "state",
|
|
400
|
+
sortText: `0_${name}`,
|
|
401
|
+
}));
|
|
402
|
+
}
|
|
403
|
+
// 5. Build semantic completion items (keywords, operators, types, symbols)
|
|
404
|
+
const semanticItems = (0, completionBuilder_1.buildSemanticCompletionItems)(info, symbolKinds, symbolIndex_1.symbolIndex, reachableFiles);
|
|
405
|
+
// Merge use_path items (if any) with semantic items, deduplicating
|
|
406
|
+
for (const item of semanticItems) {
|
|
407
|
+
if (!seen.has(item.label)) {
|
|
408
|
+
seen.add(item.label);
|
|
409
|
+
items.push(item);
|
|
332
410
|
}
|
|
333
411
|
}
|
|
334
412
|
return items;
|
|
335
413
|
});
|
|
414
|
+
// ── Shared symbol resolution (used by go-to-def and hover) ──────────────────
|
|
415
|
+
/**
|
|
416
|
+
* Resolve symbol(s) at a given position. Thin wrapper around the shared
|
|
417
|
+
* resolver that handles reachable-file computation from the document context.
|
|
418
|
+
*/
|
|
419
|
+
function resolveSymbolAtPosition(docPath, content, line, col) {
|
|
420
|
+
const reachableFiles = ensureImportsIndexed(docPath, content);
|
|
421
|
+
return (0, resolver_1.resolveSymbolAtPosition)(symbolIndex_1.symbolIndex, docPath, content, line, col, reachableFiles);
|
|
422
|
+
}
|
|
336
423
|
connection.onDefinition(async (params) => {
|
|
337
424
|
const document = getDocument(params.textDocument.uri);
|
|
338
|
-
if (!document)
|
|
339
|
-
return [];
|
|
340
|
-
}
|
|
341
|
-
if (!symbolIndexReady) {
|
|
425
|
+
if (!document || !symbolIndexReady)
|
|
342
426
|
return [];
|
|
343
|
-
}
|
|
344
427
|
const docPath = getDocumentFilePath(document);
|
|
345
|
-
if (!docPath)
|
|
428
|
+
if (!docPath)
|
|
346
429
|
return [];
|
|
347
|
-
}
|
|
348
430
|
const token = symbolIndex_1.symbolIndex.getTokenAtPosition(docPath, document.getText(), params.position.line, params.position.character);
|
|
349
|
-
if (!token)
|
|
431
|
+
if (!token)
|
|
350
432
|
return [];
|
|
351
|
-
}
|
|
352
433
|
// use statement with .ump extension: resolve as file reference
|
|
353
434
|
if (token.word.endsWith(".ump")) {
|
|
354
435
|
const baseDir = path.dirname(docPath);
|
|
355
436
|
const targetPath = path.isAbsolute(token.word)
|
|
356
437
|
? token.word
|
|
357
438
|
: path.join(baseDir, token.word);
|
|
358
|
-
if (!fs.existsSync(targetPath))
|
|
439
|
+
if (!fs.existsSync(targetPath))
|
|
359
440
|
return [];
|
|
360
|
-
}
|
|
361
441
|
return [
|
|
362
442
|
node_1.Location.create((0, url_1.pathToFileURL)(targetPath).toString(), node_1.Range.create(node_1.Position.create(0, 0), node_1.Position.create(0, 0))),
|
|
363
443
|
];
|
|
364
444
|
}
|
|
365
|
-
|
|
445
|
+
const resolved = resolveSymbolAtPosition(docPath, document.getText(), params.position.line, params.position.character);
|
|
446
|
+
if (!resolved || resolved.symbols.length === 0)
|
|
447
|
+
return [];
|
|
448
|
+
return resolved.symbols.map((sym) => node_1.Location.create((0, url_1.pathToFileURL)(sym.file).toString(), node_1.Range.create(node_1.Position.create(sym.line, sym.column), node_1.Position.create(sym.endLine, sym.endColumn))));
|
|
449
|
+
});
|
|
450
|
+
// ── Find References ──────────────────────────────────────────────────────────
|
|
451
|
+
connection.onReferences(async (params) => {
|
|
452
|
+
const document = getDocument(params.textDocument.uri);
|
|
453
|
+
if (!document || !symbolIndexReady)
|
|
454
|
+
return [];
|
|
455
|
+
const docPath = getDocumentFilePath(document);
|
|
456
|
+
if (!docPath)
|
|
457
|
+
return [];
|
|
458
|
+
// 1. Identify symbol (full declaration set)
|
|
459
|
+
const resolved = resolveSymbolAtPosition(docPath, document.getText(), params.position.line, params.position.character);
|
|
460
|
+
if (!resolved || resolved.symbols.length === 0)
|
|
461
|
+
return [];
|
|
462
|
+
// 2. Index forward-reachable files (fast, import-chain only — no workspace crawl).
|
|
463
|
+
//
|
|
464
|
+
// Scope model: references searches the current file, forward-reachable imports,
|
|
465
|
+
// and reverse importers known to the import graph. The import graph is populated
|
|
466
|
+
// by: (1) didOpen/didChange for open files, (2) async background workspace scan
|
|
467
|
+
// on init, (3) file watcher events for disk changes. This avoids synchronous
|
|
468
|
+
// workspace-wide crawling on the request path. References is best-effort — it
|
|
469
|
+
// uses whatever graph state is available without blocking.
|
|
366
470
|
const reachableFiles = ensureImportsIndexed(docPath, document.getText());
|
|
367
|
-
//
|
|
368
|
-
const
|
|
471
|
+
// 3. Compute search scope: declaration files + forward-reachable + known reverse importers
|
|
472
|
+
const declFiles = new Set(resolved.symbols.map((s) => path.normalize(s.file)));
|
|
473
|
+
const reverseImporters = symbolIndex_1.symbolIndex.getReverseImporters(declFiles);
|
|
474
|
+
const filesToSearch = new Set([...declFiles, ...reachableFiles, ...reverseImporters]);
|
|
475
|
+
// Ensure reverse importers are fully indexed
|
|
476
|
+
for (const file of reverseImporters) {
|
|
477
|
+
if (!symbolIndex_1.symbolIndex.isFileIndexed(file)) {
|
|
478
|
+
const uri = (0, url_1.pathToFileURL)(file).toString();
|
|
479
|
+
const openDoc = getDocument(uri);
|
|
480
|
+
if (openDoc) {
|
|
481
|
+
symbolIndex_1.symbolIndex.updateFile(file, openDoc.getText());
|
|
482
|
+
}
|
|
483
|
+
else if (fs.existsSync(file)) {
|
|
484
|
+
symbolIndex_1.symbolIndex.indexFile(file);
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
// 4. Find references
|
|
489
|
+
const refs = symbolIndex_1.symbolIndex.findReferences(resolved.symbols, filesToSearch, params.context.includeDeclaration);
|
|
490
|
+
// 5. Convert to Location[]
|
|
491
|
+
return refs.map((r) => node_1.Location.create((0, url_1.pathToFileURL)(r.file).toString(), node_1.Range.create(node_1.Position.create(r.line, r.column), node_1.Position.create(r.endLine, r.endColumn))));
|
|
492
|
+
});
|
|
493
|
+
// ── Rename ───────────────────────────────────────────────────────────────────
|
|
494
|
+
const RENAMEABLE_KINDS = new Set([
|
|
495
|
+
"class",
|
|
496
|
+
"interface",
|
|
497
|
+
"trait",
|
|
498
|
+
"enum",
|
|
499
|
+
"mixset",
|
|
500
|
+
"attribute",
|
|
501
|
+
"const",
|
|
502
|
+
"state",
|
|
503
|
+
"statemachine",
|
|
504
|
+
"tracecase",
|
|
505
|
+
]);
|
|
506
|
+
function isUnambiguousRename(symbols) {
|
|
507
|
+
if (symbols.length <= 1)
|
|
508
|
+
return symbols.length === 1;
|
|
509
|
+
// All symbols must share the same kind
|
|
510
|
+
const kind = symbols[0].kind;
|
|
511
|
+
if (!symbols.every((s) => s.kind === kind))
|
|
512
|
+
return false;
|
|
513
|
+
// State: must share same statePath (different paths = different states)
|
|
514
|
+
if (kind === "state") {
|
|
515
|
+
const refPath = symbols[0].statePath?.join(".");
|
|
516
|
+
return symbols.every((s) => s.statePath?.join(".") === refPath);
|
|
517
|
+
}
|
|
518
|
+
// Container-scoped kinds: must share container + name
|
|
519
|
+
const containerScoped = new Set([
|
|
369
520
|
"attribute",
|
|
521
|
+
"const",
|
|
370
522
|
"method",
|
|
371
523
|
"template",
|
|
372
|
-
"
|
|
524
|
+
"statemachine",
|
|
373
525
|
]);
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
if (
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
526
|
+
if (containerScoped.has(kind)) {
|
|
527
|
+
const { container, name } = symbols[0];
|
|
528
|
+
return symbols.every((s) => s.container === container && s.name === name);
|
|
529
|
+
}
|
|
530
|
+
// Top-level mergeable kinds (class, interface, trait, enum, mixset):
|
|
531
|
+
// same name = partial definitions of the same entity
|
|
532
|
+
const name = symbols[0].name;
|
|
533
|
+
return symbols.every((s) => s.name === name);
|
|
534
|
+
}
|
|
535
|
+
connection.onPrepareRename(async (params) => {
|
|
536
|
+
const document = getDocument(params.textDocument.uri);
|
|
537
|
+
if (!document || !symbolIndexReady)
|
|
538
|
+
return null;
|
|
539
|
+
const docPath = getDocumentFilePath(document);
|
|
540
|
+
if (!docPath)
|
|
541
|
+
return null;
|
|
542
|
+
// Full semantic resolution
|
|
543
|
+
const resolved = resolveSymbolAtPosition(docPath, document.getText(), params.position.line, params.position.character);
|
|
544
|
+
if (!resolved || resolved.symbols.length === 0)
|
|
545
|
+
return null;
|
|
546
|
+
// Kind must be in the renameable set
|
|
547
|
+
if (!RENAMEABLE_KINDS.has(resolved.symbols[0].kind))
|
|
548
|
+
return null;
|
|
549
|
+
// Identity must be unambiguous
|
|
550
|
+
if (!isUnambiguousRename(resolved.symbols))
|
|
551
|
+
return null;
|
|
552
|
+
// Get precise identifier range
|
|
553
|
+
const range = symbolIndex_1.symbolIndex.getNodeRangeAtPosition(docPath, document.getText(), params.position.line, params.position.character);
|
|
554
|
+
if (!range)
|
|
555
|
+
return null;
|
|
556
|
+
return {
|
|
557
|
+
range: node_1.Range.create(node_1.Position.create(range.startLine, range.startColumn), node_1.Position.create(range.endLine, range.endColumn)),
|
|
558
|
+
placeholder: resolved.token.word,
|
|
559
|
+
};
|
|
560
|
+
});
|
|
561
|
+
connection.onRenameRequest(async (params) => {
|
|
562
|
+
const document = getDocument(params.textDocument.uri);
|
|
563
|
+
if (!document || !symbolIndexReady)
|
|
564
|
+
return null;
|
|
565
|
+
const docPath = getDocumentFilePath(document);
|
|
566
|
+
if (!docPath)
|
|
567
|
+
return null;
|
|
568
|
+
// Validate new name is a legal identifier
|
|
569
|
+
if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(params.newName))
|
|
570
|
+
return null;
|
|
571
|
+
// 1. Full semantic resolution (same checks as prepareRename)
|
|
572
|
+
const resolved = resolveSymbolAtPosition(docPath, document.getText(), params.position.line, params.position.character);
|
|
573
|
+
if (!resolved || resolved.symbols.length === 0)
|
|
574
|
+
return null;
|
|
575
|
+
if (!RENAMEABLE_KINDS.has(resolved.symbols[0].kind))
|
|
576
|
+
return null;
|
|
577
|
+
if (!isUnambiguousRename(resolved.symbols))
|
|
578
|
+
return null;
|
|
579
|
+
// Check workspace use-graph readiness for the DECLARATION files' roots.
|
|
580
|
+
// Rename must not return partial WorkspaceEdits — if any declaration's
|
|
581
|
+
// root is still scanning, fail explicitly rather than silently miss importers.
|
|
582
|
+
// (Checked after resolution so we know which roots actually matter.)
|
|
583
|
+
for (const sym of resolved.symbols) {
|
|
584
|
+
if (!isUseGraphReadyForFile(sym.file)) {
|
|
585
|
+
connection.window.showWarningMessage("Workspace scan in progress. Please try rename again in a moment.");
|
|
586
|
+
return null;
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
// 2. Index forward-reachable files (fast, import-chain only — no workspace crawl).
|
|
590
|
+
// Same scope model as references: current file + forward imports + known reverse
|
|
591
|
+
// importers. See onReferences comment for full rationale.
|
|
592
|
+
const reachableFiles = ensureImportsIndexed(docPath, document.getText());
|
|
593
|
+
// 3. Compute search scope: declaration files + forward-reachable + known reverse importers
|
|
594
|
+
const declFiles = new Set(resolved.symbols.map((s) => path.normalize(s.file)));
|
|
595
|
+
const reverseImporters = symbolIndex_1.symbolIndex.getReverseImporters(declFiles);
|
|
596
|
+
const filesToSearch = new Set([...declFiles, ...reachableFiles, ...reverseImporters]);
|
|
597
|
+
// Ensure reverse importers are fully indexed
|
|
598
|
+
for (const file of reverseImporters) {
|
|
599
|
+
if (!symbolIndex_1.symbolIndex.isFileIndexed(file)) {
|
|
600
|
+
const uri = (0, url_1.pathToFileURL)(file).toString();
|
|
601
|
+
const openDoc = getDocument(uri);
|
|
602
|
+
if (openDoc) {
|
|
603
|
+
symbolIndex_1.symbolIndex.updateFile(file, openDoc.getText());
|
|
604
|
+
}
|
|
605
|
+
else if (fs.existsSync(file)) {
|
|
606
|
+
symbolIndex_1.symbolIndex.indexFile(file);
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
// 4. Find ALL references including declarations
|
|
611
|
+
const refs = symbolIndex_1.symbolIndex.findReferences(resolved.symbols, filesToSearch, true);
|
|
612
|
+
// 5. Build WorkspaceEdit
|
|
613
|
+
const changes = {};
|
|
614
|
+
for (const r of refs) {
|
|
615
|
+
const uri = (0, url_1.pathToFileURL)(r.file).toString();
|
|
616
|
+
if (!changes[uri])
|
|
617
|
+
changes[uri] = [];
|
|
618
|
+
changes[uri].push(node_1.TextEdit.replace(node_1.Range.create(node_1.Position.create(r.line, r.column), node_1.Position.create(r.endLine, r.endColumn)), params.newName));
|
|
619
|
+
}
|
|
620
|
+
return { changes };
|
|
621
|
+
});
|
|
622
|
+
// ── Hover ───────────────────────────────────────────────────────────────────
|
|
623
|
+
connection.onHover(async (params) => {
|
|
624
|
+
const document = getDocument(params.textDocument.uri);
|
|
625
|
+
if (!document || !symbolIndexReady)
|
|
626
|
+
return null;
|
|
627
|
+
const docPath = getDocumentFilePath(document);
|
|
628
|
+
if (!docPath)
|
|
629
|
+
return null;
|
|
630
|
+
const resolved = resolveSymbolAtPosition(docPath, document.getText(), params.position.line, params.position.character);
|
|
631
|
+
if (!resolved || resolved.symbols.length === 0)
|
|
632
|
+
return null;
|
|
633
|
+
const sym = resolved.symbols[0];
|
|
634
|
+
const markdown = (0, hoverBuilder_1.buildHoverMarkdown)(sym, resolved.symbols, {
|
|
635
|
+
getTree: (fp) => symbolIndex_1.symbolIndex.getTree(fp),
|
|
636
|
+
getIsAParents: (name) => symbolIndex_1.symbolIndex.getIsAParents(name),
|
|
637
|
+
});
|
|
638
|
+
if (!markdown)
|
|
639
|
+
return null;
|
|
640
|
+
return { contents: { kind: "markdown", value: markdown } };
|
|
641
|
+
});
|
|
642
|
+
// ── Document Symbols (Outline) ──────────────────────────────────────────────
|
|
643
|
+
connection.onDocumentSymbol(async (params) => {
|
|
644
|
+
const document = getDocument(params.textDocument.uri);
|
|
645
|
+
if (!document || !symbolIndexReady)
|
|
646
|
+
return [];
|
|
647
|
+
const docPath = getDocumentFilePath(document);
|
|
648
|
+
if (!docPath)
|
|
649
|
+
return [];
|
|
650
|
+
symbolIndex_1.symbolIndex.updateFile(docPath, document.getText());
|
|
651
|
+
return (0, documentSymbolBuilder_1.buildDocumentSymbolTree)(symbolIndex_1.symbolIndex.getFileSymbols(docPath));
|
|
652
|
+
});
|
|
653
|
+
// ── Formatting ──────────────────────────────────────────────────────────────
|
|
654
|
+
connection.onDocumentFormatting(async (params) => {
|
|
655
|
+
const document = getDocument(params.textDocument.uri);
|
|
656
|
+
if (!document)
|
|
657
|
+
return [];
|
|
658
|
+
const docPath = getDocumentFilePath(document);
|
|
659
|
+
if (!docPath || !symbolIndexReady)
|
|
660
|
+
return [];
|
|
661
|
+
symbolIndex_1.symbolIndex.updateFile(docPath, document.getText());
|
|
662
|
+
const tree = symbolIndex_1.symbolIndex.getTree(docPath);
|
|
663
|
+
if (!tree)
|
|
664
|
+
return [];
|
|
665
|
+
let text = document.getText();
|
|
666
|
+
const originalText = text;
|
|
667
|
+
// Phase 0: expand compact state blocks (may insert newlines)
|
|
668
|
+
const expandedText = (0, formatter_1.expandCompactStates)(text, tree);
|
|
669
|
+
let formatTree = tree;
|
|
670
|
+
if (expandedText !== text) {
|
|
671
|
+
// Temporarily index expanded text to get a parsed tree.
|
|
672
|
+
// We restore the original text at the end to avoid corrupting
|
|
673
|
+
// the live index (the editor document hasn't changed yet).
|
|
674
|
+
symbolIndex_1.symbolIndex.updateFile(docPath, expandedText);
|
|
675
|
+
formatTree = symbolIndex_1.symbolIndex.getTree(docPath);
|
|
676
|
+
text = expandedText;
|
|
677
|
+
}
|
|
678
|
+
// Phase 1-2: formatting passes on the (possibly expanded) text
|
|
679
|
+
const edits = [
|
|
680
|
+
...(0, formatter_1.computeIndentEdits)(text, params.options, formatTree),
|
|
681
|
+
...(0, formatter_1.fixTransitionSpacing)(text, formatTree),
|
|
682
|
+
...(0, formatter_1.fixAssociationSpacing)(text, formatTree),
|
|
683
|
+
...(0, formatter_1.normalizeTopLevelBlankLines)(text, formatTree),
|
|
684
|
+
];
|
|
685
|
+
// Apply edits internally to produce final text
|
|
686
|
+
const lines = text.split("\n");
|
|
687
|
+
const lineOffsets = [];
|
|
688
|
+
let offset = 0;
|
|
689
|
+
for (const line of lines) {
|
|
690
|
+
lineOffsets.push(offset);
|
|
691
|
+
offset += line.length + 1;
|
|
692
|
+
}
|
|
693
|
+
const toOffset = (line, col) => (lineOffsets[line] ?? text.length) + col;
|
|
694
|
+
const sorted = [...edits].sort((a, b) => toOffset(b.range.start.line, b.range.start.character) -
|
|
695
|
+
toOffset(a.range.start.line, a.range.start.character));
|
|
696
|
+
let finalText = text;
|
|
697
|
+
for (const edit of sorted) {
|
|
698
|
+
const start = toOffset(edit.range.start.line, edit.range.start.character);
|
|
699
|
+
const end = toOffset(edit.range.end.line, edit.range.end.character);
|
|
700
|
+
finalText = finalText.substring(0, start) + edit.newText + finalText.substring(end);
|
|
701
|
+
}
|
|
702
|
+
// Restore the live index to the original document text if we mutated it
|
|
703
|
+
if (expandedText !== originalText) {
|
|
704
|
+
symbolIndex_1.symbolIndex.updateFile(docPath, originalText);
|
|
705
|
+
}
|
|
706
|
+
// Return single whole-document replace
|
|
707
|
+
if (finalText === originalText)
|
|
708
|
+
return [];
|
|
709
|
+
const lastLine = document.lineCount - 1;
|
|
710
|
+
const lastChar = (originalText.split("\n")[lastLine] ?? "").length;
|
|
711
|
+
return [
|
|
712
|
+
node_1.TextEdit.replace(node_1.Range.create(node_1.Position.create(0, 0), node_1.Position.create(lastLine, lastChar)), finalText),
|
|
713
|
+
];
|
|
401
714
|
});
|
|
402
715
|
function scheduleValidation(document) {
|
|
403
|
-
const
|
|
716
|
+
const uriKey = normalizeUri(document.uri);
|
|
717
|
+
const existing = pendingValidations.get(uriKey);
|
|
404
718
|
if (existing) {
|
|
405
719
|
clearTimeout(existing);
|
|
406
720
|
}
|
|
407
721
|
const handle = setTimeout(() => {
|
|
408
|
-
pendingValidations.delete(
|
|
722
|
+
pendingValidations.delete(uriKey);
|
|
409
723
|
void validateTextDocument(document);
|
|
410
724
|
}, 300);
|
|
411
|
-
pendingValidations.set(
|
|
725
|
+
pendingValidations.set(uriKey, handle);
|
|
412
726
|
}
|
|
413
727
|
// Debounce key for dependent validation
|
|
414
728
|
const dependentValidationKey = "__dependent__";
|
|
@@ -470,33 +784,57 @@ async function validateTextDocument(document) {
|
|
|
470
784
|
if (!jarPath) {
|
|
471
785
|
return;
|
|
472
786
|
}
|
|
787
|
+
const uriKey = normalizeUri(document.uri);
|
|
788
|
+
const docVersion = document.version;
|
|
789
|
+
// Abort any in-flight validation for this document
|
|
790
|
+
const previous = inFlightValidations.get(uriKey);
|
|
791
|
+
if (previous) {
|
|
792
|
+
previous.abort();
|
|
793
|
+
}
|
|
794
|
+
const abortController = new AbortController();
|
|
795
|
+
inFlightValidations.set(uriKey, abortController);
|
|
473
796
|
try {
|
|
474
|
-
const diagnostics = await runUmpleSyncAndParseDiagnostics(jarPath, document);
|
|
797
|
+
const diagnostics = await runUmpleSyncAndParseDiagnostics(jarPath, document, abortController.signal);
|
|
798
|
+
if (abortController.signal.aborted) {
|
|
799
|
+
return;
|
|
800
|
+
}
|
|
801
|
+
// Drop stale results if the document has been edited since we started
|
|
802
|
+
const current = getDocument(document.uri);
|
|
803
|
+
if (!current || current.version !== docVersion) {
|
|
804
|
+
return;
|
|
805
|
+
}
|
|
475
806
|
connection.sendDiagnostics({ uri: document.uri, diagnostics });
|
|
476
807
|
}
|
|
477
808
|
catch (error) {
|
|
809
|
+
if (abortController.signal.aborted) {
|
|
810
|
+
return;
|
|
811
|
+
}
|
|
812
|
+
const current = getDocument(document.uri);
|
|
813
|
+
if (!current || current.version !== docVersion) {
|
|
814
|
+
return;
|
|
815
|
+
}
|
|
478
816
|
connection.console.error(`Diagnostics failed: ${String(error)}`);
|
|
479
817
|
connection.sendDiagnostics({ uri: document.uri, diagnostics: [] });
|
|
480
818
|
}
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
if (!jarWarningShown) {
|
|
485
|
-
connection.window.showWarningMessage("UmpleSync jar path not set. Configure initializationOptions.umpleSyncJarPath or UMPLESYNC_JAR.");
|
|
486
|
-
jarWarningShown = true;
|
|
819
|
+
finally {
|
|
820
|
+
if (inFlightValidations.get(uriKey) === abortController) {
|
|
821
|
+
inFlightValidations.delete(uriKey);
|
|
487
822
|
}
|
|
488
|
-
return undefined;
|
|
489
823
|
}
|
|
490
|
-
|
|
824
|
+
}
|
|
825
|
+
function resolveJarPath() {
|
|
826
|
+
if (!umpleSyncJarPath || !fs.existsSync(umpleSyncJarPath)) {
|
|
491
827
|
if (!jarWarningShown) {
|
|
492
|
-
connection.window.showWarningMessage(
|
|
828
|
+
connection.window.showWarningMessage("Umple diagnostics are disabled: umplesync.jar was not found. " +
|
|
829
|
+
"Completion and go-to-definition still work. " +
|
|
830
|
+
"Reload the window to retry.");
|
|
493
831
|
jarWarningShown = true;
|
|
494
832
|
}
|
|
495
833
|
return undefined;
|
|
496
834
|
}
|
|
497
835
|
return umpleSyncJarPath;
|
|
498
836
|
}
|
|
499
|
-
async function runUmpleSyncAndParseDiagnostics(jarPath, document) {
|
|
837
|
+
async function runUmpleSyncAndParseDiagnostics(jarPath, document, signal) {
|
|
500
838
|
const docPath = getDocumentFilePath(document);
|
|
501
839
|
if (!docPath) {
|
|
502
840
|
return [];
|
|
@@ -513,8 +851,7 @@ async function runUmpleSyncAndParseDiagnostics(jarPath, document) {
|
|
|
513
851
|
text = text.replace(/\n?$/, "\n\n");
|
|
514
852
|
}
|
|
515
853
|
await fs.promises.writeFile(shadow.targetFile, text, "utf8");
|
|
516
|
-
const
|
|
517
|
-
const { stdout, stderr } = await sendUmpleSyncCommand(jarPath, commandLine);
|
|
854
|
+
const { stdout, stderr } = await runUmpleDirect(jarPath, shadow.targetFile, signal);
|
|
518
855
|
const tempFilename = path.basename(shadow.targetFile);
|
|
519
856
|
const documentDir = getDocumentDirectory(document);
|
|
520
857
|
return parseUmpleDiagnostics(stderr, stdout, document, tempFilename, documentDir);
|
|
@@ -523,13 +860,39 @@ async function runUmpleSyncAndParseDiagnostics(jarPath, document) {
|
|
|
523
860
|
await shadow.cleanup();
|
|
524
861
|
}
|
|
525
862
|
}
|
|
863
|
+
/**
|
|
864
|
+
* Run umplesync.jar directly as a subprocess (one process per request).
|
|
865
|
+
* This is simpler and more reliable than the socket server approach —
|
|
866
|
+
* no persistent state, no stuck connections between requests.
|
|
867
|
+
*/
|
|
868
|
+
function runUmpleDirect(jarPath, filePath, signal) {
|
|
869
|
+
return new Promise((resolve, reject) => {
|
|
870
|
+
if (signal?.aborted) {
|
|
871
|
+
reject(new Error("aborted"));
|
|
872
|
+
return;
|
|
873
|
+
}
|
|
874
|
+
(0, child_process_1.execFile)("java", ["-jar", jarPath, "-generate", "nothing", filePath], { signal, timeout: umpleSyncTimeoutMs, maxBuffer: 10 * 1024 * 1024 }, (error, stdout, stderr) => {
|
|
875
|
+
if (signal?.aborted) {
|
|
876
|
+
reject(new Error("aborted"));
|
|
877
|
+
return;
|
|
878
|
+
}
|
|
879
|
+
// Umplesync writes diagnostics to stderr and exits 0 on compile errors.
|
|
880
|
+
// Any non-null error here is a real execution failure (java not found,
|
|
881
|
+
// corrupt jar, runtime crash, timeout kill) — reject unconditionally.
|
|
882
|
+
if (error) {
|
|
883
|
+
reject(error);
|
|
884
|
+
return;
|
|
885
|
+
}
|
|
886
|
+
resolve({ stdout, stderr });
|
|
887
|
+
});
|
|
888
|
+
});
|
|
889
|
+
}
|
|
526
890
|
/**
|
|
527
891
|
* Create a shadow workspace with only the files needed for compilation:
|
|
528
892
|
* the current document and all files it imports via `use` statements.
|
|
529
893
|
*/
|
|
530
894
|
async function createShadowWorkspace(documentPath) {
|
|
531
895
|
const documentDir = path.dirname(documentPath);
|
|
532
|
-
const documentName = path.basename(documentPath);
|
|
533
896
|
// Get document content (from open doc or disk)
|
|
534
897
|
const fileUri = (0, url_1.pathToFileURL)(documentPath).toString();
|
|
535
898
|
const openDoc = getDocument(fileUri);
|
|
@@ -543,12 +906,18 @@ async function createShadowWorkspace(documentPath) {
|
|
|
543
906
|
// Find only files reachable via use statements (lazy approach)
|
|
544
907
|
const reachableFiles = collectReachableFiles(documentPath, documentContent, documentDir);
|
|
545
908
|
// Also include the current document
|
|
546
|
-
|
|
909
|
+
const normalizedDocPath = path.normalize(documentPath);
|
|
910
|
+
reachableFiles.add(normalizedDocPath);
|
|
911
|
+
// Compute a common ancestor directory so all relative paths stay inside
|
|
912
|
+
// the shadow workspace (no "../" escapes). Always include the document
|
|
913
|
+
// path in the ancestor calculation even if it doesn't exist on disk,
|
|
914
|
+
// since we always write it to the shadow workspace.
|
|
915
|
+
const allForBase = Array.from(reachableFiles);
|
|
916
|
+
const baseDir = findCommonAncestor(allForBase);
|
|
917
|
+
const allPaths = allForBase.filter((f) => fs.existsSync(f));
|
|
547
918
|
// Create directory structure and symlink/copy files
|
|
548
|
-
for (const filePath of
|
|
549
|
-
|
|
550
|
-
continue;
|
|
551
|
-
const relativePath = path.relative(documentDir, filePath);
|
|
919
|
+
for (const filePath of allPaths) {
|
|
920
|
+
const relativePath = path.relative(baseDir, filePath);
|
|
552
921
|
const shadowPath = path.join(shadowDir, relativePath);
|
|
553
922
|
const shadowFileDir = path.dirname(shadowPath);
|
|
554
923
|
// Create directory structure
|
|
@@ -565,7 +934,10 @@ async function createShadowWorkspace(documentPath) {
|
|
|
565
934
|
await fs.promises.symlink(filePath, shadowPath);
|
|
566
935
|
}
|
|
567
936
|
}
|
|
568
|
-
const targetFile = path.join(shadowDir,
|
|
937
|
+
const targetFile = path.join(shadowDir, path.relative(baseDir, normalizedDocPath));
|
|
938
|
+
// Ensure target directory exists (document may not be on disk,
|
|
939
|
+
// so the symlink/copy loop above may not have created it)
|
|
940
|
+
await fs.promises.mkdir(path.dirname(targetFile), { recursive: true });
|
|
569
941
|
return {
|
|
570
942
|
shadowDir,
|
|
571
943
|
targetFile,
|
|
@@ -580,117 +952,28 @@ async function createShadowWorkspace(documentPath) {
|
|
|
580
952
|
throw error;
|
|
581
953
|
}
|
|
582
954
|
}
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
throw retryError;
|
|
602
|
-
}
|
|
603
|
-
await delay(150);
|
|
604
|
-
}
|
|
605
|
-
}
|
|
606
|
-
throw error;
|
|
607
|
-
}
|
|
608
|
-
}
|
|
609
|
-
// Send command to UmpleSync.jar socket server and receive the output
|
|
610
|
-
function connectAndSend(commandLine) {
|
|
611
|
-
return new Promise((resolve, reject) => {
|
|
612
|
-
const socket = new net.Socket();
|
|
613
|
-
const chunks = [];
|
|
614
|
-
let settled = false;
|
|
615
|
-
const finishSuccess = (raw) => {
|
|
616
|
-
if (settled) {
|
|
617
|
-
return;
|
|
618
|
-
}
|
|
619
|
-
settled = true;
|
|
620
|
-
const { stdout, stderr } = splitUmpleSyncOutput(raw);
|
|
621
|
-
resolve({ stdout, stderr });
|
|
622
|
-
};
|
|
623
|
-
const finishError = (err) => {
|
|
624
|
-
if (settled) {
|
|
625
|
-
return;
|
|
626
|
-
}
|
|
627
|
-
settled = true;
|
|
628
|
-
socket.destroy();
|
|
629
|
-
reject(err);
|
|
630
|
-
};
|
|
631
|
-
socket.setEncoding("utf8");
|
|
632
|
-
socket.setTimeout(umpleSyncTimeoutMs);
|
|
633
|
-
socket.on("data", (chunk) => {
|
|
634
|
-
if (typeof chunk === "string") {
|
|
635
|
-
chunks.push(chunk);
|
|
636
|
-
}
|
|
637
|
-
else {
|
|
638
|
-
chunks.push(chunk.toString("utf8"));
|
|
955
|
+
/**
|
|
956
|
+
* Find the deepest common ancestor directory of a list of file paths.
|
|
957
|
+
* Used to ensure all shadow workspace relative paths stay positive (no "../").
|
|
958
|
+
*/
|
|
959
|
+
function findCommonAncestor(filePaths) {
|
|
960
|
+
if (filePaths.length === 0) {
|
|
961
|
+
return os.tmpdir();
|
|
962
|
+
}
|
|
963
|
+
const dirs = filePaths.map((f) => path.dirname(path.normalize(f)));
|
|
964
|
+
const segments = dirs[0].split(path.sep);
|
|
965
|
+
let commonLength = segments.length;
|
|
966
|
+
for (let i = 1; i < dirs.length; i++) {
|
|
967
|
+
const parts = dirs[i].split(path.sep);
|
|
968
|
+
commonLength = Math.min(commonLength, parts.length);
|
|
969
|
+
for (let j = 0; j < commonLength; j++) {
|
|
970
|
+
if (segments[j] !== parts[j]) {
|
|
971
|
+
commonLength = j;
|
|
972
|
+
break;
|
|
639
973
|
}
|
|
640
|
-
});
|
|
641
|
-
socket.on("end", () => {
|
|
642
|
-
finishSuccess(chunks.join(""));
|
|
643
|
-
});
|
|
644
|
-
socket.on("error", (err) => {
|
|
645
|
-
const error = err instanceof Error ? err : new Error(String(err));
|
|
646
|
-
finishError(error);
|
|
647
|
-
});
|
|
648
|
-
socket.on("timeout", () => {
|
|
649
|
-
finishError(new Error("umplesync socket timeout"));
|
|
650
|
-
});
|
|
651
|
-
socket.connect(umpleSyncPort, umpleSyncHost, () => {
|
|
652
|
-
socket.end(commandLine);
|
|
653
|
-
});
|
|
654
|
-
});
|
|
655
|
-
}
|
|
656
|
-
async function startUmpleSyncServer(jarPath) {
|
|
657
|
-
if (serverProcess) {
|
|
658
|
-
return true;
|
|
659
|
-
}
|
|
660
|
-
return new Promise((resolve) => {
|
|
661
|
-
const child = (0, child_process_1.spawn)("java", ["-jar", jarPath, "-server", String(umpleSyncPort)], {
|
|
662
|
-
detached: true,
|
|
663
|
-
stdio: "ignore",
|
|
664
|
-
});
|
|
665
|
-
child.on("error", (err) => {
|
|
666
|
-
connection.console.error(`Failed to start umplesync: ${String(err)}`);
|
|
667
|
-
resolve(false);
|
|
668
|
-
});
|
|
669
|
-
child.unref();
|
|
670
|
-
serverProcess = child;
|
|
671
|
-
resolve(true);
|
|
672
|
-
});
|
|
673
|
-
}
|
|
674
|
-
function splitUmpleSyncOutput(raw) {
|
|
675
|
-
let stdout = "";
|
|
676
|
-
let stderr = "";
|
|
677
|
-
let index = 0;
|
|
678
|
-
while (index < raw.length) {
|
|
679
|
-
const start = raw.indexOf("ERROR!!", index);
|
|
680
|
-
if (start === -1) {
|
|
681
|
-
stdout += raw.slice(index);
|
|
682
|
-
break;
|
|
683
|
-
}
|
|
684
|
-
stdout += raw.slice(index, start);
|
|
685
|
-
const end = raw.indexOf("!!ERROR", start + 7);
|
|
686
|
-
if (end === -1) {
|
|
687
|
-
stderr += raw.slice(start + 7);
|
|
688
|
-
break;
|
|
689
974
|
}
|
|
690
|
-
stderr += raw.slice(start + 7, end);
|
|
691
|
-
index = end + 7;
|
|
692
975
|
}
|
|
693
|
-
return
|
|
976
|
+
return segments.slice(0, commonLength).join(path.sep) || path.sep;
|
|
694
977
|
}
|
|
695
978
|
/**
|
|
696
979
|
* Collect all file paths reachable via transitive use statements.
|
|
@@ -737,20 +1020,6 @@ function collectReachableFilesRecursive(filePath, content, documentDir, visited)
|
|
|
737
1020
|
}
|
|
738
1021
|
}
|
|
739
1022
|
}
|
|
740
|
-
function isConnectionError(error) {
|
|
741
|
-
if (!error || typeof error !== "object") {
|
|
742
|
-
return false;
|
|
743
|
-
}
|
|
744
|
-
const maybeError = error;
|
|
745
|
-
return (maybeError.code === "ECONNREFUSED" ||
|
|
746
|
-
maybeError.code === "ECONNRESET" ||
|
|
747
|
-
maybeError.code === "EPIPE" ||
|
|
748
|
-
maybeError.code === "ETIMEDOUT" ||
|
|
749
|
-
(maybeError.message || "").includes("umplesync socket timeout"));
|
|
750
|
-
}
|
|
751
|
-
function delay(ms) {
|
|
752
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
753
|
-
}
|
|
754
1023
|
function parseUmpleDiagnostics(stderr, stdout, document, tempFilename, documentDir) {
|
|
755
1024
|
const jsonDiagnostics = parseUmpleJsonDiagnostics(stderr, document, tempFilename, documentDir);
|
|
756
1025
|
if (jsonDiagnostics.length === 0 && stdout.includes("Success")) {
|
|
@@ -961,39 +1230,6 @@ function getDocumentFilePath(document) {
|
|
|
961
1230
|
return null;
|
|
962
1231
|
}
|
|
963
1232
|
}
|
|
964
|
-
/**
|
|
965
|
-
* Map a SymbolKind to the appropriate LSP CompletionItemKind.
|
|
966
|
-
*/
|
|
967
|
-
function symbolKindToCompletionKind(kind) {
|
|
968
|
-
switch (kind) {
|
|
969
|
-
case "class":
|
|
970
|
-
return node_1.CompletionItemKind.Class;
|
|
971
|
-
case "interface":
|
|
972
|
-
return node_1.CompletionItemKind.Interface;
|
|
973
|
-
case "trait":
|
|
974
|
-
return node_1.CompletionItemKind.Class;
|
|
975
|
-
case "enum":
|
|
976
|
-
return node_1.CompletionItemKind.Enum;
|
|
977
|
-
case "state":
|
|
978
|
-
return node_1.CompletionItemKind.EnumMember;
|
|
979
|
-
case "statemachine":
|
|
980
|
-
return node_1.CompletionItemKind.Enum;
|
|
981
|
-
case "attribute":
|
|
982
|
-
return node_1.CompletionItemKind.Field;
|
|
983
|
-
case "method":
|
|
984
|
-
return node_1.CompletionItemKind.Method;
|
|
985
|
-
case "association":
|
|
986
|
-
return node_1.CompletionItemKind.Reference;
|
|
987
|
-
case "mixset":
|
|
988
|
-
return node_1.CompletionItemKind.Module;
|
|
989
|
-
case "requirement":
|
|
990
|
-
return node_1.CompletionItemKind.Reference;
|
|
991
|
-
case "template":
|
|
992
|
-
return node_1.CompletionItemKind.Property;
|
|
993
|
-
default:
|
|
994
|
-
return node_1.CompletionItemKind.Text;
|
|
995
|
-
}
|
|
996
|
-
}
|
|
997
1233
|
function getUseFileCompletions(document, prefix, line, character) {
|
|
998
1234
|
const docDir = getDocumentDirectory(document);
|
|
999
1235
|
if (!docDir) {
|
|
@@ -1065,8 +1301,5 @@ function extractJson(text) {
|
|
|
1065
1301
|
}
|
|
1066
1302
|
return text.slice(start, end + 1);
|
|
1067
1303
|
}
|
|
1068
|
-
function formatUmpleArg(filePath) {
|
|
1069
|
-
return JSON.stringify(filePath);
|
|
1070
|
-
}
|
|
1071
1304
|
connection.listen();
|
|
1072
1305
|
//# sourceMappingURL=server.js.map
|