@reqlan/language 1.9.6 → 1.10.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 +42 -0
- package/README.template.md +2 -0
- package/out/file-path-rewrite.d.ts +1 -1
- package/out/generated/ast.d.ts +2 -2
- package/out/generated/ast.js +1 -1
- package/out/generated/ast.js.map +1 -1
- package/out/generated/grammar.js +1 -1
- package/out/generated/grammar.js.map +1 -1
- package/out/index.d.ts +3 -1
- package/out/index.js +3 -1
- package/out/index.js.map +1 -1
- package/out/reqlan-code-action-provider.d.ts +1 -0
- package/out/reqlan-code-action-provider.js +12 -21
- package/out/reqlan-code-action-provider.js.map +1 -1
- package/out/reqlan-comment-diagnostics.d.ts +11 -0
- package/out/reqlan-comment-diagnostics.js +81 -11
- package/out/reqlan-comment-diagnostics.js.map +1 -1
- package/out/reqlan-comment-resolver.d.ts +1 -1
- package/out/reqlan-completion-provider.d.ts +2 -2
- package/out/reqlan-definition-provider.js +1 -1
- package/out/reqlan-definition-provider.js.map +1 -1
- package/out/reqlan-document-builder.d.ts +9 -0
- package/out/reqlan-document-builder.js +26 -0
- package/out/reqlan-document-builder.js.map +1 -0
- package/out/reqlan-document-link-provider.d.ts +5 -1
- package/out/reqlan-document-link-provider.js +9 -1
- package/out/reqlan-document-link-provider.js.map +1 -1
- package/out/reqlan-embedded-file-references.d.ts +3 -0
- package/out/reqlan-embedded-file-references.js +47 -5
- package/out/reqlan-embedded-file-references.js.map +1 -1
- package/out/reqlan-file-link-resolver.d.ts +8 -0
- package/out/reqlan-file-link-resolver.js +46 -11
- package/out/reqlan-file-link-resolver.js.map +1 -1
- package/out/reqlan-file-references.d.ts +1 -0
- package/out/reqlan-file-references.js +24 -16
- package/out/reqlan-file-references.js.map +1 -1
- package/out/reqlan-idea-move-imports.d.ts +24 -0
- package/out/reqlan-idea-move-imports.js +309 -0
- package/out/reqlan-idea-move-imports.js.map +1 -0
- package/out/reqlan-idea-refactor.d.ts +12 -4
- package/out/reqlan-idea-refactor.js +85 -13
- package/out/reqlan-idea-refactor.js.map +1 -1
- package/out/reqlan-ignore-error.d.ts +2 -1
- package/out/reqlan-ignore-error.js +4 -17
- package/out/reqlan-ignore-error.js.map +1 -1
- package/out/reqlan-import-edits.d.ts +1 -0
- package/out/reqlan-import-edits.js +8 -0
- package/out/reqlan-import-edits.js.map +1 -1
- package/out/reqlan-imports.d.ts +3 -1
- package/out/reqlan-imports.js +2 -5
- package/out/reqlan-imports.js.map +1 -1
- package/out/reqlan-module.js +4 -1
- package/out/reqlan-module.js.map +1 -1
- package/out/reqlan-reference-at-position.d.ts +3 -0
- package/out/reqlan-reference-at-position.js +8 -2
- package/out/reqlan-reference-at-position.js.map +1 -1
- package/out/reqlan-scope.d.ts +14 -0
- package/out/reqlan-scope.js +58 -15
- package/out/reqlan-scope.js.map +1 -1
- package/out/reqlan-validator.d.ts +12 -1
- package/out/reqlan-validator.js +38 -4
- package/out/reqlan-validator.js.map +1 -1
- package/package.json +3 -2
- package/src/file-path-rewrite.ts +1 -1
- package/src/generated/ast.ts +2 -2
- package/src/generated/grammar.ts +1 -1
- package/src/index.ts +9 -0
- package/src/reqlan-code-action-provider.ts +12 -24
- package/src/reqlan-comment-diagnostics.ts +108 -11
- package/src/reqlan-comment-resolver.ts +1 -1
- package/src/reqlan-completion-provider.ts +2 -2
- package/src/reqlan-definition-provider.ts +1 -1
- package/src/reqlan-document-builder.ts +35 -0
- package/src/reqlan-document-link-provider.ts +23 -3
- package/src/reqlan-embedded-file-references.ts +54 -5
- package/src/reqlan-file-link-resolver.ts +56 -11
- package/src/reqlan-file-references.ts +26 -16
- package/src/reqlan-idea-move-imports.ts +398 -0
- package/src/reqlan-idea-refactor.ts +116 -14
- package/src/reqlan-ignore-error.ts +4 -18
- package/src/reqlan-import-edits.ts +9 -0
- package/src/reqlan-imports.ts +4 -5
- package/src/reqlan-module.ts +4 -1
- package/src/reqlan-reference-at-position.ts +16 -2
- package/src/reqlan-scope.ts +73 -16
- package/src/reqlan-validator.ts +48 -4
- package/src/reqlan.langium +4 -1
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
* Code completion for references, import paths, file paths, and attributes.
|
|
3
3
|
* rq:["../../../reqlan rq/extension/configuration.rq".configuration_import_roots]
|
|
4
4
|
* rq:["../../../reqlan rq/language/imports.rq".configuration_import_root_alias]
|
|
5
|
-
* rq:["../../../reqlan rq/extension/features-syntax-highlighting.rq".reference_code_completion]
|
|
6
|
-
* rq:["../../../reqlan rq/extension/features-syntax-highlighting.rq".reference_code_completion_sequencing]
|
|
5
|
+
* rq:["../../../reqlan rq/extension/syntax/features-syntax-highlighting.rq".reference_code_completion]
|
|
6
|
+
* rq:["../../../reqlan rq/extension/syntax/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
9
|
* rq:["../../../reqlan rq/extension/language-support/features-imports.rq".import_code_completion_rendering]
|
|
@@ -278,7 +278,7 @@ export class ReqlanDefinitionProvider extends DefaultDefinitionProvider {
|
|
|
278
278
|
|
|
279
279
|
private createFileLink(source: CstNode, reference: FileReference | FileSymbolReference): GoToLink | undefined {
|
|
280
280
|
const resolved = resolveFileReferenceLink(reference, this.documents, this.fileSystem, this.pathContext());
|
|
281
|
-
if (!resolved) {
|
|
281
|
+
if (!resolved || resolved.resolution === 'missing') {
|
|
282
282
|
return undefined;
|
|
283
283
|
}
|
|
284
284
|
const targetDocument = this.documents.getDocument(URI.parse(resolved.targetUri));
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rebuilds documents whose comment-reference diagnostics depend on other `.rq` files.
|
|
3
|
+
* rq:["../../../reqlan rq/extension/features-non-rq-code-comment/functional-code-comment-references.rq".comment_reference_resolution_error_state]
|
|
4
|
+
*/
|
|
5
|
+
import {
|
|
6
|
+
DefaultDocumentBuilder,
|
|
7
|
+
type LangiumDocument,
|
|
8
|
+
type LangiumSharedCoreServices
|
|
9
|
+
} from 'langium';
|
|
10
|
+
import { shouldRelinkCommentReferences } from './reqlan-comment-diagnostics.js';
|
|
11
|
+
import { pathResolveContextFromServices } from './reqlan-path-resolve.js';
|
|
12
|
+
|
|
13
|
+
export class ReqlanDocumentBuilder extends DefaultDocumentBuilder {
|
|
14
|
+
constructor(services: LangiumSharedCoreServices) {
|
|
15
|
+
super(services);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
protected override shouldRelink(document: LangiumDocument, changedUris: Set<string>): boolean {
|
|
19
|
+
if (super.shouldRelink(document, changedUris)) {
|
|
20
|
+
return true;
|
|
21
|
+
}
|
|
22
|
+
return shouldRelinkCommentReferences(
|
|
23
|
+
document,
|
|
24
|
+
changedUris,
|
|
25
|
+
pathResolveContextFromServices({
|
|
26
|
+
shared: {
|
|
27
|
+
workspace: {
|
|
28
|
+
WorkspaceManager: this.workspaceManager(),
|
|
29
|
+
FileSystemProvider: this.fileSystemProvider
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
})
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -1,11 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Clickable document links for idea references, file references, and
|
|
2
|
+
* Clickable document links for idea references, file references, import paths, and comment references.
|
|
3
3
|
* Same-file and cross-file idea refs both get links so the editor underline is consistent.
|
|
4
|
+
* Comment-reference links come from the same presentation as the missing-idea underline.
|
|
5
|
+
* Missing file refs get an error underline and no document link.
|
|
6
|
+
* rq:["../../../reqlan rq/extension/features-non-rq-code-comment/functional-code-comment-references.rq".comment_reference_resolution_error_state]
|
|
7
|
+
* rq:["../../../reqlan rq/extension/language-support/language-server-errors.rq".file_reference_errors]
|
|
4
8
|
*/
|
|
5
9
|
import type { LangiumDocument } from 'langium';
|
|
10
|
+
import { URI } from 'langium';
|
|
6
11
|
import type { DocumentLinkProvider } from 'langium/lsp';
|
|
7
12
|
import type { DocumentLink, DocumentLinkParams } from 'vscode-languageserver';
|
|
8
13
|
import { DocumentLink as LspDocumentLink } from 'vscode-languageserver';
|
|
14
|
+
import { presentCommentReferencesForDocument } from './reqlan-comment-diagnostics.js';
|
|
9
15
|
import { collectFileLinks, resolvedFileLinkTargetUri } from './reqlan-file-link-resolver.js';
|
|
10
16
|
import { folderReferenceCommandTarget } from './reqlan-reference-at-position.js';
|
|
11
17
|
import { wildcardReferenceCommandTarget } from './reqlan-wildcard-resolve.js';
|
|
@@ -25,11 +31,12 @@ export class ReqlanDocumentLinkProvider implements DocumentLinkProvider {
|
|
|
25
31
|
}
|
|
26
32
|
|
|
27
33
|
getDocumentLinks(document: LangiumDocument, _params: DocumentLinkParams): DocumentLink[] {
|
|
28
|
-
|
|
34
|
+
const pathContext = pathResolveContextFromServices(this.services);
|
|
35
|
+
const fileLinks = collectFileLinks(
|
|
29
36
|
document,
|
|
30
37
|
this.documents,
|
|
31
38
|
this.fileSystem,
|
|
32
|
-
|
|
39
|
+
pathContext
|
|
33
40
|
).flatMap(link => {
|
|
34
41
|
if (link.resolution === 'folder') {
|
|
35
42
|
return [LspDocumentLink.create(
|
|
@@ -37,6 +44,9 @@ export class ReqlanDocumentLinkProvider implements DocumentLinkProvider {
|
|
|
37
44
|
folderReferenceCommandTarget(link.targetUri)
|
|
38
45
|
)];
|
|
39
46
|
}
|
|
47
|
+
if (link.resolution === 'missing') {
|
|
48
|
+
return [];
|
|
49
|
+
}
|
|
40
50
|
if (link.resolution === 'wildcard' && link.wildcardArgs) {
|
|
41
51
|
return [LspDocumentLink.create(
|
|
42
52
|
link.sourceRange,
|
|
@@ -49,5 +59,15 @@ export class ReqlanDocumentLinkProvider implements DocumentLinkProvider {
|
|
|
49
59
|
}
|
|
50
60
|
return [LspDocumentLink.create(link.sourceRange, target)];
|
|
51
61
|
});
|
|
62
|
+
const commentLinks = presentCommentReferencesForDocument(
|
|
63
|
+
document,
|
|
64
|
+
this.documents,
|
|
65
|
+
this.fileSystem,
|
|
66
|
+
pathContext
|
|
67
|
+
).links.map(link => LspDocumentLink.create(
|
|
68
|
+
link.range,
|
|
69
|
+
link.targetUri ?? URI.file(link.targetPath).toString()
|
|
70
|
+
));
|
|
71
|
+
return [...fileLinks, ...commentLinks];
|
|
52
72
|
}
|
|
53
73
|
}
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Locates quoted file paths embedded in .rq source text, including @tests list entries.
|
|
3
|
+
* Skips inline code (`…`) and fenced ``` blocks — those are opaque examples, not live refs.
|
|
4
|
+
* rq:["../../../reqlan rq/language/syntax.rq".inline_code]
|
|
5
|
+
* rq:["../../../reqlan rq/language/syntax.rq".code_snippets]
|
|
3
6
|
*/
|
|
4
7
|
import type { LangiumDocument } from 'langium';
|
|
5
8
|
import type { Position, Range } from 'vscode-languageserver';
|
|
@@ -18,27 +21,69 @@ const FILE_REFERENCE_LIKE = /(?:\.\w[\w.]*|\/)/;
|
|
|
18
21
|
export function findEmbeddedFileReferencesInText(text: string, lineOffset = 0): EmbeddedFileReference[] {
|
|
19
22
|
const references: EmbeddedFileReference[] = [];
|
|
20
23
|
const lines = text.split(/\r?\n/);
|
|
24
|
+
let inFence = false;
|
|
21
25
|
for (let lineIndex = 0; lineIndex < lines.length; lineIndex++) {
|
|
22
26
|
const line = lines[lineIndex];
|
|
27
|
+
if (isCodeFenceLine(line)) {
|
|
28
|
+
inFence = !inFence;
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
if (inFence) {
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
const opaque = inlineCodeSpans(line);
|
|
23
35
|
for (const match of line.matchAll(BRACKETED_FILE_REFERENCE_PATTERN)) {
|
|
24
|
-
pushEmbeddedReference(references, match, lineIndex, lineOffset, line);
|
|
36
|
+
pushEmbeddedReference(references, match, lineIndex, lineOffset, line, opaque);
|
|
25
37
|
}
|
|
26
38
|
for (const match of line.matchAll(QUOTED_FILE_REFERENCE_PATTERN)) {
|
|
27
39
|
if (line.slice(Math.max(0, (match.index ?? 0) - 1), match.index).includes('[')) {
|
|
28
40
|
continue;
|
|
29
41
|
}
|
|
30
|
-
pushEmbeddedReference(references, match, lineIndex, lineOffset, line);
|
|
42
|
+
pushEmbeddedReference(references, match, lineIndex, lineOffset, line, opaque);
|
|
31
43
|
}
|
|
32
44
|
}
|
|
33
45
|
return references;
|
|
34
46
|
}
|
|
35
47
|
|
|
48
|
+
function isCodeFenceLine(line: string): boolean {
|
|
49
|
+
return line.trimStart().startsWith('```');
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function inlineCodeSpans(line: string): Array<{ start: number; end: number }> {
|
|
53
|
+
const spans: Array<{ start: number; end: number }> = [];
|
|
54
|
+
let index = 0;
|
|
55
|
+
while (index < line.length) {
|
|
56
|
+
if (line[index] !== '`') {
|
|
57
|
+
index++;
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
if (line.startsWith('```', index)) {
|
|
61
|
+
index += 3;
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
const close = line.indexOf('`', index + 1);
|
|
65
|
+
if (close < 0) {
|
|
66
|
+
break;
|
|
67
|
+
}
|
|
68
|
+
if (close > index + 1) {
|
|
69
|
+
spans.push({ start: index, end: close + 1 });
|
|
70
|
+
}
|
|
71
|
+
index = close + 1;
|
|
72
|
+
}
|
|
73
|
+
return spans;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function rangeOverlapsOpaque(start: number, end: number, opaque: Array<{ start: number; end: number }>): boolean {
|
|
77
|
+
return opaque.some(span => start < span.end && end > span.start);
|
|
78
|
+
}
|
|
79
|
+
|
|
36
80
|
function pushEmbeddedReference(
|
|
37
81
|
references: EmbeddedFileReference[],
|
|
38
82
|
match: RegExpMatchArray,
|
|
39
83
|
lineIndex: number,
|
|
40
84
|
lineOffset: number,
|
|
41
|
-
line: string
|
|
85
|
+
line: string,
|
|
86
|
+
opaque: Array<{ start: number; end: number }>
|
|
42
87
|
): void {
|
|
43
88
|
const quoted = match[1];
|
|
44
89
|
const file = parseReqlanQuotedString(quoted);
|
|
@@ -46,14 +91,18 @@ function pushEmbeddedReference(
|
|
|
46
91
|
return;
|
|
47
92
|
}
|
|
48
93
|
const start = match.index ?? 0;
|
|
49
|
-
|
|
94
|
+
const end = start + match[0].length;
|
|
95
|
+
if (rangeOverlapsOpaque(start, end, opaque)) {
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
if (isRangeInsideMarkdownLinkLabel(line, start, end)) {
|
|
50
99
|
return;
|
|
51
100
|
}
|
|
52
101
|
references.push({
|
|
53
102
|
file,
|
|
54
103
|
range: {
|
|
55
104
|
start: { line: lineOffset + lineIndex, character: start },
|
|
56
|
-
end: { line: lineOffset + lineIndex, character:
|
|
105
|
+
end: { line: lineOffset + lineIndex, character: end }
|
|
57
106
|
}
|
|
58
107
|
});
|
|
59
108
|
}
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Resolves file references, idea references, and import paths to target URIs and editor ranges.
|
|
3
3
|
* Document links use these so same-file and cross-file refs share the same underline/click affordance.
|
|
4
|
+
* Missing file refs stay in this result so the validator can underline them without creating a link.
|
|
5
|
+
* rq:["../../../reqlan rq/extension/language-support/language-server-errors.rq".file_reference_errors]
|
|
6
|
+
* rq:["../../../reqlan rq/extension/syntax/features-syntax.rq".file_references]
|
|
7
|
+
* rq:["../../../reqlan rq/extension/language-support/features-imports.rq".import_folder_targets]
|
|
4
8
|
*/
|
|
5
9
|
import type { AstNode, CstNode, FileSystemProvider, LangiumDocument, LangiumDocuments, Reference, URI } from 'langium';
|
|
6
10
|
import { AstUtils, CstUtils, GrammarUtils } from 'langium';
|
|
@@ -50,6 +54,8 @@ export type ReferenceResolution = 'file' | 'folder' | 'missing' | 'wildcard';
|
|
|
50
54
|
|
|
51
55
|
export type FileLinkTargetIssue = 'empty' | 'parse-error';
|
|
52
56
|
|
|
57
|
+
export const FILE_REFERENCE_MISSING = 'file-reference-missing';
|
|
58
|
+
|
|
53
59
|
export interface ResolvedFileLink {
|
|
54
60
|
sourceRange: Range;
|
|
55
61
|
targetUri: string;
|
|
@@ -57,6 +63,8 @@ export interface ResolvedFileLink {
|
|
|
57
63
|
resolution?: ReferenceResolution;
|
|
58
64
|
folderFiles?: string[];
|
|
59
65
|
targetIssue?: FileLinkTargetIssue;
|
|
66
|
+
/** Authored path (no test/line suffix) for missing-file diagnostics. */
|
|
67
|
+
authoredPath?: string;
|
|
60
68
|
/** Present when resolution is `wildcard`. */
|
|
61
69
|
wildcardArgs?: WildcardReferenceArgs;
|
|
62
70
|
wildcardMatches?: WildcardMatch[];
|
|
@@ -126,14 +134,18 @@ export function resolveImportPathLink(
|
|
|
126
134
|
}
|
|
127
135
|
const imported = findImportedDocument(path, document, documents, context);
|
|
128
136
|
const target = imported?.parseResult.value.$cstNode;
|
|
129
|
-
if (
|
|
137
|
+
if (imported && target) {
|
|
138
|
+
return {
|
|
139
|
+
sourceRange,
|
|
140
|
+
targetUri: imported.textDocument.uri,
|
|
141
|
+
targetRange: target.range
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
const fileSystem = context?.fileSystem;
|
|
145
|
+
if (!fileSystem) {
|
|
130
146
|
return undefined;
|
|
131
147
|
}
|
|
132
|
-
return
|
|
133
|
-
sourceRange,
|
|
134
|
-
targetUri: imported.textDocument.uri,
|
|
135
|
-
targetRange: target.range
|
|
136
|
-
};
|
|
148
|
+
return resolveImportedFileLink(document, documents, fileSystem, path, sourceRange, context);
|
|
137
149
|
}
|
|
138
150
|
|
|
139
151
|
export function resolveQualifiedReferencePathLink(
|
|
@@ -238,21 +250,28 @@ function resolvePathStringLink(
|
|
|
238
250
|
targetUri: URI,
|
|
239
251
|
sourceRange: Range
|
|
240
252
|
): ResolvedFileLink | undefined {
|
|
253
|
+
if (!parsed.filePath.trim()) {
|
|
254
|
+
return missingFileLink(sourceRange, targetUri, parsed.filePath);
|
|
255
|
+
}
|
|
256
|
+
if (isRemoteFileReferencePath(parsed.filePath)) {
|
|
257
|
+
return undefined;
|
|
258
|
+
}
|
|
241
259
|
const resolution = classifyReferenceUri(targetUri, documents, fileSystem);
|
|
242
260
|
if (resolution === 'missing') {
|
|
243
|
-
return
|
|
261
|
+
return missingFileLink(sourceRange, targetUri, parsed.filePath);
|
|
244
262
|
}
|
|
245
263
|
if (resolution === 'folder') {
|
|
246
264
|
return {
|
|
247
265
|
sourceRange,
|
|
248
266
|
targetUri: targetUri.toString(),
|
|
249
267
|
resolution,
|
|
250
|
-
folderFiles: listFolderFileNames(fileSystem, targetUri)
|
|
268
|
+
folderFiles: listFolderFileNames(fileSystem, targetUri),
|
|
269
|
+
authoredPath: parsed.filePath
|
|
251
270
|
};
|
|
252
271
|
}
|
|
253
272
|
const text = readTargetText(targetUri, documents, fileSystem);
|
|
254
273
|
if (text === undefined) {
|
|
255
|
-
return
|
|
274
|
+
return missingFileLink(sourceRange, targetUri, parsed.filePath);
|
|
256
275
|
}
|
|
257
276
|
const targetIssue = detectFileLinkTargetIssue(text, findTargetDocument(targetUri, documents));
|
|
258
277
|
let targetRange: Range | undefined;
|
|
@@ -271,10 +290,24 @@ function resolvePathStringLink(
|
|
|
271
290
|
targetUri: targetUri.toString(),
|
|
272
291
|
targetRange,
|
|
273
292
|
resolution: 'file',
|
|
274
|
-
targetIssue
|
|
293
|
+
targetIssue,
|
|
294
|
+
authoredPath: parsed.filePath
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function missingFileLink(sourceRange: Range, targetUri: URI, authoredPath: string): ResolvedFileLink {
|
|
299
|
+
return {
|
|
300
|
+
sourceRange,
|
|
301
|
+
targetUri: targetUri.toString(),
|
|
302
|
+
resolution: 'missing',
|
|
303
|
+
authoredPath
|
|
275
304
|
};
|
|
276
305
|
}
|
|
277
306
|
|
|
307
|
+
function isRemoteFileReferencePath(path: string): boolean {
|
|
308
|
+
return /^[a-zA-Z][a-zA-Z0-9+.-]*:\/\//.test(path);
|
|
309
|
+
}
|
|
310
|
+
|
|
278
311
|
export function detectFileLinkTargetIssue(
|
|
279
312
|
text: string,
|
|
280
313
|
targetDocument: LangiumDocument | undefined
|
|
@@ -297,6 +330,14 @@ export function fileLinkTargetIssueMessage(issue: FileLinkTargetIssue): string {
|
|
|
297
330
|
}
|
|
298
331
|
}
|
|
299
332
|
|
|
333
|
+
export function fileLinkMissingMessage(path: string): string {
|
|
334
|
+
const trimmed = path.trim();
|
|
335
|
+
if (!trimmed) {
|
|
336
|
+
return 'Could not resolve file reference.';
|
|
337
|
+
}
|
|
338
|
+
return `Could not resolve file reference '${trimmed}'.`;
|
|
339
|
+
}
|
|
340
|
+
|
|
300
341
|
function findTargetDocument(targetUri: URI, documents: LangiumDocuments): LangiumDocument | undefined {
|
|
301
342
|
const direct = documents.getDocument(targetUri);
|
|
302
343
|
if (direct) {
|
|
@@ -347,7 +388,11 @@ export function resolveImportedFileLink(
|
|
|
347
388
|
context?: PathResolveContext
|
|
348
389
|
): ResolvedFileLink | undefined {
|
|
349
390
|
const targetUri = resolveExistingImportUri(filePath, document, documents, fileSystem, context);
|
|
350
|
-
|
|
391
|
+
const link = resolvePathStringLink(document, documents, fileSystem, { filePath }, targetUri, sourceRange);
|
|
392
|
+
if (!link || link.resolution === 'missing') {
|
|
393
|
+
return undefined;
|
|
394
|
+
}
|
|
395
|
+
return link;
|
|
351
396
|
}
|
|
352
397
|
|
|
353
398
|
export function filePathRangeInStringNode(pathNode: CstNode, parsed: ParsedFileReference): Range {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Parses file path strings from bracket references, including L# line suffixes and :test name suffixes.
|
|
3
|
+
* rq:["../../../reqlan rq/language/syntax.rq".reference_file]
|
|
3
4
|
*/
|
|
4
5
|
|
|
5
6
|
export interface ParsedFileReference {
|
|
@@ -30,28 +31,37 @@ export function parseFileReferenceString(file: string): ParsedFileReference {
|
|
|
30
31
|
}
|
|
31
32
|
|
|
32
33
|
function parseTestNameSuffix(file: string): { filePath: string; testName?: string } {
|
|
33
|
-
let
|
|
34
|
-
for (let index = file.length - 1; index >= 0; index--) {
|
|
34
|
+
for (let index = 0; index < file.length; index++) {
|
|
35
35
|
if (file[index] !== ':') {
|
|
36
36
|
continue;
|
|
37
37
|
}
|
|
38
|
-
if (
|
|
38
|
+
if (isUriSchemeColon(file, index) || isWindowsDriveColon(file, index)) {
|
|
39
39
|
continue;
|
|
40
40
|
}
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
return {
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
return { filePath: file };
|
|
41
|
+
const testName = file.slice(index + 1);
|
|
42
|
+
if (!testName) {
|
|
43
|
+
return { filePath: file };
|
|
44
|
+
}
|
|
45
|
+
return {
|
|
46
|
+
filePath: file.slice(0, index),
|
|
47
|
+
testName
|
|
48
|
+
};
|
|
50
49
|
}
|
|
51
|
-
return {
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
50
|
+
return { filePath: file };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** `https://` / `file://` — the colon that starts `://`. */
|
|
54
|
+
function isUriSchemeColon(file: string, index: number): boolean {
|
|
55
|
+
return file[index + 1] === '/' && file[index + 2] === '/';
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Drive letter in `C:\…` or `file://C:/…`. */
|
|
59
|
+
function isWindowsDriveColon(file: string, index: number): boolean {
|
|
60
|
+
const previous = file[index - 1];
|
|
61
|
+
const next = file[index + 1];
|
|
62
|
+
return previous !== undefined
|
|
63
|
+
&& /[A-Za-z]/.test(previous)
|
|
64
|
+
&& (next === '\\' || next === '/');
|
|
55
65
|
}
|
|
56
66
|
|
|
57
67
|
const NON_RQ_FILE_EXTENSION = /\.[^./\\]+$/;
|