@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
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Inlay hints showing inbound references on idea declarations as a computed attribute.
|
|
3
|
+
*
|
|
4
|
+
* Builds a single workspace inbound index per request so large `.rq` files stay responsive
|
|
5
|
+
* (avoids per-idea full-workspace `findAllReferences` scans).
|
|
6
|
+
* rq:["../../../reqlan rq/extension/syntax/features-syntax-highlighting.rq".inbound_inlay_index_performance]
|
|
7
|
+
* rq:["../../../reqlan rq/extension/syntax/features-syntax-highlighting.rq".view_references_as_inlay_hints]
|
|
3
8
|
*/
|
|
4
|
-
import { AstUtils, GrammarUtils, type AstNode, type LangiumDocument } from 'langium';
|
|
9
|
+
import { AstUtils, GrammarUtils, interruptAndCheck, type AstNode, type LangiumDocument } from 'langium';
|
|
5
10
|
import { AbstractInlayHintProvider } from 'langium/lsp';
|
|
6
11
|
import type { CancellationToken } from 'vscode-languageserver';
|
|
7
|
-
import { InlayHintKind, type InlayHint, type InlayHintParams } from 'vscode-languageserver';
|
|
12
|
+
import { CancellationToken as CancelToken, InlayHintKind, type InlayHint, type InlayHintParams } from 'vscode-languageserver';
|
|
8
13
|
import {
|
|
9
14
|
isIdea,
|
|
10
15
|
isIdeaSet,
|
|
@@ -12,7 +17,9 @@ import {
|
|
|
12
17
|
} from './generated/ast.js';
|
|
13
18
|
import {
|
|
14
19
|
buildInboundReferencesInlayLabel,
|
|
15
|
-
|
|
20
|
+
buildInboundReferencerIndex,
|
|
21
|
+
lookupInboundReferencers,
|
|
22
|
+
type InboundReferencerIndex,
|
|
16
23
|
type ReferencedDeclaration
|
|
17
24
|
} from './reqlan-inbound-reference-inlay-label.js';
|
|
18
25
|
import {
|
|
@@ -25,6 +32,7 @@ import type { ReqlanServices } from './reqlan-module.js';
|
|
|
25
32
|
export class ReqlanInlayHintProvider extends AbstractInlayHintProvider {
|
|
26
33
|
|
|
27
34
|
private readonly services: ReqlanServices;
|
|
35
|
+
private inboundIndex: InboundReferencerIndex | undefined;
|
|
28
36
|
|
|
29
37
|
constructor(services: ReqlanServices) {
|
|
30
38
|
super();
|
|
@@ -34,7 +42,7 @@ export class ReqlanInlayHintProvider extends AbstractInlayHintProvider {
|
|
|
34
42
|
override async getInlayHints(
|
|
35
43
|
document: LangiumDocument,
|
|
36
44
|
params: InlayHintParams,
|
|
37
|
-
cancelToken
|
|
45
|
+
cancelToken: CancellationToken = CancelToken.None
|
|
38
46
|
): Promise<InlayHint[] | undefined> {
|
|
39
47
|
const settings = await this.services.shared.workspace.ConfigurationProvider.getConfiguration(
|
|
40
48
|
'reqlan',
|
|
@@ -43,7 +51,28 @@ export class ReqlanInlayHintProvider extends AbstractInlayHintProvider {
|
|
|
43
51
|
if (!referenceInlayHintsEnabled(settings)) {
|
|
44
52
|
return [];
|
|
45
53
|
}
|
|
46
|
-
|
|
54
|
+
// Must throw Langium's OperationCancelled symbol so the LSP maps this to
|
|
55
|
+
// RequestCancelled (-32800), not InternalError (-32603) with "Operation cancelled".
|
|
56
|
+
await interruptAndCheck(cancelToken);
|
|
57
|
+
// One inverse index for the whole request — not findReferences per idea.
|
|
58
|
+
this.inboundIndex = buildInboundReferencerIndex(this.services);
|
|
59
|
+
try {
|
|
60
|
+
const root = document.parseResult.value;
|
|
61
|
+
const inlayHints: InlayHint[] = [];
|
|
62
|
+
const acceptor = (hint: InlayHint): void => {
|
|
63
|
+
inlayHints.push(hint);
|
|
64
|
+
};
|
|
65
|
+
let checked = 0;
|
|
66
|
+
for (const node of AstUtils.streamAst(root, { range: params.range })) {
|
|
67
|
+
if ((++checked & 31) === 0) {
|
|
68
|
+
await interruptAndCheck(cancelToken);
|
|
69
|
+
}
|
|
70
|
+
this.computeInlayHint(node, acceptor);
|
|
71
|
+
}
|
|
72
|
+
return inlayHints;
|
|
73
|
+
} finally {
|
|
74
|
+
this.inboundIndex = undefined;
|
|
75
|
+
}
|
|
47
76
|
}
|
|
48
77
|
|
|
49
78
|
computeInlayHint(node: AstNode, acceptor: (hint: InlayHint) => void): void {
|
|
@@ -64,7 +93,8 @@ export class ReqlanInlayHintProvider extends AbstractInlayHintProvider {
|
|
|
64
93
|
if (!nameNode?.range) {
|
|
65
94
|
return;
|
|
66
95
|
}
|
|
67
|
-
const
|
|
96
|
+
const index = this.inboundIndex ?? buildInboundReferencerIndex(this.services);
|
|
97
|
+
const referencers = lookupInboundReferencers(index, this.services, declaration);
|
|
68
98
|
const document = AstUtils.getDocument(declaration);
|
|
69
99
|
const formatted = buildInboundReferencesInlayLabel(
|
|
70
100
|
referencers,
|
package/src/reqlan-linker.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import type { AstNode, FileSystemProvider, LangiumDocument, LangiumDocuments, ReferenceInfo } from 'langium';
|
|
7
7
|
import { AstUtils, DefaultLinker, type DefaultReference } from 'langium';
|
|
8
|
-
import { isLocalReference, isModel, isQualifiedReference } from './generated/ast.js';
|
|
8
|
+
import { isIdea, isIdeaSet, isLocalReference, isModel, isOneLinerIdea, isQualifiedReference } from './generated/ast.js';
|
|
9
9
|
import { isOpaqueFileReferencePath } from './reqlan-file-references.js';
|
|
10
10
|
import { findNamespaceImportByAlias } from './reqlan-import-bindings.js';
|
|
11
11
|
import { isResolvableImportPath } from './reqlan-imports.js';
|
|
@@ -69,7 +69,9 @@ export class ReqlanLinker extends DefaultLinker {
|
|
|
69
69
|
return false;
|
|
70
70
|
}
|
|
71
71
|
const name = refInfo.reference.$refText;
|
|
72
|
-
if (model.elements.some(element =>
|
|
72
|
+
if (model.elements.some(element =>
|
|
73
|
+
(isIdea(element) || isOneLinerIdea(element) || isIdeaSet(element)) && element.name === name
|
|
74
|
+
)) {
|
|
73
75
|
return false;
|
|
74
76
|
}
|
|
75
77
|
return findNamespaceImportByAlias(model.imports, name) !== undefined;
|
|
@@ -40,6 +40,8 @@ export function isMarkdownLinkLabelPosition(document: LangiumDocument, position:
|
|
|
40
40
|
const offset = document.textDocument.offsetAt(position);
|
|
41
41
|
const root = document.parseResult.value.$cstNode;
|
|
42
42
|
if (root) {
|
|
43
|
+
// The CST covers the entire document. Walk up from the leaf; if no MarkdownLink ancestor
|
|
44
|
+
// is found, the position is definitively not inside a markdown label — no text scan needed.
|
|
43
45
|
const leaf = CstUtils.findLeafNodeAtOffset(root, offset);
|
|
44
46
|
let current: CstNode | undefined = leaf;
|
|
45
47
|
while (current) {
|
|
@@ -52,7 +54,9 @@ export function isMarkdownLinkLabelPosition(document: LangiumDocument, position:
|
|
|
52
54
|
}
|
|
53
55
|
current = current.container;
|
|
54
56
|
}
|
|
57
|
+
return false;
|
|
55
58
|
}
|
|
59
|
+
// No CST (e.g. plain-text document) — fall back to text scan.
|
|
56
60
|
return isMarkdownLinkLabelOffsetInText(document.textDocument.getText(), offset);
|
|
57
61
|
}
|
|
58
62
|
|
package/src/reqlan-module.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type Module, inject } from 'langium';
|
|
1
|
+
import { type Module, inject, type PartialLangiumSharedCoreServices } from 'langium';
|
|
2
2
|
import { createDefaultModule, createDefaultSharedModule, type DefaultSharedModuleContext, type LangiumServices, type LangiumSharedServices, type PartialLangiumServices } from 'langium/lsp';
|
|
3
3
|
import { ReqlanGeneratedModule, ReqlanGeneratedSharedModule } from './generated/module.js';
|
|
4
4
|
import { ReqlanDefinitionProvider } from './reqlan-definition-provider.js';
|
|
@@ -18,6 +18,9 @@ import { ReqlanSemanticTokenProvider } from './reqlan-semantic-token-provider.js
|
|
|
18
18
|
import { ReqlanTokenBuilder } from './reqlan-token-builder.js';
|
|
19
19
|
import { registerRqIgnoreErrorFiltering } from './reqlan-ignore-error.js';
|
|
20
20
|
import { ReqlanValidator, registerValidationChecks } from './reqlan-validator.js';
|
|
21
|
+
import { ReqlanWorkspaceManager } from './reqlan-workspace-manager.js';
|
|
22
|
+
import { ReqlanAsyncParser } from './reqlan-async-parser.js';
|
|
23
|
+
import { ReqlanLangiumDocumentFactory } from './reqlan-document-factory.js';
|
|
21
24
|
|
|
22
25
|
/**
|
|
23
26
|
* Declaration of custom services - add your own service classes here.
|
|
@@ -39,6 +42,13 @@ export type ReqlanServices = LangiumServices & ReqlanAddedServices
|
|
|
39
42
|
* declared custom services. The Langium defaults can be partially specified to override only
|
|
40
43
|
* selected services, while the custom services must be fully specified.
|
|
41
44
|
*/
|
|
45
|
+
export const ReqlanSharedModule: Module<LangiumSharedServices, PartialLangiumSharedCoreServices> = {
|
|
46
|
+
workspace: {
|
|
47
|
+
WorkspaceManager: services => new ReqlanWorkspaceManager(services),
|
|
48
|
+
LangiumDocumentFactory: services => new ReqlanLangiumDocumentFactory(services)
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
|
|
42
52
|
export const ReqlanModule: Module<ReqlanServices, PartialLangiumServices & ReqlanAddedServices> = {
|
|
43
53
|
validation: {
|
|
44
54
|
ReqlanValidator: services => new ReqlanValidator(services)
|
|
@@ -51,7 +61,8 @@ export const ReqlanModule: Module<ReqlanServices, PartialLangiumServices & Reqla
|
|
|
51
61
|
},
|
|
52
62
|
parser: {
|
|
53
63
|
GrammarConfig: createReqlanGrammarConfig,
|
|
54
|
-
TokenBuilder: () => new ReqlanTokenBuilder()
|
|
64
|
+
TokenBuilder: () => new ReqlanTokenBuilder(),
|
|
65
|
+
AsyncParser: (services: ReqlanServices) => new ReqlanAsyncParser(services)
|
|
55
66
|
},
|
|
56
67
|
lsp: {
|
|
57
68
|
DefinitionProvider: services => new ReqlanDefinitionProvider(services),
|
|
@@ -87,7 +98,8 @@ export function createReqlanServices(context: DefaultSharedModuleContext): {
|
|
|
87
98
|
} {
|
|
88
99
|
const shared = inject(
|
|
89
100
|
createDefaultSharedModule(context),
|
|
90
|
-
ReqlanGeneratedSharedModule
|
|
101
|
+
ReqlanGeneratedSharedModule,
|
|
102
|
+
ReqlanSharedModule
|
|
91
103
|
);
|
|
92
104
|
const Reqlan = inject(
|
|
93
105
|
createDefaultModule({ shared }),
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-file parse/lex budget: if a document cannot finish within the budget,
|
|
3
|
+
* return an empty model plus warning + error diagnostics instead of hanging the host.
|
|
4
|
+
* rq:["../../../reqlan rq/language/parser_lexer.rq".parse_budget_timeout]
|
|
5
|
+
*/
|
|
6
|
+
import type { AstNode, LangiumParser, ParseResult } from 'langium';
|
|
7
|
+
|
|
8
|
+
/** Default wall-clock budget for lex+parse of one file (worker-enforced). */
|
|
9
|
+
export const DEFAULT_PARSE_BUDGET_MS = 8_000;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Prefer in-process sync parse below this size. Worker threads (killable budget)
|
|
13
|
+
* are reserved for larger inputs, hang-sentinel tests, sticky timeout escalation,
|
|
14
|
+
* or an explicit force.
|
|
15
|
+
*/
|
|
16
|
+
export const DEFAULT_WORKER_PARSE_THRESHOLD_CHARS = 64_000;
|
|
17
|
+
|
|
18
|
+
/** Test-only sentinel: a worker that sees this marker hangs until terminated. */
|
|
19
|
+
export const PARSE_HANG_SENTINEL = '__REQLAN_PARSE_HANG__';
|
|
20
|
+
|
|
21
|
+
export const PARSE_TIMEOUT_WARNING =
|
|
22
|
+
'Parse budget exceeded for this file; semantic features may be incomplete until the file is simplified or the budget is raised.';
|
|
23
|
+
|
|
24
|
+
export function parseTimeoutErrorMessage(timeoutMs: number): string {
|
|
25
|
+
return `Failed to lex/parse this file within ${timeoutMs}ms; left unloaded so the rest of the workspace can continue.`;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function parseFailureErrorMessage(cause: string): string {
|
|
29
|
+
return `Lex/parse aborted for this file (${cause}); left unloaded so the rest of the workspace can continue.`;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export type ParseWorkerMode = boolean | 'auto';
|
|
33
|
+
|
|
34
|
+
export interface ParseWorkerEscalationOptions {
|
|
35
|
+
/** `auto` (default): sync below threshold; worker for large / sentinel / force. */
|
|
36
|
+
useWorker?: ParseWorkerMode;
|
|
37
|
+
forceWorker?: boolean;
|
|
38
|
+
thresholdChars?: number;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Whether this parse should run in a killable worker thread. */
|
|
42
|
+
export function shouldEscalateToParseWorker(
|
|
43
|
+
text: string,
|
|
44
|
+
options: ParseWorkerEscalationOptions = {}
|
|
45
|
+
): boolean {
|
|
46
|
+
const mode = options.useWorker ?? 'auto';
|
|
47
|
+
if (mode === false) {
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
if (mode === true || options.forceWorker) {
|
|
51
|
+
return true;
|
|
52
|
+
}
|
|
53
|
+
if (text.includes(PARSE_HANG_SENTINEL)) {
|
|
54
|
+
return true;
|
|
55
|
+
}
|
|
56
|
+
const threshold = options.thresholdChars ?? DEFAULT_WORKER_PARSE_THRESHOLD_CHARS;
|
|
57
|
+
return text.length >= threshold;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Lexer error plus optional severity used for budget-timeout warnings. */
|
|
61
|
+
export type ReqlanLexerError = ParseResult['lexerErrors'][number] & {
|
|
62
|
+
severity?: 'warning' | 'error' | 'info' | 'hint';
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
export interface ReqlanParseResult<T extends AstNode = AstNode> extends Omit<ParseResult<T>, 'lexerErrors'> {
|
|
66
|
+
lexerErrors: ReqlanLexerError[];
|
|
67
|
+
/** Set when lex/parse did not complete successfully within budget (timeout or crash). */
|
|
68
|
+
reqlanIncomplete?: {
|
|
69
|
+
reason: 'timeout' | 'failure';
|
|
70
|
+
timeoutMs?: number;
|
|
71
|
+
cause?: string;
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function isReqlanIncompleteParseResult(
|
|
76
|
+
result: ParseResult
|
|
77
|
+
): result is ReqlanParseResult {
|
|
78
|
+
return Boolean((result as ReqlanParseResult).reqlanIncomplete);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
interface PlaceholderToken {
|
|
82
|
+
image: string;
|
|
83
|
+
startOffset: number;
|
|
84
|
+
endOffset: number;
|
|
85
|
+
startLine: number;
|
|
86
|
+
endLine: number;
|
|
87
|
+
startColumn: number;
|
|
88
|
+
endColumn: number;
|
|
89
|
+
tokenTypeIdx: number;
|
|
90
|
+
tokenType: { name: string };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function placeholderToken(): PlaceholderToken {
|
|
94
|
+
return {
|
|
95
|
+
image: '',
|
|
96
|
+
startOffset: Number.NaN,
|
|
97
|
+
endOffset: Number.NaN,
|
|
98
|
+
startLine: Number.NaN,
|
|
99
|
+
endLine: Number.NaN,
|
|
100
|
+
startColumn: Number.NaN,
|
|
101
|
+
endColumn: Number.NaN,
|
|
102
|
+
tokenTypeIdx: -1,
|
|
103
|
+
tokenType: { name: 'EOF' }
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function timeoutParserError(message: string) {
|
|
108
|
+
return {
|
|
109
|
+
name: 'ReqlanParseTimeout',
|
|
110
|
+
message,
|
|
111
|
+
token: placeholderToken(),
|
|
112
|
+
resyncedTokens: [],
|
|
113
|
+
context: {
|
|
114
|
+
ruleStack: [],
|
|
115
|
+
ruleOccurrenceStack: []
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function timeoutLexerWarning(message: string): ReqlanLexerError {
|
|
121
|
+
return {
|
|
122
|
+
offset: 0,
|
|
123
|
+
line: 1,
|
|
124
|
+
column: 1,
|
|
125
|
+
length: 1,
|
|
126
|
+
message,
|
|
127
|
+
severity: 'warning'
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Empty model with a lexer warning and a parser error so the editor shows both severities.
|
|
133
|
+
*/
|
|
134
|
+
export function createIncompleteParseResult<T extends AstNode>(
|
|
135
|
+
parser: LangiumParser,
|
|
136
|
+
options: {
|
|
137
|
+
reason: 'timeout' | 'failure';
|
|
138
|
+
timeoutMs?: number;
|
|
139
|
+
cause?: string;
|
|
140
|
+
}
|
|
141
|
+
): ReqlanParseResult<T> {
|
|
142
|
+
const empty = parser.parse('') as ReqlanParseResult<T>;
|
|
143
|
+
const errorMessage = options.reason === 'timeout'
|
|
144
|
+
? parseTimeoutErrorMessage(options.timeoutMs ?? DEFAULT_PARSE_BUDGET_MS)
|
|
145
|
+
: parseFailureErrorMessage(options.cause ?? 'unexpected error');
|
|
146
|
+
// Casts: Chevrotain exception shapes are structural; Langium only needs message + token positions.
|
|
147
|
+
empty.parserErrors = [timeoutParserError(errorMessage) as ReqlanParseResult<T>['parserErrors'][number]];
|
|
148
|
+
empty.lexerErrors = [timeoutLexerWarning(PARSE_TIMEOUT_WARNING)];
|
|
149
|
+
empty.reqlanIncomplete = {
|
|
150
|
+
reason: options.reason,
|
|
151
|
+
timeoutMs: options.timeoutMs,
|
|
152
|
+
cause: options.cause
|
|
153
|
+
};
|
|
154
|
+
return empty;
|
|
155
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Langium worker-thread parse entry: sync lex/parse, dehydrate, postMessage.
|
|
3
|
+
* The parent enforces the wall-clock budget via worker.terminate().
|
|
4
|
+
* rq:["../../../reqlan rq/language/parser_lexer.rq".parse_budget_timeout]
|
|
5
|
+
*/
|
|
6
|
+
import { parentPort } from 'node:worker_threads';
|
|
7
|
+
import { EmptyFileSystem } from 'langium';
|
|
8
|
+
import { createReqlanServices } from './reqlan-module.js';
|
|
9
|
+
import { PARSE_HANG_SENTINEL } from './reqlan-parse-budget.js';
|
|
10
|
+
|
|
11
|
+
if (!parentPort) {
|
|
12
|
+
throw new Error('reqlan-parse-worker must run as a worker thread');
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const { Reqlan } = createReqlanServices(EmptyFileSystem);
|
|
16
|
+
const parser = Reqlan.parser.LangiumParser;
|
|
17
|
+
const hydrator = Reqlan.serializer.Hydrator;
|
|
18
|
+
|
|
19
|
+
parentPort.on('message', (text: unknown) => {
|
|
20
|
+
const input = typeof text === 'string' ? text : '';
|
|
21
|
+
if (input.includes(PARSE_HANG_SENTINEL)) {
|
|
22
|
+
// Intentional hang for tests — parent terminates on budget expiry.
|
|
23
|
+
for (;;) {
|
|
24
|
+
/* spin */
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
try {
|
|
28
|
+
const result = parser.parse(input);
|
|
29
|
+
parentPort!.postMessage(hydrator.dehydrate(result));
|
|
30
|
+
} catch (error) {
|
|
31
|
+
// Surface as worker 'error' so ParserWorker rejects instead of hydrating garbage.
|
|
32
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
33
|
+
throw new Error(`reqlan parse worker failed: ${message}`);
|
|
34
|
+
}
|
|
35
|
+
});
|
|
@@ -60,6 +60,13 @@ export interface PathResolveContext {
|
|
|
60
60
|
* `null` skips loading and uses defaults only.
|
|
61
61
|
*/
|
|
62
62
|
config?: RqConfig | null;
|
|
63
|
+
/**
|
|
64
|
+
* Shared cache for resolved configs, keyed by directory URI string.
|
|
65
|
+
* When provided, `resolveRqConfig` reads and populates this cache instead of hitting the filesystem
|
|
66
|
+
* on every call. Lives on the workspace manager so it is scoped to a service instance and cleared
|
|
67
|
+
* on workspace reinitialisation.
|
|
68
|
+
*/
|
|
69
|
+
configCache?: Map<string, RqConfig | undefined>;
|
|
63
70
|
}
|
|
64
71
|
|
|
65
72
|
export function defaultRqConfig(): RqConfig {
|
|
@@ -297,9 +304,21 @@ export function resolveRqConfig(document: LangiumDocument, context?: PathResolve
|
|
|
297
304
|
return context.config;
|
|
298
305
|
}
|
|
299
306
|
if (context?.fileSystem) {
|
|
300
|
-
const
|
|
301
|
-
|
|
302
|
-
|
|
307
|
+
const dirKey = UriUtils.dirname(document.uri).toString();
|
|
308
|
+
const cache = context.configCache;
|
|
309
|
+
if (cache) {
|
|
310
|
+
if (!cache.has(dirKey)) {
|
|
311
|
+
cache.set(dirKey, loadApplyingRqConfig(UriUtils.dirname(document.uri), context.fileSystem));
|
|
312
|
+
}
|
|
313
|
+
const cached = cache.get(dirKey);
|
|
314
|
+
if (cached) {
|
|
315
|
+
return cached;
|
|
316
|
+
}
|
|
317
|
+
} else {
|
|
318
|
+
const loaded = loadApplyingRqConfig(UriUtils.dirname(document.uri), context.fileSystem);
|
|
319
|
+
if (loaded) {
|
|
320
|
+
return loaded;
|
|
321
|
+
}
|
|
303
322
|
}
|
|
304
323
|
}
|
|
305
324
|
return defaultRqConfig();
|
|
@@ -351,7 +370,10 @@ export function workspaceFolderUrisFromManager(
|
|
|
351
370
|
export function pathResolveContextFromServices(services: {
|
|
352
371
|
shared: {
|
|
353
372
|
workspace: {
|
|
354
|
-
WorkspaceManager: {
|
|
373
|
+
WorkspaceManager: {
|
|
374
|
+
workspaceFolders?: ReadonlyArray<{ uri: string }>;
|
|
375
|
+
rqConfigCache?: Map<string, RqConfig | undefined>;
|
|
376
|
+
};
|
|
355
377
|
FileSystemProvider: FileSystemProvider;
|
|
356
378
|
};
|
|
357
379
|
};
|
|
@@ -360,6 +382,7 @@ export function pathResolveContextFromServices(services: {
|
|
|
360
382
|
fileSystem: services.shared.workspace.FileSystemProvider,
|
|
361
383
|
workspaceFolderUris: workspaceFolderUrisFromManager(
|
|
362
384
|
services.shared.workspace.WorkspaceManager.workspaceFolders
|
|
363
|
-
)
|
|
385
|
+
),
|
|
386
|
+
configCache: services.shared.workspace.WorkspaceManager.rqConfigCache
|
|
364
387
|
};
|
|
365
388
|
}
|
|
@@ -15,6 +15,9 @@ import {
|
|
|
15
15
|
} from './reqlan-idea-reference-site.js';
|
|
16
16
|
import { isIdea, isOneLinerIdea, type IdeaDeclaration } from './generated/ast.js';
|
|
17
17
|
|
|
18
|
+
/** Custom LSP request: resolve search/wrap site at a range (no command-arg cache). */
|
|
19
|
+
export const REQLAN_REFERENCE_SEARCH_SITE_REQUEST = 'reqlan/referenceSearchSite';
|
|
20
|
+
|
|
18
21
|
export type ReferenceSearchMode = 'replace' | 'wrap';
|
|
19
22
|
|
|
20
23
|
export interface ReferenceSearchContext {
|
|
@@ -35,6 +38,28 @@ export interface ReferenceSearchSite {
|
|
|
35
38
|
context?: ReferenceSearchContext;
|
|
36
39
|
}
|
|
37
40
|
|
|
41
|
+
/** Result of {@link REQLAN_REFERENCE_SEARCH_SITE_REQUEST} / extension command payload. */
|
|
42
|
+
export interface ReferenceSearchSiteRequestResult {
|
|
43
|
+
documentUri: string;
|
|
44
|
+
refText: string;
|
|
45
|
+
range: Range;
|
|
46
|
+
mode: ReferenceSearchMode;
|
|
47
|
+
context?: ReferenceSearchContext;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function toReferenceSearchCommandArgs(
|
|
51
|
+
documentUri: string,
|
|
52
|
+
site: ReferenceSearchSite
|
|
53
|
+
): ReferenceSearchSiteRequestResult {
|
|
54
|
+
return {
|
|
55
|
+
documentUri,
|
|
56
|
+
refText: site.refText,
|
|
57
|
+
range: site.range,
|
|
58
|
+
mode: site.mode,
|
|
59
|
+
context: site.context
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
38
63
|
/**
|
|
39
64
|
* Prefer an existing [reference]/[[wikilink]] under the cursor; otherwise a
|
|
40
65
|
* selection or word in idea-body prose that can be wrapped as `[name]`.
|
package/src/reqlan-scope.ts
CHANGED
|
@@ -43,10 +43,25 @@ export class ReqlanScopeComputation extends DefaultScopeComputation {
|
|
|
43
43
|
}
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
+
interface ScopeEntry {
|
|
47
|
+
version: number | undefined;
|
|
48
|
+
scope: Scope;
|
|
49
|
+
}
|
|
50
|
+
|
|
46
51
|
export class ReqlanScopeProvider extends DefaultScopeProvider {
|
|
47
52
|
|
|
48
53
|
protected readonly documents: ReqlanServices['shared']['workspace']['LangiumDocuments'];
|
|
49
54
|
private readonly services: ReqlanServices;
|
|
55
|
+
/**
|
|
56
|
+
* Per-document cache for `scopeForFileIdeas`. Key is the document URI string.
|
|
57
|
+
* Invalidated when the document version changes so edits always get a fresh scope.
|
|
58
|
+
*/
|
|
59
|
+
private readonly fileIdeasCache = new Map<string, ScopeEntry>();
|
|
60
|
+
/**
|
|
61
|
+
* Per-(document, importPath) cache for `scopeForImportPath`. Key is the imported document URI string.
|
|
62
|
+
* The cached scope is keyed to the imported document's version.
|
|
63
|
+
*/
|
|
64
|
+
private readonly importPathCache = new Map<string, ScopeEntry>();
|
|
50
65
|
|
|
51
66
|
constructor(services: ReqlanServices) {
|
|
52
67
|
super(services);
|
|
@@ -120,6 +135,17 @@ export class ReqlanScopeProvider extends DefaultScopeProvider {
|
|
|
120
135
|
}
|
|
121
136
|
|
|
122
137
|
private scopeForFileIdeas(document: LangiumDocument): Scope {
|
|
138
|
+
const key = document.uri.toString();
|
|
139
|
+
const cached = this.fileIdeasCache.get(key);
|
|
140
|
+
if (cached && cached.version === document.textDocument.version) {
|
|
141
|
+
return cached.scope;
|
|
142
|
+
}
|
|
143
|
+
const scope = this.buildFileIdeasScope(document);
|
|
144
|
+
this.fileIdeasCache.set(key, { version: document.textDocument.version, scope });
|
|
145
|
+
return scope;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
private buildFileIdeasScope(document: LangiumDocument): Scope {
|
|
123
149
|
const model = document.parseResult.value as Model;
|
|
124
150
|
if (!isModel(model)) {
|
|
125
151
|
return new StreamScope(stream([]));
|
|
@@ -279,6 +305,11 @@ export class ReqlanScopeProvider extends DefaultScopeProvider {
|
|
|
279
305
|
if (!imported) {
|
|
280
306
|
return undefined;
|
|
281
307
|
}
|
|
308
|
+
const key = imported.uri.toString();
|
|
309
|
+
const cached = this.importPathCache.get(key);
|
|
310
|
+
if (cached && cached.version === imported.textDocument.version) {
|
|
311
|
+
return cached.scope;
|
|
312
|
+
}
|
|
282
313
|
const model = imported.parseResult.value;
|
|
283
314
|
if (!isModel(model)) {
|
|
284
315
|
return undefined;
|
|
@@ -286,7 +317,9 @@ export class ReqlanScopeProvider extends DefaultScopeProvider {
|
|
|
286
317
|
const descriptions = model.elements
|
|
287
318
|
.filter(element => isIdea(element) || isOneLinerIdea(element))
|
|
288
319
|
.map(idea => this.descriptions.createDescription(idea, idea.name, imported));
|
|
289
|
-
|
|
320
|
+
const scope = new StreamScope(stream(descriptions));
|
|
321
|
+
this.importPathCache.set(key, { version: imported.textDocument.version, scope });
|
|
322
|
+
return scope;
|
|
290
323
|
}
|
|
291
324
|
|
|
292
325
|
private findImportedDocument(path: string, document: LangiumDocument): LangiumDocument | undefined {
|
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
isQualifiedImport,
|
|
21
21
|
isQualifiedReference,
|
|
22
22
|
isOneLinerIdea,
|
|
23
|
+
isWildcardReference,
|
|
23
24
|
type Import
|
|
24
25
|
} from './generated/ast.js';
|
|
25
26
|
import { isWellFormedFromImport } from './reqlan-import-bindings.js';
|
|
@@ -50,6 +51,11 @@ export class ReqlanSemanticTokenProvider extends AbstractSemanticTokenProvider {
|
|
|
50
51
|
});
|
|
51
52
|
return;
|
|
52
53
|
}
|
|
54
|
+
if (isWildcardReference(node)) {
|
|
55
|
+
acceptor({ node, property: 'pathPattern', type: SemanticTokenTypes.string });
|
|
56
|
+
acceptor({ node, property: 'ideaPattern', type: SemanticTokenTypes.variable });
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
53
59
|
if (isQualifiedReference(node)) {
|
|
54
60
|
if (node.qualifier) {
|
|
55
61
|
acceptor({ node, property: 'qualifier', type: SemanticTokenTypes.namespace });
|