umple-lsp-server 0.1.1 → 0.2.1
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 +72 -0
- package/definitions.scm +37 -0
- 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 +4 -36
- package/out/keywords.js +14 -200
- package/out/keywords.js.map +1 -1
- package/out/log.d.ts +7 -0
- package/out/log.js +22 -0
- package/out/log.js.map +1 -0
- package/out/server.js +266 -311
- package/out/server.js.map +1 -1
- package/out/symbolIndex.d.ts +123 -90
- package/out/symbolIndex.js +619 -426
- package/out/symbolIndex.js.map +1 -1
- package/out/tsconfig.tsbuildinfo +1 -0
- package/out/utils/debug.d.ts +1 -0
- package/out/utils/debug.js +8 -0
- package/out/utils/debug.js.map +1 -0
- package/package.json +6 -4
- package/references.scm +99 -0
- package/tree-sitter-umple.wasm +0 -0
package/out/server.js
CHANGED
|
@@ -113,7 +113,7 @@ connection.onInitialize((params) => {
|
|
|
113
113
|
textDocumentSync: node_1.TextDocumentSyncKind.Incremental,
|
|
114
114
|
completionProvider: {
|
|
115
115
|
resolveProvider: false,
|
|
116
|
-
triggerCharacters: ["
|
|
116
|
+
triggerCharacters: ["/"],
|
|
117
117
|
},
|
|
118
118
|
definitionProvider: true,
|
|
119
119
|
},
|
|
@@ -210,65 +210,193 @@ connection.onCompletion(async (params) => {
|
|
|
210
210
|
return [];
|
|
211
211
|
}
|
|
212
212
|
const docPath = getDocumentFilePath(document);
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
213
|
+
if (!docPath || !symbolIndexReady) {
|
|
214
|
+
return [];
|
|
215
|
+
}
|
|
216
|
+
const text = document.getText();
|
|
217
|
+
const { line, character } = params.position;
|
|
218
|
+
// 1. Get completion info from LookaheadIterator + scope query
|
|
219
|
+
const info = symbolIndex_1.symbolIndex.getCompletionInfo(text, line, character);
|
|
220
|
+
// 2. Suppress completions
|
|
221
|
+
if (info.isComment || info.isDefinitionName) {
|
|
222
|
+
return [];
|
|
223
|
+
}
|
|
224
|
+
if (info.symbolKinds === "suppress") {
|
|
225
|
+
return [];
|
|
226
|
+
}
|
|
227
|
+
// 3. Ensure imported files are indexed
|
|
228
|
+
const reachableFiles = ensureImportsIndexed(docPath, text);
|
|
229
|
+
const items = [];
|
|
230
|
+
const seen = new Set();
|
|
231
|
+
// 4. Normalize symbolKinds: use_path → file completions + mixset symbols
|
|
232
|
+
// "/" trigger outside use_path is suppressed
|
|
233
|
+
let symbolKinds = info.symbolKinds;
|
|
234
|
+
if (symbolKinds === "use_path") {
|
|
235
|
+
for (const item of getUseFileCompletions(document, info.prefix, line, character)) {
|
|
236
|
+
seen.add(item.label);
|
|
237
|
+
items.push(item);
|
|
238
|
+
}
|
|
239
|
+
// Path prefix (contains /) → only file completions, no keywords/mixsets
|
|
240
|
+
if (info.prefix.includes("/")) {
|
|
241
|
+
return items;
|
|
242
|
+
}
|
|
243
|
+
symbolKinds = ["mixset"];
|
|
217
244
|
}
|
|
218
|
-
|
|
219
|
-
if (context === "comment") {
|
|
245
|
+
else if (params.context?.triggerCharacter === "/") {
|
|
220
246
|
return [];
|
|
221
247
|
}
|
|
222
|
-
//
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
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
|
+
}
|
|
226
275
|
}
|
|
227
|
-
//
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
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;
|
|
231
292
|
}
|
|
232
|
-
//
|
|
233
|
-
|
|
234
|
-
|
|
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
|
+
}
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
return items;
|
|
235
335
|
});
|
|
236
336
|
connection.onDefinition(async (params) => {
|
|
237
337
|
const document = getDocument(params.textDocument.uri);
|
|
238
338
|
if (!document) {
|
|
239
339
|
return [];
|
|
240
340
|
}
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
return [useLocation];
|
|
341
|
+
if (!symbolIndexReady) {
|
|
342
|
+
return [];
|
|
244
343
|
}
|
|
245
|
-
|
|
246
|
-
if (
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
344
|
+
const docPath = getDocumentFilePath(document);
|
|
345
|
+
if (!docPath) {
|
|
346
|
+
return [];
|
|
347
|
+
}
|
|
348
|
+
const token = symbolIndex_1.symbolIndex.getTokenAtPosition(docPath, document.getText(), params.position.line, params.position.character);
|
|
349
|
+
if (!token) {
|
|
350
|
+
return [];
|
|
351
|
+
}
|
|
352
|
+
// use statement with .ump extension: resolve as file reference
|
|
353
|
+
if (token.word.endsWith(".ump")) {
|
|
354
|
+
const baseDir = path.dirname(docPath);
|
|
355
|
+
const targetPath = path.isAbsolute(token.word)
|
|
356
|
+
? token.word
|
|
357
|
+
: path.join(baseDir, token.word);
|
|
358
|
+
if (!fs.existsSync(targetPath)) {
|
|
253
359
|
return [];
|
|
254
360
|
}
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
361
|
+
return [
|
|
362
|
+
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
|
+
];
|
|
364
|
+
}
|
|
365
|
+
// Symbol lookup, filtered by reachable files
|
|
366
|
+
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([
|
|
369
|
+
"attribute",
|
|
370
|
+
"method",
|
|
371
|
+
"template",
|
|
372
|
+
"state",
|
|
373
|
+
]);
|
|
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))));
|
|
270
399
|
}
|
|
271
|
-
// No definition found in symbol index
|
|
272
400
|
return [];
|
|
273
401
|
});
|
|
274
402
|
function scheduleValidation(document) {
|
|
@@ -579,6 +707,10 @@ function collectReachableFiles(filePath, content, documentDir) {
|
|
|
579
707
|
function collectReachableFilesRecursive(filePath, content, documentDir, visited) {
|
|
580
708
|
const useStatements = symbolIndex_1.symbolIndex.extractUseStatements(filePath, content);
|
|
581
709
|
for (const usePath of useStatements) {
|
|
710
|
+
// Skip mixset names (no .ump extension = not a file reference)
|
|
711
|
+
if (!usePath.endsWith(".ump")) {
|
|
712
|
+
continue;
|
|
713
|
+
}
|
|
582
714
|
// Resolve the file path
|
|
583
715
|
let resolvedPath;
|
|
584
716
|
if (path.isAbsolute(usePath)) {
|
|
@@ -587,10 +719,6 @@ function collectReachableFilesRecursive(filePath, content, documentDir, visited)
|
|
|
587
719
|
else {
|
|
588
720
|
resolvedPath = path.resolve(documentDir, usePath);
|
|
589
721
|
}
|
|
590
|
-
// Ensure .ump extension
|
|
591
|
-
if (!resolvedPath.endsWith(".ump")) {
|
|
592
|
-
resolvedPath += ".ump";
|
|
593
|
-
}
|
|
594
722
|
const normalizedPath = path.normalize(resolvedPath);
|
|
595
723
|
if (visited.has(normalizedPath)) {
|
|
596
724
|
continue; // Already visited, skip to avoid cycles
|
|
@@ -638,14 +766,15 @@ function buildImportMaps(useStatements, documentDir) {
|
|
|
638
766
|
const directImports = new Map();
|
|
639
767
|
const transitiveMap = new Map();
|
|
640
768
|
for (const useStmt of useStatements) {
|
|
769
|
+
// Skip mixset names (no .ump extension = not a file reference)
|
|
770
|
+
if (!useStmt.path.endsWith(".ump")) {
|
|
771
|
+
continue;
|
|
772
|
+
}
|
|
641
773
|
// Resolve the use path to a filename
|
|
642
774
|
let resolvedPath = useStmt.path;
|
|
643
775
|
if (!path.isAbsolute(resolvedPath)) {
|
|
644
776
|
resolvedPath = path.resolve(documentDir, resolvedPath);
|
|
645
777
|
}
|
|
646
|
-
if (!resolvedPath.endsWith(".ump")) {
|
|
647
|
-
resolvedPath += ".ump";
|
|
648
|
-
}
|
|
649
778
|
const filename = path.basename(resolvedPath);
|
|
650
779
|
// Map direct import filename to line
|
|
651
780
|
directImports.set(filename, useStmt.line);
|
|
@@ -674,13 +803,14 @@ function collectTransitiveFilenames(filePath, collected, visited = new Set()) {
|
|
|
674
803
|
const fileDir = path.dirname(filePath);
|
|
675
804
|
const useStatements = symbolIndex_1.symbolIndex.extractUseStatements(filePath, content);
|
|
676
805
|
for (const usePath of useStatements) {
|
|
806
|
+
// Skip mixset names (no .ump extension = not a file reference)
|
|
807
|
+
if (!usePath.endsWith(".ump")) {
|
|
808
|
+
continue;
|
|
809
|
+
}
|
|
677
810
|
let resolvedPath = usePath;
|
|
678
811
|
if (!path.isAbsolute(resolvedPath)) {
|
|
679
812
|
resolvedPath = path.resolve(fileDir, resolvedPath);
|
|
680
813
|
}
|
|
681
|
-
if (!resolvedPath.endsWith(".ump")) {
|
|
682
|
-
resolvedPath += ".ump";
|
|
683
|
-
}
|
|
684
814
|
const filename = path.basename(resolvedPath);
|
|
685
815
|
collected.add(filename);
|
|
686
816
|
collectTransitiveFilenames(resolvedPath, collected, visited);
|
|
@@ -831,276 +961,101 @@ function getDocumentFilePath(document) {
|
|
|
831
961
|
return null;
|
|
832
962
|
}
|
|
833
963
|
}
|
|
834
|
-
function resolveUseDefinitionFromLine(document, position) {
|
|
835
|
-
const docPath = getDocumentFilePath(document);
|
|
836
|
-
if (!docPath) {
|
|
837
|
-
return null;
|
|
838
|
-
}
|
|
839
|
-
// Use tree-sitter to find the use path at this position
|
|
840
|
-
const usePath = symbolIndex_1.symbolIndex.getUsePathAtPosition(docPath, document.getText(), position.line, Math.max(0, position.character - 1));
|
|
841
|
-
if (!usePath) {
|
|
842
|
-
return null;
|
|
843
|
-
}
|
|
844
|
-
// Ensure .ump extension
|
|
845
|
-
let fileRef = usePath;
|
|
846
|
-
if (!fileRef.endsWith(".ump")) {
|
|
847
|
-
fileRef += ".ump";
|
|
848
|
-
}
|
|
849
|
-
const baseDir = path.dirname(docPath);
|
|
850
|
-
const targetPath = path.isAbsolute(fileRef)
|
|
851
|
-
? fileRef
|
|
852
|
-
: path.join(baseDir, fileRef);
|
|
853
|
-
const uri = (0, url_1.pathToFileURL)(targetPath).toString();
|
|
854
|
-
return node_1.Location.create(uri, node_1.Range.create(node_1.Position.create(0, 0), node_1.Position.create(0, 0)));
|
|
855
|
-
}
|
|
856
|
-
function getCompletionPrefix(document, line, character) {
|
|
857
|
-
const lineText = document.getText(node_1.Range.create(node_1.Position.create(line, 0), node_1.Position.create(line, character)));
|
|
858
|
-
const match = lineText.match(/[A-Za-z_][A-Za-z0-9_]*$/);
|
|
859
|
-
return match ? match[0] : "";
|
|
860
|
-
}
|
|
861
|
-
/**
|
|
862
|
-
* Get the prefix for use-path completion (allows dots, slashes, underscores).
|
|
863
|
-
*/
|
|
864
|
-
function getUsePathPrefix(document, line, character) {
|
|
865
|
-
const lineText = document.getText(node_1.Range.create(node_1.Position.create(line, 0), node_1.Position.create(line, character)));
|
|
866
|
-
const match = lineText.match(/[A-Za-z_][A-Za-z0-9_.\/]*$/);
|
|
867
|
-
return match ? match[0] : "";
|
|
868
|
-
}
|
|
869
964
|
/**
|
|
870
|
-
*
|
|
871
|
-
* Used for go-to-definition symbol lookup.
|
|
965
|
+
* Map a SymbolKind to the appropriate LSP CompletionItemKind.
|
|
872
966
|
*/
|
|
873
|
-
function
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
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;
|
|
899
995
|
}
|
|
900
|
-
const lowerPrefix = prefix.toLowerCase();
|
|
901
|
-
return items.filter((item) => item.label.toLowerCase().startsWith(lowerPrefix));
|
|
902
|
-
}
|
|
903
|
-
/**
|
|
904
|
-
* Build completion items for a given tree-sitter based context.
|
|
905
|
-
* Combines context-specific keywords with symbol-based completions.
|
|
906
|
-
*/
|
|
907
|
-
function buildCompletionsForContext(context, prefix, reachableFiles) {
|
|
908
|
-
const items = [];
|
|
909
|
-
const seen = new Set();
|
|
910
|
-
// Helper: get symbols of a kind, filtered to reachable files
|
|
911
|
-
const getSymbols = (kind) => {
|
|
912
|
-
const all = symbolIndex_1.symbolIndex.getSymbolsByKind(kind);
|
|
913
|
-
if (!reachableFiles)
|
|
914
|
-
return all;
|
|
915
|
-
return all.filter((sym) => reachableFiles.has(path.normalize(sym.file)));
|
|
916
|
-
};
|
|
917
|
-
// Add context-specific keywords
|
|
918
|
-
const keywords = keywords_1.COMPLETION_KEYWORDS[context] ?? [];
|
|
919
|
-
for (const kw of keywords) {
|
|
920
|
-
if (!seen.has(`kw:${kw}`)) {
|
|
921
|
-
seen.add(`kw:${kw}`);
|
|
922
|
-
items.push({ label: kw, kind: node_1.CompletionItemKind.Keyword });
|
|
923
|
-
}
|
|
924
|
-
}
|
|
925
|
-
// Add symbol-based completions depending on context
|
|
926
|
-
if (symbolIndexReady) {
|
|
927
|
-
switch (context) {
|
|
928
|
-
case "top":
|
|
929
|
-
// No symbol completions at top level
|
|
930
|
-
break;
|
|
931
|
-
case "class_body": {
|
|
932
|
-
// Offer attribute modifiers and types
|
|
933
|
-
for (const mod of keywords_1.COMPLETION_KEYWORDS.attribute_modifiers) {
|
|
934
|
-
if (!seen.has(`kw:${mod}`)) {
|
|
935
|
-
seen.add(`kw:${mod}`);
|
|
936
|
-
items.push({ label: mod, kind: node_1.CompletionItemKind.Keyword });
|
|
937
|
-
}
|
|
938
|
-
}
|
|
939
|
-
for (const typ of keywords_1.COMPLETION_KEYWORDS.attribute_types) {
|
|
940
|
-
if (!seen.has(`type:${typ}`)) {
|
|
941
|
-
seen.add(`type:${typ}`);
|
|
942
|
-
items.push({
|
|
943
|
-
label: typ,
|
|
944
|
-
kind: node_1.CompletionItemKind.TypeParameter,
|
|
945
|
-
detail: "type",
|
|
946
|
-
});
|
|
947
|
-
}
|
|
948
|
-
}
|
|
949
|
-
// Offer class/interface/trait names (for isA, type references)
|
|
950
|
-
for (const sym of getSymbols("class")) {
|
|
951
|
-
if (!seen.has(`sym:${sym.name}`)) {
|
|
952
|
-
seen.add(`sym:${sym.name}`);
|
|
953
|
-
items.push({
|
|
954
|
-
label: sym.name,
|
|
955
|
-
kind: node_1.CompletionItemKind.Class,
|
|
956
|
-
detail: "class",
|
|
957
|
-
});
|
|
958
|
-
}
|
|
959
|
-
}
|
|
960
|
-
for (const sym of getSymbols("interface")) {
|
|
961
|
-
if (!seen.has(`sym:${sym.name}`)) {
|
|
962
|
-
seen.add(`sym:${sym.name}`);
|
|
963
|
-
items.push({
|
|
964
|
-
label: sym.name,
|
|
965
|
-
kind: node_1.CompletionItemKind.Interface,
|
|
966
|
-
detail: "interface",
|
|
967
|
-
});
|
|
968
|
-
}
|
|
969
|
-
}
|
|
970
|
-
for (const sym of getSymbols("trait")) {
|
|
971
|
-
if (!seen.has(`sym:${sym.name}`)) {
|
|
972
|
-
seen.add(`sym:${sym.name}`);
|
|
973
|
-
items.push({
|
|
974
|
-
label: sym.name,
|
|
975
|
-
kind: node_1.CompletionItemKind.Class,
|
|
976
|
-
detail: "trait",
|
|
977
|
-
});
|
|
978
|
-
}
|
|
979
|
-
}
|
|
980
|
-
break;
|
|
981
|
-
}
|
|
982
|
-
case "isa_type": {
|
|
983
|
-
// After "isA" keyword: only offer class/interface/trait names
|
|
984
|
-
for (const sym of getSymbols("class")) {
|
|
985
|
-
if (!seen.has(`sym:${sym.name}`)) {
|
|
986
|
-
seen.add(`sym:${sym.name}`);
|
|
987
|
-
items.push({
|
|
988
|
-
label: sym.name,
|
|
989
|
-
kind: node_1.CompletionItemKind.Class,
|
|
990
|
-
detail: "class",
|
|
991
|
-
});
|
|
992
|
-
}
|
|
993
|
-
}
|
|
994
|
-
for (const sym of getSymbols("interface")) {
|
|
995
|
-
if (!seen.has(`sym:${sym.name}`)) {
|
|
996
|
-
seen.add(`sym:${sym.name}`);
|
|
997
|
-
items.push({
|
|
998
|
-
label: sym.name,
|
|
999
|
-
kind: node_1.CompletionItemKind.Interface,
|
|
1000
|
-
detail: "interface",
|
|
1001
|
-
});
|
|
1002
|
-
}
|
|
1003
|
-
}
|
|
1004
|
-
for (const sym of getSymbols("trait")) {
|
|
1005
|
-
if (!seen.has(`sym:${sym.name}`)) {
|
|
1006
|
-
seen.add(`sym:${sym.name}`);
|
|
1007
|
-
items.push({
|
|
1008
|
-
label: sym.name,
|
|
1009
|
-
kind: node_1.CompletionItemKind.Class,
|
|
1010
|
-
detail: "trait",
|
|
1011
|
-
});
|
|
1012
|
-
}
|
|
1013
|
-
}
|
|
1014
|
-
break;
|
|
1015
|
-
}
|
|
1016
|
-
case "transition_target": {
|
|
1017
|
-
// After "->" in state: only offer state names
|
|
1018
|
-
for (const sym of getSymbols("state")) {
|
|
1019
|
-
if (!seen.has(`sym:${sym.name}`)) {
|
|
1020
|
-
seen.add(`sym:${sym.name}`);
|
|
1021
|
-
items.push({
|
|
1022
|
-
label: sym.name,
|
|
1023
|
-
kind: node_1.CompletionItemKind.EnumMember,
|
|
1024
|
-
detail: "state",
|
|
1025
|
-
});
|
|
1026
|
-
}
|
|
1027
|
-
}
|
|
1028
|
-
break;
|
|
1029
|
-
}
|
|
1030
|
-
case "association_type": {
|
|
1031
|
-
// Type position in association: only offer class names
|
|
1032
|
-
for (const sym of getSymbols("class")) {
|
|
1033
|
-
if (!seen.has(`sym:${sym.name}`)) {
|
|
1034
|
-
seen.add(`sym:${sym.name}`);
|
|
1035
|
-
items.push({
|
|
1036
|
-
label: sym.name,
|
|
1037
|
-
kind: node_1.CompletionItemKind.Class,
|
|
1038
|
-
detail: "class",
|
|
1039
|
-
});
|
|
1040
|
-
}
|
|
1041
|
-
}
|
|
1042
|
-
break;
|
|
1043
|
-
}
|
|
1044
|
-
case "state_machine":
|
|
1045
|
-
case "state": {
|
|
1046
|
-
// Offer state names from the index
|
|
1047
|
-
for (const sym of getSymbols("state")) {
|
|
1048
|
-
if (!seen.has(`sym:${sym.name}`)) {
|
|
1049
|
-
seen.add(`sym:${sym.name}`);
|
|
1050
|
-
items.push({
|
|
1051
|
-
label: sym.name,
|
|
1052
|
-
kind: node_1.CompletionItemKind.EnumMember,
|
|
1053
|
-
detail: "state",
|
|
1054
|
-
});
|
|
1055
|
-
}
|
|
1056
|
-
}
|
|
1057
|
-
break;
|
|
1058
|
-
}
|
|
1059
|
-
case "association": {
|
|
1060
|
-
// Offer class names for association endpoints
|
|
1061
|
-
for (const sym of getSymbols("class")) {
|
|
1062
|
-
if (!seen.has(`sym:${sym.name}`)) {
|
|
1063
|
-
seen.add(`sym:${sym.name}`);
|
|
1064
|
-
items.push({
|
|
1065
|
-
label: sym.name,
|
|
1066
|
-
kind: node_1.CompletionItemKind.Class,
|
|
1067
|
-
detail: "class",
|
|
1068
|
-
});
|
|
1069
|
-
}
|
|
1070
|
-
}
|
|
1071
|
-
break;
|
|
1072
|
-
}
|
|
1073
|
-
// depend_package, enum, method, comment, unknown: no additional symbol completions
|
|
1074
|
-
}
|
|
1075
|
-
}
|
|
1076
|
-
return filterCompletions(items, prefix);
|
|
1077
996
|
}
|
|
1078
997
|
function getUseFileCompletions(document, prefix, line, character) {
|
|
1079
998
|
const docDir = getDocumentDirectory(document);
|
|
1080
999
|
if (!docDir) {
|
|
1081
1000
|
return [];
|
|
1082
1001
|
}
|
|
1083
|
-
|
|
1084
|
-
|
|
1002
|
+
// Split prefix into directory part and filename filter
|
|
1003
|
+
const lastSlash = prefix.lastIndexOf("/");
|
|
1004
|
+
const dirPart = lastSlash >= 0 ? prefix.substring(0, lastSlash + 1) : "";
|
|
1005
|
+
const filePart = lastSlash >= 0 ? prefix.substring(lastSlash + 1) : prefix;
|
|
1006
|
+
// Resolve target directory
|
|
1007
|
+
const targetDir = path.resolve(docDir, dirPart);
|
|
1008
|
+
let entries;
|
|
1085
1009
|
try {
|
|
1086
|
-
|
|
1087
|
-
.readdirSync(docDir)
|
|
1088
|
-
.filter((f) => f.endsWith(".ump") && f !== docBasename);
|
|
1010
|
+
entries = fs.readdirSync(targetDir, { withFileTypes: true });
|
|
1089
1011
|
}
|
|
1090
1012
|
catch {
|
|
1091
1013
|
return [];
|
|
1092
1014
|
}
|
|
1093
|
-
|
|
1094
|
-
const
|
|
1095
|
-
const
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1015
|
+
const docBasename = path.basename(getDocumentFilePath(document) ?? "");
|
|
1016
|
+
const isSameDir = path.normalize(targetDir) === path.normalize(docDir);
|
|
1017
|
+
const lowerFilter = filePart.toLowerCase();
|
|
1018
|
+
// Replace range covers only the filePart (after the last '/').
|
|
1019
|
+
// The dirPart is already in the document and stays untouched.
|
|
1020
|
+
// This ensures VS Code filters items against the filePart, not the full prefix.
|
|
1021
|
+
const replaceRange = node_1.Range.create(node_1.Position.create(line, character - filePart.length), node_1.Position.create(line, character));
|
|
1022
|
+
const items = [];
|
|
1023
|
+
for (const entry of entries) {
|
|
1024
|
+
const name = entry.name;
|
|
1025
|
+
// Skip hidden files/dirs
|
|
1026
|
+
if (name.startsWith("."))
|
|
1027
|
+
continue;
|
|
1028
|
+
if (entry.isDirectory()) {
|
|
1029
|
+
const label = name + "/";
|
|
1030
|
+
if (!label.toLowerCase().startsWith(lowerFilter))
|
|
1031
|
+
continue;
|
|
1032
|
+
items.push({
|
|
1033
|
+
label,
|
|
1034
|
+
kind: node_1.CompletionItemKind.Folder,
|
|
1035
|
+
detail: "Directory",
|
|
1036
|
+
textEdit: { range: replaceRange, newText: label },
|
|
1037
|
+
// Re-trigger completions after inserting folder name
|
|
1038
|
+
command: {
|
|
1039
|
+
title: "Continue completion",
|
|
1040
|
+
command: "editor.action.triggerSuggest",
|
|
1041
|
+
},
|
|
1042
|
+
});
|
|
1043
|
+
}
|
|
1044
|
+
else if (name.endsWith(".ump")) {
|
|
1045
|
+
// Skip current file if listing the same directory
|
|
1046
|
+
if (isSameDir && name === docBasename)
|
|
1047
|
+
continue;
|
|
1048
|
+
if (!name.toLowerCase().startsWith(lowerFilter))
|
|
1049
|
+
continue;
|
|
1050
|
+
items.push({
|
|
1051
|
+
label: name,
|
|
1052
|
+
kind: node_1.CompletionItemKind.File,
|
|
1053
|
+
detail: "Umple file",
|
|
1054
|
+
textEdit: { range: replaceRange, newText: name },
|
|
1055
|
+
});
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
return items;
|
|
1104
1059
|
}
|
|
1105
1060
|
function extractJson(text) {
|
|
1106
1061
|
const start = text.indexOf("{");
|