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/symbolIndex.js
CHANGED
|
@@ -12,50 +12,11 @@ const path = require("path");
|
|
|
12
12
|
// web-tree-sitter types and module
|
|
13
13
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
14
14
|
const TreeSitter = require("web-tree-sitter");
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
const
|
|
20
|
-
"class",
|
|
21
|
-
"interface",
|
|
22
|
-
"trait",
|
|
23
|
-
"enum",
|
|
24
|
-
"mixset",
|
|
25
|
-
"req",
|
|
26
|
-
"associationClass",
|
|
27
|
-
"statemachine",
|
|
28
|
-
"namespace",
|
|
29
|
-
"generate",
|
|
30
|
-
// State machine modifiers — next token is SM name (or another modifier)
|
|
31
|
-
"queued",
|
|
32
|
-
"pooled",
|
|
33
|
-
// Emit methods — next token is the method name
|
|
34
|
-
"emit",
|
|
35
|
-
]);
|
|
36
|
-
/** Structural tokens that should NOT appear in completions. */
|
|
37
|
-
const STRUCTURAL_TOKENS = new Set([
|
|
38
|
-
"{",
|
|
39
|
-
"}",
|
|
40
|
-
"(",
|
|
41
|
-
")",
|
|
42
|
-
"[",
|
|
43
|
-
"]",
|
|
44
|
-
";",
|
|
45
|
-
",",
|
|
46
|
-
".",
|
|
47
|
-
"<",
|
|
48
|
-
">",
|
|
49
|
-
"=",
|
|
50
|
-
"/",
|
|
51
|
-
"[]",
|
|
52
|
-
"*",
|
|
53
|
-
"||",
|
|
54
|
-
]);
|
|
55
|
-
function isOperatorToken(name) {
|
|
56
|
-
// Matches association/transition arrows: --, ->, <-, <@>-, -<@>, >->, <-<
|
|
57
|
-
return /^[<>-]/.test(name) && name.length > 1;
|
|
58
|
-
}
|
|
15
|
+
const treeUtils_1 = require("./treeUtils");
|
|
16
|
+
const tokenAnalysis_1 = require("./tokenAnalysis");
|
|
17
|
+
const completionAnalysis_1 = require("./completionAnalysis");
|
|
18
|
+
const referenceSearch_1 = require("./referenceSearch");
|
|
19
|
+
const importGraph_1 = require("./importGraph");
|
|
59
20
|
class SymbolIndex {
|
|
60
21
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
61
22
|
parser = null;
|
|
@@ -69,6 +30,8 @@ class SymbolIndex {
|
|
|
69
30
|
isAByFile = new Map();
|
|
70
31
|
// Global isA graph: className → parent names (merged from all files)
|
|
71
32
|
isAGraph = new Map();
|
|
33
|
+
// Import graph: forward/reverse edge management
|
|
34
|
+
importGraph = new importGraph_1.ImportGraph();
|
|
72
35
|
initialized = false;
|
|
73
36
|
/**
|
|
74
37
|
* Initialize the tree-sitter parser with the Umple grammar.
|
|
@@ -116,6 +79,41 @@ class SymbolIndex {
|
|
|
116
79
|
isReady() {
|
|
117
80
|
return this.initialized && this.parser !== null;
|
|
118
81
|
}
|
|
82
|
+
/**
|
|
83
|
+
* Check if a file has been fully indexed (has symbols + tree).
|
|
84
|
+
*/
|
|
85
|
+
isFileIndexed(filePath) {
|
|
86
|
+
return this.files.has(filePath);
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Remove import graph edges for a file (e.g., when it's deleted from disk).
|
|
90
|
+
*/
|
|
91
|
+
removeImportEdges(filePath) {
|
|
92
|
+
this.importGraph.removeEdges(filePath);
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Update import graph edges for a file from its use statements,
|
|
96
|
+
* WITHOUT full symbol indexing. Used by the async workspace use-graph scanner.
|
|
97
|
+
* Skips files that are already fully indexed (their edges are fresh from didOpen/didChange).
|
|
98
|
+
*/
|
|
99
|
+
updateUseGraphEdges(filePath, useStatements) {
|
|
100
|
+
// Don't overwrite edges for already-indexed files
|
|
101
|
+
if (this.files.has(filePath))
|
|
102
|
+
return;
|
|
103
|
+
const fileDir = path.dirname(filePath);
|
|
104
|
+
const resolvedImports = new Set();
|
|
105
|
+
for (const usePath of useStatements) {
|
|
106
|
+
if (!usePath.endsWith(".ump"))
|
|
107
|
+
continue;
|
|
108
|
+
const resolved = path.isAbsolute(usePath)
|
|
109
|
+
? path.normalize(usePath)
|
|
110
|
+
: path.normalize(path.resolve(fileDir, usePath));
|
|
111
|
+
if (fs.existsSync(resolved)) {
|
|
112
|
+
resolvedImports.add(resolved);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
this.importGraph.setEdges(filePath, resolvedImports);
|
|
116
|
+
}
|
|
119
117
|
/**
|
|
120
118
|
* Index a file or update its index if content changed.
|
|
121
119
|
* @param filePath Absolute path to the file
|
|
@@ -134,14 +132,32 @@ class SymbolIndex {
|
|
|
134
132
|
// Content unchanged, skip re-indexing
|
|
135
133
|
return false;
|
|
136
134
|
}
|
|
137
|
-
//
|
|
135
|
+
// Parse the file
|
|
136
|
+
const tree = this.parser.parse(fileContent);
|
|
137
|
+
// Remove old symbols for this file from the container index
|
|
138
138
|
if (existing) {
|
|
139
139
|
this.removeFileSymbols(filePath);
|
|
140
140
|
}
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
//
|
|
144
|
-
|
|
141
|
+
const newSymbols = this.extractSymbols(filePath, tree.rootNode);
|
|
142
|
+
// Kind-sensitive error preservation: when the tree has errors,
|
|
143
|
+
// live-update recovery-safe kinds (whose identity doesn't depend
|
|
144
|
+
// on AST nesting) but preserve recovery-fragile kinds (state,
|
|
145
|
+
// statemachine) from the last clean snapshot.
|
|
146
|
+
let symbols;
|
|
147
|
+
if (tree.rootNode.hasError) {
|
|
148
|
+
const LIVE_KINDS = new Set([
|
|
149
|
+
"class", "interface", "trait", "enum",
|
|
150
|
+
"mixset", "attribute", "const",
|
|
151
|
+
]);
|
|
152
|
+
const liveSymbols = newSymbols.filter((s) => LIVE_KINDS.has(s.kind));
|
|
153
|
+
const preservedSymbols = existing
|
|
154
|
+
? existing.symbols.filter((s) => !LIVE_KINDS.has(s.kind))
|
|
155
|
+
: [];
|
|
156
|
+
symbols = [...liveSymbols, ...preservedSymbols];
|
|
157
|
+
}
|
|
158
|
+
else {
|
|
159
|
+
symbols = newSymbols;
|
|
160
|
+
}
|
|
145
161
|
// Extract isA relationships
|
|
146
162
|
const isAMap = this.extractIsARelationships(tree.rootNode);
|
|
147
163
|
this.isAByFile.set(filePath, isAMap);
|
|
@@ -160,11 +176,12 @@ class SymbolIndex {
|
|
|
160
176
|
this.symbolsByContainer.set(symbol.container, containerSyms);
|
|
161
177
|
}
|
|
162
178
|
}
|
|
179
|
+
// Update forward/reverse import maps
|
|
180
|
+
this.updateImportMaps(filePath, fileContent);
|
|
163
181
|
return true;
|
|
164
182
|
}
|
|
165
183
|
/**
|
|
166
|
-
* Update a file with new content.
|
|
167
|
-
* For web-tree-sitter, we do a full reparse but the index diffing is still efficient.
|
|
184
|
+
* Update a file with new content from the live editor.
|
|
168
185
|
*/
|
|
169
186
|
updateFile(filePath, content) {
|
|
170
187
|
return this.indexFile(filePath, content);
|
|
@@ -211,6 +228,22 @@ class SymbolIndex {
|
|
|
211
228
|
}
|
|
212
229
|
return result;
|
|
213
230
|
}
|
|
231
|
+
/**
|
|
232
|
+
* Get all symbols defined in a specific file (for document outline).
|
|
233
|
+
*/
|
|
234
|
+
getFileSymbols(filePath) {
|
|
235
|
+
return this.files.get(filePath)?.symbols ?? [];
|
|
236
|
+
}
|
|
237
|
+
/**
|
|
238
|
+
* Get the parsed tree for a file (for formatting, etc.).
|
|
239
|
+
*/
|
|
240
|
+
getTree(filePath) {
|
|
241
|
+
return this.files.get(filePath)?.tree ?? null;
|
|
242
|
+
}
|
|
243
|
+
/** Get the direct isA parents for a class name. */
|
|
244
|
+
getIsAParents(className) {
|
|
245
|
+
return this.isAGraph.get(className) ?? [];
|
|
246
|
+
}
|
|
214
247
|
collectFromContainerChain(container, kindSet, name, visited, result) {
|
|
215
248
|
if (visited.has(container))
|
|
216
249
|
return;
|
|
@@ -256,8 +289,8 @@ class SymbolIndex {
|
|
|
256
289
|
const usePaths = [];
|
|
257
290
|
const visit = (node) => {
|
|
258
291
|
if (node.type === "use_statement") {
|
|
259
|
-
const
|
|
260
|
-
|
|
292
|
+
const pathNodes = node.childrenForFieldName("path");
|
|
293
|
+
for (const pathNode of pathNodes) {
|
|
261
294
|
usePaths.push(pathNode.text);
|
|
262
295
|
}
|
|
263
296
|
}
|
|
@@ -298,8 +331,8 @@ class SymbolIndex {
|
|
|
298
331
|
const useStatements = [];
|
|
299
332
|
const visit = (node) => {
|
|
300
333
|
if (node.type === "use_statement") {
|
|
301
|
-
const
|
|
302
|
-
|
|
334
|
+
const pathNodes = node.childrenForFieldName("path");
|
|
335
|
+
for (const pathNode of pathNodes) {
|
|
303
336
|
useStatements.push({
|
|
304
337
|
path: pathNode.text,
|
|
305
338
|
line: node.startPosition.row,
|
|
@@ -318,17 +351,8 @@ class SymbolIndex {
|
|
|
318
351
|
return useStatements;
|
|
319
352
|
}
|
|
320
353
|
/**
|
|
321
|
-
* Get completion information at a position
|
|
322
|
-
*
|
|
323
|
-
* Operates on the ORIGINAL parse tree (no dummy insertion). Uses:
|
|
324
|
-
* 1. Previous leaf node's nextParseState → LookaheadIterator for keywords
|
|
325
|
-
* 2. completions.scm query for symbol kind detection
|
|
326
|
-
* 3. Simple tree checks for comments, definition names
|
|
327
|
-
*
|
|
328
|
-
* @param content The document text (original)
|
|
329
|
-
* @param line 0-indexed cursor line
|
|
330
|
-
* @param column 0-indexed cursor column
|
|
331
|
-
* @returns CompletionInfo with keywords, operators, and symbol kinds
|
|
354
|
+
* Get completion information at a position.
|
|
355
|
+
* Delegates to the pure analyzeCompletion() function after tree acquisition.
|
|
332
356
|
*/
|
|
333
357
|
getCompletionInfo(content, line, column) {
|
|
334
358
|
const empty = {
|
|
@@ -339,96 +363,22 @@ class SymbolIndex {
|
|
|
339
363
|
isComment: false,
|
|
340
364
|
prefix: "",
|
|
341
365
|
};
|
|
342
|
-
if (!this.initialized || !this.parser || !this.language) {
|
|
366
|
+
if (!this.initialized || !this.parser || !this.language || !this.completionsQuery) {
|
|
343
367
|
return empty;
|
|
344
368
|
}
|
|
345
|
-
// Parse original text (no dummy insertion)
|
|
346
369
|
const tree = this.parser.parse(content);
|
|
347
370
|
if (!tree)
|
|
348
371
|
return empty;
|
|
349
|
-
|
|
350
|
-
// Use column - 1 to land inside the token when cursor is at its end boundary
|
|
351
|
-
// (tree-sitter uses half-open intervals [start, end))
|
|
352
|
-
const nodeAtCursor = tree.rootNode.descendantForPosition({
|
|
353
|
-
row: line,
|
|
354
|
-
column: Math.max(0, column - 1),
|
|
355
|
-
});
|
|
356
|
-
if (nodeAtCursor && this.isInsideComment(nodeAtCursor)) {
|
|
357
|
-
return { ...empty, isComment: true };
|
|
358
|
-
}
|
|
359
|
-
// --- Extract prefix from the token at cursor ---
|
|
360
|
-
let prefix = "";
|
|
361
|
-
if (column > 0 &&
|
|
362
|
-
nodeAtCursor &&
|
|
363
|
-
(nodeAtCursor.type === "identifier" || nodeAtCursor.type === "use_path")) {
|
|
364
|
-
const nodeStartCol = nodeAtCursor.startPosition.row === line
|
|
365
|
-
? nodeAtCursor.startPosition.column
|
|
366
|
-
: 0;
|
|
367
|
-
prefix = nodeAtCursor.text.substring(0, column - nodeStartCol);
|
|
368
|
-
}
|
|
369
|
-
// --- Definition name check ---
|
|
370
|
-
const lastToken = this.lastTokenBeforeCursor(content, line, column);
|
|
371
|
-
if ((lastToken && DEFINITION_KEYWORDS.has(lastToken)) ||
|
|
372
|
-
this.isAtAttributeNamePosition(tree, content, line, column)) {
|
|
373
|
-
return { ...empty, isDefinitionName: true };
|
|
374
|
-
}
|
|
375
|
-
// --- LookaheadIterator for keywords ---
|
|
376
|
-
const prevLeaf = this.findPreviousLeaf(tree, content, line, column);
|
|
377
|
-
// When no previous token exists (file start / after only comments),
|
|
378
|
-
// use the node at cursor's parseState instead of state 0.
|
|
379
|
-
// State 0 is the initial LR state which is overly broad.
|
|
380
|
-
const stateId = prevLeaf
|
|
381
|
-
? prevLeaf.nextParseState
|
|
382
|
-
: (nodeAtCursor?.parseState ?? 0);
|
|
383
|
-
const keywords = [];
|
|
384
|
-
const operators = [];
|
|
385
|
-
const iter = this.language.lookaheadIterator(stateId);
|
|
386
|
-
if (iter) {
|
|
387
|
-
try {
|
|
388
|
-
for (const symbolName of iter) {
|
|
389
|
-
const typeId = iter.currentTypeId;
|
|
390
|
-
// Skip named nodes (identifier, type_name, etc.)
|
|
391
|
-
if (this.language.nodeTypeIsNamed(typeId))
|
|
392
|
-
continue;
|
|
393
|
-
// Skip structural tokens
|
|
394
|
-
if (STRUCTURAL_TOKENS.has(symbolName))
|
|
395
|
-
continue;
|
|
396
|
-
if (isOperatorToken(symbolName)) {
|
|
397
|
-
operators.push(symbolName);
|
|
398
|
-
}
|
|
399
|
-
else if (/^[a-zA-Z]/.test(symbolName)) {
|
|
400
|
-
keywords.push(symbolName);
|
|
401
|
-
}
|
|
402
|
-
}
|
|
403
|
-
}
|
|
404
|
-
finally {
|
|
405
|
-
iter.delete(); // MUST free WASM memory
|
|
406
|
-
}
|
|
407
|
-
}
|
|
408
|
-
// --- Scope query for symbol kinds ---
|
|
409
|
-
const symbolKinds = this.resolveCompletionScope(tree, line, column);
|
|
410
|
-
// --- Enclosing scope for scoped lookups ---
|
|
411
|
-
const { enclosingClass, enclosingStateMachine } = this.resolveEnclosingScope(tree, line, column);
|
|
412
|
-
return {
|
|
413
|
-
keywords,
|
|
414
|
-
operators,
|
|
415
|
-
symbolKinds,
|
|
416
|
-
isDefinitionName: false,
|
|
417
|
-
isComment: false,
|
|
418
|
-
prefix,
|
|
419
|
-
enclosingClass,
|
|
420
|
-
enclosingStateMachine,
|
|
421
|
-
};
|
|
372
|
+
return (0, completionAnalysis_1.analyzeCompletion)(tree, this.language, this.completionsQuery, content, line, column);
|
|
422
373
|
}
|
|
423
374
|
/**
|
|
424
375
|
* Get the token (identifier) at a position using tree-sitter, along with
|
|
425
|
-
*
|
|
376
|
+
* context information for symbol resolution.
|
|
426
377
|
*
|
|
427
|
-
*
|
|
428
|
-
* for the cursor's context. See queries/references.scm for supported patterns.
|
|
378
|
+
* Delegates to the pure analyzeToken() function after tree acquisition.
|
|
429
379
|
*/
|
|
430
380
|
getTokenAtPosition(filePath, content, line, column) {
|
|
431
|
-
if (!this.initialized || !this.parser) {
|
|
381
|
+
if (!this.initialized || !this.parser || !this.referencesQuery) {
|
|
432
382
|
return null;
|
|
433
383
|
}
|
|
434
384
|
const fileIndex = this.files.get(filePath);
|
|
@@ -440,14 +390,39 @@ class SymbolIndex {
|
|
|
440
390
|
else {
|
|
441
391
|
tree = this.parser.parse(content);
|
|
442
392
|
}
|
|
393
|
+
return (0, tokenAnalysis_1.analyzeToken)(tree, this.referencesQuery, line, column);
|
|
394
|
+
}
|
|
395
|
+
/**
|
|
396
|
+
* Get the exact range of the token node at a position.
|
|
397
|
+
* Used by prepareRename to return the precise rename range.
|
|
398
|
+
* Accepts the same node types as getTokenAtPosition(): identifier,
|
|
399
|
+
* use_path, and filter_pattern.
|
|
400
|
+
*/
|
|
401
|
+
getNodeRangeAtPosition(filePath, content, line, column) {
|
|
402
|
+
if (!this.initialized || !this.parser)
|
|
403
|
+
return null;
|
|
404
|
+
const fileIndex = this.files.get(filePath);
|
|
405
|
+
let tree;
|
|
406
|
+
if (fileIndex?.tree &&
|
|
407
|
+
fileIndex.contentHash === this.hashContent(content)) {
|
|
408
|
+
tree = fileIndex.tree;
|
|
409
|
+
}
|
|
410
|
+
else {
|
|
411
|
+
tree = this.parser.parse(content);
|
|
412
|
+
}
|
|
443
413
|
const node = tree.rootNode.descendantForPosition({ row: line, column });
|
|
444
|
-
if (!node ||
|
|
414
|
+
if (!node ||
|
|
415
|
+
(node.type !== "identifier" &&
|
|
416
|
+
node.type !== "use_path" &&
|
|
417
|
+
node.type !== "filter_pattern")) {
|
|
445
418
|
return null;
|
|
446
419
|
}
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
420
|
+
return {
|
|
421
|
+
startLine: node.startPosition.row,
|
|
422
|
+
startColumn: node.startPosition.column,
|
|
423
|
+
endLine: node.endPosition.row,
|
|
424
|
+
endColumn: node.endPosition.column,
|
|
425
|
+
};
|
|
451
426
|
}
|
|
452
427
|
/**
|
|
453
428
|
* Use the references.scm query to determine which symbol kinds an
|
|
@@ -457,263 +432,83 @@ class SymbolIndex {
|
|
|
457
432
|
* encoding the valid symbol kinds directly. This replaces the old parent-chain
|
|
458
433
|
* walking approach with a declarative .scm file.
|
|
459
434
|
*/
|
|
460
|
-
resolveDefinitionKinds(tree, node) {
|
|
461
|
-
if (!this.referencesQuery)
|
|
462
|
-
return null;
|
|
463
|
-
// Run query with position filtering to find captures at this node
|
|
464
|
-
const captures = this.referencesQuery.captures(tree.rootNode, {
|
|
465
|
-
startPosition: node.startPosition,
|
|
466
|
-
endPosition: node.endPosition,
|
|
467
|
-
});
|
|
468
|
-
// Find the capture whose node best matches our target.
|
|
469
|
-
// When multiple patterns capture the same node (e.g. isA type matched
|
|
470
|
-
// by both type_name and isa_declaration), pick the most specific:
|
|
471
|
-
// 1. Smallest node size (fewest bytes)
|
|
472
|
-
// 2. Fewest kinds in the capture name (fewer = more specific)
|
|
473
|
-
let bestCapture = null;
|
|
474
|
-
let bestSize = Infinity;
|
|
475
|
-
let bestKindCount = Infinity;
|
|
476
|
-
for (const capture of captures) {
|
|
477
|
-
if (capture.node.startIndex <= node.startIndex &&
|
|
478
|
-
capture.node.endIndex >= node.endIndex) {
|
|
479
|
-
const size = capture.node.endIndex - capture.node.startIndex;
|
|
480
|
-
const kindCount = capture.name.split("_").length;
|
|
481
|
-
if (size < bestSize ||
|
|
482
|
-
(size === bestSize && kindCount < bestKindCount)) {
|
|
483
|
-
bestSize = size;
|
|
484
|
-
bestKindCount = kindCount;
|
|
485
|
-
bestCapture = capture;
|
|
486
|
-
}
|
|
487
|
-
}
|
|
488
|
-
}
|
|
489
|
-
if (!bestCapture)
|
|
490
|
-
return null;
|
|
491
|
-
// Parse capture name: "reference.class_interface_trait" → ["class", "interface", "trait"]
|
|
492
|
-
const prefix = "reference.";
|
|
493
|
-
if (!bestCapture.name.startsWith(prefix))
|
|
494
|
-
return null;
|
|
495
|
-
const kindStr = bestCapture.name.substring(prefix.length);
|
|
496
|
-
return kindStr.split("_");
|
|
497
|
-
}
|
|
498
|
-
// =====================
|
|
499
|
-
// Private methods
|
|
500
|
-
// =====================
|
|
501
435
|
/**
|
|
502
|
-
*
|
|
503
|
-
*
|
|
436
|
+
* Get the names of direct child states of a given state path within a state machine.
|
|
437
|
+
* Uses pre-computed statePath on each SymbolEntry for O(n) lookup without AST walking.
|
|
504
438
|
*
|
|
505
|
-
* @param
|
|
506
|
-
* @param
|
|
507
|
-
* @
|
|
508
|
-
* @param column 0-indexed cursor column
|
|
509
|
-
* @returns The previous leaf node, or null if cursor is at file start
|
|
510
|
-
*/
|
|
511
|
-
findPreviousLeaf(tree, content, line, column) {
|
|
512
|
-
// Convert (line, column) to absolute offset
|
|
513
|
-
const lines = content.split("\n");
|
|
514
|
-
let offset = 0;
|
|
515
|
-
for (let i = 0; i < line && i < lines.length; i++) {
|
|
516
|
-
offset += lines[i].length + 1; // +1 for newline
|
|
517
|
-
}
|
|
518
|
-
offset += Math.min(column, lines[line]?.length ?? 0);
|
|
519
|
-
// Skip the current partial identifier backwards
|
|
520
|
-
let pos = offset;
|
|
521
|
-
while (pos > 0 && /[a-zA-Z_0-9]/.test(content[pos - 1])) {
|
|
522
|
-
pos--;
|
|
523
|
-
}
|
|
524
|
-
// Skip whitespace backwards
|
|
525
|
-
while (pos > 0 && /\s/.test(content[pos - 1])) {
|
|
526
|
-
pos--;
|
|
527
|
-
}
|
|
528
|
-
if (pos === 0)
|
|
529
|
-
return null;
|
|
530
|
-
// Find the node at (pos - 1) — the last character before the gap
|
|
531
|
-
let node = tree.rootNode.descendantForIndex(pos - 1, pos - 1);
|
|
532
|
-
if (!node)
|
|
533
|
-
return null;
|
|
534
|
-
// Skip extra nodes (comments) by walking to previous siblings
|
|
535
|
-
while (node && node.isExtra) {
|
|
536
|
-
const prev = node.previousSibling;
|
|
537
|
-
if (prev) {
|
|
538
|
-
node = prev;
|
|
539
|
-
while (node.childCount > 0) {
|
|
540
|
-
node = node.lastChild;
|
|
541
|
-
}
|
|
542
|
-
}
|
|
543
|
-
else if (node.parent) {
|
|
544
|
-
node = node.parent;
|
|
545
|
-
}
|
|
546
|
-
else {
|
|
547
|
-
return null;
|
|
548
|
-
}
|
|
549
|
-
}
|
|
550
|
-
// Walk to leaf
|
|
551
|
-
while (node && node.childCount > 0) {
|
|
552
|
-
node = node.lastChild;
|
|
553
|
-
}
|
|
554
|
-
return node;
|
|
555
|
-
}
|
|
556
|
-
/**
|
|
557
|
-
* Run completions.scm to find the innermost scope at the cursor position.
|
|
558
|
-
* Returns symbol kinds to offer, "suppress", "use_path", or null (keywords only).
|
|
559
|
-
*/
|
|
560
|
-
resolveCompletionScope(tree, line, column) {
|
|
561
|
-
if (!this.completionsQuery)
|
|
562
|
-
return null;
|
|
563
|
-
// Don't pass position filtering to the query — tree-sitter uses
|
|
564
|
-
// half-open intervals [start, end), so a point query at a node's
|
|
565
|
-
// exact end boundary misses it (e.g., `use Per|` at use_statement's
|
|
566
|
-
// end). The manual containment check below uses inclusive boundaries
|
|
567
|
-
// and handles this correctly.
|
|
568
|
-
const captures = this.completionsQuery.captures(tree.rootNode);
|
|
569
|
-
// Find the innermost (smallest) scope that contains the cursor
|
|
570
|
-
let best = null;
|
|
571
|
-
for (const capture of captures) {
|
|
572
|
-
const node = capture.node;
|
|
573
|
-
const startOk = node.startPosition.row < line ||
|
|
574
|
-
(node.startPosition.row === line &&
|
|
575
|
-
node.startPosition.column <= column);
|
|
576
|
-
const endOk = node.endPosition.row > line ||
|
|
577
|
-
(node.endPosition.row === line && node.endPosition.column >= column);
|
|
578
|
-
if (startOk && endOk) {
|
|
579
|
-
const size = node.endIndex - node.startIndex;
|
|
580
|
-
if (!best || size < best.size) {
|
|
581
|
-
best = { name: capture.name, size };
|
|
582
|
-
}
|
|
583
|
-
}
|
|
584
|
-
}
|
|
585
|
-
if (!best)
|
|
586
|
-
return null;
|
|
587
|
-
const prefix = "scope.";
|
|
588
|
-
if (!best.name.startsWith(prefix))
|
|
589
|
-
return null;
|
|
590
|
-
const kindStr = best.name.substring(prefix.length);
|
|
591
|
-
if (kindStr === "suppress")
|
|
592
|
-
return "suppress";
|
|
593
|
-
if (kindStr === "use_path")
|
|
594
|
-
return "use_path";
|
|
595
|
-
if (kindStr === "own_attribute")
|
|
596
|
-
return "own_attribute";
|
|
597
|
-
if (kindStr === "none")
|
|
598
|
-
return null;
|
|
599
|
-
return kindStr.split("_");
|
|
600
|
-
}
|
|
601
|
-
/**
|
|
602
|
-
* Check if a node is inside a comment (line_comment or block_comment).
|
|
603
|
-
*/
|
|
604
|
-
isInsideComment(node) {
|
|
605
|
-
let current = node;
|
|
606
|
-
while (current) {
|
|
607
|
-
if (current.type === "line_comment" || current.type === "block_comment") {
|
|
608
|
-
return true;
|
|
609
|
-
}
|
|
610
|
-
current = current.parent;
|
|
611
|
-
}
|
|
612
|
-
return false;
|
|
613
|
-
}
|
|
614
|
-
/**
|
|
615
|
-
* Resolve enclosing class and root state machine names at a position.
|
|
616
|
-
* For state machines, keeps walking to find the outermost (root) SM.
|
|
439
|
+
* @param parentPath Path segments to the parent state (e.g., ["EEE", "Open"])
|
|
440
|
+
* @param smContainer Qualified SM container (e.g., "ClassName.smName")
|
|
441
|
+
* @returns Names of direct child states of the resolved parent
|
|
617
442
|
*/
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
443
|
+
getChildStateNames(parentPath, smContainer, reachableFiles) {
|
|
444
|
+
if (parentPath.length === 0)
|
|
445
|
+
return [];
|
|
446
|
+
const effectivePath = (0, treeUtils_1.stripSmPrefix)(parentPath, smContainer);
|
|
447
|
+
if (effectivePath.length === 0)
|
|
448
|
+
return [];
|
|
449
|
+
const allStates = this.getSymbols({
|
|
450
|
+
container: smContainer,
|
|
451
|
+
kind: "state",
|
|
622
452
|
});
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
if (
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
[
|
|
638
|
-
"class_definition",
|
|
639
|
-
"trait_definition",
|
|
640
|
-
"interface_definition",
|
|
641
|
-
"association_class_definition",
|
|
642
|
-
].includes(node.type)) {
|
|
643
|
-
enclosingClass = node.childForFieldName("name")?.text;
|
|
644
|
-
}
|
|
645
|
-
node = node.parent;
|
|
646
|
-
}
|
|
647
|
-
return { enclosingClass, enclosingStateMachine };
|
|
648
|
-
}
|
|
649
|
-
/**
|
|
650
|
-
* Check if the cursor is at an attribute name position (after a type name).
|
|
651
|
-
* E.g., "Integer |" — the previous leaf is inside a type_name that is the
|
|
652
|
-
* "type" field of an attribute_declaration, const_declaration, method, or param.
|
|
653
|
-
*/
|
|
654
|
-
isAtAttributeNamePosition(tree, content, line, column) {
|
|
655
|
-
const prevLeaf = this.findPreviousLeaf(tree, content, line, column);
|
|
656
|
-
if (!prevLeaf)
|
|
657
|
-
return false;
|
|
658
|
-
// Walk up to find if prevLeaf is inside a type_name
|
|
659
|
-
let node = prevLeaf;
|
|
660
|
-
while (node) {
|
|
661
|
-
if (node.type === "type_name") {
|
|
662
|
-
const parent = node.parent;
|
|
663
|
-
if (parent) {
|
|
664
|
-
for (let i = 0; i < parent.childCount; i++) {
|
|
665
|
-
if (parent.child(i)?.id === node.id) {
|
|
666
|
-
const fieldName = parent.fieldNameForChild(i);
|
|
667
|
-
if (fieldName === "type" || fieldName === "return_type") {
|
|
668
|
-
return true;
|
|
669
|
-
}
|
|
670
|
-
}
|
|
671
|
-
}
|
|
453
|
+
const names = new Set();
|
|
454
|
+
for (const s of allStates) {
|
|
455
|
+
if (reachableFiles && !reachableFiles.has(path.normalize(s.file)))
|
|
456
|
+
continue;
|
|
457
|
+
if (!s.statePath || s.statePath.length < effectivePath.length + 1)
|
|
458
|
+
continue;
|
|
459
|
+
// Suffix match: check if effectivePath matches the segments ending
|
|
460
|
+
// just before the child name (e.g., ["Closed"] matches tail of ["EEE","Closed","Inner"])
|
|
461
|
+
const suffixStart = s.statePath.length - effectivePath.length - 1;
|
|
462
|
+
let match = true;
|
|
463
|
+
for (let i = 0; i < effectivePath.length; i++) {
|
|
464
|
+
if (s.statePath[suffixStart + i] !== effectivePath[i]) {
|
|
465
|
+
match = false;
|
|
466
|
+
break;
|
|
672
467
|
}
|
|
673
|
-
break;
|
|
674
468
|
}
|
|
675
|
-
|
|
469
|
+
if (match)
|
|
470
|
+
names.add(s.name);
|
|
676
471
|
}
|
|
677
|
-
return
|
|
472
|
+
return [...names];
|
|
678
473
|
}
|
|
679
474
|
/**
|
|
680
|
-
*
|
|
681
|
-
*
|
|
682
|
-
* then whitespace, then extracts the previous token.
|
|
475
|
+
* Resolve a state within a dotted path context, returning the matching SymbolEntry.
|
|
476
|
+
* Uses pre-computed statePath for exact path matching without AST walking.
|
|
683
477
|
*
|
|
684
|
-
*
|
|
685
|
-
*
|
|
686
|
-
*
|
|
687
|
-
*
|
|
478
|
+
* @param precedingPath Path segments before the target (e.g., ["EEE", "Open"])
|
|
479
|
+
* @param targetName The target state name (e.g., "Inner")
|
|
480
|
+
* @param smContainer Qualified SM container (e.g., "ClassName.smName")
|
|
481
|
+
* @returns The SymbolEntry for the target state, or undefined if not found
|
|
688
482
|
*/
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
}
|
|
702
|
-
//
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
return null; // Hit punctuation, not a word
|
|
715
|
-
return content.substring(start, pos);
|
|
483
|
+
resolveStateInPath(precedingPath, targetName, smContainer, reachableFiles) {
|
|
484
|
+
if (precedingPath.length === 0)
|
|
485
|
+
return undefined;
|
|
486
|
+
const effectivePath = (0, treeUtils_1.stripSmPrefix)(precedingPath, smContainer);
|
|
487
|
+
const targetPath = [...effectivePath, targetName];
|
|
488
|
+
let candidates = this.getSymbols({
|
|
489
|
+
container: smContainer,
|
|
490
|
+
kind: "state",
|
|
491
|
+
name: targetName,
|
|
492
|
+
});
|
|
493
|
+
if (reachableFiles) {
|
|
494
|
+
candidates = candidates.filter((s) => reachableFiles.has(path.normalize(s.file)));
|
|
495
|
+
}
|
|
496
|
+
// Suffix match: targetPath may be a partial path (e.g., ["Closed","Inner"])
|
|
497
|
+
// that matches the tail of a full statePath (e.g., ["EEE","Closed","Inner"])
|
|
498
|
+
return candidates.find((s) => {
|
|
499
|
+
if (!s.statePath || s.statePath.length < targetPath.length)
|
|
500
|
+
return false;
|
|
501
|
+
const offset = s.statePath.length - targetPath.length;
|
|
502
|
+
for (let i = 0; i < targetPath.length; i++) {
|
|
503
|
+
if (s.statePath[offset + i] !== targetPath[i])
|
|
504
|
+
return false;
|
|
505
|
+
}
|
|
506
|
+
return true;
|
|
507
|
+
});
|
|
716
508
|
}
|
|
509
|
+
// =====================
|
|
510
|
+
// Private methods
|
|
511
|
+
// =====================
|
|
717
512
|
readFileSafe(filePath) {
|
|
718
513
|
try {
|
|
719
514
|
return fs.readFileSync(filePath, "utf-8");
|
|
@@ -838,6 +633,7 @@ class SymbolIndex {
|
|
|
838
633
|
*/
|
|
839
634
|
resolveStateMachineContainer(node) {
|
|
840
635
|
let rootSmName;
|
|
636
|
+
let className;
|
|
841
637
|
let current = node.parent;
|
|
842
638
|
while (current) {
|
|
843
639
|
if (current.type === "state_machine") {
|
|
@@ -846,10 +642,36 @@ class SymbolIndex {
|
|
|
846
642
|
if (current.type === "statemachine_definition") {
|
|
847
643
|
rootSmName = current.childForFieldName("name")?.text ?? rootSmName;
|
|
848
644
|
}
|
|
645
|
+
if (!className &&
|
|
646
|
+
[
|
|
647
|
+
"class_definition",
|
|
648
|
+
"trait_definition",
|
|
649
|
+
"interface_definition",
|
|
650
|
+
"association_class_definition",
|
|
651
|
+
].includes(current.type)) {
|
|
652
|
+
className = current.childForFieldName("name")?.text;
|
|
653
|
+
}
|
|
849
654
|
current = current.parent;
|
|
850
655
|
}
|
|
851
|
-
|
|
656
|
+
if (!rootSmName)
|
|
657
|
+
return undefined;
|
|
658
|
+
return className ? `${className}.${rootSmName}` : rootSmName;
|
|
852
659
|
}
|
|
660
|
+
/** For enum values: walk up to find the enclosing enum_definition name. */
|
|
661
|
+
resolveEnumContainer(node) {
|
|
662
|
+
let current = node.parent;
|
|
663
|
+
while (current) {
|
|
664
|
+
if (current.type === "enum_definition") {
|
|
665
|
+
return current.childForFieldName("name")?.text;
|
|
666
|
+
}
|
|
667
|
+
current = current.parent;
|
|
668
|
+
}
|
|
669
|
+
return undefined;
|
|
670
|
+
}
|
|
671
|
+
/**
|
|
672
|
+
* Build the nesting path for a state by walking up parent state nodes.
|
|
673
|
+
* E.g., for Inner inside Open inside EEE: ["EEE", "Open", "Inner"]
|
|
674
|
+
*/
|
|
853
675
|
/**
|
|
854
676
|
* Extract symbol definitions from the AST using the definitions.scm query.
|
|
855
677
|
* Falls back to an empty list if the query isn't loaded.
|
|
@@ -870,15 +692,22 @@ class SymbolIndex {
|
|
|
870
692
|
container = this.resolveStateMachineContainer(node);
|
|
871
693
|
}
|
|
872
694
|
else if (kind === "attribute" ||
|
|
695
|
+
kind === "const" ||
|
|
873
696
|
kind === "method" ||
|
|
874
|
-
kind === "template"
|
|
697
|
+
kind === "template" ||
|
|
698
|
+
kind === "tracecase") {
|
|
875
699
|
container = this.resolveClassContainer(node);
|
|
876
700
|
}
|
|
701
|
+
else if (kind === "enum_value") {
|
|
702
|
+
// Enum values belong to their enclosing enum
|
|
703
|
+
container = this.resolveEnumContainer(node);
|
|
704
|
+
}
|
|
877
705
|
else {
|
|
878
706
|
// Top-level symbols (class, interface, trait, enum, etc.) are self-containers
|
|
879
707
|
container = node.text;
|
|
880
708
|
}
|
|
881
|
-
|
|
709
|
+
const defNode = node.parent;
|
|
710
|
+
const entry = {
|
|
882
711
|
name: node.text,
|
|
883
712
|
kind,
|
|
884
713
|
file: filePath,
|
|
@@ -887,10 +716,174 @@ class SymbolIndex {
|
|
|
887
716
|
endLine: node.endPosition.row,
|
|
888
717
|
endColumn: node.endPosition.column,
|
|
889
718
|
container,
|
|
890
|
-
|
|
719
|
+
defLine: defNode?.startPosition.row,
|
|
720
|
+
defColumn: defNode?.startPosition.column,
|
|
721
|
+
defEndLine: defNode?.endPosition.row,
|
|
722
|
+
defEndColumn: defNode?.endPosition.column,
|
|
723
|
+
};
|
|
724
|
+
if (kind === "state") {
|
|
725
|
+
entry.statePath = (0, treeUtils_1.resolveStatePath)(node);
|
|
726
|
+
}
|
|
727
|
+
symbols.push(entry);
|
|
891
728
|
}
|
|
892
729
|
return symbols;
|
|
893
730
|
}
|
|
731
|
+
// ── Workspace indexing & import graph ──────────────────────────────────
|
|
732
|
+
/**
|
|
733
|
+
* Scan workspace roots for all .ump files, follow use chains to external
|
|
734
|
+
* files, and index everything. Content-hash skips unchanged files.
|
|
735
|
+
*
|
|
736
|
+
* @param workspaceRoots Workspace root directories
|
|
737
|
+
* @param getOpenDocContent Returns in-memory editor content if the file is open, undefined otherwise
|
|
738
|
+
*/
|
|
739
|
+
indexWorkspace(workspaceRoots, getOpenDocContent) {
|
|
740
|
+
if (!this.initialized || !this.parser)
|
|
741
|
+
return;
|
|
742
|
+
// 1. Discover all .ump files under workspace roots
|
|
743
|
+
const discoveredFiles = new Set();
|
|
744
|
+
for (const root of workspaceRoots) {
|
|
745
|
+
this.globUmpFiles(root, discoveredFiles);
|
|
746
|
+
}
|
|
747
|
+
// 2. Index each discovered file (open-doc content takes precedence)
|
|
748
|
+
for (const filePath of discoveredFiles) {
|
|
749
|
+
const openContent = getOpenDocContent(filePath);
|
|
750
|
+
if (openContent !== undefined) {
|
|
751
|
+
this.indexFile(filePath, openContent);
|
|
752
|
+
}
|
|
753
|
+
else {
|
|
754
|
+
this.indexFile(filePath);
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
// 3. Follow use chains to index external files (outside workspace roots)
|
|
758
|
+
// Only treat an external file as live if it's open in the editor or exists on disk.
|
|
759
|
+
// This prevents stale forward-import edges from re-adding deleted files.
|
|
760
|
+
const externalFiles = new Set();
|
|
761
|
+
for (const filePath of discoveredFiles) {
|
|
762
|
+
const imports = this.importGraph.getForward(filePath);
|
|
763
|
+
if (imports) {
|
|
764
|
+
for (const imp of imports) {
|
|
765
|
+
if (!discoveredFiles.has(imp) &&
|
|
766
|
+
!externalFiles.has(imp) &&
|
|
767
|
+
(getOpenDocContent(imp) !== undefined || fs.existsSync(imp))) {
|
|
768
|
+
externalFiles.add(imp);
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
// Index external files and follow their chains too (transitive)
|
|
774
|
+
const queue = [...externalFiles];
|
|
775
|
+
while (queue.length > 0) {
|
|
776
|
+
const filePath = queue.pop();
|
|
777
|
+
const openContent = getOpenDocContent(filePath);
|
|
778
|
+
if (openContent !== undefined) {
|
|
779
|
+
this.indexFile(filePath, openContent);
|
|
780
|
+
}
|
|
781
|
+
else {
|
|
782
|
+
this.indexFile(filePath);
|
|
783
|
+
}
|
|
784
|
+
const imports = this.importGraph.getForward(filePath);
|
|
785
|
+
if (imports) {
|
|
786
|
+
for (const imp of imports) {
|
|
787
|
+
if (!discoveredFiles.has(imp) &&
|
|
788
|
+
!externalFiles.has(imp) &&
|
|
789
|
+
(getOpenDocContent(imp) !== undefined || fs.existsSync(imp))) {
|
|
790
|
+
externalFiles.add(imp);
|
|
791
|
+
queue.push(imp);
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
// 4. Remove stale indexed files.
|
|
797
|
+
// A file is removed if it's not in the discovered/external set AND not open in the editor.
|
|
798
|
+
// Note: fs.existsSync is intentionally NOT checked here — a file that still exists on disk
|
|
799
|
+
// but is no longer reachable (e.g., use statement removed) should be dropped from the index.
|
|
800
|
+
const allKnownFiles = new Set([...discoveredFiles, ...externalFiles]);
|
|
801
|
+
for (const filePath of this.files.keys()) {
|
|
802
|
+
if (!allKnownFiles.has(filePath) &&
|
|
803
|
+
getOpenDocContent(filePath) === undefined) {
|
|
804
|
+
this.removeFile(filePath);
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
/**
|
|
809
|
+
* Get all files whose use chain can reach any of the given declaration files.
|
|
810
|
+
* Transitive reverse closure.
|
|
811
|
+
*/
|
|
812
|
+
getReverseImporters(declarationFiles) {
|
|
813
|
+
return this.importGraph.getReverseImporters(declarationFiles);
|
|
814
|
+
}
|
|
815
|
+
/**
|
|
816
|
+
* Find all references to a symbol across the given files.
|
|
817
|
+
* Delegates to the extracted searchReferences() function.
|
|
818
|
+
*/
|
|
819
|
+
findReferences(declarations, filesToSearch, includeDeclaration) {
|
|
820
|
+
if (!this.referencesQuery || declarations.length === 0)
|
|
821
|
+
return [];
|
|
822
|
+
// Build filePath→tree map for the files to search
|
|
823
|
+
const fileTreeMap = new Map();
|
|
824
|
+
for (const fp of filesToSearch) {
|
|
825
|
+
const tree = this.files.get(fp)?.tree;
|
|
826
|
+
if (tree)
|
|
827
|
+
fileTreeMap.set(fp, tree);
|
|
828
|
+
}
|
|
829
|
+
return (0, referenceSearch_1.searchReferences)(declarations, includeDeclaration, this.referencesQuery, fileTreeMap, this.isAGraph);
|
|
830
|
+
}
|
|
831
|
+
/**
|
|
832
|
+
* Update forward/reverse import maps for a file.
|
|
833
|
+
*/
|
|
834
|
+
updateImportMaps(filePath, content) {
|
|
835
|
+
// Compute resolved imports from use statements
|
|
836
|
+
const usePaths = this.extractUseStatements(filePath, content);
|
|
837
|
+
const fileDir = path.dirname(filePath);
|
|
838
|
+
const newImports = new Set();
|
|
839
|
+
for (const usePath of usePaths) {
|
|
840
|
+
if (!usePath.endsWith(".ump"))
|
|
841
|
+
continue;
|
|
842
|
+
const resolved = path.isAbsolute(usePath)
|
|
843
|
+
? path.normalize(usePath)
|
|
844
|
+
: path.normalize(path.resolve(fileDir, usePath));
|
|
845
|
+
if (fs.existsSync(resolved)) {
|
|
846
|
+
newImports.add(resolved);
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
// Delegate edge management to ImportGraph
|
|
850
|
+
this.importGraph.setEdges(filePath, newImports);
|
|
851
|
+
}
|
|
852
|
+
/**
|
|
853
|
+
* Fully remove a file from the index (symbols, imports, isA).
|
|
854
|
+
*/
|
|
855
|
+
removeFile(filePath) {
|
|
856
|
+
this.removeFileSymbols(filePath);
|
|
857
|
+
this.files.delete(filePath);
|
|
858
|
+
this.importGraph.removeEdges(filePath);
|
|
859
|
+
this.isAByFile.delete(filePath);
|
|
860
|
+
this.rebuildIsAGraph();
|
|
861
|
+
}
|
|
862
|
+
/**
|
|
863
|
+
* Recursively find all .ump files under a directory.
|
|
864
|
+
*/
|
|
865
|
+
globUmpFiles(dir, result) {
|
|
866
|
+
let entries;
|
|
867
|
+
try {
|
|
868
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
869
|
+
}
|
|
870
|
+
catch {
|
|
871
|
+
return; // permission denied, etc.
|
|
872
|
+
}
|
|
873
|
+
for (const entry of entries) {
|
|
874
|
+
const fullPath = path.join(dir, entry.name);
|
|
875
|
+
if (entry.isDirectory()) {
|
|
876
|
+
// Skip common non-source directories
|
|
877
|
+
if (entry.name === "node_modules" || entry.name === ".git" || entry.name === "out") {
|
|
878
|
+
continue;
|
|
879
|
+
}
|
|
880
|
+
this.globUmpFiles(fullPath, result);
|
|
881
|
+
}
|
|
882
|
+
else if (entry.name.endsWith(".ump")) {
|
|
883
|
+
result.add(path.normalize(fullPath));
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
}
|
|
894
887
|
}
|
|
895
888
|
exports.SymbolIndex = SymbolIndex;
|
|
896
889
|
// Singleton instance
|