@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.
Files changed (109) hide show
  1. package/README.md +18 -0
  2. package/out/generated/ast.d.ts +31 -4
  3. package/out/generated/ast.js +28 -7
  4. package/out/generated/ast.js.map +1 -1
  5. package/out/generated/grammar.js +2 -3318
  6. package/out/generated/grammar.js.map +1 -1
  7. package/out/generated/module.d.ts +2 -2
  8. package/out/generated/module.js +2 -2
  9. package/out/generated/module.js.map +1 -1
  10. package/out/index.d.ts +4 -0
  11. package/out/index.js +4 -0
  12. package/out/index.js.map +1 -1
  13. package/out/reqlan-async-parser.d.ts +37 -0
  14. package/out/reqlan-async-parser.js +117 -0
  15. package/out/reqlan-async-parser.js.map +1 -0
  16. package/out/reqlan-code-action-provider.d.ts +15 -12
  17. package/out/reqlan-code-action-provider.js +55 -20
  18. package/out/reqlan-code-action-provider.js.map +1 -1
  19. package/out/reqlan-comment-resolver.js +5 -0
  20. package/out/reqlan-comment-resolver.js.map +1 -1
  21. package/out/reqlan-completion-provider.d.ts +10 -0
  22. package/out/reqlan-completion-provider.js +64 -30
  23. package/out/reqlan-completion-provider.js.map +1 -1
  24. package/out/reqlan-definition-provider.d.ts +7 -0
  25. package/out/reqlan-definition-provider.js +86 -19
  26. package/out/reqlan-definition-provider.js.map +1 -1
  27. package/out/reqlan-document-factory.d.ts +16 -0
  28. package/out/reqlan-document-factory.js +50 -0
  29. package/out/reqlan-document-factory.js.map +1 -0
  30. package/out/reqlan-document-link-provider.d.ts +2 -1
  31. package/out/reqlan-document-link-provider.js +4 -0
  32. package/out/reqlan-document-link-provider.js.map +1 -1
  33. package/out/reqlan-file-link-resolver.d.ts +17 -4
  34. package/out/reqlan-file-link-resolver.js +89 -14
  35. package/out/reqlan-file-link-resolver.js.map +1 -1
  36. package/out/reqlan-import-edits.d.ts +3 -1
  37. package/out/reqlan-import-edits.js.map +1 -1
  38. package/out/reqlan-inbound-reference-inlay-label.d.ts +11 -0
  39. package/out/reqlan-inbound-reference-inlay-label.js +60 -35
  40. package/out/reqlan-inbound-reference-inlay-label.js.map +1 -1
  41. package/out/reqlan-inlay-hint-provider.d.ts +6 -0
  42. package/out/reqlan-inlay-hint-provider.js +34 -6
  43. package/out/reqlan-inlay-hint-provider.js.map +1 -1
  44. package/out/reqlan-linker.js +2 -2
  45. package/out/reqlan-linker.js.map +1 -1
  46. package/out/reqlan-markdown-links.js +4 -0
  47. package/out/reqlan-markdown-links.js.map +1 -1
  48. package/out/reqlan-module.d.ts +2 -1
  49. package/out/reqlan-module.js +12 -2
  50. package/out/reqlan-module.js.map +1 -1
  51. package/out/reqlan-parse-budget.d.ts +50 -0
  52. package/out/reqlan-parse-budget.js +89 -0
  53. package/out/reqlan-parse-budget.js.map +1 -0
  54. package/out/reqlan-parse-worker.d.ts +1 -0
  55. package/out/reqlan-parse-worker.js +34 -0
  56. package/out/reqlan-parse-worker.js.map +1 -0
  57. package/out/reqlan-path-resolve.d.ts +8 -0
  58. package/out/reqlan-path-resolve.js +18 -4
  59. package/out/reqlan-path-resolve.js.map +1 -1
  60. package/out/reqlan-reference-search-site.d.ts +11 -0
  61. package/out/reqlan-reference-search-site.js +11 -0
  62. package/out/reqlan-reference-search-site.js.map +1 -1
  63. package/out/reqlan-scope.d.ts +11 -0
  64. package/out/reqlan-scope.js +28 -1
  65. package/out/reqlan-scope.js.map +1 -1
  66. package/out/reqlan-semantic-token-provider.js +6 -1
  67. package/out/reqlan-semantic-token-provider.js.map +1 -1
  68. package/out/reqlan-token-builder.d.ts +3 -2
  69. package/out/reqlan-token-builder.js +150 -26
  70. package/out/reqlan-token-builder.js.map +1 -1
  71. package/out/reqlan-validator.d.ts +10 -1
  72. package/out/reqlan-validator.js +36 -2
  73. package/out/reqlan-validator.js.map +1 -1
  74. package/out/reqlan-wildcard-resolve.d.ts +45 -0
  75. package/out/reqlan-wildcard-resolve.js +203 -0
  76. package/out/reqlan-wildcard-resolve.js.map +1 -0
  77. package/out/reqlan-workspace-manager.d.ts +18 -0
  78. package/out/reqlan-workspace-manager.js +31 -0
  79. package/out/reqlan-workspace-manager.js.map +1 -0
  80. package/package.json +1 -1
  81. package/src/generated/ast.ts +41 -10
  82. package/src/generated/grammar.ts +2 -3318
  83. package/src/generated/module.ts +2 -2
  84. package/src/index.ts +4 -0
  85. package/src/reqlan-async-parser.ts +178 -0
  86. package/src/reqlan-code-action-provider.ts +67 -32
  87. package/src/reqlan-comment-resolver.ts +5 -0
  88. package/src/reqlan-completion-provider.ts +82 -28
  89. package/src/reqlan-definition-provider.ts +91 -18
  90. package/src/reqlan-document-factory.ts +75 -0
  91. package/src/reqlan-document-link-provider.ts +9 -1
  92. package/src/reqlan-file-link-resolver.ts +112 -17
  93. package/src/reqlan-import-edits.ts +3 -1
  94. package/src/reqlan-inbound-reference-inlay-label.ts +93 -38
  95. package/src/reqlan-inlay-hint-provider.ts +36 -6
  96. package/src/reqlan-linker.ts +4 -2
  97. package/src/reqlan-markdown-links.ts +4 -0
  98. package/src/reqlan-module.ts +15 -3
  99. package/src/reqlan-parse-budget.ts +155 -0
  100. package/src/reqlan-parse-worker.ts +35 -0
  101. package/src/reqlan-path-resolve.ts +28 -5
  102. package/src/reqlan-reference-search-site.ts +25 -0
  103. package/src/reqlan-scope.ts +34 -1
  104. package/src/reqlan-semantic-token-provider.ts +6 -0
  105. package/src/reqlan-token-builder.ts +174 -28
  106. package/src/reqlan-validator.ts +56 -1
  107. package/src/reqlan-wildcard-resolve.ts +281 -0
  108. package/src/reqlan-workspace-manager.ts +47 -0
  109. 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 { AstUtils, CstUtils, GrammarUtils, URI } from 'langium';
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 (fileReference.resolution === 'folder' || fileReference.resolution === 'missing') {
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
- for (const node of AstUtils.streamAst(document.parseResult.value)) {
234
- if (!isQualifiedReference(node) || node.path?.ref) {
235
- continue;
236
- }
237
- const pathNode = node.path?.$refNode ?? GrammarUtils.findNodeForProperty(node.$cstNode, 'path');
238
- if (!pathNode || offset < pathNode.offset || offset >= pathNode.end) {
239
- continue;
240
- }
241
- const resolved = resolveQualifiedReferencePathLink(node, this.documents, this.pathContext());
242
- if (!resolved) {
243
- continue;
244
- }
245
- const targetDocument = this.documents.getDocument(URI.parse(resolved.targetUri.split('#')[0]));
246
- if (!targetDocument) {
247
- continue;
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
- return resolvedFileLinkToGoToTarget(resolved, pathNode, targetDocument);
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
- links.push(link);
385
- linkedRanges.push(rangeKey(link.sourceRange));
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
- links.push(link);
392
- linkedRanges.push(rangeKey(link.sourceRange));
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
- links.push(link);
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
- links.push(link);
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
- links.push(link);
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
- links.push(link);
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
- * Collect inbound idea referencers from the whole Langium workspace index
33
- * (every indexed `.rq` document), not only the declaration's file.
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
- export function collectInboundReferencers(
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
- ): InboundReferencer[] {
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 referencers = new Map<string, InboundReferencer>();
42
-
43
- // findReferences without documentUri walks IndexManager across the workspace.
44
- for (const reference of services.references.References.findReferences(declaration, { includeDeclaration: false }).toArray()) {
45
- const sourceDocument = documents.getDocument(reference.sourceUri);
46
- if (!sourceDocument) {
47
- continue;
48
- }
49
- const sourceNode = locator.getAstNode(sourceDocument.parseResult.value, reference.sourcePath);
50
- if (!sourceNode) {
51
- continue;
52
- }
53
- const referrer = enclosingReferrerDeclaration(sourceNode);
54
- if (!referrer || isSameDeclaration(referrer, declaration)) {
55
- continue;
56
- }
57
- const location = locationForReferrer(referrer);
58
- if (!location) {
59
- continue;
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
- return [...referencers.values()].sort((left, right) => left.name.localeCompare(right.name));
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) {