@xano/xanoscript-language-server 11.9.1 → 11.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/.claude/memory/feedback_no_git.md +11 -0
- package/.claude/settings.local.json +0 -5
- package/cache/documentCache.js +19 -2
- package/language-server-worker.js +2 -1
- package/onCompletion/onCompletion.js +24 -3
- package/onDidChangeContent/onDidChangeContent.js +30 -6
- package/onHover/onHoverDocument.js +44 -1
- package/onHover/onHoverDocument.spec.js +174 -0
- package/onSemanticCheck/highlight.js +45 -4
- package/onSemanticCheck/onSemanticCheck.js +14 -3
- package/package.json +1 -1
- package/parser/multidoc.js +273 -0
- package/parser/multidoc.spec.js +940 -0
|
@@ -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
|
+
}
|