@reqlan/language 1.6.2 → 1.8.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/README.md +18 -0
- package/out/generated/ast.d.ts +31 -4
- package/out/generated/ast.js +28 -7
- package/out/generated/ast.js.map +1 -1
- package/out/generated/grammar.js +2 -3318
- package/out/generated/grammar.js.map +1 -1
- package/out/generated/module.d.ts +2 -2
- package/out/generated/module.js +2 -2
- package/out/generated/module.js.map +1 -1
- package/out/index.d.ts +4 -0
- package/out/index.js +4 -0
- package/out/index.js.map +1 -1
- package/out/reqlan-async-parser.d.ts +37 -0
- package/out/reqlan-async-parser.js +117 -0
- package/out/reqlan-async-parser.js.map +1 -0
- 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 +64 -30
- 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-factory.d.ts +16 -0
- package/out/reqlan-document-factory.js +50 -0
- package/out/reqlan-document-factory.js.map +1 -0
- 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-linker.js +2 -2
- package/out/reqlan-linker.js.map +1 -1
- package/out/reqlan-markdown-links.js +4 -0
- package/out/reqlan-markdown-links.js.map +1 -1
- package/out/reqlan-module.d.ts +2 -1
- package/out/reqlan-module.js +12 -2
- package/out/reqlan-module.js.map +1 -1
- package/out/reqlan-parse-budget.d.ts +50 -0
- package/out/reqlan-parse-budget.js +89 -0
- package/out/reqlan-parse-budget.js.map +1 -0
- package/out/reqlan-parse-worker.d.ts +1 -0
- package/out/reqlan-parse-worker.js +34 -0
- package/out/reqlan-parse-worker.js.map +1 -0
- 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 +150 -26
- package/out/reqlan-token-builder.js.map +1 -1
- package/out/reqlan-validator.d.ts +10 -1
- package/out/reqlan-validator.js +36 -2
- 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 +18 -0
- package/out/reqlan-workspace-manager.js +31 -0
- package/out/reqlan-workspace-manager.js.map +1 -0
- package/package.json +1 -1
- package/src/generated/ast.ts +41 -10
- package/src/generated/grammar.ts +2 -3318
- package/src/generated/module.ts +2 -2
- package/src/index.ts +4 -0
- package/src/reqlan-async-parser.ts +178 -0
- package/src/reqlan-code-action-provider.ts +67 -32
- package/src/reqlan-comment-resolver.ts +5 -0
- package/src/reqlan-completion-provider.ts +82 -28
- package/src/reqlan-definition-provider.ts +91 -18
- package/src/reqlan-document-factory.ts +75 -0
- 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-linker.ts +4 -2
- package/src/reqlan-markdown-links.ts +4 -0
- package/src/reqlan-module.ts +15 -3
- package/src/reqlan-parse-budget.ts +155 -0
- package/src/reqlan-parse-worker.ts +35 -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 +174 -28
- package/src/reqlan-validator.ts +56 -1
- package/src/reqlan-wildcard-resolve.ts +281 -0
- package/src/reqlan-workspace-manager.ts +47 -0
- package/src/reqlan.langium +17 -8
|
@@ -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
|
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Document factory that never lets a sync lex/parse throw escape into workspace init.
|
|
3
|
+
* Async path uses [ReqlanAsyncParser]: sync fast path by default, killable worker when escalated.
|
|
4
|
+
* rq:["../../../reqlan rq/language/parser_lexer.rq".parse_budget_timeout]
|
|
5
|
+
*/
|
|
6
|
+
import {
|
|
7
|
+
DefaultLangiumDocumentFactory,
|
|
8
|
+
type AstNode,
|
|
9
|
+
type LangiumDocument,
|
|
10
|
+
type LangiumSharedCoreServices,
|
|
11
|
+
type ParseResult,
|
|
12
|
+
type ParserOptions,
|
|
13
|
+
URI
|
|
14
|
+
} from 'langium';
|
|
15
|
+
import type { CancellationToken } from 'vscode-languageserver';
|
|
16
|
+
import { CancellationToken as CancelToken } from 'vscode-languageserver';
|
|
17
|
+
import { ReqlanAsyncParser } from './reqlan-async-parser.js';
|
|
18
|
+
import {
|
|
19
|
+
createIncompleteParseResult,
|
|
20
|
+
type ReqlanParseResult
|
|
21
|
+
} from './reqlan-parse-budget.js';
|
|
22
|
+
|
|
23
|
+
export class ReqlanLangiumDocumentFactory extends DefaultLangiumDocumentFactory {
|
|
24
|
+
/** URIs that previously hit the parse budget — keep using a killable worker. */
|
|
25
|
+
private readonly workerEscalateUris = new Set<string>();
|
|
26
|
+
|
|
27
|
+
constructor(services: LangiumSharedCoreServices) {
|
|
28
|
+
super(services);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
protected override parse<T extends AstNode>(
|
|
32
|
+
uri: URI,
|
|
33
|
+
text: string,
|
|
34
|
+
options?: ParserOptions
|
|
35
|
+
): ParseResult<T> {
|
|
36
|
+
const services = this.serviceRegistry.getServices(uri);
|
|
37
|
+
try {
|
|
38
|
+
return services.parser.LangiumParser.parse(text, options);
|
|
39
|
+
} catch (error) {
|
|
40
|
+
return createIncompleteParseResult<T>(services.parser.LangiumParser, {
|
|
41
|
+
reason: 'failure',
|
|
42
|
+
cause: error instanceof Error ? error.message : String(error)
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
protected override async parseAsync<T extends AstNode>(
|
|
48
|
+
uri: URI,
|
|
49
|
+
text: string,
|
|
50
|
+
cancellationToken: CancellationToken = CancelToken.None
|
|
51
|
+
): Promise<ParseResult<T>> {
|
|
52
|
+
const services = this.serviceRegistry.getServices(uri);
|
|
53
|
+
const asyncParser = services.parser.AsyncParser;
|
|
54
|
+
const uriKey = uri.toString();
|
|
55
|
+
const forceWorker = this.workerEscalateUris.has(uriKey);
|
|
56
|
+
|
|
57
|
+
const result = asyncParser instanceof ReqlanAsyncParser
|
|
58
|
+
? await asyncParser.parse<T>(text, cancellationToken, { forceWorker })
|
|
59
|
+
: await asyncParser.parse<T>(text, cancellationToken);
|
|
60
|
+
|
|
61
|
+
const reqlanResult = result as ReqlanParseResult<T>;
|
|
62
|
+
if (reqlanResult.reqlanIncomplete?.reason === 'timeout') {
|
|
63
|
+
this.workerEscalateUris.add(uriKey);
|
|
64
|
+
} else if (!reqlanResult.reqlanIncomplete) {
|
|
65
|
+
this.workerEscalateUris.delete(uriKey);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return result;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Test helper: expose incomplete marker after async parse. */
|
|
72
|
+
static isIncompleteDocument(document: LangiumDocument): boolean {
|
|
73
|
+
return Boolean((document.parseResult as ReqlanParseResult).reqlanIncomplete);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
@@ -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';
|
|
@@ -28,43 +28,108 @@ export type ReferencedDeclaration = IdeaDeclaration | IdeaSet;
|
|
|
28
28
|
|
|
29
29
|
const MAX_INLINE_NAMES = 3;
|
|
30
30
|
|
|
31
|
+
/** Target key: `${documentUri}#${astNodePath}` — matches IndexManager reference descriptions. */
|
|
32
|
+
export type InboundReferencerIndex = Map<string, InboundReferencer[]>;
|
|
33
|
+
|
|
31
34
|
/**
|
|
32
|
-
*
|
|
33
|
-
*
|
|
35
|
+
* Langium's IndexManager stores outbound refs per source document and scans every
|
|
36
|
+
* document for each `findAllReferences` call. Building the inverse once makes
|
|
37
|
+
* inlay hints O(workspace refs) instead of O(ideas × workspace refs).
|
|
38
|
+
* rq:["../../../reqlan rq/extension/syntax/features-syntax-highlighting.rq".inbound_inlay_index_performance]
|
|
34
39
|
*/
|
|
35
|
-
|
|
40
|
+
type IndexManagerReferenceIndex = {
|
|
41
|
+
referenceIndex: Map<string, Array<{
|
|
42
|
+
sourceUri: { toString(): string };
|
|
43
|
+
sourcePath: string;
|
|
44
|
+
targetUri: { toString(): string };
|
|
45
|
+
targetPath: string;
|
|
46
|
+
}>>;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
export function declarationInboundKey(
|
|
36
50
|
services: ReqlanServices,
|
|
37
51
|
declaration: ReferencedDeclaration
|
|
38
|
-
):
|
|
52
|
+
): string {
|
|
53
|
+
const document = AstUtils.getDocument(declaration);
|
|
54
|
+
const path = services.workspace.AstNodeLocator.getAstNodePath(declaration);
|
|
55
|
+
return `${document.uri.toString()}#${path}`;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Build inbound referencers for every idea/ideaset target in one pass.
|
|
60
|
+
*/
|
|
61
|
+
export function buildInboundReferencerIndex(services: ReqlanServices): InboundReferencerIndex {
|
|
39
62
|
const documents = services.shared.workspace.LangiumDocuments;
|
|
40
63
|
const locator = services.workspace.AstNodeLocator;
|
|
41
|
-
const
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
for (const
|
|
45
|
-
const
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
64
|
+
const indexManager = services.shared.workspace.IndexManager as unknown as IndexManagerReferenceIndex;
|
|
65
|
+
const buckets = new Map<string, Map<string, InboundReferencer>>();
|
|
66
|
+
|
|
67
|
+
for (const docRefs of indexManager.referenceIndex.values()) {
|
|
68
|
+
for (const reference of docRefs) {
|
|
69
|
+
const sourceDocument = documents.getDocument(reference.sourceUri as never);
|
|
70
|
+
if (!sourceDocument) {
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
const sourceNode = locator.getAstNode(sourceDocument.parseResult.value, reference.sourcePath);
|
|
74
|
+
if (!sourceNode) {
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
const referrer = enclosingReferrerDeclaration(sourceNode);
|
|
78
|
+
if (!referrer) {
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
const location = locationForReferrer(referrer);
|
|
82
|
+
if (!location) {
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
const targetKey = `${reference.targetUri.toString()}#${reference.targetPath}`;
|
|
86
|
+
const referrerDoc = AstUtils.getDocument(referrer).uri.toString();
|
|
87
|
+
const referrerPath = locator.getAstNodePath(referrer);
|
|
88
|
+
if (targetKey === `${referrerDoc}#${referrerPath}`) {
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
let byLocation = buckets.get(targetKey);
|
|
92
|
+
if (!byLocation) {
|
|
93
|
+
byLocation = new Map();
|
|
94
|
+
buckets.set(targetKey, byLocation);
|
|
95
|
+
}
|
|
96
|
+
byLocation.set(locationKey(location), {
|
|
97
|
+
name: referrer.name,
|
|
98
|
+
location
|
|
99
|
+
});
|
|
60
100
|
}
|
|
61
|
-
referencers.set(locationKey(location), {
|
|
62
|
-
name: referrer.name,
|
|
63
|
-
location
|
|
64
|
-
});
|
|
65
101
|
}
|
|
66
102
|
|
|
67
|
-
|
|
103
|
+
const result: InboundReferencerIndex = new Map();
|
|
104
|
+
for (const [targetKey, byLocation] of buckets) {
|
|
105
|
+
result.set(
|
|
106
|
+
targetKey,
|
|
107
|
+
[...byLocation.values()].sort((left, right) => left.name.localeCompare(right.name))
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
return result;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function lookupInboundReferencers(
|
|
114
|
+
index: InboundReferencerIndex,
|
|
115
|
+
services: ReqlanServices,
|
|
116
|
+
declaration: ReferencedDeclaration
|
|
117
|
+
): InboundReferencer[] {
|
|
118
|
+
return index.get(declarationInboundKey(services, declaration)) ?? [];
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Collect inbound idea referencers from the whole Langium workspace index
|
|
123
|
+
* (every indexed `.rq` document), not only the declaration's file.
|
|
124
|
+
*
|
|
125
|
+
* Prefer {@link buildInboundReferencerIndex} + {@link lookupInboundReferencers}
|
|
126
|
+
* when resolving many declarations (e.g. inlay hints).
|
|
127
|
+
*/
|
|
128
|
+
export function collectInboundReferencers(
|
|
129
|
+
services: ReqlanServices,
|
|
130
|
+
declaration: ReferencedDeclaration
|
|
131
|
+
): InboundReferencer[] {
|
|
132
|
+
return lookupInboundReferencers(buildInboundReferencerIndex(services), services, declaration);
|
|
68
133
|
}
|
|
69
134
|
|
|
70
135
|
/** @deprecated Use {@link collectInboundReferencers} */
|
|
@@ -86,16 +151,6 @@ function enclosingReferrerDeclaration(node: AstNode): ReferencedDeclaration | un
|
|
|
86
151
|
return undefined;
|
|
87
152
|
}
|
|
88
153
|
|
|
89
|
-
function isSameDeclaration(left: ReferencedDeclaration, right: ReferencedDeclaration): boolean {
|
|
90
|
-
if (left === right) {
|
|
91
|
-
return true;
|
|
92
|
-
}
|
|
93
|
-
if (left.name !== right.name) {
|
|
94
|
-
return false;
|
|
95
|
-
}
|
|
96
|
-
return AstUtils.getDocument(left).uri.toString() === AstUtils.getDocument(right).uri.toString();
|
|
97
|
-
}
|
|
98
|
-
|
|
99
154
|
function locationForReferrer(referrer: ReferencedDeclaration): Location | undefined {
|
|
100
155
|
const nameNode = GrammarUtils.findNodeForProperty(referrer.$cstNode, 'name');
|
|
101
156
|
if (!nameNode?.range) {
|