@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.
@@ -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
+ }