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.
Files changed (49) hide show
  1. package/completions.scm +21 -6
  2. package/definitions.scm +4 -0
  3. package/out/completionAnalysis.d.ts +44 -0
  4. package/out/completionAnalysis.js +391 -0
  5. package/out/completionAnalysis.js.map +1 -0
  6. package/out/completionBuilder.d.ts +28 -0
  7. package/out/completionBuilder.js +251 -0
  8. package/out/completionBuilder.js.map +1 -0
  9. package/out/documentSymbolBuilder.d.ts +13 -0
  10. package/out/documentSymbolBuilder.js +95 -0
  11. package/out/documentSymbolBuilder.js.map +1 -0
  12. package/out/formatRules.d.ts +27 -0
  13. package/out/formatRules.js +114 -0
  14. package/out/formatRules.js.map +1 -0
  15. package/out/formatter.d.ts +53 -0
  16. package/out/formatter.js +380 -0
  17. package/out/formatter.js.map +1 -0
  18. package/out/hoverBuilder.d.ts +21 -0
  19. package/out/hoverBuilder.js +308 -0
  20. package/out/hoverBuilder.js.map +1 -0
  21. package/out/importGraph.d.ts +28 -0
  22. package/out/importGraph.js +91 -0
  23. package/out/importGraph.js.map +1 -0
  24. package/out/referenceSearch.d.ts +22 -0
  25. package/out/referenceSearch.js +271 -0
  26. package/out/referenceSearch.js.map +1 -0
  27. package/out/resolver.d.ts +21 -0
  28. package/out/resolver.js +174 -0
  29. package/out/resolver.js.map +1 -0
  30. package/out/server.js +560 -327
  31. package/out/server.js.map +1 -1
  32. package/out/symbolIndex.d.ts +100 -94
  33. package/out/symbolIndex.js +392 -399
  34. package/out/symbolIndex.js.map +1 -1
  35. package/out/symbolTypes.d.ts +34 -0
  36. package/out/symbolTypes.js +9 -0
  37. package/out/symbolTypes.js.map +1 -0
  38. package/out/tokenAnalysis.d.ts +24 -0
  39. package/out/tokenAnalysis.js +195 -0
  40. package/out/tokenAnalysis.js.map +1 -0
  41. package/out/tokenTypes.d.ts +46 -0
  42. package/out/tokenTypes.js +28 -0
  43. package/out/tokenTypes.js.map +1 -0
  44. package/out/treeUtils.d.ts +32 -0
  45. package/out/treeUtils.js +89 -0
  46. package/out/treeUtils.js.map +1 -0
  47. package/package.json +4 -2
  48. package/references.scm +78 -10
  49. 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 umpleSyncHost = "localhost";
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 = 50000;
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.updateFile(filePath, params.textDocument.text);
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
- // 5a. Keywords from LookaheadIterator
249
- for (const kw of info.keywords) {
250
- if (!seen.has(kw)) {
251
- seen.add(kw);
252
- items.push({ label: kw, kind: node_1.CompletionItemKind.Keyword });
253
- }
254
- }
255
- // 5b. Operators from LookaheadIterator
256
- for (const op of info.operators) {
257
- if (!seen.has(op)) {
258
- seen.add(op);
259
- items.push({ label: op, kind: node_1.CompletionItemKind.Operator });
260
- }
261
- }
262
- // 5c. Built-in types (when in type-compatible scope)
263
- if (Array.isArray(symbolKinds) &&
264
- symbolKinds.some((k) => ["class", "interface", "trait", "enum"].includes(k))) {
265
- for (const typ of keywords_1.BUILTIN_TYPES) {
266
- if (!seen.has(typ)) {
267
- seen.add(typ);
268
- items.push({
269
- label: typ,
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
- // Symbol lookup, filtered by reachable files
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
- // For container-scoped kinds, try scoped lookup first (with inheritance), then global fallback
368
- const containerKinds = new Set([
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
- "state",
524
+ "statemachine",
373
525
  ]);
374
- const isScoped = token.kinds?.some((k) => containerKinds.has(k));
375
- let container;
376
- if (isScoped) {
377
- container = token.kinds?.some((k) => k === "state")
378
- ? token.enclosingStateMachine
379
- : token.enclosingClass;
380
- }
381
- let filteredSymbols = [];
382
- if (container) {
383
- filteredSymbols = symbolIndex_1.symbolIndex
384
- .getSymbols({
385
- name: token.word,
386
- kind: token.kinds ?? undefined,
387
- container,
388
- inherited: true,
389
- })
390
- .filter((s) => reachableFiles.has(path.normalize(s.file)));
391
- }
392
- if (filteredSymbols.length === 0) {
393
- filteredSymbols = symbolIndex_1.symbolIndex
394
- .getSymbols({ name: token.word, kind: token.kinds ?? undefined })
395
- .filter((s) => reachableFiles.has(path.normalize(s.file)));
396
- }
397
- if (filteredSymbols.length > 0) {
398
- return filteredSymbols.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))));
399
- }
400
- return [];
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 existing = pendingValidations.get(document.uri);
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(document.uri);
722
+ pendingValidations.delete(uriKey);
409
723
  void validateTextDocument(document);
410
724
  }, 300);
411
- pendingValidations.set(document.uri, handle);
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
- function resolveJarPath() {
483
- if (!umpleSyncJarPath) {
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
- if (!fs.existsSync(umpleSyncJarPath)) {
824
+ }
825
+ function resolveJarPath() {
826
+ if (!umpleSyncJarPath || !fs.existsSync(umpleSyncJarPath)) {
491
827
  if (!jarWarningShown) {
492
- connection.window.showWarningMessage(`UmpleSync jar not found at ${umpleSyncJarPath}. Update the path or UMPLESYNC_JAR.`);
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 commandLine = `-generate nothing ${formatUmpleArg(shadow.targetFile)}`;
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
- reachableFiles.add(path.normalize(documentPath));
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 reachableFiles) {
549
- if (!fs.existsSync(filePath))
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, documentName);
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
- async function sendUmpleSyncCommand(jarPath, commandLine) {
584
- try {
585
- return await connectAndSend(commandLine);
586
- }
587
- catch (error) {
588
- if (!isConnectionError(error)) {
589
- throw error;
590
- }
591
- const started = await startUmpleSyncServer(jarPath);
592
- if (!started) {
593
- throw error;
594
- }
595
- for (let attempt = 0; attempt < 5; attempt += 1) {
596
- try {
597
- return await connectAndSend(commandLine);
598
- }
599
- catch (retryError) {
600
- if (!isConnectionError(retryError)) {
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 { stdout, stderr };
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