@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
package/src/generated/module.ts
CHANGED
|
@@ -11,14 +11,14 @@ export const ReqlanLanguageMetaData = {
|
|
|
11
11
|
languageId: 'reqlan',
|
|
12
12
|
fileExtensions: ['.rq'],
|
|
13
13
|
caseInsensitive: false,
|
|
14
|
-
mode: '
|
|
14
|
+
mode: 'production'
|
|
15
15
|
} as const satisfies LanguageMetaData;
|
|
16
16
|
|
|
17
17
|
export const ReqlanCommentLanguageMetaData = {
|
|
18
18
|
languageId: 'reqlan-comment',
|
|
19
19
|
fileExtensions: [],
|
|
20
20
|
caseInsensitive: false,
|
|
21
|
-
mode: '
|
|
21
|
+
mode: 'production'
|
|
22
22
|
} as const satisfies LanguageMetaData;
|
|
23
23
|
|
|
24
24
|
export const ReqlanGeneratedSharedModule: Module<LangiumSharedCoreServices, LangiumGeneratedSharedCoreServices> = {
|
package/src/index.ts
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
export * from './reqlan-module.js';
|
|
2
|
+
export * from './reqlan-parse-budget.js';
|
|
3
|
+
export { ReqlanAsyncParser, resolveParseWorkerPath } from './reqlan-async-parser.js';
|
|
4
|
+
export { ReqlanLangiumDocumentFactory } from './reqlan-document-factory.js';
|
|
2
5
|
export * from './reqlan-validator.js';
|
|
3
6
|
export * from './reqlan-comment-resolver.js';
|
|
4
7
|
export * from './reqlan-ignore-error.js';
|
|
@@ -20,6 +23,7 @@ export * from './reqlan-path-resolve.js';
|
|
|
20
23
|
export * from './reqlan-imports.js';
|
|
21
24
|
export * from './reqlan-quoted-strings.js';
|
|
22
25
|
export * from './reqlan-references.js';
|
|
26
|
+
export * from './reqlan-wildcard-resolve.js';
|
|
23
27
|
export * from './reqlan-reference-at-position.js';
|
|
24
28
|
export * from './reqlan-attribute-catalog.js';
|
|
25
29
|
export * from './reqlan-completion-context.js';
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-file parse budget via Langium's worker-thread async parser.
|
|
3
|
+
* Happy path is in-process sync parse; workers enforce a killable wall-clock budget
|
|
4
|
+
* for large files, sticky timeout escalation, or explicit force.
|
|
5
|
+
* rq:["../../../reqlan rq/language/parser_lexer.rq".parse_budget_timeout]
|
|
6
|
+
*/
|
|
7
|
+
import { existsSync } from 'node:fs';
|
|
8
|
+
import { dirname, join } from 'node:path';
|
|
9
|
+
import { fileURLToPath } from 'node:url';
|
|
10
|
+
import {
|
|
11
|
+
type AstNode,
|
|
12
|
+
type LangiumCoreServices,
|
|
13
|
+
type LangiumParser
|
|
14
|
+
} from 'langium';
|
|
15
|
+
import { WorkerThreadAsyncParser } from 'langium/node';
|
|
16
|
+
import {
|
|
17
|
+
CancellationToken,
|
|
18
|
+
CancellationTokenSource
|
|
19
|
+
} from 'vscode-languageserver';
|
|
20
|
+
import {
|
|
21
|
+
createIncompleteParseResult,
|
|
22
|
+
DEFAULT_PARSE_BUDGET_MS,
|
|
23
|
+
DEFAULT_WORKER_PARSE_THRESHOLD_CHARS,
|
|
24
|
+
type ParseWorkerMode,
|
|
25
|
+
type ReqlanParseResult,
|
|
26
|
+
shouldEscalateToParseWorker
|
|
27
|
+
} from './reqlan-parse-budget.js';
|
|
28
|
+
|
|
29
|
+
export interface ReqlanAsyncParserOptions {
|
|
30
|
+
timeoutMs?: number;
|
|
31
|
+
/** Override worker script path (tests). */
|
|
32
|
+
workerPath?: string;
|
|
33
|
+
/**
|
|
34
|
+
* `auto` (default): sync below size threshold; worker when escalated.
|
|
35
|
+
* `true` / `false`: always / never use workers.
|
|
36
|
+
*/
|
|
37
|
+
useWorker?: ParseWorkerMode;
|
|
38
|
+
/** Characters at or above which `auto` mode uses a worker. */
|
|
39
|
+
workerThresholdChars?: number;
|
|
40
|
+
/** Worker pool size (Langium default is 8; keep small for startup). */
|
|
41
|
+
threadCount?: number;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface ReqlanParseCallOptions {
|
|
45
|
+
/** Force the killable worker path for this call (sticky URI escalate, tests). */
|
|
46
|
+
forceWorker?: boolean;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Directory of this module (or of the CJS bundle that inlined it).
|
|
51
|
+
* Avoid static `import.meta.url`: esbuild format cjs + target es2017 empties it
|
|
52
|
+
* (see reqlan rq/extension/startup-performance.rq invalid_url_activation_failure).
|
|
53
|
+
*/
|
|
54
|
+
declare const __dirname: string | undefined;
|
|
55
|
+
|
|
56
|
+
function moduleDirectory(): string {
|
|
57
|
+
if (typeof __dirname === 'string') {
|
|
58
|
+
return __dirname;
|
|
59
|
+
}
|
|
60
|
+
// Native ESM only — hide from esbuild's empty-import-meta rewrite.
|
|
61
|
+
const metaUrl = (0, eval)('import.meta.url') as string;
|
|
62
|
+
return dirname(fileURLToPath(metaUrl));
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function resolveParseWorkerPath(explicit?: string): string {
|
|
66
|
+
if (explicit) {
|
|
67
|
+
return explicit;
|
|
68
|
+
}
|
|
69
|
+
const dir = moduleDirectory();
|
|
70
|
+
const jsPath = join(dir, 'reqlan-parse-worker.js');
|
|
71
|
+
if (existsSync(jsPath)) {
|
|
72
|
+
return jsPath;
|
|
73
|
+
}
|
|
74
|
+
const cjsPath = join(dir, 'reqlan-parse-worker.cjs');
|
|
75
|
+
if (existsSync(cjsPath)) {
|
|
76
|
+
return cjsPath;
|
|
77
|
+
}
|
|
78
|
+
return jsPath;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export class ReqlanAsyncParser extends WorkerThreadAsyncParser {
|
|
82
|
+
private readonly timeoutMs: number;
|
|
83
|
+
private readonly useWorker: ParseWorkerMode;
|
|
84
|
+
private readonly workerThresholdChars: number;
|
|
85
|
+
private readonly syncParser: LangiumParser;
|
|
86
|
+
|
|
87
|
+
constructor(services: LangiumCoreServices, options: ReqlanAsyncParserOptions = {}) {
|
|
88
|
+
super(services, () => resolveParseWorkerPath(options.workerPath));
|
|
89
|
+
this.syncParser = services.parser.LangiumParser;
|
|
90
|
+
this.timeoutMs = options.timeoutMs ?? DEFAULT_PARSE_BUDGET_MS;
|
|
91
|
+
this.useWorker = options.useWorker ?? 'auto';
|
|
92
|
+
this.workerThresholdChars = options.workerThresholdChars ?? DEFAULT_WORKER_PARSE_THRESHOLD_CHARS;
|
|
93
|
+
this.threadCount = options.threadCount ?? 2;
|
|
94
|
+
// Fail fast after cancel so the budget is close to timeoutMs wall-clock.
|
|
95
|
+
this.terminationDelay = 50;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
override async parse<T extends AstNode>(
|
|
99
|
+
text: string,
|
|
100
|
+
cancelToken: CancellationToken,
|
|
101
|
+
callOptions: ReqlanParseCallOptions = {}
|
|
102
|
+
): Promise<ReqlanParseResult<T>> {
|
|
103
|
+
if (cancelToken.isCancellationRequested) {
|
|
104
|
+
return createIncompleteParseResult<T>(this.syncParser, {
|
|
105
|
+
reason: 'failure',
|
|
106
|
+
cause: 'cancelled'
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const escalate = shouldEscalateToParseWorker(text, {
|
|
111
|
+
useWorker: this.useWorker,
|
|
112
|
+
forceWorker: callOptions.forceWorker,
|
|
113
|
+
thresholdChars: this.workerThresholdChars
|
|
114
|
+
});
|
|
115
|
+
if (!escalate) {
|
|
116
|
+
return this.parseOnThread<T>(text);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return this.parseInWorker<T>(text, cancelToken);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** Sync entry used by document factory / tests. */
|
|
123
|
+
parseSync<T extends AstNode>(text: string): ReqlanParseResult<T> {
|
|
124
|
+
return this.parseOnThread<T>(text);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
private async parseInWorker<T extends AstNode>(
|
|
128
|
+
text: string,
|
|
129
|
+
cancelToken: CancellationToken
|
|
130
|
+
): Promise<ReqlanParseResult<T>> {
|
|
131
|
+
const timeoutSource = new CancellationTokenSource();
|
|
132
|
+
const timer = setTimeout(() => timeoutSource.cancel(), this.timeoutMs);
|
|
133
|
+
const callerCancel = cancelToken.onCancellationRequested(() => {
|
|
134
|
+
timeoutSource.cancel();
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
try {
|
|
138
|
+
return await super.parse<T>(text, timeoutSource.token) as ReqlanParseResult<T>;
|
|
139
|
+
} catch (error) {
|
|
140
|
+
if (timeoutSource.token.isCancellationRequested) {
|
|
141
|
+
if (cancelToken.isCancellationRequested) {
|
|
142
|
+
return createIncompleteParseResult<T>(this.syncParser, {
|
|
143
|
+
reason: 'failure',
|
|
144
|
+
cause: 'cancelled'
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
return createIncompleteParseResult<T>(this.syncParser, {
|
|
148
|
+
reason: 'timeout',
|
|
149
|
+
timeoutMs: this.timeoutMs
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
return createIncompleteParseResult<T>(this.syncParser, {
|
|
153
|
+
reason: 'failure',
|
|
154
|
+
cause: error instanceof Error ? error.message : String(error)
|
|
155
|
+
});
|
|
156
|
+
} finally {
|
|
157
|
+
clearTimeout(timer);
|
|
158
|
+
callerCancel.dispose();
|
|
159
|
+
timeoutSource.dispose();
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
private parseOnThread<T extends AstNode>(text: string): ReqlanParseResult<T> {
|
|
164
|
+
try {
|
|
165
|
+
return this.syncParser.parse(text) as ReqlanParseResult<T>;
|
|
166
|
+
} catch (error) {
|
|
167
|
+
return createIncompleteParseResult<T>(this.syncParser, {
|
|
168
|
+
reason: 'failure',
|
|
169
|
+
cause: error instanceof Error ? error.message : String(error)
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/** Exposed for tests that need the underlying sync parser. */
|
|
175
|
+
get langiumParser(): LangiumParser {
|
|
176
|
+
return this.syncParser;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Quick fixes for unresolved references: add import, rewrite qualified ref, search, create.
|
|
3
3
|
* Also offers a search code action when the cursor is inside a [reference] / [[wikilink]].
|
|
4
|
+
* File-level source actions include barrel page.
|
|
4
5
|
* rq:["../../../reqlan rq/extension/language-support/features-imports.rq".import_error]
|
|
5
6
|
* rq:["../../../reqlan rq/extension/features-commands.rq".search_code_actions]
|
|
7
|
+
* rq:["../../../reqlan rq/extension/features-commands.rq".barrel_page]
|
|
8
|
+
* rq:["../../../reqlan rq/extension/features-commands.rq".file_based_code_actions]
|
|
6
9
|
*/
|
|
7
10
|
import type { LangiumDocument, URI } from 'langium';
|
|
8
11
|
import { AstUtils, DocumentValidator, URI as UriCtor, UriUtils } from 'langium';
|
|
@@ -27,13 +30,16 @@ import {
|
|
|
27
30
|
type NameCatalogEntry,
|
|
28
31
|
type NameCatalogKind
|
|
29
32
|
} from './reqlan-name-catalog.js';
|
|
30
|
-
import { findReferenceSearchSite
|
|
33
|
+
import { findReferenceSearchSite } from './reqlan-reference-search-site.js';
|
|
31
34
|
|
|
32
35
|
export const REQLAN_IMPORT_ERROR_SEARCH_COMMAND = 'reqlan.importError.search';
|
|
33
36
|
export const REQLAN_IMPORT_ERROR_CREATE_COMMAND = 'reqlan.importError.createFile';
|
|
34
37
|
export const REQLAN_SEARCH_REFERENCE_COMMAND = 'reqlan.searchReference';
|
|
35
38
|
export const REQLAN_REFACTOR_DELETE_IDEA_COMMAND = 'reqlan.refactor.deleteIdea';
|
|
36
39
|
export const REQLAN_REFACTOR_MOVE_IDEA_COMMAND = 'reqlan.refactor.moveIdea';
|
|
40
|
+
export const REQLAN_BARREL_PAGE_COMMAND = 'reqlan.barrelPage';
|
|
41
|
+
/** Source-action kind for whole-file barrel page. */
|
|
42
|
+
export const REQLAN_BARREL_PAGE_KIND = `${CodeActionKind.Source}.barrelPage`;
|
|
37
43
|
|
|
38
44
|
/** Args for idea move/delete refactor commands. */
|
|
39
45
|
export interface IdeaRefactorCommandArgs {
|
|
@@ -58,18 +64,7 @@ export interface ImportErrorCommandArgs {
|
|
|
58
64
|
}
|
|
59
65
|
|
|
60
66
|
/** Args for reference search webview (replace existing ref or wrap prose). */
|
|
61
|
-
export
|
|
62
|
-
documentUri: string;
|
|
63
|
-
refText: string;
|
|
64
|
-
range: Diagnostic['range'];
|
|
65
|
-
mode: 'replace' | 'wrap';
|
|
66
|
-
context?: {
|
|
67
|
-
ideaName: string;
|
|
68
|
-
before: string;
|
|
69
|
-
target: string;
|
|
70
|
-
after: string;
|
|
71
|
-
};
|
|
72
|
-
}
|
|
67
|
+
export type SearchReferenceCommandArgs = import('./reqlan-reference-search-site.js').ReferenceSearchSiteRequestResult;
|
|
73
68
|
|
|
74
69
|
interface SymbolMatch {
|
|
75
70
|
name: string;
|
|
@@ -99,9 +94,40 @@ export class ReqlanCodeActionProvider implements CodeActionProvider {
|
|
|
99
94
|
actions.push(searchFromCursor);
|
|
100
95
|
}
|
|
101
96
|
actions.push(...this.createIdeaRefactorActions(document, params));
|
|
97
|
+
const barrelPage = this.createBarrelPageAction(document, params);
|
|
98
|
+
if (barrelPage) {
|
|
99
|
+
actions.push(barrelPage);
|
|
100
|
+
}
|
|
102
101
|
return actions;
|
|
103
102
|
}
|
|
104
103
|
|
|
104
|
+
/**
|
|
105
|
+
* Whole-file source action: barrel top-level ideas into sibling files + container.
|
|
106
|
+
* rq:["../../../reqlan rq/extension/features-commands.rq".barrel_page]
|
|
107
|
+
* rq:["../../../reqlan rq/extension/features-commands.rq".file_based_code_actions]
|
|
108
|
+
*/
|
|
109
|
+
private createBarrelPageAction(
|
|
110
|
+
document: LangiumDocument,
|
|
111
|
+
params: CodeActionParams
|
|
112
|
+
): CodeActionLike | undefined {
|
|
113
|
+
if (!actionKindAllowed(params.context.only, REQLAN_BARREL_PAGE_KIND)) {
|
|
114
|
+
return undefined;
|
|
115
|
+
}
|
|
116
|
+
if (!documentHasTopLevelIdeas(document)) {
|
|
117
|
+
return undefined;
|
|
118
|
+
}
|
|
119
|
+
// No command arguments: VS Code caches arg-bearing commands and may dispose
|
|
120
|
+
// them when code actions refresh. The extension resolves the active editor.
|
|
121
|
+
return {
|
|
122
|
+
title: 'Barrel page into container…',
|
|
123
|
+
kind: REQLAN_BARREL_PAGE_KIND,
|
|
124
|
+
command: {
|
|
125
|
+
title: 'Barrel page',
|
|
126
|
+
command: REQLAN_BARREL_PAGE_COMMAND
|
|
127
|
+
}
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
105
131
|
/**
|
|
106
132
|
* rq:["../../../reqlan rq/extension/refactor_support.rq".refactor_symbol_move]
|
|
107
133
|
* rq:["../../../reqlan rq/extension/refactor_support.rq".refactor_symbol_delete]
|
|
@@ -163,13 +189,11 @@ export class ReqlanCodeActionProvider implements CodeActionProvider {
|
|
|
163
189
|
if (alreadyHasSearch) {
|
|
164
190
|
return undefined;
|
|
165
191
|
}
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
context: site.context
|
|
172
|
-
};
|
|
192
|
+
// No command arguments: VS Code caches arg-bearing commands and disposes
|
|
193
|
+
// them when code actions refresh (common under Langium revalidation),
|
|
194
|
+
// which surfaces as "Actual command not found, wanted to execute
|
|
195
|
+
// reqlan.searchReference /N". The extension resolves the site from the
|
|
196
|
+
// active editor when the command runs.
|
|
173
197
|
const title = site.mode === 'wrap'
|
|
174
198
|
? (site.refText
|
|
175
199
|
? `Wrap '${site.refText}' as idea reference…`
|
|
@@ -182,8 +206,7 @@ export class ReqlanCodeActionProvider implements CodeActionProvider {
|
|
|
182
206
|
kind: CodeActionKind.Refactor,
|
|
183
207
|
command: {
|
|
184
208
|
title: 'Search for idea reference',
|
|
185
|
-
command: REQLAN_SEARCH_REFERENCE_COMMAND
|
|
186
|
-
arguments: [args]
|
|
209
|
+
command: REQLAN_SEARCH_REFERENCE_COMMAND
|
|
187
210
|
}
|
|
188
211
|
};
|
|
189
212
|
}
|
|
@@ -335,25 +358,17 @@ export class ReqlanCodeActionProvider implements CodeActionProvider {
|
|
|
335
358
|
}
|
|
336
359
|
|
|
337
360
|
private createSearchCommand(
|
|
338
|
-
|
|
361
|
+
_document: LangiumDocument,
|
|
339
362
|
diagnostic: Diagnostic,
|
|
340
363
|
refText: string
|
|
341
364
|
): CodeActionLike {
|
|
342
|
-
const args: SearchReferenceCommandArgs = {
|
|
343
|
-
documentUri: document.textDocument.uri,
|
|
344
|
-
refText,
|
|
345
|
-
range: diagnostic.range,
|
|
346
|
-
mode: 'replace',
|
|
347
|
-
context: buildContextForRange(document, diagnostic.range)
|
|
348
|
-
};
|
|
349
365
|
return {
|
|
350
366
|
title: `Search for idea to replace '${refText}'…`,
|
|
351
367
|
kind: CodeActionKind.QuickFix,
|
|
352
368
|
diagnostics: [diagnostic],
|
|
353
369
|
command: {
|
|
354
370
|
title: `Search for idea to replace '${refText}'`,
|
|
355
|
-
command: REQLAN_SEARCH_REFERENCE_COMMAND
|
|
356
|
-
arguments: [args]
|
|
371
|
+
command: REQLAN_SEARCH_REFERENCE_COMMAND
|
|
357
372
|
}
|
|
358
373
|
};
|
|
359
374
|
}
|
|
@@ -385,6 +400,26 @@ function isResolvableIdeaProperty(property: string | undefined): boolean {
|
|
|
385
400
|
return property === 'idea' || property === 'ideaset' || property === 'members';
|
|
386
401
|
}
|
|
387
402
|
|
|
403
|
+
/** True when `only` is unset or the action kind matches a requested kind (LSP subtype rules). */
|
|
404
|
+
export function actionKindAllowed(only: readonly string[] | undefined, kind: string): boolean {
|
|
405
|
+
if (!only || only.length === 0) {
|
|
406
|
+
return true;
|
|
407
|
+
}
|
|
408
|
+
return only.some(requested =>
|
|
409
|
+
kind === requested
|
|
410
|
+
|| kind.startsWith(`${requested}.`)
|
|
411
|
+
|| requested.startsWith(`${kind}.`)
|
|
412
|
+
);
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
function documentHasTopLevelIdeas(document: LangiumDocument): boolean {
|
|
416
|
+
const model = document.parseResult.value;
|
|
417
|
+
if (!isModel(model)) {
|
|
418
|
+
return false;
|
|
419
|
+
}
|
|
420
|
+
return model.elements.some(element => isRefactorIdeaDeclaration(element));
|
|
421
|
+
}
|
|
422
|
+
|
|
388
423
|
function rangesOverlap(
|
|
389
424
|
left: { start: { line: number; character: number }; end: { line: number; character: number } },
|
|
390
425
|
right: { start: { line: number; character: number }; end: { line: number; character: number } }
|
|
@@ -169,6 +169,11 @@ export function findCommentSpansInText(text: string): CommentSpan[] {
|
|
|
169
169
|
continue;
|
|
170
170
|
}
|
|
171
171
|
if (char === '/' && next === '*') {
|
|
172
|
+
// Empty slash-star-star-slash is a glob segment, not a block comment (same as .rq lexer).
|
|
173
|
+
if (text[index + 2] === '*' && text[index + 3] === '/') {
|
|
174
|
+
index += 4;
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
172
177
|
blockComment = 'slash';
|
|
173
178
|
lineCommentStart = index;
|
|
174
179
|
index += 2;
|
|
@@ -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 {
|
|
@@ -124,7 +133,7 @@ export class ReqlanCompletionProvider extends DefaultCompletionProvider {
|
|
|
124
133
|
return [];
|
|
125
134
|
}
|
|
126
135
|
return model.elements
|
|
127
|
-
.filter(element => {
|
|
136
|
+
.filter((element): element is typeof element & { name: string } => {
|
|
128
137
|
if (refInfo.property === 'ideaset') {
|
|
129
138
|
return isIdeaSet(element);
|
|
130
139
|
}
|
|
@@ -194,47 +203,92 @@ export class ReqlanCompletionProvider extends DefaultCompletionProvider {
|
|
|
194
203
|
if (!context) {
|
|
195
204
|
return LspCompletionList.create([], true);
|
|
196
205
|
}
|
|
197
|
-
const names = new Set<string>();
|
|
198
206
|
const docs = this.documents.all.toArray();
|
|
199
|
-
if (!docs.some(doc => doc.uri
|
|
207
|
+
if (!docs.some(doc => UriUtils.equals(doc.uri, document.uri))) {
|
|
200
208
|
docs.push(document);
|
|
201
209
|
}
|
|
210
|
+
const candidates = this.collectReferenceCompletionCandidates(docs, context.prefix);
|
|
211
|
+
const center = findContainingIdea(document, params.position)?.name;
|
|
212
|
+
const distances = center
|
|
213
|
+
? hopDistancesFromCenter(center, buildIdeaReferenceAdjacency(docs))
|
|
214
|
+
: new Map<string, number>();
|
|
215
|
+
const items = candidates.map(candidate =>
|
|
216
|
+
this.toReferenceCompletionItem(document, candidate, context, distances)
|
|
217
|
+
).sort((left, right) => {
|
|
218
|
+
const leftDistance = distances.get(String(left.label)) ?? UNREACHABLE_DISTANCE;
|
|
219
|
+
const rightDistance = distances.get(String(right.label)) ?? UNREACHABLE_DISTANCE;
|
|
220
|
+
return leftDistance - rightDistance
|
|
221
|
+
|| String(left.label).localeCompare(String(right.label))
|
|
222
|
+
|| String(left.detail ?? '').localeCompare(String(right.detail ?? ''));
|
|
223
|
+
});
|
|
224
|
+
return LspCompletionList.create(items, true);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/** Collect named ideas/ideasets across loaded documents for bracket/wikilink completion. */
|
|
228
|
+
private collectReferenceCompletionCandidates(
|
|
229
|
+
docs: LangiumDocument[],
|
|
230
|
+
prefix: string
|
|
231
|
+
): ReferenceCompletionCandidate[] {
|
|
232
|
+
const byKey = new Map<string, ReferenceCompletionCandidate>();
|
|
202
233
|
for (const doc of docs) {
|
|
203
234
|
const model = doc.parseResult.value;
|
|
204
235
|
if (!isModel(model)) {
|
|
205
236
|
continue;
|
|
206
237
|
}
|
|
207
238
|
for (const element of model.elements) {
|
|
208
|
-
|
|
209
|
-
|
|
239
|
+
const importable = isIdea(element) || isOneLinerIdea(element);
|
|
240
|
+
if (!importable && !isIdeaSet(element)) {
|
|
241
|
+
continue;
|
|
242
|
+
}
|
|
243
|
+
if (prefix && !element.name.startsWith(prefix)) {
|
|
244
|
+
continue;
|
|
245
|
+
}
|
|
246
|
+
const key = `${doc.uri.toString()}::${element.name}`;
|
|
247
|
+
if (!byKey.has(key)) {
|
|
248
|
+
byKey.set(key, { name: element.name, document: doc, importable });
|
|
210
249
|
}
|
|
211
250
|
}
|
|
212
251
|
}
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
252
|
+
return [...byKey.values()];
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Cross-file idea completions show their import path and may insert a from-import.
|
|
257
|
+
* rq:["../../../reqlan rq/extension/language-support/features-imports.rq".import_code_completion_rendering]
|
|
258
|
+
* rq:["../../../reqlan rq/extension/language-support/features-imports.rq".import_code_completion_auto_file_import]
|
|
259
|
+
*/
|
|
260
|
+
private toReferenceCompletionItem(
|
|
261
|
+
document: LangiumDocument,
|
|
262
|
+
candidate: ReferenceCompletionCandidate,
|
|
263
|
+
context: NonNullable<ReturnType<typeof getReferencePrefixContext>>,
|
|
264
|
+
distances: Map<string, number>
|
|
265
|
+
): CompletionItem {
|
|
266
|
+
const sameFile = UriUtils.equals(candidate.document.uri, document.uri);
|
|
267
|
+
const distance = distances.get(candidate.name) ?? UNREACHABLE_DISTANCE;
|
|
268
|
+
const item: CompletionItem = {
|
|
269
|
+
label: candidate.name,
|
|
270
|
+
kind: CompletionItemKind.Reference,
|
|
271
|
+
sortText: `${String(distance).padStart(4, '0')}_${candidate.name}`,
|
|
272
|
+
textEdit: {
|
|
273
|
+
newText: candidate.name,
|
|
227
274
|
range: {
|
|
228
275
|
start: context.replaceStart,
|
|
229
276
|
end: context.replaceEnd
|
|
230
277
|
}
|
|
231
|
-
}
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
278
|
+
}
|
|
279
|
+
};
|
|
280
|
+
if (sameFile) {
|
|
281
|
+
return item;
|
|
282
|
+
}
|
|
283
|
+
const importPath = relativeRqImportPath(document.uri, candidate.document.uri);
|
|
284
|
+
item.detail = importPath;
|
|
285
|
+
if (candidate.importable) {
|
|
286
|
+
const importEdit = buildFromImportEdit(document, importPath, candidate.name);
|
|
287
|
+
if (importEdit) {
|
|
288
|
+
item.additionalTextEdits = [importEdit];
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
return item;
|
|
238
292
|
}
|
|
239
293
|
|
|
240
294
|
private completeAnonymousImportPath(document: LangiumDocument, params: CompletionParams): CompletionList {
|