@reqlan/language 1.7.0 → 1.8.2
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/README.md +18 -0
- package/out/generated/ast.d.ts +28 -1
- package/out/generated/ast.js +27 -6
- package/out/generated/ast.js.map +1 -1
- package/out/generated/grammar.js +1 -1
- package/out/generated/grammar.js.map +1 -1
- package/out/index.d.ts +1 -0
- package/out/index.js +1 -0
- package/out/index.js.map +1 -1
- package/out/reqlan-code-action-provider.d.ts +15 -12
- package/out/reqlan-code-action-provider.js +55 -20
- package/out/reqlan-code-action-provider.js.map +1 -1
- package/out/reqlan-comment-resolver.js +5 -0
- package/out/reqlan-comment-resolver.js.map +1 -1
- package/out/reqlan-completion-provider.d.ts +10 -0
- package/out/reqlan-completion-provider.js +63 -29
- package/out/reqlan-completion-provider.js.map +1 -1
- package/out/reqlan-definition-provider.d.ts +7 -0
- package/out/reqlan-definition-provider.js +86 -19
- package/out/reqlan-definition-provider.js.map +1 -1
- package/out/reqlan-document-link-provider.d.ts +2 -1
- package/out/reqlan-document-link-provider.js +4 -0
- package/out/reqlan-document-link-provider.js.map +1 -1
- package/out/reqlan-file-link-resolver.d.ts +17 -4
- package/out/reqlan-file-link-resolver.js +89 -14
- package/out/reqlan-file-link-resolver.js.map +1 -1
- package/out/reqlan-import-edits.d.ts +3 -1
- package/out/reqlan-import-edits.js.map +1 -1
- package/out/reqlan-inbound-reference-inlay-label.d.ts +11 -0
- package/out/reqlan-inbound-reference-inlay-label.js +60 -35
- package/out/reqlan-inbound-reference-inlay-label.js.map +1 -1
- package/out/reqlan-inlay-hint-provider.d.ts +6 -0
- package/out/reqlan-inlay-hint-provider.js +34 -6
- package/out/reqlan-inlay-hint-provider.js.map +1 -1
- package/out/reqlan-markdown-links.js +4 -0
- package/out/reqlan-markdown-links.js.map +1 -1
- package/out/reqlan-path-resolve.d.ts +8 -0
- package/out/reqlan-path-resolve.js +18 -4
- package/out/reqlan-path-resolve.js.map +1 -1
- package/out/reqlan-reference-search-site.d.ts +11 -0
- package/out/reqlan-reference-search-site.js +11 -0
- package/out/reqlan-reference-search-site.js.map +1 -1
- package/out/reqlan-scope.d.ts +11 -0
- package/out/reqlan-scope.js +28 -1
- package/out/reqlan-scope.js.map +1 -1
- package/out/reqlan-semantic-token-provider.js +6 -1
- package/out/reqlan-semantic-token-provider.js.map +1 -1
- package/out/reqlan-token-builder.d.ts +3 -2
- package/out/reqlan-token-builder.js +48 -1
- package/out/reqlan-token-builder.js.map +1 -1
- package/out/reqlan-validator.d.ts +1 -0
- package/out/reqlan-validator.js +21 -1
- package/out/reqlan-validator.js.map +1 -1
- package/out/reqlan-wildcard-resolve.d.ts +45 -0
- package/out/reqlan-wildcard-resolve.js +203 -0
- package/out/reqlan-wildcard-resolve.js.map +1 -0
- package/out/reqlan-workspace-manager.d.ts +7 -0
- package/out/reqlan-workspace-manager.js +7 -0
- package/out/reqlan-workspace-manager.js.map +1 -1
- package/package.json +1 -1
- package/src/generated/ast.ts +38 -7
- package/src/generated/grammar.ts +1 -1
- package/src/index.ts +1 -0
- package/src/reqlan-code-action-provider.ts +67 -32
- package/src/reqlan-comment-resolver.ts +5 -0
- package/src/reqlan-completion-provider.ts +81 -27
- package/src/reqlan-definition-provider.ts +91 -18
- package/src/reqlan-document-link-provider.ts +9 -1
- package/src/reqlan-file-link-resolver.ts +112 -17
- package/src/reqlan-import-edits.ts +3 -1
- package/src/reqlan-inbound-reference-inlay-label.ts +93 -38
- package/src/reqlan-inlay-hint-provider.ts +36 -6
- package/src/reqlan-markdown-links.ts +4 -0
- package/src/reqlan-path-resolve.ts +28 -5
- package/src/reqlan-reference-search-site.ts +25 -0
- package/src/reqlan-scope.ts +34 -1
- package/src/reqlan-semantic-token-provider.ts +6 -0
- package/src/reqlan-token-builder.ts +52 -3
- package/src/reqlan-validator.ts +34 -0
- package/src/reqlan-wildcard-resolve.ts +281 -0
- package/src/reqlan-workspace-manager.ts +9 -0
- package/src/reqlan.langium +14 -7
|
@@ -6,10 +6,12 @@
|
|
|
6
6
|
* rq:["../../../reqlan rq/extension/features-syntax-highlighting.rq".reference_code_completion_sequencing]
|
|
7
7
|
* rq:["../../../reqlan rq/extension/language-support/features-imports.rq".import_code_completion]
|
|
8
8
|
* rq:["../../../reqlan rq/extension/language-support/features-imports.rq".import_code_completion_ranking]
|
|
9
|
+
* rq:["../../../reqlan rq/extension/language-support/features-imports.rq".import_code_completion_rendering]
|
|
10
|
+
* rq:["../../../reqlan rq/extension/language-support/features-imports.rq".import_code_completion_auto_file_import]
|
|
9
11
|
* rq:["../../../reqlan rq/extension/language-support/features-imports.rq".anonymous_reference_code_completion]
|
|
10
12
|
*/
|
|
11
13
|
import type { AstNode, AstNodeDescription, FileSystemProvider, LangiumDocument, LangiumDocuments } from 'langium';
|
|
12
|
-
import { AstUtils, stream } from 'langium';
|
|
14
|
+
import { AstUtils, UriUtils, stream } from 'langium';
|
|
13
15
|
import type { MaybePromise } from 'langium';
|
|
14
16
|
import {
|
|
15
17
|
DefaultCompletionProvider,
|
|
@@ -18,7 +20,7 @@ import {
|
|
|
18
20
|
type CompletionProviderOptions
|
|
19
21
|
} from 'langium/lsp';
|
|
20
22
|
import { reqlanStringDelimiter } from './reqlan-quoted-strings.js';
|
|
21
|
-
import type { CompletionList, CompletionParams } from 'vscode-languageserver';
|
|
23
|
+
import type { CompletionItem, CompletionList, CompletionParams } from 'vscode-languageserver';
|
|
22
24
|
import { CompletionItemKind, CompletionList as LspCompletionList } from 'vscode-languageserver';
|
|
23
25
|
import {
|
|
24
26
|
isAttribute,
|
|
@@ -39,6 +41,7 @@ import {
|
|
|
39
41
|
getCompletionSite,
|
|
40
42
|
getReferencePrefixContext
|
|
41
43
|
} from './reqlan-completion-context.js';
|
|
44
|
+
import { buildFromImportEdit, relativeRqImportPath } from './reqlan-import-edits.js';
|
|
42
45
|
import {
|
|
43
46
|
collectPathCompletionCandidates,
|
|
44
47
|
comparePathCompletionCandidates,
|
|
@@ -51,6 +54,12 @@ import { referenceIdea } from './reqlan-references.js';
|
|
|
51
54
|
import { collectWorkspaceAttributeCatalog } from './reqlan-workspace-attribute-catalog.js';
|
|
52
55
|
import type { ReqlanServices } from './reqlan-module.js';
|
|
53
56
|
|
|
57
|
+
type ReferenceCompletionCandidate = {
|
|
58
|
+
name: string;
|
|
59
|
+
document: LangiumDocument;
|
|
60
|
+
importable: boolean;
|
|
61
|
+
};
|
|
62
|
+
|
|
54
63
|
const UNREACHABLE_DISTANCE = 9999;
|
|
55
64
|
|
|
56
65
|
export class ReqlanCompletionProvider extends DefaultCompletionProvider {
|
|
@@ -194,47 +203,92 @@ export class ReqlanCompletionProvider extends DefaultCompletionProvider {
|
|
|
194
203
|
if (!context) {
|
|
195
204
|
return LspCompletionList.create([], true);
|
|
196
205
|
}
|
|
197
|
-
const names = new Set<string>();
|
|
198
206
|
const docs = this.documents.all.toArray();
|
|
199
|
-
if (!docs.some(doc => doc.uri
|
|
207
|
+
if (!docs.some(doc => UriUtils.equals(doc.uri, document.uri))) {
|
|
200
208
|
docs.push(document);
|
|
201
209
|
}
|
|
210
|
+
const candidates = this.collectReferenceCompletionCandidates(docs, context.prefix);
|
|
211
|
+
const center = findContainingIdea(document, params.position)?.name;
|
|
212
|
+
const distances = center
|
|
213
|
+
? hopDistancesFromCenter(center, buildIdeaReferenceAdjacency(docs))
|
|
214
|
+
: new Map<string, number>();
|
|
215
|
+
const items = candidates.map(candidate =>
|
|
216
|
+
this.toReferenceCompletionItem(document, candidate, context, distances)
|
|
217
|
+
).sort((left, right) => {
|
|
218
|
+
const leftDistance = distances.get(String(left.label)) ?? UNREACHABLE_DISTANCE;
|
|
219
|
+
const rightDistance = distances.get(String(right.label)) ?? UNREACHABLE_DISTANCE;
|
|
220
|
+
return leftDistance - rightDistance
|
|
221
|
+
|| String(left.label).localeCompare(String(right.label))
|
|
222
|
+
|| String(left.detail ?? '').localeCompare(String(right.detail ?? ''));
|
|
223
|
+
});
|
|
224
|
+
return LspCompletionList.create(items, true);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/** Collect named ideas/ideasets across loaded documents for bracket/wikilink completion. */
|
|
228
|
+
private collectReferenceCompletionCandidates(
|
|
229
|
+
docs: LangiumDocument[],
|
|
230
|
+
prefix: string
|
|
231
|
+
): ReferenceCompletionCandidate[] {
|
|
232
|
+
const byKey = new Map<string, ReferenceCompletionCandidate>();
|
|
202
233
|
for (const doc of docs) {
|
|
203
234
|
const model = doc.parseResult.value;
|
|
204
235
|
if (!isModel(model)) {
|
|
205
236
|
continue;
|
|
206
237
|
}
|
|
207
238
|
for (const element of model.elements) {
|
|
208
|
-
|
|
209
|
-
|
|
239
|
+
const importable = isIdea(element) || isOneLinerIdea(element);
|
|
240
|
+
if (!importable && !isIdeaSet(element)) {
|
|
241
|
+
continue;
|
|
242
|
+
}
|
|
243
|
+
if (prefix && !element.name.startsWith(prefix)) {
|
|
244
|
+
continue;
|
|
245
|
+
}
|
|
246
|
+
const key = `${doc.uri.toString()}::${element.name}`;
|
|
247
|
+
if (!byKey.has(key)) {
|
|
248
|
+
byKey.set(key, { name: element.name, document: doc, importable });
|
|
210
249
|
}
|
|
211
250
|
}
|
|
212
251
|
}
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
252
|
+
return [...byKey.values()];
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Cross-file idea completions show their import path and may insert a from-import.
|
|
257
|
+
* rq:["../../../reqlan rq/extension/language-support/features-imports.rq".import_code_completion_rendering]
|
|
258
|
+
* rq:["../../../reqlan rq/extension/language-support/features-imports.rq".import_code_completion_auto_file_import]
|
|
259
|
+
*/
|
|
260
|
+
private toReferenceCompletionItem(
|
|
261
|
+
document: LangiumDocument,
|
|
262
|
+
candidate: ReferenceCompletionCandidate,
|
|
263
|
+
context: NonNullable<ReturnType<typeof getReferencePrefixContext>>,
|
|
264
|
+
distances: Map<string, number>
|
|
265
|
+
): CompletionItem {
|
|
266
|
+
const sameFile = UriUtils.equals(candidate.document.uri, document.uri);
|
|
267
|
+
const distance = distances.get(candidate.name) ?? UNREACHABLE_DISTANCE;
|
|
268
|
+
const item: CompletionItem = {
|
|
269
|
+
label: candidate.name,
|
|
270
|
+
kind: CompletionItemKind.Reference,
|
|
271
|
+
sortText: `${String(distance).padStart(4, '0')}_${candidate.name}`,
|
|
272
|
+
textEdit: {
|
|
273
|
+
newText: candidate.name,
|
|
227
274
|
range: {
|
|
228
275
|
start: context.replaceStart,
|
|
229
276
|
end: context.replaceEnd
|
|
230
277
|
}
|
|
231
|
-
}
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
278
|
+
}
|
|
279
|
+
};
|
|
280
|
+
if (sameFile) {
|
|
281
|
+
return item;
|
|
282
|
+
}
|
|
283
|
+
const importPath = relativeRqImportPath(document.uri, candidate.document.uri);
|
|
284
|
+
item.detail = importPath;
|
|
285
|
+
if (candidate.importable) {
|
|
286
|
+
const importEdit = buildFromImportEdit(document, importPath, candidate.name);
|
|
287
|
+
if (importEdit) {
|
|
288
|
+
item.additionalTextEdits = [importEdit];
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
return item;
|
|
238
292
|
}
|
|
239
293
|
|
|
240
294
|
private completeAnonymousImportPath(document: LangiumDocument, params: CompletionParams): CompletionList {
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Go-to-definition for imports, file references, symbols, and embedded comment references.
|
|
3
3
|
*/
|
|
4
4
|
import type { CstNode, FileSystemProvider, LangiumDocument, MaybePromise } from 'langium';
|
|
5
|
-
import {
|
|
5
|
+
import { CstUtils, GrammarUtils, URI } from 'langium';
|
|
6
6
|
import { DefaultDefinitionProvider, type GoToLink } from 'langium/lsp';
|
|
7
7
|
import type { DefinitionParams, Position } from 'vscode-languageserver';
|
|
8
8
|
import { LocationLink } from 'vscode-languageserver';
|
|
@@ -33,6 +33,7 @@ import {
|
|
|
33
33
|
isInvalidFromImport,
|
|
34
34
|
isLocalReference,
|
|
35
35
|
isQualifiedReference,
|
|
36
|
+
isWildcardReference,
|
|
36
37
|
type FileReference,
|
|
37
38
|
type FileSymbolReference,
|
|
38
39
|
type Import,
|
|
@@ -41,6 +42,7 @@ import {
|
|
|
41
42
|
} from './generated/ast.js';
|
|
42
43
|
import type { ReqlanServices } from './reqlan-module.js';
|
|
43
44
|
import { pathResolveContextFromServices } from './reqlan-path-resolve.js';
|
|
45
|
+
import { findWildcardReferenceAtPosition } from './reqlan-wildcard-resolve.js';
|
|
44
46
|
|
|
45
47
|
export class ReqlanDefinitionProvider extends DefaultDefinitionProvider {
|
|
46
48
|
|
|
@@ -60,9 +62,20 @@ export class ReqlanDefinitionProvider extends DefaultDefinitionProvider {
|
|
|
60
62
|
}
|
|
61
63
|
|
|
62
64
|
override getDefinition(document: LangiumDocument, params: DefinitionParams): MaybePromise<LocationLink[] | undefined> {
|
|
65
|
+
// Fast path: when the cursor is on an idea token inside a bracket/wiki reference the
|
|
66
|
+
// answer is just the cached Langium ref target — no text scans, no file-system calls.
|
|
67
|
+
// Same-file and cross-file idea refs both take this path, so neither is slower than the other.
|
|
68
|
+
if (this.isIdeaReferencePosition(document, params.position)) {
|
|
69
|
+
return super.getDefinition(document, params);
|
|
70
|
+
}
|
|
63
71
|
if (isMarkdownLinkLabelPosition(document, params.position)) {
|
|
64
72
|
return undefined;
|
|
65
73
|
}
|
|
74
|
+
// Wildcard refs open the activity-bar search pane via extension command (document link / middleware).
|
|
75
|
+
const offset = document.textDocument.offsetAt(params.position);
|
|
76
|
+
if (findWildcardReferenceAtPosition(document, offset)) {
|
|
77
|
+
return undefined;
|
|
78
|
+
}
|
|
66
79
|
const commentPart = findCommentReferencePartAt(document, params.position);
|
|
67
80
|
if (commentPart?.property === 'idea') {
|
|
68
81
|
return this.createCommentIdeaReferenceLinks(document, commentPart.reference);
|
|
@@ -75,7 +88,11 @@ export class ReqlanDefinitionProvider extends DefaultDefinitionProvider {
|
|
|
75
88
|
this.pathContext()
|
|
76
89
|
);
|
|
77
90
|
if (fileReference) {
|
|
78
|
-
if (
|
|
91
|
+
if (
|
|
92
|
+
fileReference.resolution === 'folder'
|
|
93
|
+
|| fileReference.resolution === 'missing'
|
|
94
|
+
|| fileReference.resolution === 'wildcard'
|
|
95
|
+
) {
|
|
79
96
|
return undefined;
|
|
80
97
|
}
|
|
81
98
|
return [this.resolvedFileLinkToLocationLink(fileReference)];
|
|
@@ -95,6 +112,56 @@ export class ReqlanDefinitionProvider extends DefaultDefinitionProvider {
|
|
|
95
112
|
return super.getDefinition(document, params);
|
|
96
113
|
}
|
|
97
114
|
|
|
115
|
+
/**
|
|
116
|
+
* Returns true when the cursor sits on an idea/ideaset name token inside a bracket or
|
|
117
|
+
* wikilink reference (LocalReference or QualifiedReference) but NOT on the path/qualifier
|
|
118
|
+
* token that points to an import. This lets us skip all text-scan pre-checks and jump
|
|
119
|
+
* straight to the cached Langium ref resolution.
|
|
120
|
+
*/
|
|
121
|
+
private isIdeaReferencePosition(document: LangiumDocument, position: Position): boolean {
|
|
122
|
+
const root = document.parseResult.value.$cstNode;
|
|
123
|
+
if (!root) {
|
|
124
|
+
return false;
|
|
125
|
+
}
|
|
126
|
+
const offset = document.textDocument.offsetAt(position);
|
|
127
|
+
let current: CstNode | undefined = CstUtils.findLeafNodeAtOffset(root, offset);
|
|
128
|
+
while (current) {
|
|
129
|
+
const node = current.astNode;
|
|
130
|
+
if (isLocalReference(node)) {
|
|
131
|
+
// A local ref whose idea is already resolved → fast O(1) path.
|
|
132
|
+
// Unresolved ones still go through the normal flow so file-link checks can try.
|
|
133
|
+
return node.idea?.ref !== undefined;
|
|
134
|
+
}
|
|
135
|
+
if (isQualifiedReference(node)) {
|
|
136
|
+
const assignment = GrammarUtils.findAssignment(current);
|
|
137
|
+
if (!assignment) {
|
|
138
|
+
current = current.container;
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
// On the path/qualifier token → defer to import-path checks.
|
|
142
|
+
if (assignment.feature === 'path' || assignment.feature === 'qualifier') {
|
|
143
|
+
return false;
|
|
144
|
+
}
|
|
145
|
+
// On the idea or ideaset token → O(1) cached ref lookup.
|
|
146
|
+
if (assignment.feature === 'idea' || assignment.feature === 'ideaset') {
|
|
147
|
+
return node.idea?.ref !== undefined || node.ideaset?.ref !== undefined;
|
|
148
|
+
}
|
|
149
|
+
return false;
|
|
150
|
+
}
|
|
151
|
+
// FileReference / FileSymbolReference / Import / WildcardReference → not idea refs, stop.
|
|
152
|
+
if (
|
|
153
|
+
isFileReference(node)
|
|
154
|
+
|| isFileSymbolReference(node)
|
|
155
|
+
|| isImport(node)
|
|
156
|
+
|| isWildcardReference(node)
|
|
157
|
+
) {
|
|
158
|
+
return false;
|
|
159
|
+
}
|
|
160
|
+
current = current.container;
|
|
161
|
+
}
|
|
162
|
+
return false;
|
|
163
|
+
}
|
|
164
|
+
|
|
98
165
|
protected override findLinks(source: CstNode): GoToLink[] {
|
|
99
166
|
const fileLink = this.linkForFileReference(source) ?? this.linkForNamespaceImportReference(source);
|
|
100
167
|
if (fileLink) {
|
|
@@ -229,24 +296,30 @@ export class ReqlanDefinitionProvider extends DefaultDefinitionProvider {
|
|
|
229
296
|
}
|
|
230
297
|
|
|
231
298
|
private findAnonymousImportPathLinkAtPosition(document: LangiumDocument, position: Position): GoToLink | undefined {
|
|
299
|
+
const root = document.parseResult.value.$cstNode;
|
|
300
|
+
if (!root) {
|
|
301
|
+
return undefined;
|
|
302
|
+
}
|
|
232
303
|
const offset = document.textDocument.offsetAt(position);
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
304
|
+
// Walk up from the leaf node — O(depth) rather than O(all AST nodes).
|
|
305
|
+
let current: CstNode | undefined = CstUtils.findLeafNodeAtOffset(root, offset);
|
|
306
|
+
while (current) {
|
|
307
|
+
const node = current.astNode;
|
|
308
|
+
if (isQualifiedReference(node) && !node.path?.ref) {
|
|
309
|
+
const pathNode = node.path?.$refNode ?? GrammarUtils.findNodeForProperty(node.$cstNode, 'path');
|
|
310
|
+
if (pathNode && offset >= pathNode.offset && offset < pathNode.end) {
|
|
311
|
+
const resolved = resolveQualifiedReferencePathLink(node, this.documents, this.pathContext());
|
|
312
|
+
if (!resolved) {
|
|
313
|
+
return undefined;
|
|
314
|
+
}
|
|
315
|
+
const targetDocument = this.documents.getDocument(URI.parse(resolved.targetUri.split('#')[0]));
|
|
316
|
+
if (!targetDocument) {
|
|
317
|
+
return undefined;
|
|
318
|
+
}
|
|
319
|
+
return resolvedFileLinkToGoToTarget(resolved, pathNode, targetDocument);
|
|
320
|
+
}
|
|
248
321
|
}
|
|
249
|
-
|
|
322
|
+
current = current.container;
|
|
250
323
|
}
|
|
251
324
|
return undefined;
|
|
252
325
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Clickable document links for file references and import paths.
|
|
2
|
+
* Clickable document links for idea references, file references, and import paths.
|
|
3
|
+
* Same-file and cross-file idea refs both get links so the editor underline is consistent.
|
|
3
4
|
*/
|
|
4
5
|
import type { LangiumDocument } from 'langium';
|
|
5
6
|
import type { DocumentLinkProvider } from 'langium/lsp';
|
|
@@ -7,6 +8,7 @@ import type { DocumentLink, DocumentLinkParams } from 'vscode-languageserver';
|
|
|
7
8
|
import { DocumentLink as LspDocumentLink } from 'vscode-languageserver';
|
|
8
9
|
import { collectFileLinks, resolvedFileLinkTargetUri } from './reqlan-file-link-resolver.js';
|
|
9
10
|
import { folderReferenceCommandTarget } from './reqlan-reference-at-position.js';
|
|
11
|
+
import { wildcardReferenceCommandTarget } from './reqlan-wildcard-resolve.js';
|
|
10
12
|
import type { ReqlanServices } from './reqlan-module.js';
|
|
11
13
|
import { pathResolveContextFromServices } from './reqlan-path-resolve.js';
|
|
12
14
|
|
|
@@ -35,6 +37,12 @@ export class ReqlanDocumentLinkProvider implements DocumentLinkProvider {
|
|
|
35
37
|
folderReferenceCommandTarget(link.targetUri)
|
|
36
38
|
)];
|
|
37
39
|
}
|
|
40
|
+
if (link.resolution === 'wildcard' && link.wildcardArgs) {
|
|
41
|
+
return [LspDocumentLink.create(
|
|
42
|
+
link.sourceRange,
|
|
43
|
+
wildcardReferenceCommandTarget(link.wildcardArgs)
|
|
44
|
+
)];
|
|
45
|
+
}
|
|
38
46
|
const target = resolvedFileLinkTargetUri(link);
|
|
39
47
|
if (!target) {
|
|
40
48
|
return [];
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Resolves file references and import paths to target URIs and editor ranges.
|
|
2
|
+
* Resolves file references, idea references, and import paths to target URIs and editor ranges.
|
|
3
|
+
* Document links use these so same-file and cross-file refs share the same underline/click affordance.
|
|
3
4
|
*/
|
|
4
|
-
import type { CstNode, FileSystemProvider, LangiumDocument, LangiumDocuments, URI } from 'langium';
|
|
5
|
+
import type { AstNode, CstNode, FileSystemProvider, LangiumDocument, LangiumDocuments, Reference, URI } from 'langium';
|
|
5
6
|
import { AstUtils, CstUtils, GrammarUtils } from 'langium';
|
|
6
7
|
import type { Range } from 'vscode-languageserver';
|
|
7
8
|
import { resolveFileUri } from './reqlan-comment-resolver.js';
|
|
@@ -23,11 +24,14 @@ import {
|
|
|
23
24
|
isLocalReference,
|
|
24
25
|
isMarkdownLink,
|
|
25
26
|
isQualifiedReference,
|
|
27
|
+
isWildcardReference,
|
|
26
28
|
type FileReference,
|
|
27
29
|
type FileSymbolReference,
|
|
28
30
|
type Import,
|
|
31
|
+
type LocalReference,
|
|
29
32
|
type MarkdownLink,
|
|
30
|
-
type QualifiedReference
|
|
33
|
+
type QualifiedReference,
|
|
34
|
+
type WildcardReference
|
|
31
35
|
} from './generated/ast.js';
|
|
32
36
|
import { parseMarkdownLink } from './reqlan-references.js';
|
|
33
37
|
import { reqlanStringDelimiter } from './reqlan-quoted-strings.js';
|
|
@@ -35,8 +39,14 @@ import {
|
|
|
35
39
|
isNamespaceImportOnlyReference,
|
|
36
40
|
resolveNamespaceImportReferenceLink
|
|
37
41
|
} from './reqlan-namespace-import-links.js';
|
|
42
|
+
import {
|
|
43
|
+
resolveWildcardReferenceMatches,
|
|
44
|
+
wildcardArgsFromReference,
|
|
45
|
+
type WildcardMatch,
|
|
46
|
+
type WildcardReferenceArgs
|
|
47
|
+
} from './reqlan-wildcard-resolve.js';
|
|
38
48
|
|
|
39
|
-
export type ReferenceResolution = 'file' | 'folder' | 'missing';
|
|
49
|
+
export type ReferenceResolution = 'file' | 'folder' | 'missing' | 'wildcard';
|
|
40
50
|
|
|
41
51
|
export type FileLinkTargetIssue = 'empty' | 'parse-error';
|
|
42
52
|
|
|
@@ -47,6 +57,9 @@ export interface ResolvedFileLink {
|
|
|
47
57
|
resolution?: ReferenceResolution;
|
|
48
58
|
folderFiles?: string[];
|
|
49
59
|
targetIssue?: FileLinkTargetIssue;
|
|
60
|
+
/** Present when resolution is `wildcard`. */
|
|
61
|
+
wildcardArgs?: WildcardReferenceArgs;
|
|
62
|
+
wildcardMatches?: WildcardMatch[];
|
|
50
63
|
}
|
|
51
64
|
|
|
52
65
|
function withFileSystem(
|
|
@@ -368,6 +381,80 @@ function fileStartRange(): Range {
|
|
|
368
381
|
};
|
|
369
382
|
}
|
|
370
383
|
|
|
384
|
+
/**
|
|
385
|
+
* Document link for a resolved Langium cross-reference (same-file or imported idea/ideaset).
|
|
386
|
+
* Source range is the reference name; target is the declaration name (or whole node).
|
|
387
|
+
*/
|
|
388
|
+
export function resolveLinkedAstReferenceLink(reference: Reference<AstNode> | undefined): ResolvedFileLink | undefined {
|
|
389
|
+
const target = reference?.ref;
|
|
390
|
+
const sourceNode = reference?.$refNode;
|
|
391
|
+
if (!target || !sourceNode) {
|
|
392
|
+
return undefined;
|
|
393
|
+
}
|
|
394
|
+
const targetDocument = AstUtils.getDocument(target);
|
|
395
|
+
const nameNode = GrammarUtils.findNodeForProperty(target.$cstNode, 'name') ?? target.$cstNode;
|
|
396
|
+
if (!nameNode) {
|
|
397
|
+
return undefined;
|
|
398
|
+
}
|
|
399
|
+
return {
|
|
400
|
+
sourceRange: sourceNode.range,
|
|
401
|
+
targetUri: targetDocument.textDocument.uri,
|
|
402
|
+
targetRange: nameNode.range,
|
|
403
|
+
resolution: 'file'
|
|
404
|
+
};
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
/** Idea/ideaset name links inside bracket and qualified references (not namespace-file aliases). */
|
|
408
|
+
export function resolveIdeaReferenceLinks(
|
|
409
|
+
reference: LocalReference | QualifiedReference
|
|
410
|
+
): ResolvedFileLink[] {
|
|
411
|
+
if (isLocalReference(reference)) {
|
|
412
|
+
if (isNamespaceImportOnlyReference(reference)) {
|
|
413
|
+
return [];
|
|
414
|
+
}
|
|
415
|
+
const link = resolveLinkedAstReferenceLink(reference.idea);
|
|
416
|
+
return link ? [link] : [];
|
|
417
|
+
}
|
|
418
|
+
const links: ResolvedFileLink[] = [];
|
|
419
|
+
const ideasetLink = resolveLinkedAstReferenceLink(reference.ideaset);
|
|
420
|
+
if (ideasetLink) {
|
|
421
|
+
links.push(ideasetLink);
|
|
422
|
+
}
|
|
423
|
+
const ideaLink = resolveLinkedAstReferenceLink(reference.idea);
|
|
424
|
+
if (ideaLink) {
|
|
425
|
+
links.push(ideaLink);
|
|
426
|
+
}
|
|
427
|
+
return links;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
export function resolveWildcardReferenceLink(
|
|
431
|
+
reference: WildcardReference,
|
|
432
|
+
documents: LangiumDocuments,
|
|
433
|
+
context?: PathResolveContext
|
|
434
|
+
): ResolvedFileLink | undefined {
|
|
435
|
+
const pathNode = GrammarUtils.findNodeForProperty(reference.$cstNode, 'pathPattern');
|
|
436
|
+
const ideaNode = GrammarUtils.findNodeForProperty(reference.$cstNode, 'ideaPattern');
|
|
437
|
+
const sourceRange = reference.$cstNode?.range
|
|
438
|
+
?? (pathNode && ideaNode
|
|
439
|
+
? {
|
|
440
|
+
start: pathNode.range.start,
|
|
441
|
+
end: ideaNode.range.end
|
|
442
|
+
}
|
|
443
|
+
: pathNode?.range);
|
|
444
|
+
if (!sourceRange) {
|
|
445
|
+
return undefined;
|
|
446
|
+
}
|
|
447
|
+
const matches = resolveWildcardReferenceMatches(reference, documents, context);
|
|
448
|
+
const args = wildcardArgsFromReference(reference);
|
|
449
|
+
return {
|
|
450
|
+
sourceRange,
|
|
451
|
+
targetUri: AstUtils.getDocument(reference).uri.toString(),
|
|
452
|
+
resolution: 'wildcard',
|
|
453
|
+
wildcardArgs: args,
|
|
454
|
+
wildcardMatches: matches
|
|
455
|
+
};
|
|
456
|
+
}
|
|
457
|
+
|
|
371
458
|
export function collectFileLinks(
|
|
372
459
|
document: LangiumDocument,
|
|
373
460
|
documents: LangiumDocuments,
|
|
@@ -377,40 +464,49 @@ export function collectFileLinks(
|
|
|
377
464
|
const pathContext = withFileSystem(fileSystem, context);
|
|
378
465
|
const links: ResolvedFileLink[] = [];
|
|
379
466
|
const linkedRanges: string[] = [];
|
|
467
|
+
const pushLink = (link: ResolvedFileLink): void => {
|
|
468
|
+
links.push(link);
|
|
469
|
+
linkedRanges.push(rangeKey(link.sourceRange));
|
|
470
|
+
};
|
|
380
471
|
for (const node of AstUtils.streamAst(document.parseResult.value)) {
|
|
381
472
|
if (isFileReference(node) || isFileSymbolReference(node)) {
|
|
382
473
|
const link = resolveFileReferenceLink(node, documents, fileSystem, pathContext);
|
|
383
474
|
if (link) {
|
|
384
|
-
|
|
385
|
-
|
|
475
|
+
pushLink(link);
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
if (isWildcardReference(node)) {
|
|
479
|
+
const link = resolveWildcardReferenceLink(node, documents, pathContext);
|
|
480
|
+
if (link) {
|
|
481
|
+
pushLink(link);
|
|
386
482
|
}
|
|
387
483
|
}
|
|
388
484
|
if ((isLocalReference(node) || isQualifiedReference(node)) && isNamespaceImportOnlyReference(node)) {
|
|
389
485
|
const link = resolveNamespaceImportReferenceLink(node, documents, fileSystem, pathContext);
|
|
390
486
|
if (link) {
|
|
391
|
-
|
|
392
|
-
|
|
487
|
+
pushLink(link);
|
|
488
|
+
}
|
|
489
|
+
} else if (isLocalReference(node) || isQualifiedReference(node)) {
|
|
490
|
+
for (const link of resolveIdeaReferenceLinks(node)) {
|
|
491
|
+
pushLink(link);
|
|
393
492
|
}
|
|
394
493
|
}
|
|
395
494
|
if (isImport(node)) {
|
|
396
495
|
const link = resolveImportPathLink(node, documents, pathContext);
|
|
397
496
|
if (link) {
|
|
398
|
-
|
|
399
|
-
linkedRanges.push(rangeKey(link.sourceRange));
|
|
497
|
+
pushLink(link);
|
|
400
498
|
}
|
|
401
499
|
}
|
|
402
500
|
if (isQualifiedReference(node) && node.path && !node.path.ref) {
|
|
403
501
|
const link = resolveQualifiedReferencePathLink(node, documents, pathContext);
|
|
404
502
|
if (link) {
|
|
405
|
-
|
|
406
|
-
linkedRanges.push(rangeKey(link.sourceRange));
|
|
503
|
+
pushLink(link);
|
|
407
504
|
}
|
|
408
505
|
}
|
|
409
506
|
if (isMarkdownLink(node)) {
|
|
410
507
|
const link = resolveMarkdownLinkTargetLink(node, document, documents, fileSystem, pathContext);
|
|
411
508
|
if (link) {
|
|
412
|
-
|
|
413
|
-
linkedRanges.push(rangeKey(link.sourceRange));
|
|
509
|
+
pushLink(link);
|
|
414
510
|
}
|
|
415
511
|
}
|
|
416
512
|
}
|
|
@@ -421,8 +517,7 @@ export function collectFileLinks(
|
|
|
421
517
|
}
|
|
422
518
|
const link = resolveEmbeddedFileReferenceLink(reference, document, documents, fileSystem, pathContext);
|
|
423
519
|
if (link) {
|
|
424
|
-
|
|
425
|
-
linkedRanges.push(key);
|
|
520
|
+
pushLink(link);
|
|
426
521
|
}
|
|
427
522
|
}
|
|
428
523
|
return links;
|
|
@@ -440,7 +535,7 @@ function rangesOverlap(left: Range, right: Range): boolean {
|
|
|
440
535
|
}
|
|
441
536
|
|
|
442
537
|
export function resolvedFileLinkTargetUri(link: ResolvedFileLink): string | undefined {
|
|
443
|
-
if (link.resolution === 'folder' || link.resolution === 'missing') {
|
|
538
|
+
if (link.resolution === 'folder' || link.resolution === 'missing' || link.resolution === 'wildcard') {
|
|
444
539
|
return undefined;
|
|
445
540
|
}
|
|
446
541
|
if (link.targetRange) {
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Shared WorkspaceEdit helpers for import insertion used by unresolved-reference quick fixes
|
|
2
|
+
* Shared WorkspaceEdit helpers for import insertion used by unresolved-reference quick fixes
|
|
3
|
+
* and cross-file reference completion auto-import.
|
|
3
4
|
* rq:["../../../reqlan rq/extension/language-support/features-imports.rq".import_error]
|
|
5
|
+
* rq:["../../../reqlan rq/extension/language-support/features-imports.rq".import_code_completion_auto_file_import]
|
|
4
6
|
*/
|
|
5
7
|
import type { LangiumDocument, URI } from 'langium';
|
|
6
8
|
import { UriUtils } from 'langium';
|