@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.
Files changed (82) hide show
  1. package/README.md +18 -0
  2. package/out/generated/ast.d.ts +28 -1
  3. package/out/generated/ast.js +27 -6
  4. package/out/generated/ast.js.map +1 -1
  5. package/out/generated/grammar.js +1 -1
  6. package/out/generated/grammar.js.map +1 -1
  7. package/out/index.d.ts +1 -0
  8. package/out/index.js +1 -0
  9. package/out/index.js.map +1 -1
  10. package/out/reqlan-code-action-provider.d.ts +15 -12
  11. package/out/reqlan-code-action-provider.js +55 -20
  12. package/out/reqlan-code-action-provider.js.map +1 -1
  13. package/out/reqlan-comment-resolver.js +5 -0
  14. package/out/reqlan-comment-resolver.js.map +1 -1
  15. package/out/reqlan-completion-provider.d.ts +10 -0
  16. package/out/reqlan-completion-provider.js +63 -29
  17. package/out/reqlan-completion-provider.js.map +1 -1
  18. package/out/reqlan-definition-provider.d.ts +7 -0
  19. package/out/reqlan-definition-provider.js +86 -19
  20. package/out/reqlan-definition-provider.js.map +1 -1
  21. package/out/reqlan-document-link-provider.d.ts +2 -1
  22. package/out/reqlan-document-link-provider.js +4 -0
  23. package/out/reqlan-document-link-provider.js.map +1 -1
  24. package/out/reqlan-file-link-resolver.d.ts +17 -4
  25. package/out/reqlan-file-link-resolver.js +89 -14
  26. package/out/reqlan-file-link-resolver.js.map +1 -1
  27. package/out/reqlan-import-edits.d.ts +3 -1
  28. package/out/reqlan-import-edits.js.map +1 -1
  29. package/out/reqlan-inbound-reference-inlay-label.d.ts +11 -0
  30. package/out/reqlan-inbound-reference-inlay-label.js +60 -35
  31. package/out/reqlan-inbound-reference-inlay-label.js.map +1 -1
  32. package/out/reqlan-inlay-hint-provider.d.ts +6 -0
  33. package/out/reqlan-inlay-hint-provider.js +34 -6
  34. package/out/reqlan-inlay-hint-provider.js.map +1 -1
  35. package/out/reqlan-markdown-links.js +4 -0
  36. package/out/reqlan-markdown-links.js.map +1 -1
  37. package/out/reqlan-path-resolve.d.ts +8 -0
  38. package/out/reqlan-path-resolve.js +18 -4
  39. package/out/reqlan-path-resolve.js.map +1 -1
  40. package/out/reqlan-reference-search-site.d.ts +11 -0
  41. package/out/reqlan-reference-search-site.js +11 -0
  42. package/out/reqlan-reference-search-site.js.map +1 -1
  43. package/out/reqlan-scope.d.ts +11 -0
  44. package/out/reqlan-scope.js +28 -1
  45. package/out/reqlan-scope.js.map +1 -1
  46. package/out/reqlan-semantic-token-provider.js +6 -1
  47. package/out/reqlan-semantic-token-provider.js.map +1 -1
  48. package/out/reqlan-token-builder.d.ts +3 -2
  49. package/out/reqlan-token-builder.js +48 -1
  50. package/out/reqlan-token-builder.js.map +1 -1
  51. package/out/reqlan-validator.d.ts +1 -0
  52. package/out/reqlan-validator.js +21 -1
  53. package/out/reqlan-validator.js.map +1 -1
  54. package/out/reqlan-wildcard-resolve.d.ts +45 -0
  55. package/out/reqlan-wildcard-resolve.js +203 -0
  56. package/out/reqlan-wildcard-resolve.js.map +1 -0
  57. package/out/reqlan-workspace-manager.d.ts +7 -0
  58. package/out/reqlan-workspace-manager.js +7 -0
  59. package/out/reqlan-workspace-manager.js.map +1 -1
  60. package/package.json +1 -1
  61. package/src/generated/ast.ts +38 -7
  62. package/src/generated/grammar.ts +1 -1
  63. package/src/index.ts +1 -0
  64. package/src/reqlan-code-action-provider.ts +67 -32
  65. package/src/reqlan-comment-resolver.ts +5 -0
  66. package/src/reqlan-completion-provider.ts +81 -27
  67. package/src/reqlan-definition-provider.ts +91 -18
  68. package/src/reqlan-document-link-provider.ts +9 -1
  69. package/src/reqlan-file-link-resolver.ts +112 -17
  70. package/src/reqlan-import-edits.ts +3 -1
  71. package/src/reqlan-inbound-reference-inlay-label.ts +93 -38
  72. package/src/reqlan-inlay-hint-provider.ts +36 -6
  73. package/src/reqlan-markdown-links.ts +4 -0
  74. package/src/reqlan-path-resolve.ts +28 -5
  75. package/src/reqlan-reference-search-site.ts +25 -0
  76. package/src/reqlan-scope.ts +34 -1
  77. package/src/reqlan-semantic-token-provider.ts +6 -0
  78. package/src/reqlan-token-builder.ts +52 -3
  79. package/src/reqlan-validator.ts +34 -0
  80. package/src/reqlan-wildcard-resolve.ts +281 -0
  81. package/src/reqlan-workspace-manager.ts +9 -0
  82. 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.toString() === document.uri.toString())) {
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
- if (isIdea(element) || isOneLinerIdea(element) || isIdeaSet(element)) {
209
- names.add(element.name);
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
- const center = findContainingIdea(document, params.position)?.name;
214
- const distances = center
215
- ? hopDistancesFromCenter(center, buildIdeaReferenceAdjacency(docs))
216
- : new Map<string, number>();
217
- const items = [...names].flatMap(name => {
218
- if (context.prefix && !name.startsWith(context.prefix)) {
219
- return [];
220
- }
221
- const distance = distances.get(name) ?? UNREACHABLE_DISTANCE;
222
- return [{
223
- label: name,
224
- kind: CompletionItemKind.Reference,
225
- insertText: name,
226
- sortText: `${String(distance).padStart(4, '0')}_${name}`,
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
- }).sort((left, right) => {
233
- const leftDistance = distances.get(String(left.label)) ?? UNREACHABLE_DISTANCE;
234
- const rightDistance = distances.get(String(right.label)) ?? UNREACHABLE_DISTANCE;
235
- return leftDistance - rightDistance || String(left.label).localeCompare(String(right.label));
236
- });
237
- return LspCompletionList.create(items, true);
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 { 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
  }
@@ -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';