umple-lsp-server 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/out/bin.d.ts +2 -0
- package/out/bin.js +5 -0
- package/out/bin.js.map +1 -0
- package/out/keywords.d.ts +38 -0
- package/out/keywords.js +205 -0
- package/out/keywords.js.map +1 -0
- package/out/server.d.ts +1 -0
- package/out/server.js +1117 -0
- package/out/server.js.map +1 -0
- package/out/symbolIndex.d.ts +170 -0
- package/out/symbolIndex.js +705 -0
- package/out/symbolIndex.js.map +1 -0
- package/package.json +39 -0
- package/tree-sitter-umple.wasm +0 -0
|
@@ -0,0 +1,705 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Symbol Index for fast go-to-definition lookups.
|
|
4
|
+
*
|
|
5
|
+
* Uses tree-sitter (via WASM) for incremental parsing and maintains an
|
|
6
|
+
* in-memory index of all symbol definitions in the workspace.
|
|
7
|
+
*/
|
|
8
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
exports.symbolIndex = exports.SymbolIndex = void 0;
|
|
10
|
+
const fs = require("fs");
|
|
11
|
+
// web-tree-sitter types and module
|
|
12
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
13
|
+
const TreeSitter = require("web-tree-sitter");
|
|
14
|
+
const DUMMY_IDENTIFIER = "__CURSOR__";
|
|
15
|
+
class SymbolIndex {
|
|
16
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
17
|
+
parser = null;
|
|
18
|
+
language = null;
|
|
19
|
+
files = new Map();
|
|
20
|
+
symbolsByName = new Map();
|
|
21
|
+
initialized = false;
|
|
22
|
+
/**
|
|
23
|
+
* Initialize the tree-sitter parser with the Umple grammar.
|
|
24
|
+
* @param wasmPath Path to the tree-sitter-umple.wasm file
|
|
25
|
+
*/
|
|
26
|
+
async initialize(wasmPath) {
|
|
27
|
+
try {
|
|
28
|
+
// Initialize the WASM module first
|
|
29
|
+
await TreeSitter.Parser.init();
|
|
30
|
+
// Create parser instance
|
|
31
|
+
this.parser = new TreeSitter.Parser();
|
|
32
|
+
// Load the WASM language
|
|
33
|
+
this.language = await TreeSitter.Language.load(wasmPath);
|
|
34
|
+
this.parser.setLanguage(this.language);
|
|
35
|
+
this.initialized = true;
|
|
36
|
+
return true;
|
|
37
|
+
}
|
|
38
|
+
catch (err) {
|
|
39
|
+
console.error("Failed to initialize tree-sitter parser:", err);
|
|
40
|
+
this.parser = null;
|
|
41
|
+
this.language = null;
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Check if the symbol index is ready to use.
|
|
47
|
+
*/
|
|
48
|
+
isReady() {
|
|
49
|
+
return this.initialized && this.parser !== null;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Index a file or update its index if content changed.
|
|
53
|
+
* @param filePath Absolute path to the file
|
|
54
|
+
* @param content File content (optional, will be read from disk if not provided)
|
|
55
|
+
* @returns true if the file was (re)indexed, false if cache hit
|
|
56
|
+
*/
|
|
57
|
+
indexFile(filePath, content) {
|
|
58
|
+
if (!this.parser)
|
|
59
|
+
return false;
|
|
60
|
+
const fileContent = content ?? this.readFileSafe(filePath);
|
|
61
|
+
if (fileContent === null)
|
|
62
|
+
return false;
|
|
63
|
+
const hash = this.hashContent(fileContent);
|
|
64
|
+
const existing = this.files.get(filePath);
|
|
65
|
+
if (existing && existing.contentHash === hash) {
|
|
66
|
+
// Content unchanged, skip re-indexing
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
// Remove old symbols for this file from the name index
|
|
70
|
+
if (existing) {
|
|
71
|
+
this.removeFileSymbols(filePath);
|
|
72
|
+
}
|
|
73
|
+
// Parse the file
|
|
74
|
+
const tree = this.parser.parse(fileContent);
|
|
75
|
+
// Extract symbols from the AST
|
|
76
|
+
const symbols = this.extractSymbols(filePath, tree.rootNode);
|
|
77
|
+
// Store the file index
|
|
78
|
+
this.files.set(filePath, {
|
|
79
|
+
symbols,
|
|
80
|
+
tree,
|
|
81
|
+
contentHash: hash,
|
|
82
|
+
});
|
|
83
|
+
// Add symbols to the name index
|
|
84
|
+
for (const symbol of symbols) {
|
|
85
|
+
const existing = this.symbolsByName.get(symbol.name) ?? [];
|
|
86
|
+
existing.push(symbol);
|
|
87
|
+
this.symbolsByName.set(symbol.name, existing);
|
|
88
|
+
}
|
|
89
|
+
return true;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Update a file with new content.
|
|
93
|
+
* For web-tree-sitter, we do a full reparse but the index diffing is still efficient.
|
|
94
|
+
*/
|
|
95
|
+
updateFile(filePath, content) {
|
|
96
|
+
return this.indexFile(filePath, content);
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Find definition of a symbol by name.
|
|
100
|
+
* @param name Symbol name to look up
|
|
101
|
+
* @param kind Optional kind filter
|
|
102
|
+
* @returns Array of matching symbol entries
|
|
103
|
+
*/
|
|
104
|
+
findDefinition(name, kind) {
|
|
105
|
+
const symbols = this.symbolsByName.get(name) ?? [];
|
|
106
|
+
if (kind) {
|
|
107
|
+
return symbols.filter((s) => s.kind === kind);
|
|
108
|
+
}
|
|
109
|
+
return symbols;
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Find definition with context (for resolving attributes/states within a class/statemachine).
|
|
113
|
+
* @param name Symbol name
|
|
114
|
+
* @param parentName Parent symbol name (class or statemachine name)
|
|
115
|
+
*/
|
|
116
|
+
findDefinitionInContext(name, parentName) {
|
|
117
|
+
const symbols = this.symbolsByName.get(name) ?? [];
|
|
118
|
+
return symbols.filter((s) => s.parent === parentName);
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Get all symbols in a file.
|
|
122
|
+
*/
|
|
123
|
+
getFileSymbols(filePath) {
|
|
124
|
+
return this.files.get(filePath)?.symbols ?? [];
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Get all indexed files.
|
|
128
|
+
*/
|
|
129
|
+
getIndexedFiles() {
|
|
130
|
+
return Array.from(this.files.keys());
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Get all symbol names.
|
|
134
|
+
*/
|
|
135
|
+
getAllSymbolNames() {
|
|
136
|
+
return Array.from(this.symbolsByName.keys());
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Remove a file from the index.
|
|
140
|
+
*/
|
|
141
|
+
removeFile(filePath) {
|
|
142
|
+
this.removeFileSymbols(filePath);
|
|
143
|
+
this.files.delete(filePath);
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Clear the entire index.
|
|
147
|
+
*/
|
|
148
|
+
clear() {
|
|
149
|
+
this.files.clear();
|
|
150
|
+
this.symbolsByName.clear();
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Get index statistics.
|
|
154
|
+
*/
|
|
155
|
+
getStats() {
|
|
156
|
+
let totalSymbols = 0;
|
|
157
|
+
for (const fileIndex of this.files.values()) {
|
|
158
|
+
totalSymbols += fileIndex.symbols.length;
|
|
159
|
+
}
|
|
160
|
+
return {
|
|
161
|
+
files: this.files.size,
|
|
162
|
+
symbols: totalSymbols,
|
|
163
|
+
uniqueNames: this.symbolsByName.size,
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Check if a position is inside a comment.
|
|
168
|
+
* @param filePath Path to the file
|
|
169
|
+
* @param content File content (if available, otherwise reads from disk)
|
|
170
|
+
* @param line 0-indexed line number
|
|
171
|
+
* @param column 0-indexed column number
|
|
172
|
+
*/
|
|
173
|
+
isPositionInComment(filePath, content, line, column) {
|
|
174
|
+
if (!this.initialized || !this.parser) {
|
|
175
|
+
return false;
|
|
176
|
+
}
|
|
177
|
+
// Get or create tree
|
|
178
|
+
let tree = null;
|
|
179
|
+
const fileIndex = this.files.get(filePath);
|
|
180
|
+
if (fileIndex?.tree) {
|
|
181
|
+
tree = fileIndex.tree;
|
|
182
|
+
}
|
|
183
|
+
else if (content) {
|
|
184
|
+
tree = this.parser.parse(content);
|
|
185
|
+
}
|
|
186
|
+
else {
|
|
187
|
+
const fileContent = this.readFileSafe(filePath);
|
|
188
|
+
if (fileContent) {
|
|
189
|
+
tree = this.parser.parse(fileContent);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
if (!tree) {
|
|
193
|
+
return false;
|
|
194
|
+
}
|
|
195
|
+
const node = tree.rootNode.descendantForPosition({ row: line, column });
|
|
196
|
+
if (!node) {
|
|
197
|
+
return false;
|
|
198
|
+
}
|
|
199
|
+
// Check if the node or any ancestor is a comment
|
|
200
|
+
let current = node;
|
|
201
|
+
while (current) {
|
|
202
|
+
if (current.type === "line_comment" || current.type === "block_comment") {
|
|
203
|
+
return true;
|
|
204
|
+
}
|
|
205
|
+
current = current.parent;
|
|
206
|
+
}
|
|
207
|
+
return false;
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* Extract use statement paths from a file using tree-sitter.
|
|
211
|
+
* @param filePath Path to the file
|
|
212
|
+
* @param content File content (optional, will be read from disk if not provided)
|
|
213
|
+
* @returns Array of use paths (without quotes)
|
|
214
|
+
*/
|
|
215
|
+
extractUseStatements(filePath, content) {
|
|
216
|
+
if (!this.initialized || !this.parser) {
|
|
217
|
+
return [];
|
|
218
|
+
}
|
|
219
|
+
const fileContent = content ?? this.readFileSafe(filePath);
|
|
220
|
+
if (!fileContent) {
|
|
221
|
+
return [];
|
|
222
|
+
}
|
|
223
|
+
// Use cached tree if available and content matches
|
|
224
|
+
const fileIndex = this.files.get(filePath);
|
|
225
|
+
let tree;
|
|
226
|
+
if (fileIndex?.tree && !content) {
|
|
227
|
+
tree = fileIndex.tree;
|
|
228
|
+
}
|
|
229
|
+
else {
|
|
230
|
+
tree = this.parser.parse(fileContent);
|
|
231
|
+
}
|
|
232
|
+
const usePaths = [];
|
|
233
|
+
const visit = (node) => {
|
|
234
|
+
if (node.type === "use_statement") {
|
|
235
|
+
const pathNode = node.childForFieldName("path");
|
|
236
|
+
if (pathNode) {
|
|
237
|
+
usePaths.push(pathNode.text);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
else {
|
|
241
|
+
for (let i = 0; i < node.childCount; i++) {
|
|
242
|
+
const child = node.child(i);
|
|
243
|
+
if (child)
|
|
244
|
+
visit(child);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
};
|
|
248
|
+
visit(tree.rootNode);
|
|
249
|
+
return usePaths;
|
|
250
|
+
}
|
|
251
|
+
/**
|
|
252
|
+
* Extract use statement paths with their positions from a file using tree-sitter.
|
|
253
|
+
* @param filePath Path to the file
|
|
254
|
+
* @param content File content (optional, will be read from disk if not provided)
|
|
255
|
+
* @returns Array of use statements with path and line number
|
|
256
|
+
*/
|
|
257
|
+
extractUseStatementsWithPositions(filePath, content) {
|
|
258
|
+
if (!this.initialized || !this.parser) {
|
|
259
|
+
return [];
|
|
260
|
+
}
|
|
261
|
+
const fileContent = content ?? this.readFileSafe(filePath);
|
|
262
|
+
if (!fileContent) {
|
|
263
|
+
return [];
|
|
264
|
+
}
|
|
265
|
+
// Use cached tree if available and content matches
|
|
266
|
+
const fileIndex = this.files.get(filePath);
|
|
267
|
+
let tree;
|
|
268
|
+
if (fileIndex?.tree && !content) {
|
|
269
|
+
tree = fileIndex.tree;
|
|
270
|
+
}
|
|
271
|
+
else {
|
|
272
|
+
tree = this.parser.parse(fileContent);
|
|
273
|
+
}
|
|
274
|
+
const useStatements = [];
|
|
275
|
+
const visit = (node) => {
|
|
276
|
+
if (node.type === "use_statement") {
|
|
277
|
+
const pathNode = node.childForFieldName("path");
|
|
278
|
+
if (pathNode) {
|
|
279
|
+
useStatements.push({
|
|
280
|
+
path: pathNode.text,
|
|
281
|
+
line: node.startPosition.row,
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
else {
|
|
286
|
+
for (let i = 0; i < node.childCount; i++) {
|
|
287
|
+
const child = node.child(i);
|
|
288
|
+
if (child)
|
|
289
|
+
visit(child);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
};
|
|
293
|
+
visit(tree.rootNode);
|
|
294
|
+
return useStatements;
|
|
295
|
+
}
|
|
296
|
+
/**
|
|
297
|
+
* Get the use path at a specific position, if the cursor is on a use statement.
|
|
298
|
+
* @param filePath Path to the file
|
|
299
|
+
* @param content File content
|
|
300
|
+
* @param line 0-indexed line number
|
|
301
|
+
* @param column 0-indexed column number
|
|
302
|
+
* @returns The use path (without quotes) or null if not on a use statement
|
|
303
|
+
*/
|
|
304
|
+
getUsePathAtPosition(filePath, content, line, column) {
|
|
305
|
+
if (!this.initialized || !this.parser) {
|
|
306
|
+
return null;
|
|
307
|
+
}
|
|
308
|
+
// Use cached tree if available
|
|
309
|
+
const fileIndex = this.files.get(filePath);
|
|
310
|
+
let tree;
|
|
311
|
+
if (fileIndex?.tree &&
|
|
312
|
+
fileIndex.contentHash === this.hashContent(content)) {
|
|
313
|
+
tree = fileIndex.tree;
|
|
314
|
+
}
|
|
315
|
+
else {
|
|
316
|
+
tree = this.parser.parse(content);
|
|
317
|
+
}
|
|
318
|
+
const node = tree.rootNode.descendantForPosition({ row: line, column });
|
|
319
|
+
if (!node) {
|
|
320
|
+
return null;
|
|
321
|
+
}
|
|
322
|
+
// Walk up to find if we're inside a use_statement
|
|
323
|
+
let current = node;
|
|
324
|
+
while (current) {
|
|
325
|
+
if (current.type === "use_statement") {
|
|
326
|
+
const pathNode = current.childForFieldName("path");
|
|
327
|
+
if (pathNode) {
|
|
328
|
+
return pathNode.text;
|
|
329
|
+
}
|
|
330
|
+
return null;
|
|
331
|
+
}
|
|
332
|
+
current = current.parent;
|
|
333
|
+
}
|
|
334
|
+
return null;
|
|
335
|
+
}
|
|
336
|
+
/**
|
|
337
|
+
* Get the completion context at a specific position using the dummy identifier trick.
|
|
338
|
+
*
|
|
339
|
+
* Inserts a dummy identifier (__CURSOR__) at the cursor position, parses the
|
|
340
|
+
* modified text, then walks up from the dummy node to determine context.
|
|
341
|
+
* This produces reliable results because the dummy forces the parser to place
|
|
342
|
+
* it in a grammatically valid position (e.g. "isA __CURSOR__" parses as an
|
|
343
|
+
* isa_declaration with type __CURSOR__).
|
|
344
|
+
*
|
|
345
|
+
* @param filePath Path to the file
|
|
346
|
+
* @param content File content (original, without dummy)
|
|
347
|
+
* @param line 0-indexed line number
|
|
348
|
+
* @param column 0-indexed column number (raw cursor position, NOT column-1)
|
|
349
|
+
* @returns The completion context type
|
|
350
|
+
*/
|
|
351
|
+
getCompletionContext(filePath, content, line, column) {
|
|
352
|
+
if (!this.initialized || !this.parser) {
|
|
353
|
+
return "unknown";
|
|
354
|
+
}
|
|
355
|
+
// Insert dummy identifier at cursor position
|
|
356
|
+
const lines = content.split("\n");
|
|
357
|
+
if (line < 0 || line >= lines.length) {
|
|
358
|
+
return "unknown";
|
|
359
|
+
}
|
|
360
|
+
const lineText = lines[line];
|
|
361
|
+
lines[line] =
|
|
362
|
+
lineText.substring(0, column) +
|
|
363
|
+
DUMMY_IDENTIFIER +
|
|
364
|
+
lineText.substring(column);
|
|
365
|
+
const modifiedText = lines.join("\n");
|
|
366
|
+
// Parse modified text (throwaway — do NOT update index)
|
|
367
|
+
const tree = this.parser.parse(modifiedText);
|
|
368
|
+
// Find the dummy node — it starts at (line, column) in the modified text
|
|
369
|
+
const dummyNode = tree.rootNode.descendantForPosition({
|
|
370
|
+
row: line,
|
|
371
|
+
column,
|
|
372
|
+
});
|
|
373
|
+
if (!dummyNode) {
|
|
374
|
+
return "unknown";
|
|
375
|
+
}
|
|
376
|
+
// Walk up from the dummy node to determine context
|
|
377
|
+
let current = dummyNode;
|
|
378
|
+
while (current) {
|
|
379
|
+
switch (current.type) {
|
|
380
|
+
// Comments: suppress all completions
|
|
381
|
+
case "line_comment":
|
|
382
|
+
case "block_comment":
|
|
383
|
+
return "comment";
|
|
384
|
+
// Specific keyword contexts (checked before structural)
|
|
385
|
+
case "use_statement":
|
|
386
|
+
return "use_path";
|
|
387
|
+
case "isa_declaration":
|
|
388
|
+
return "isa_type";
|
|
389
|
+
case "transition": {
|
|
390
|
+
const targetNode = current.childForFieldName("target");
|
|
391
|
+
if (targetNode && targetNode.text.includes(DUMMY_IDENTIFIER)) {
|
|
392
|
+
return "transition_target";
|
|
393
|
+
}
|
|
394
|
+
// Dummy is in event position — fall through to state context
|
|
395
|
+
return "state";
|
|
396
|
+
}
|
|
397
|
+
case "depend_statement":
|
|
398
|
+
return "depend_package";
|
|
399
|
+
case "association_inline": {
|
|
400
|
+
const rightType = current.childForFieldName("right_type");
|
|
401
|
+
if (rightType && rightType.text.includes(DUMMY_IDENTIFIER)) {
|
|
402
|
+
return "association_type";
|
|
403
|
+
}
|
|
404
|
+
// Dummy is in role or other position — fall through
|
|
405
|
+
break;
|
|
406
|
+
}
|
|
407
|
+
case "association_member": {
|
|
408
|
+
const leftType = current.childForFieldName("left_type");
|
|
409
|
+
const rightType = current.childForFieldName("right_type");
|
|
410
|
+
if ((leftType && leftType.text.includes(DUMMY_IDENTIFIER)) ||
|
|
411
|
+
(rightType && rightType.text.includes(DUMMY_IDENTIFIER))) {
|
|
412
|
+
return "association_type";
|
|
413
|
+
}
|
|
414
|
+
return "association";
|
|
415
|
+
}
|
|
416
|
+
// Brace-delimited: always reliable
|
|
417
|
+
case "state":
|
|
418
|
+
return "state";
|
|
419
|
+
case "state_machine":
|
|
420
|
+
return "state_machine";
|
|
421
|
+
case "association_definition":
|
|
422
|
+
return "association";
|
|
423
|
+
case "enum_definition":
|
|
424
|
+
return "enum";
|
|
425
|
+
case "class_definition":
|
|
426
|
+
case "trait_definition":
|
|
427
|
+
case "interface_definition":
|
|
428
|
+
return "class_body";
|
|
429
|
+
case "source_file":
|
|
430
|
+
return "top";
|
|
431
|
+
case "code_content":
|
|
432
|
+
case "code_block":
|
|
433
|
+
return "method";
|
|
434
|
+
}
|
|
435
|
+
current = current.parent;
|
|
436
|
+
}
|
|
437
|
+
return "unknown";
|
|
438
|
+
}
|
|
439
|
+
/**
|
|
440
|
+
* Get all symbols of a specific kind from the index.
|
|
441
|
+
* @param kind The kind of symbols to retrieve
|
|
442
|
+
* @returns Array of symbol entries of that kind
|
|
443
|
+
*/
|
|
444
|
+
getSymbolsByKind(kind) {
|
|
445
|
+
const result = [];
|
|
446
|
+
for (const symbols of this.symbolsByName.values()) {
|
|
447
|
+
for (const symbol of symbols) {
|
|
448
|
+
if (symbol.kind === kind) {
|
|
449
|
+
result.push(symbol);
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
return result;
|
|
454
|
+
}
|
|
455
|
+
/**
|
|
456
|
+
* Get all symbols (useful for type completions).
|
|
457
|
+
*/
|
|
458
|
+
getAllSymbols() {
|
|
459
|
+
const result = [];
|
|
460
|
+
for (const symbols of this.symbolsByName.values()) {
|
|
461
|
+
result.push(...symbols);
|
|
462
|
+
}
|
|
463
|
+
return result;
|
|
464
|
+
}
|
|
465
|
+
// =====================
|
|
466
|
+
// Private methods
|
|
467
|
+
// =====================
|
|
468
|
+
/**
|
|
469
|
+
* Debug helper: print a tree-sitter AST as an S-expression with positions.
|
|
470
|
+
* Output matches the format used by `tree-sitter parse` and Neovim InspectTree.
|
|
471
|
+
*/
|
|
472
|
+
debugPrintTree(content) {
|
|
473
|
+
if (!this.initialized || !this.parser) {
|
|
474
|
+
return null;
|
|
475
|
+
}
|
|
476
|
+
const tree = this.parser.parse(content);
|
|
477
|
+
const lines = [];
|
|
478
|
+
const visit = (node, depth) => {
|
|
479
|
+
const indent = " ".repeat(depth);
|
|
480
|
+
const field = node.parent ? this.getFieldName(node.parent, node) : null;
|
|
481
|
+
const prefix = field ? `${field}: ` : "";
|
|
482
|
+
const pos = `[${node.startPosition.row}, ${node.startPosition.column}] - [${node.endPosition.row}, ${node.endPosition.column}]`;
|
|
483
|
+
if (node.childCount === 0) {
|
|
484
|
+
lines.push(`${indent}${prefix}(${node.type} ${pos}) "${node.text}"`);
|
|
485
|
+
}
|
|
486
|
+
else {
|
|
487
|
+
lines.push(`${indent}${prefix}(${node.type} ${pos}`);
|
|
488
|
+
for (let i = 0; i < node.childCount; i++) {
|
|
489
|
+
const child = node.child(i);
|
|
490
|
+
if (child)
|
|
491
|
+
visit(child, depth + 1);
|
|
492
|
+
}
|
|
493
|
+
lines.push(`${indent})`);
|
|
494
|
+
}
|
|
495
|
+
};
|
|
496
|
+
visit(tree.rootNode, 0);
|
|
497
|
+
return lines.join("\n");
|
|
498
|
+
}
|
|
499
|
+
/**
|
|
500
|
+
* Get the field name for a child node within its parent.
|
|
501
|
+
*/
|
|
502
|
+
getFieldName(parent, child) {
|
|
503
|
+
// Check common field names used in the Umple grammar
|
|
504
|
+
const fieldNames = [
|
|
505
|
+
"name",
|
|
506
|
+
"path",
|
|
507
|
+
"type",
|
|
508
|
+
"return_type",
|
|
509
|
+
"left_role",
|
|
510
|
+
"right_role",
|
|
511
|
+
"right_type",
|
|
512
|
+
"left_type",
|
|
513
|
+
"event",
|
|
514
|
+
"target",
|
|
515
|
+
"package",
|
|
516
|
+
"language",
|
|
517
|
+
];
|
|
518
|
+
for (const name of fieldNames) {
|
|
519
|
+
const fieldNode = parent.childForFieldName(name);
|
|
520
|
+
if (fieldNode && fieldNode.id === child.id) {
|
|
521
|
+
return name;
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
return null;
|
|
525
|
+
}
|
|
526
|
+
readFileSafe(filePath) {
|
|
527
|
+
try {
|
|
528
|
+
return fs.readFileSync(filePath, "utf-8");
|
|
529
|
+
}
|
|
530
|
+
catch {
|
|
531
|
+
return null;
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
removeFileSymbols(filePath) {
|
|
535
|
+
const fileIndex = this.files.get(filePath);
|
|
536
|
+
if (!fileIndex)
|
|
537
|
+
return;
|
|
538
|
+
for (const symbol of fileIndex.symbols) {
|
|
539
|
+
const symbols = this.symbolsByName.get(symbol.name);
|
|
540
|
+
if (symbols) {
|
|
541
|
+
const filtered = symbols.filter((s) => s.file !== filePath);
|
|
542
|
+
if (filtered.length === 0) {
|
|
543
|
+
this.symbolsByName.delete(symbol.name);
|
|
544
|
+
}
|
|
545
|
+
else {
|
|
546
|
+
this.symbolsByName.set(symbol.name, filtered);
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
hashContent(content) {
|
|
552
|
+
// Simple hash for change detection
|
|
553
|
+
// For production, consider using crypto.createHash('sha256')
|
|
554
|
+
let hash = 0;
|
|
555
|
+
for (let i = 0; i < content.length; i++) {
|
|
556
|
+
const char = content.charCodeAt(i);
|
|
557
|
+
hash = (hash << 5) - hash + char;
|
|
558
|
+
hash = hash & hash; // Convert to 32bit integer
|
|
559
|
+
}
|
|
560
|
+
return hash.toString(16);
|
|
561
|
+
}
|
|
562
|
+
extractSymbols(filePath, rootNode) {
|
|
563
|
+
const symbols = [];
|
|
564
|
+
const visit = (node, parent) => {
|
|
565
|
+
switch (node.type) {
|
|
566
|
+
case "class_definition":
|
|
567
|
+
case "interface_definition":
|
|
568
|
+
case "trait_definition":
|
|
569
|
+
case "enum_definition":
|
|
570
|
+
case "external_definition": {
|
|
571
|
+
const nameNode = node.childForFieldName("name");
|
|
572
|
+
if (nameNode) {
|
|
573
|
+
let kind = node.type.replace("_definition", "");
|
|
574
|
+
if (node.type === "external_definition") {
|
|
575
|
+
kind = "class";
|
|
576
|
+
}
|
|
577
|
+
symbols.push({
|
|
578
|
+
name: nameNode.text,
|
|
579
|
+
kind,
|
|
580
|
+
file: filePath,
|
|
581
|
+
line: nameNode.startPosition.row,
|
|
582
|
+
column: nameNode.startPosition.column,
|
|
583
|
+
endLine: nameNode.endPosition.row,
|
|
584
|
+
endColumn: nameNode.endPosition.column,
|
|
585
|
+
});
|
|
586
|
+
// Visit children with this class as parent
|
|
587
|
+
for (let i = 0; i < node.childCount; i++) {
|
|
588
|
+
const child = node.child(i);
|
|
589
|
+
if (child)
|
|
590
|
+
visit(child, nameNode.text);
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
break;
|
|
594
|
+
}
|
|
595
|
+
case "attribute_declaration": {
|
|
596
|
+
const nameNode = node.childForFieldName("name");
|
|
597
|
+
if (nameNode && parent) {
|
|
598
|
+
symbols.push({
|
|
599
|
+
name: nameNode.text,
|
|
600
|
+
kind: "attribute",
|
|
601
|
+
file: filePath,
|
|
602
|
+
line: nameNode.startPosition.row,
|
|
603
|
+
column: nameNode.startPosition.column,
|
|
604
|
+
endLine: nameNode.endPosition.row,
|
|
605
|
+
endColumn: nameNode.endPosition.column,
|
|
606
|
+
parent,
|
|
607
|
+
});
|
|
608
|
+
}
|
|
609
|
+
break;
|
|
610
|
+
}
|
|
611
|
+
case "state_machine": {
|
|
612
|
+
const nameNode = node.childForFieldName("name");
|
|
613
|
+
if (nameNode && parent) {
|
|
614
|
+
symbols.push({
|
|
615
|
+
name: nameNode.text,
|
|
616
|
+
kind: "statemachine",
|
|
617
|
+
file: filePath,
|
|
618
|
+
line: nameNode.startPosition.row,
|
|
619
|
+
column: nameNode.startPosition.column,
|
|
620
|
+
endLine: nameNode.endPosition.row,
|
|
621
|
+
endColumn: nameNode.endPosition.column,
|
|
622
|
+
parent,
|
|
623
|
+
});
|
|
624
|
+
// Visit states with this statemachine as parent
|
|
625
|
+
for (let i = 0; i < node.childCount; i++) {
|
|
626
|
+
const child = node.child(i);
|
|
627
|
+
if (child && child.type === "state") {
|
|
628
|
+
visit(child, nameNode.text);
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
break;
|
|
633
|
+
}
|
|
634
|
+
case "state": {
|
|
635
|
+
const nameNode = node.childForFieldName("name");
|
|
636
|
+
if (nameNode && parent) {
|
|
637
|
+
symbols.push({
|
|
638
|
+
name: nameNode.text,
|
|
639
|
+
kind: "state",
|
|
640
|
+
file: filePath,
|
|
641
|
+
line: nameNode.startPosition.row,
|
|
642
|
+
column: nameNode.startPosition.column,
|
|
643
|
+
endLine: nameNode.endPosition.row,
|
|
644
|
+
endColumn: nameNode.endPosition.column,
|
|
645
|
+
parent,
|
|
646
|
+
});
|
|
647
|
+
// Visit nested states
|
|
648
|
+
for (let i = 0; i < node.childCount; i++) {
|
|
649
|
+
const child = node.child(i);
|
|
650
|
+
if (child && child.type === "state") {
|
|
651
|
+
visit(child, nameNode.text);
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
break;
|
|
656
|
+
}
|
|
657
|
+
case "method_declaration":
|
|
658
|
+
case "method_signature": {
|
|
659
|
+
const nameNode = node.childForFieldName("name");
|
|
660
|
+
if (nameNode && parent) {
|
|
661
|
+
symbols.push({
|
|
662
|
+
name: nameNode.text,
|
|
663
|
+
kind: "method",
|
|
664
|
+
file: filePath,
|
|
665
|
+
line: nameNode.startPosition.row,
|
|
666
|
+
column: nameNode.startPosition.column,
|
|
667
|
+
endLine: nameNode.endPosition.row,
|
|
668
|
+
endColumn: nameNode.endPosition.column,
|
|
669
|
+
parent,
|
|
670
|
+
});
|
|
671
|
+
}
|
|
672
|
+
break;
|
|
673
|
+
}
|
|
674
|
+
case "association_definition": {
|
|
675
|
+
const nameNode = node.childForFieldName("name");
|
|
676
|
+
if (nameNode) {
|
|
677
|
+
symbols.push({
|
|
678
|
+
name: nameNode.text,
|
|
679
|
+
kind: "association",
|
|
680
|
+
file: filePath,
|
|
681
|
+
line: nameNode.startPosition.row,
|
|
682
|
+
column: nameNode.startPosition.column,
|
|
683
|
+
endLine: nameNode.endPosition.row,
|
|
684
|
+
endColumn: nameNode.endPosition.column,
|
|
685
|
+
});
|
|
686
|
+
}
|
|
687
|
+
break;
|
|
688
|
+
}
|
|
689
|
+
default:
|
|
690
|
+
// Visit all children for other node types
|
|
691
|
+
for (let i = 0; i < node.childCount; i++) {
|
|
692
|
+
const child = node.child(i);
|
|
693
|
+
if (child)
|
|
694
|
+
visit(child, parent);
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
};
|
|
698
|
+
visit(rootNode);
|
|
699
|
+
return symbols;
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
exports.SymbolIndex = SymbolIndex;
|
|
703
|
+
// Singleton instance
|
|
704
|
+
exports.symbolIndex = new SymbolIndex();
|
|
705
|
+
//# sourceMappingURL=symbolIndex.js.map
|