@xano/xanoscript-language-server 11.9.0 → 11.10.0
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/.claude/memory/feedback_no_git.md +11 -0
- package/.claude/settings.local.json +0 -5
- package/cache/documentCache.js +19 -2
- package/onDidChangeContent/onDidChangeContent.js +30 -6
- package/onHover/onHoverDocument.js +44 -1
- package/onHover/onHoverDocument.spec.js +174 -0
- package/package.json +1 -1
- package/parser/functions/db/dbQueryFn.spec.js +7 -0
- package/parser/generic/expressionFn.js +1 -0
- package/parser/multidoc.js +273 -0
- package/parser/multidoc.spec.js +882 -0
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: no-git-interactions
|
|
3
|
+
description: User does not want Claude to interact with git (no commits, no staging, no git commands)
|
|
4
|
+
type: feedback
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
Do not interact with git — no commits, no staging, no git commands. Focus purely on implementation and testing.
|
|
8
|
+
|
|
9
|
+
**Why:** User manages their own git workflow.
|
|
10
|
+
|
|
11
|
+
**How to apply:** Skip all git-related steps in plans. When dispatching subagents, explicitly tell them not to run any git commands. Remove commit steps from task instructions.
|
|
@@ -2,18 +2,13 @@
|
|
|
2
2
|
"permissions": {
|
|
3
3
|
"allow": [
|
|
4
4
|
"Bash(npm test:*)",
|
|
5
|
-
"Bash(node -e:*)",
|
|
6
|
-
"Bash(node --input-type=module -e:*)",
|
|
7
5
|
"Bash(git checkout:*)",
|
|
8
6
|
"Bash(ls:*)",
|
|
9
7
|
"Bash(npm run lint:*)",
|
|
10
8
|
"Bash(grep:*)",
|
|
11
|
-
"Bash(node --input-type=module:*)",
|
|
12
9
|
"Bash(find:*)",
|
|
13
10
|
"Read(//tmp/**)",
|
|
14
11
|
"Bash(git stash:*)",
|
|
15
|
-
"Bash(node:*)",
|
|
16
|
-
"Bash(for:*)",
|
|
17
12
|
"Bash(do)",
|
|
18
13
|
"Bash(if ! grep -q \"guidFieldAttribute\" \"$f\")",
|
|
19
14
|
"Bash(then)",
|
package/cache/documentCache.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { lexDocument } from "../lexer/lexer.js";
|
|
2
|
+
import { multidocParser } from "../parser/multidoc.js";
|
|
2
3
|
import { xanoscriptParser } from "../parser/parser.js";
|
|
3
4
|
import { getSchemeFromContent } from "../utils.js";
|
|
4
5
|
|
|
@@ -34,14 +35,30 @@ class DocumentCache {
|
|
|
34
35
|
cached.version === version &&
|
|
35
36
|
cached.textLength === textLength
|
|
36
37
|
) {
|
|
37
|
-
// Re-lex (cheap) to provide tokens; return cached parser state
|
|
38
38
|
return {
|
|
39
|
-
lexResult: lexDocument(text),
|
|
39
|
+
lexResult: cached.scheme === "multidoc" ? { tokens: [] } : lexDocument(text),
|
|
40
40
|
parser: cached.parserState,
|
|
41
41
|
scheme: cached.scheme,
|
|
42
42
|
};
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
+
// Multidoc detection — split on \n---\n separator
|
|
46
|
+
if (text.includes("\n---\n")) {
|
|
47
|
+
const parserState = multidocParser(text);
|
|
48
|
+
const cacheEntry = {
|
|
49
|
+
version,
|
|
50
|
+
textLength,
|
|
51
|
+
parserState,
|
|
52
|
+
scheme: "multidoc",
|
|
53
|
+
};
|
|
54
|
+
this.cache.set(uri, cacheEntry);
|
|
55
|
+
return {
|
|
56
|
+
lexResult: { tokens: [] },
|
|
57
|
+
parser: parserState,
|
|
58
|
+
scheme: "multidoc",
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
45
62
|
// Parse and cache a snapshot of the parser state
|
|
46
63
|
const scheme = getSchemeFromContent(text);
|
|
47
64
|
const lexResult = lexDocument(text);
|
|
@@ -73,6 +73,10 @@ export function onDidChangeContent(params, connection, features = {}) {
|
|
|
73
73
|
const text = document.getText();
|
|
74
74
|
|
|
75
75
|
try {
|
|
76
|
+
// Capture previous segment count before getOrParse overwrites the cache
|
|
77
|
+
const prevCached = documentCache.cache.get(document.uri);
|
|
78
|
+
const prevSegmentCount = prevCached?.parserState?.segmentCount ?? 0;
|
|
79
|
+
|
|
76
80
|
// Parse the XanoScript file using cache
|
|
77
81
|
const { lexResult, parser, scheme } = documentCache.getOrParse(
|
|
78
82
|
document.uri,
|
|
@@ -80,8 +84,28 @@ export function onDidChangeContent(params, connection, features = {}) {
|
|
|
80
84
|
text,
|
|
81
85
|
);
|
|
82
86
|
|
|
83
|
-
// Update workspace index
|
|
84
|
-
|
|
87
|
+
// Update workspace index
|
|
88
|
+
// Clean up old fragment URIs from previous parse
|
|
89
|
+
for (let i = 0; i < prevSegmentCount; i++) {
|
|
90
|
+
workspaceIndex.removeFile(`${document.uri}#${i}`);
|
|
91
|
+
}
|
|
92
|
+
if (parser.isMultidoc) {
|
|
93
|
+
// Remove the base URI in case document switched from single to multidoc
|
|
94
|
+
workspaceIndex.removeFile(document.uri);
|
|
95
|
+
// Index each segment with fragment URIs
|
|
96
|
+
if (parser.segments) {
|
|
97
|
+
for (let i = 0; i < parser.segments.length; i++) {
|
|
98
|
+
const seg = parser.segments[i];
|
|
99
|
+
workspaceIndex.addParsed(
|
|
100
|
+
`${document.uri}#${i}`,
|
|
101
|
+
seg.text,
|
|
102
|
+
seg.symbolTable,
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
} else {
|
|
107
|
+
workspaceIndex.addParsed(document.uri, text, parser.__symbolTable);
|
|
108
|
+
}
|
|
85
109
|
|
|
86
110
|
for (const error of parser.errors) {
|
|
87
111
|
console.error(
|
|
@@ -92,8 +116,8 @@ export function onDidChangeContent(params, connection, features = {}) {
|
|
|
92
116
|
// Create diagnostics in a single pass
|
|
93
117
|
const diagnostics = createDiagnostics(parser, document);
|
|
94
118
|
|
|
95
|
-
// Run cross-file validation
|
|
96
|
-
if (features.crossFileValidation !== false && parser.__symbolTable?.references) {
|
|
119
|
+
// Run cross-file validation (skip for multidoc — already done internally)
|
|
120
|
+
if (!parser.isMultidoc && features.crossFileValidation !== false && parser.__symbolTable?.references) {
|
|
97
121
|
const crossFileWarnings = crossFileValidate(
|
|
98
122
|
parser.__symbolTable.references,
|
|
99
123
|
workspaceIndex,
|
|
@@ -110,8 +134,8 @@ export function onDidChangeContent(params, connection, features = {}) {
|
|
|
110
134
|
}
|
|
111
135
|
}
|
|
112
136
|
|
|
113
|
-
// Run variable validation
|
|
114
|
-
if (features.variableValidation !== false && parser.__symbolTable?.varDeclarations) {
|
|
137
|
+
// Run variable validation (skip for multidoc — already done per-segment internally)
|
|
138
|
+
if (!parser.isMultidoc && features.variableValidation !== false && parser.__symbolTable?.varDeclarations) {
|
|
115
139
|
const varResult = validateVariables(
|
|
116
140
|
parser.__symbolTable,
|
|
117
141
|
lexResult.tokens,
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { documentCache } from "../cache/documentCache.js";
|
|
2
|
+
import { lexDocument } from "../lexer/lexer.js";
|
|
3
|
+
import { findSegmentAtOffset } from "../parser/multidoc.js";
|
|
2
4
|
|
|
3
5
|
/**
|
|
4
6
|
* Binary search to find token index at the given offset.
|
|
@@ -54,9 +56,50 @@ export function onHoverDocument(params, documents, hoverProviders = []) {
|
|
|
54
56
|
text,
|
|
55
57
|
);
|
|
56
58
|
|
|
59
|
+
let tokens;
|
|
60
|
+
let hoverParser;
|
|
61
|
+
let tokenOffsetAdjustment = 0;
|
|
62
|
+
|
|
63
|
+
if (parser.isMultidoc) {
|
|
64
|
+
// For multidoc, find the segment at the cursor position and lex it
|
|
65
|
+
const match = findSegmentAtOffset(parser, offset);
|
|
66
|
+
if (!match) return null;
|
|
67
|
+
|
|
68
|
+
const segLexResult = lexDocument(match.segment.text);
|
|
69
|
+
if (segLexResult.errors.length > 0) return null;
|
|
70
|
+
|
|
71
|
+
tokens = segLexResult.tokens;
|
|
72
|
+
hoverParser = match.segment.parser;
|
|
73
|
+
tokenOffsetAdjustment = match.segment.globalOffset;
|
|
74
|
+
|
|
75
|
+
// Find token using segment-local offset
|
|
76
|
+
const tokenIdx = findTokenAtOffset(tokens, match.localOffset);
|
|
77
|
+
if (tokenIdx === -1) return null;
|
|
78
|
+
|
|
79
|
+
const messageProvider = hoverProviders.find((provider) =>
|
|
80
|
+
provider.isMatch(tokenIdx, tokens, hoverParser),
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
if (messageProvider) {
|
|
84
|
+
return {
|
|
85
|
+
contents: {
|
|
86
|
+
kind: "markdown",
|
|
87
|
+
value: messageProvider.render(tokenIdx, tokens, hoverParser),
|
|
88
|
+
},
|
|
89
|
+
range: {
|
|
90
|
+
// Convert segment-local token offsets to global document positions
|
|
91
|
+
start: document.positionAt(tokens[tokenIdx].startOffset + tokenOffsetAdjustment),
|
|
92
|
+
end: document.positionAt(tokens[tokenIdx].endOffset + 1 + tokenOffsetAdjustment),
|
|
93
|
+
},
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
|
|
57
100
|
if (lexResult.errors.length > 0) return null;
|
|
58
101
|
|
|
59
|
-
|
|
102
|
+
tokens = lexResult.tokens;
|
|
60
103
|
|
|
61
104
|
// Find the token under the cursor using binary search (O(log n))
|
|
62
105
|
const tokenIdx = findTokenAtOffset(tokens, offset);
|
|
@@ -452,6 +452,180 @@ describe("onHoverDocument", () => {
|
|
|
452
452
|
expect(result).to.have.property("range");
|
|
453
453
|
});
|
|
454
454
|
|
|
455
|
+
it("should return hover info for var in second segment of multidoc", () => {
|
|
456
|
+
const code = `function foo {
|
|
457
|
+
input {
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
stack {
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
response = null
|
|
464
|
+
}
|
|
465
|
+
---
|
|
466
|
+
query test verb=GET {
|
|
467
|
+
stack {
|
|
468
|
+
var $result {
|
|
469
|
+
value = "hello"
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
}`;
|
|
473
|
+
|
|
474
|
+
const document = TextDocument.create(
|
|
475
|
+
"file://test-multidoc.xs",
|
|
476
|
+
"xanoscript",
|
|
477
|
+
1,
|
|
478
|
+
code
|
|
479
|
+
);
|
|
480
|
+
const params = {
|
|
481
|
+
textDocument: { uri: "file://test-multidoc.xs" },
|
|
482
|
+
position: { line: 12, character: 4 }, // Position on "var" in second segment
|
|
483
|
+
};
|
|
484
|
+
|
|
485
|
+
const result = onHoverDocument(
|
|
486
|
+
params,
|
|
487
|
+
{ get: () => document },
|
|
488
|
+
hoverProviders
|
|
489
|
+
);
|
|
490
|
+
|
|
491
|
+
expect(result).to.not.be.null;
|
|
492
|
+
expect(result.contents.value).to.include("Create a variable");
|
|
493
|
+
expect(result).to.have.property("range");
|
|
494
|
+
// Range should be in the global document, not segment-local
|
|
495
|
+
expect(result.range.start.line).to.be.at.least(10);
|
|
496
|
+
});
|
|
497
|
+
|
|
498
|
+
it("should return hover info for input variable in multidoc segment", () => {
|
|
499
|
+
const code = `function foo {
|
|
500
|
+
input {
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
stack {
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
response = null
|
|
507
|
+
}
|
|
508
|
+
---
|
|
509
|
+
query test verb=GET {
|
|
510
|
+
input {
|
|
511
|
+
text username
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
stack {
|
|
515
|
+
var $result {
|
|
516
|
+
value = $input.username
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
response = $result
|
|
521
|
+
}`;
|
|
522
|
+
|
|
523
|
+
const document = TextDocument.create(
|
|
524
|
+
"file://test-multidoc-input.xs",
|
|
525
|
+
"xanoscript",
|
|
526
|
+
1,
|
|
527
|
+
code
|
|
528
|
+
);
|
|
529
|
+
// "username" after $input. in the second segment
|
|
530
|
+
const params = {
|
|
531
|
+
textDocument: { uri: "file://test-multidoc-input.xs" },
|
|
532
|
+
position: { line: 17, character: 20 },
|
|
533
|
+
};
|
|
534
|
+
|
|
535
|
+
const result = onHoverDocument(
|
|
536
|
+
params,
|
|
537
|
+
{ get: () => document },
|
|
538
|
+
hoverProviders
|
|
539
|
+
);
|
|
540
|
+
|
|
541
|
+
if (result) {
|
|
542
|
+
expect(result.contents.value).to.include("username");
|
|
543
|
+
expect(result.range.start.line).to.be.at.least(10);
|
|
544
|
+
}
|
|
545
|
+
});
|
|
546
|
+
|
|
547
|
+
it("should return hover info for first segment of multidoc", () => {
|
|
548
|
+
const code = `query test verb=GET {
|
|
549
|
+
stack {
|
|
550
|
+
var $result {
|
|
551
|
+
value = "hello"
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
---
|
|
556
|
+
function bar {
|
|
557
|
+
input {
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
stack {
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
response = null
|
|
564
|
+
}`;
|
|
565
|
+
|
|
566
|
+
const document = TextDocument.create(
|
|
567
|
+
"file://test-multidoc-first.xs",
|
|
568
|
+
"xanoscript",
|
|
569
|
+
1,
|
|
570
|
+
code
|
|
571
|
+
);
|
|
572
|
+
const params = {
|
|
573
|
+
textDocument: { uri: "file://test-multidoc-first.xs" },
|
|
574
|
+
position: { line: 2, character: 4 }, // "var" in first segment
|
|
575
|
+
};
|
|
576
|
+
|
|
577
|
+
const result = onHoverDocument(
|
|
578
|
+
params,
|
|
579
|
+
{ get: () => document },
|
|
580
|
+
hoverProviders
|
|
581
|
+
);
|
|
582
|
+
|
|
583
|
+
expect(result).to.not.be.null;
|
|
584
|
+
expect(result.contents.value).to.include("Create a variable");
|
|
585
|
+
expect(result.range.start.line).to.equal(2);
|
|
586
|
+
});
|
|
587
|
+
|
|
588
|
+
it("should return null for hover on separator line in multidoc", () => {
|
|
589
|
+
const code = `function foo {
|
|
590
|
+
input {
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
stack {
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
response = null
|
|
597
|
+
}
|
|
598
|
+
---
|
|
599
|
+
function bar {
|
|
600
|
+
input {
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
stack {
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
response = null
|
|
607
|
+
}`;
|
|
608
|
+
|
|
609
|
+
const document = TextDocument.create(
|
|
610
|
+
"file://test-multidoc-sep.xs",
|
|
611
|
+
"xanoscript",
|
|
612
|
+
1,
|
|
613
|
+
code
|
|
614
|
+
);
|
|
615
|
+
const params = {
|
|
616
|
+
textDocument: { uri: "file://test-multidoc-sep.xs" },
|
|
617
|
+
position: { line: 9, character: 1 }, // On the "---" separator line
|
|
618
|
+
};
|
|
619
|
+
|
|
620
|
+
const result = onHoverDocument(
|
|
621
|
+
params,
|
|
622
|
+
{ get: () => document },
|
|
623
|
+
hoverProviders
|
|
624
|
+
);
|
|
625
|
+
|
|
626
|
+
expect(result).to.be.null;
|
|
627
|
+
});
|
|
628
|
+
|
|
455
629
|
it("should return hover info for query filter tokens in search context", () => {
|
|
456
630
|
const code = `query books verb=GET {
|
|
457
631
|
where = $db.embedding.vector|inner_product:$input.query_vector
|
package/package.json
CHANGED
|
@@ -83,6 +83,13 @@ describe("dbQueryFn", () => {
|
|
|
83
83
|
expect(parser.errors).to.be.empty;
|
|
84
84
|
});
|
|
85
85
|
|
|
86
|
+
it("dbQueryFn accepts a search expression with dynamic field", () => {
|
|
87
|
+
const parser = parse(`query user {
|
|
88
|
+
where = $db.my_table["$search value"] search? $input.values
|
|
89
|
+
} as $user`);
|
|
90
|
+
expect(parser.errors).to.be.empty;
|
|
91
|
+
});
|
|
92
|
+
|
|
86
93
|
it("dbQueryFn accepts sub addon", () => {
|
|
87
94
|
const parser = parse(`query user {
|
|
88
95
|
where = $db.array_columns @> $db.array_columns.id
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
import { lexDocument } from "../lexer/lexer.js";
|
|
2
|
+
import { crossFileValidate } from "../workspace/crossFileValidator.js";
|
|
3
|
+
import { WorkspaceIndex } from "../workspace/workspaceIndex.js";
|
|
4
|
+
import { xanoscriptParser } from "./parser.js";
|
|
5
|
+
import { validateVariables } from "./variableValidator.js";
|
|
6
|
+
|
|
7
|
+
const SEPARATOR = "\n---\n";
|
|
8
|
+
const SEPARATOR_LENGTH = 5;
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Find the segment that contains the given global offset.
|
|
12
|
+
* @param {Object} parserResult - The multidoc parser result (must have segments array)
|
|
13
|
+
* @param {number} offset - Global character offset in the full document
|
|
14
|
+
* @returns {{ segment: Object, localOffset: number } | null}
|
|
15
|
+
*/
|
|
16
|
+
export function findSegmentAtOffset(parserResult, offset) {
|
|
17
|
+
if (!parserResult?.segments) return null;
|
|
18
|
+
|
|
19
|
+
for (const segment of parserResult.segments) {
|
|
20
|
+
const segEnd = segment.globalOffset + segment.text.length;
|
|
21
|
+
if (offset >= segment.globalOffset && offset < segEnd) {
|
|
22
|
+
return {
|
|
23
|
+
segment,
|
|
24
|
+
localOffset: offset - segment.globalOffset,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Offset might be at the very end of the last segment
|
|
30
|
+
const last = parserResult.segments[parserResult.segments.length - 1];
|
|
31
|
+
if (last && offset === last.globalOffset + last.text.length) {
|
|
32
|
+
return {
|
|
33
|
+
segment: last,
|
|
34
|
+
localOffset: offset - last.globalOffset,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Snapshot parser state into a plain object.
|
|
43
|
+
* Must be called immediately after xanoscriptParser() before parsing next segment,
|
|
44
|
+
* because the parser is a singleton that gets mutated on each call.
|
|
45
|
+
* @param {import('./base_parser.js').XanoBaseParser} parser
|
|
46
|
+
* @returns {Object} Plain object snapshot
|
|
47
|
+
*/
|
|
48
|
+
function snapshotParser(parser) {
|
|
49
|
+
return {
|
|
50
|
+
errors: parser.errors.map((e) => ({
|
|
51
|
+
message: e.message,
|
|
52
|
+
token: e.token
|
|
53
|
+
? { startOffset: e.token.startOffset, endOffset: e.token.endOffset }
|
|
54
|
+
: null,
|
|
55
|
+
})),
|
|
56
|
+
warnings: parser.warnings.map((w) => ({
|
|
57
|
+
message: w.message,
|
|
58
|
+
token: w.token
|
|
59
|
+
? { startOffset: w.token.startOffset, endOffset: w.token.endOffset }
|
|
60
|
+
: null,
|
|
61
|
+
})),
|
|
62
|
+
informations: parser.informations.map((i) => ({
|
|
63
|
+
message: i.message,
|
|
64
|
+
token: i.token
|
|
65
|
+
? { startOffset: i.token.startOffset, endOffset: i.token.endOffset }
|
|
66
|
+
: null,
|
|
67
|
+
})),
|
|
68
|
+
hints: parser.hints.map((h) => ({
|
|
69
|
+
message: h.message,
|
|
70
|
+
token: h.token
|
|
71
|
+
? { startOffset: h.token.startOffset, endOffset: h.token.endOffset }
|
|
72
|
+
: null,
|
|
73
|
+
})),
|
|
74
|
+
__symbolTable: parser.__symbolTable
|
|
75
|
+
? {
|
|
76
|
+
input: { ...parser.__symbolTable.input },
|
|
77
|
+
var: { ...parser.__symbolTable.var },
|
|
78
|
+
auth: { ...parser.__symbolTable.auth },
|
|
79
|
+
env: { ...parser.__symbolTable.env },
|
|
80
|
+
references: parser.__symbolTable.references.map((r) => ({
|
|
81
|
+
...r,
|
|
82
|
+
args: r.args ? { ...r.args } : undefined,
|
|
83
|
+
dataKeys: r.dataKeys ? [...r.dataKeys] : undefined,
|
|
84
|
+
})),
|
|
85
|
+
varDeclarations: parser.__symbolTable.varDeclarations.map((d) => ({
|
|
86
|
+
...d,
|
|
87
|
+
})),
|
|
88
|
+
}
|
|
89
|
+
: null,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Apply a global offset to all position-bearing fields in a parser snapshot.
|
|
95
|
+
* @param {Object} snapshot - The parser state snapshot
|
|
96
|
+
* @param {number} globalOffset - The character offset to add
|
|
97
|
+
*/
|
|
98
|
+
function offsetResult(snapshot, globalOffset) {
|
|
99
|
+
if (globalOffset === 0) return;
|
|
100
|
+
|
|
101
|
+
const offsetToken = (token) => {
|
|
102
|
+
if (!token) return;
|
|
103
|
+
token.startOffset += globalOffset;
|
|
104
|
+
token.endOffset += globalOffset;
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
for (const err of snapshot.errors) offsetToken(err.token);
|
|
108
|
+
for (const warn of snapshot.warnings) offsetToken(warn.token);
|
|
109
|
+
for (const info of snapshot.informations) offsetToken(info.token);
|
|
110
|
+
for (const hint of snapshot.hints) offsetToken(hint.token);
|
|
111
|
+
|
|
112
|
+
if (!snapshot.__symbolTable) return;
|
|
113
|
+
|
|
114
|
+
for (const ref of snapshot.__symbolTable.references) {
|
|
115
|
+
ref.startOffset += globalOffset;
|
|
116
|
+
ref.endOffset += globalOffset;
|
|
117
|
+
if (ref.fieldNameStartOffset != null) {
|
|
118
|
+
ref.fieldNameStartOffset += globalOffset;
|
|
119
|
+
ref.fieldNameEndOffset += globalOffset;
|
|
120
|
+
}
|
|
121
|
+
if (ref.args) {
|
|
122
|
+
for (const arg of Object.values(ref.args)) {
|
|
123
|
+
if (arg && arg.startOffset != null) {
|
|
124
|
+
arg.startOffset += globalOffset;
|
|
125
|
+
arg.endOffset += globalOffset;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
if (ref.dataKeys) {
|
|
130
|
+
for (const dk of ref.dataKeys) {
|
|
131
|
+
if (dk.startOffset != null) {
|
|
132
|
+
dk.startOffset += globalOffset;
|
|
133
|
+
dk.endOffset += globalOffset;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
for (const decl of snapshot.__symbolTable.varDeclarations) {
|
|
140
|
+
if (decl.startOffset != null) {
|
|
141
|
+
decl.startOffset += globalOffset;
|
|
142
|
+
decl.endOffset += globalOffset;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Parse a multidoc or single-doc XanoScript text.
|
|
149
|
+
* Detects `\n---\n` separator. If absent, delegates to xanoscriptParser as-is.
|
|
150
|
+
* If present, splits, parses each segment, offsets positions, and merges results.
|
|
151
|
+
* @param {string} text - The full document text
|
|
152
|
+
* @returns {Object} Merged parser result
|
|
153
|
+
*/
|
|
154
|
+
export function multidocParser(text) {
|
|
155
|
+
if (!text.includes(SEPARATOR)) {
|
|
156
|
+
// Single document — delegate as-is
|
|
157
|
+
const parser = xanoscriptParser(text);
|
|
158
|
+
return snapshotParser(parser);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const segments = text.split(SEPARATOR);
|
|
162
|
+
const results = [];
|
|
163
|
+
let globalOffset = 0;
|
|
164
|
+
|
|
165
|
+
for (const segmentText of segments) {
|
|
166
|
+
const parser = xanoscriptParser(segmentText);
|
|
167
|
+
const snapshot = snapshotParser(parser);
|
|
168
|
+
|
|
169
|
+
// Variable validation with segment-local offsets and tokens
|
|
170
|
+
const segLexResult = lexDocument(segmentText);
|
|
171
|
+
if (snapshot.__symbolTable?.varDeclarations) {
|
|
172
|
+
const varResult = validateVariables(
|
|
173
|
+
snapshot.__symbolTable,
|
|
174
|
+
segLexResult.tokens,
|
|
175
|
+
);
|
|
176
|
+
for (const warning of varResult.warnings) {
|
|
177
|
+
snapshot.warnings.push({
|
|
178
|
+
message: warning.message,
|
|
179
|
+
token: {
|
|
180
|
+
startOffset: warning.startOffset,
|
|
181
|
+
endOffset: warning.endOffset,
|
|
182
|
+
},
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
for (const hint of varResult.hints) {
|
|
186
|
+
snapshot.hints.push({
|
|
187
|
+
message: hint.message,
|
|
188
|
+
token: {
|
|
189
|
+
startOffset: hint.startOffset,
|
|
190
|
+
endOffset: hint.endOffset,
|
|
191
|
+
},
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Save a copy of the segment's symbol table before offsetting (for workspace indexing and hover)
|
|
197
|
+
const segmentSymbolTable = snapshot.__symbolTable
|
|
198
|
+
? { input: { ...snapshot.__symbolTable.input } }
|
|
199
|
+
: null;
|
|
200
|
+
// Deep-copy the snapshot before offsetResult mutates it (needed for hover providers)
|
|
201
|
+
const localSnapshot = JSON.parse(JSON.stringify(snapshot));
|
|
202
|
+
results.push({ snapshot, localSnapshot, segmentText, segmentSymbolTable, globalOffset });
|
|
203
|
+
globalOffset += segmentText.length + SEPARATOR_LENGTH;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// For now, merge without offsetting (next task)
|
|
207
|
+
const merged = {
|
|
208
|
+
errors: [],
|
|
209
|
+
warnings: [],
|
|
210
|
+
informations: [],
|
|
211
|
+
hints: [],
|
|
212
|
+
__symbolTable: {
|
|
213
|
+
input: {},
|
|
214
|
+
var: {},
|
|
215
|
+
auth: {},
|
|
216
|
+
env: {},
|
|
217
|
+
references: [],
|
|
218
|
+
varDeclarations: [],
|
|
219
|
+
},
|
|
220
|
+
isMultidoc: true,
|
|
221
|
+
segmentCount: segments.length,
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
for (const { snapshot, globalOffset } of results) {
|
|
225
|
+
offsetResult(snapshot, globalOffset);
|
|
226
|
+
merged.errors.push(...snapshot.errors);
|
|
227
|
+
merged.warnings.push(...snapshot.warnings);
|
|
228
|
+
merged.informations.push(...snapshot.informations);
|
|
229
|
+
merged.hints.push(...snapshot.hints);
|
|
230
|
+
if (snapshot.__symbolTable) {
|
|
231
|
+
merged.__symbolTable.references.push(
|
|
232
|
+
...snapshot.__symbolTable.references,
|
|
233
|
+
);
|
|
234
|
+
merged.__symbolTable.varDeclarations.push(
|
|
235
|
+
...snapshot.__symbolTable.varDeclarations,
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// Build local workspace index from all segments for cross-reference validation
|
|
241
|
+
const localIndex = new WorkspaceIndex();
|
|
242
|
+
for (let i = 0; i < results.length; i++) {
|
|
243
|
+
const { segmentText, segmentSymbolTable } = results[i];
|
|
244
|
+
localIndex.addParsed(`multidoc#${i}`, segmentText, segmentSymbolTable);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// Validate references against local index only (self-contained)
|
|
248
|
+
if (merged.__symbolTable.references.length > 0) {
|
|
249
|
+
const crossRefWarnings = crossFileValidate(
|
|
250
|
+
merged.__symbolTable.references,
|
|
251
|
+
localIndex,
|
|
252
|
+
);
|
|
253
|
+
for (const warning of crossRefWarnings) {
|
|
254
|
+
merged.warnings.push({
|
|
255
|
+
message: warning.message,
|
|
256
|
+
token: {
|
|
257
|
+
startOffset: warning.startOffset,
|
|
258
|
+
endOffset: warning.endOffset,
|
|
259
|
+
},
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// Store segment info for workspace indexing and hover support
|
|
265
|
+
merged.segments = results.map(({ segmentText, segmentSymbolTable, localSnapshot, globalOffset }) => ({
|
|
266
|
+
text: segmentText,
|
|
267
|
+
symbolTable: segmentSymbolTable,
|
|
268
|
+
parser: localSnapshot,
|
|
269
|
+
globalOffset,
|
|
270
|
+
}));
|
|
271
|
+
|
|
272
|
+
return merged;
|
|
273
|
+
}
|