bonsai-js 0.2.1 → 0.4.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/CHANGELOG.md +55 -5
- package/README.md +211 -8
- package/dist/autocomplete/index.d.mts +47 -0
- package/dist/autocomplete/index.d.mts.map +1 -0
- package/dist/autocomplete/index.mjs +1038 -0
- package/dist/autocomplete/index.mjs.map +1 -0
- package/dist/{errors-t5i9AEaf.mjs → errors-BONB6P_5.mjs} +2 -3
- package/dist/{errors-t5i9AEaf.mjs.map → errors-BONB6P_5.mjs.map} +1 -1
- package/dist/execution-context-B-5hQ92X.mjs +564 -0
- package/dist/execution-context-B-5hQ92X.mjs.map +1 -0
- package/dist/index.d.mts +16 -4
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +148 -601
- package/dist/index.mjs.map +1 -1
- package/dist/stdlib/index.d.mts +1 -1
- package/dist/stdlib/index.mjs +1 -1
- package/dist/stdlib/index.mjs.map +1 -1
- package/dist/{types-ClpZDF5z.d.mts → types-C58c6SlH.d.mts} +73 -16
- package/dist/types-C58c6SlH.d.mts.map +1 -0
- package/package.json +6 -2
- package/dist/types-ClpZDF5z.d.mts.map +0 -1
|
@@ -0,0 +1,1038 @@
|
|
|
1
|
+
import { i as ExpressionError, n as BonsaiSecurityError, r as BonsaiTypeError, t as BonsaiReferenceError } from "../errors-BONB6P_5.mjs";
|
|
2
|
+
import { i as tokenize, t as BLOCKED_PROPERTIES } from "../execution-context-B-5hQ92X.mjs";
|
|
3
|
+
//#region src/autocomplete/tokenizer.ts
|
|
4
|
+
function tolerantTokenize(expression, cursor, onError) {
|
|
5
|
+
if (expression.length === 0) return {
|
|
6
|
+
tokens: [],
|
|
7
|
+
partial: false,
|
|
8
|
+
insideString: false
|
|
9
|
+
};
|
|
10
|
+
const insideString = isCursorInsideString(expression, cursor);
|
|
11
|
+
try {
|
|
12
|
+
return {
|
|
13
|
+
tokens: tokenize(expression).filter((t) => t.type !== "EOF"),
|
|
14
|
+
partial: false,
|
|
15
|
+
insideString
|
|
16
|
+
};
|
|
17
|
+
} catch (err) {
|
|
18
|
+
if (!(err instanceof ExpressionError || err instanceof SyntaxError)) onError?.(err, "tokenize");
|
|
19
|
+
return {
|
|
20
|
+
tokens: regexScan(expression, cursor),
|
|
21
|
+
partial: true,
|
|
22
|
+
insideString
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
function isCursorInsideString(expression, cursor) {
|
|
27
|
+
let inSingle = false;
|
|
28
|
+
let inDouble = false;
|
|
29
|
+
let inTemplate = false;
|
|
30
|
+
let templateDepth = 0;
|
|
31
|
+
const braceStack = [];
|
|
32
|
+
let escaped = false;
|
|
33
|
+
for (let i = 0; i < expression.length && i < cursor; i++) {
|
|
34
|
+
const ch = expression[i];
|
|
35
|
+
if (escaped) {
|
|
36
|
+
escaped = false;
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
if (ch === "\\" && (inSingle || inDouble || inTemplate)) {
|
|
40
|
+
escaped = true;
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
if (!inSingle && !inDouble && !inTemplate) {
|
|
44
|
+
if (ch === "'") inSingle = true;
|
|
45
|
+
else if (ch === "\"") inDouble = true;
|
|
46
|
+
else if (ch === "`") {
|
|
47
|
+
inTemplate = true;
|
|
48
|
+
templateDepth = 0;
|
|
49
|
+
braceStack.length = 0;
|
|
50
|
+
}
|
|
51
|
+
} else if (inSingle && ch === "'") inSingle = false;
|
|
52
|
+
else if (inDouble && ch === "\"") inDouble = false;
|
|
53
|
+
else if (inTemplate) {
|
|
54
|
+
if (ch === "`" && templateDepth === 0) inTemplate = false;
|
|
55
|
+
else if (ch === "$" && i + 1 < expression.length && expression[i + 1] === "{") {
|
|
56
|
+
templateDepth++;
|
|
57
|
+
braceStack.push(0);
|
|
58
|
+
i++;
|
|
59
|
+
} else if (templateDepth > 0 && ch === "{") braceStack[braceStack.length - 1]++;
|
|
60
|
+
else if (templateDepth > 0 && ch === "}") if (braceStack.length > 0 && braceStack[braceStack.length - 1] > 0) braceStack[braceStack.length - 1]--;
|
|
61
|
+
else {
|
|
62
|
+
braceStack.pop();
|
|
63
|
+
templateDepth--;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return inSingle || inDouble || inTemplate && templateDepth === 0;
|
|
68
|
+
}
|
|
69
|
+
const TOKEN_RE = /\?\.|\.\.\.|\|>|\?\?|&&|\|\||[!=<>]=?|[+\-*/%]|\*\*|[.(){}[\],?:]|"(?:[^"\\]|\\.)*"?|'(?:[^'\\]|\\.)*'?|`(?:[^`\\$]|\\.|\$(?!\{))*`?|\d[\d_]*(?:\.\d[\d_]*)?(?:[eE][+-]?\d+)?|[a-zA-Z_$][\w$]*/gu;
|
|
70
|
+
function regexScan(expression, cursor) {
|
|
71
|
+
const tokens = [];
|
|
72
|
+
const text = expression.slice(0, cursor);
|
|
73
|
+
for (const match of text.matchAll(TOKEN_RE)) {
|
|
74
|
+
const value = match[0];
|
|
75
|
+
const start = match.index;
|
|
76
|
+
const token = classifyToken(value, start, start + value.length);
|
|
77
|
+
if (token) tokens.push(token);
|
|
78
|
+
}
|
|
79
|
+
return tokens;
|
|
80
|
+
}
|
|
81
|
+
function classifyToken(value, start, end) {
|
|
82
|
+
if (value === "|>") return {
|
|
83
|
+
type: "Pipe",
|
|
84
|
+
value,
|
|
85
|
+
start,
|
|
86
|
+
end
|
|
87
|
+
};
|
|
88
|
+
if (value === "?.") return {
|
|
89
|
+
type: "OptionalChain",
|
|
90
|
+
value,
|
|
91
|
+
start,
|
|
92
|
+
end
|
|
93
|
+
};
|
|
94
|
+
if (value === "??") return {
|
|
95
|
+
type: "NullishCoalescing",
|
|
96
|
+
value,
|
|
97
|
+
start,
|
|
98
|
+
end
|
|
99
|
+
};
|
|
100
|
+
if (value === "...") return {
|
|
101
|
+
type: "Spread",
|
|
102
|
+
value,
|
|
103
|
+
start,
|
|
104
|
+
end
|
|
105
|
+
};
|
|
106
|
+
if (value === "true" || value === "false") return {
|
|
107
|
+
type: "Boolean",
|
|
108
|
+
value,
|
|
109
|
+
start,
|
|
110
|
+
end
|
|
111
|
+
};
|
|
112
|
+
if (value === "null") return {
|
|
113
|
+
type: "Null",
|
|
114
|
+
value,
|
|
115
|
+
start,
|
|
116
|
+
end
|
|
117
|
+
};
|
|
118
|
+
if (value === "undefined") return {
|
|
119
|
+
type: "Undefined",
|
|
120
|
+
value,
|
|
121
|
+
start,
|
|
122
|
+
end
|
|
123
|
+
};
|
|
124
|
+
if (/^[a-zA-Z_$]/u.test(value)) return {
|
|
125
|
+
type: "Identifier",
|
|
126
|
+
value,
|
|
127
|
+
start,
|
|
128
|
+
end
|
|
129
|
+
};
|
|
130
|
+
if (/^\d/u.test(value)) return {
|
|
131
|
+
type: "Number",
|
|
132
|
+
value,
|
|
133
|
+
start,
|
|
134
|
+
end
|
|
135
|
+
};
|
|
136
|
+
if (value.startsWith("\"") || value.startsWith("'")) return {
|
|
137
|
+
type: "String",
|
|
138
|
+
value,
|
|
139
|
+
start,
|
|
140
|
+
end
|
|
141
|
+
};
|
|
142
|
+
if (value.startsWith("`")) return {
|
|
143
|
+
type: "TemplateLiteral",
|
|
144
|
+
value,
|
|
145
|
+
start,
|
|
146
|
+
end
|
|
147
|
+
};
|
|
148
|
+
if (value.length === 1 && "(){}[],:?.".includes(value)) return {
|
|
149
|
+
type: "Punctuation",
|
|
150
|
+
value,
|
|
151
|
+
start,
|
|
152
|
+
end
|
|
153
|
+
};
|
|
154
|
+
if ("+-*/%!=<>&|".includes(value[0]) || value === "**") return {
|
|
155
|
+
type: "Operator",
|
|
156
|
+
value,
|
|
157
|
+
start,
|
|
158
|
+
end
|
|
159
|
+
};
|
|
160
|
+
return null;
|
|
161
|
+
}
|
|
162
|
+
//#endregion
|
|
163
|
+
//#region src/autocomplete/context.ts
|
|
164
|
+
/** Extract prefix from any non-none CursorContext. */
|
|
165
|
+
function getPrefix(ctx) {
|
|
166
|
+
return ctx.kind !== "none" ? ctx.prefix : "";
|
|
167
|
+
}
|
|
168
|
+
const OPERATOR_TYPES = new Set(["Operator", "NullishCoalescing"]);
|
|
169
|
+
function tokenKey(t) {
|
|
170
|
+
return t.type === "Punctuation" ? `${t.type}:${t.value}` : t.type;
|
|
171
|
+
}
|
|
172
|
+
const LAMBDA_TRIGGERS = new Set([
|
|
173
|
+
"Punctuation:(",
|
|
174
|
+
"Punctuation:,",
|
|
175
|
+
"Punctuation:?",
|
|
176
|
+
"Punctuation::"
|
|
177
|
+
]);
|
|
178
|
+
const VALUE_PRODUCERS = new Set([
|
|
179
|
+
"Identifier",
|
|
180
|
+
"Number",
|
|
181
|
+
"String",
|
|
182
|
+
"Boolean",
|
|
183
|
+
"Punctuation:)",
|
|
184
|
+
"Punctuation:]"
|
|
185
|
+
]);
|
|
186
|
+
function classifyCursor(tokens, cursor) {
|
|
187
|
+
const before = tokens.filter((t) => t.start < cursor);
|
|
188
|
+
if (before.length === 0) return {
|
|
189
|
+
kind: "identifier",
|
|
190
|
+
prefix: ""
|
|
191
|
+
};
|
|
192
|
+
const last = before[before.length - 1];
|
|
193
|
+
const secondLast = before.length >= 2 ? before[before.length - 2] : null;
|
|
194
|
+
const lastIsPartial = last.type === "Identifier" && last.end >= cursor;
|
|
195
|
+
if (last.type === "Pipe") return {
|
|
196
|
+
kind: "pipe-transform",
|
|
197
|
+
prefix: ""
|
|
198
|
+
};
|
|
199
|
+
if (lastIsPartial && secondLast?.type === "Pipe") return {
|
|
200
|
+
kind: "pipe-transform",
|
|
201
|
+
prefix: last.value
|
|
202
|
+
};
|
|
203
|
+
const state = buildLambdaState(before);
|
|
204
|
+
const dotToken = lastIsPartial ? secondLast : last;
|
|
205
|
+
const prefix = lastIsPartial ? last.value : "";
|
|
206
|
+
if (dotToken && isDot(dotToken)) {
|
|
207
|
+
const beforeDotIdx = before.indexOf(dotToken);
|
|
208
|
+
const prevToken = beforeDotIdx > 0 ? before[beforeDotIdx - 1] : null;
|
|
209
|
+
if (state.depth > 0) {
|
|
210
|
+
const prevKey = prevToken ? tokenKey(prevToken) : "";
|
|
211
|
+
if (!prevToken || LAMBDA_TRIGGERS.has(prevKey) || OPERATOR_TYPES.has(prevToken.type)) return {
|
|
212
|
+
kind: "lambda-start",
|
|
213
|
+
prefix,
|
|
214
|
+
depth: state.depth
|
|
215
|
+
};
|
|
216
|
+
if (state.lambdaActive[state.depth] && prevToken && VALUE_PRODUCERS.has(tokenKey(prevToken))) return {
|
|
217
|
+
kind: "lambda-member",
|
|
218
|
+
prefix,
|
|
219
|
+
chain: [...state.currentChain],
|
|
220
|
+
depth: state.depth
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
return {
|
|
224
|
+
kind: "top-level-member",
|
|
225
|
+
prefix,
|
|
226
|
+
precedingTokens: before.slice(0, before.indexOf(dotToken))
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
if (OPERATOR_TYPES.has(last.type) || last.type === "Punctuation" && "(,".includes(last.value)) return {
|
|
230
|
+
kind: "identifier",
|
|
231
|
+
prefix: ""
|
|
232
|
+
};
|
|
233
|
+
if (lastIsPartial) {
|
|
234
|
+
if (!secondLast || OPERATOR_TYPES.has(secondLast.type) || secondLast.type === "Punctuation" && "(,".includes(secondLast.value)) {
|
|
235
|
+
if (state.depth > 0 && secondLast && (LAMBDA_TRIGGERS.has(tokenKey(secondLast)) || OPERATOR_TYPES.has(secondLast.type))) return {
|
|
236
|
+
kind: "lambda-start",
|
|
237
|
+
prefix: last.value,
|
|
238
|
+
depth: state.depth
|
|
239
|
+
};
|
|
240
|
+
return {
|
|
241
|
+
kind: "identifier",
|
|
242
|
+
prefix: last.value
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
return { kind: "none" };
|
|
247
|
+
}
|
|
248
|
+
function isDot(t) {
|
|
249
|
+
return t.type === "Punctuation" && t.value === "." || t.type === "OptionalChain";
|
|
250
|
+
}
|
|
251
|
+
function buildLambdaState(tokens) {
|
|
252
|
+
let depth = 0;
|
|
253
|
+
const lambdaActive = {};
|
|
254
|
+
const currentChain = [];
|
|
255
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
256
|
+
const t = tokens[i];
|
|
257
|
+
if (t.type === "Punctuation" && t.value === "(") {
|
|
258
|
+
depth++;
|
|
259
|
+
lambdaActive[depth] = false;
|
|
260
|
+
} else if (t.type === "Punctuation" && t.value === ")") {
|
|
261
|
+
delete lambdaActive[depth];
|
|
262
|
+
depth = Math.max(0, depth - 1);
|
|
263
|
+
} else if (depth > 0 && isDot(t)) {
|
|
264
|
+
const prev = i > 0 ? tokens[i - 1] : null;
|
|
265
|
+
const prevKey = prev ? tokenKey(prev) : "";
|
|
266
|
+
if (!prev || LAMBDA_TRIGGERS.has(prevKey) || OPERATOR_TYPES.has(prev?.type ?? "")) {
|
|
267
|
+
lambdaActive[depth] = true;
|
|
268
|
+
currentChain.length = 0;
|
|
269
|
+
} else if (lambdaActive[depth] && prev && VALUE_PRODUCERS.has(tokenKey(prev))) {
|
|
270
|
+
if (prev.type === "Identifier") currentChain.push(prev.value);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
return {
|
|
275
|
+
depth,
|
|
276
|
+
lambdaActive,
|
|
277
|
+
currentChain
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
//#endregion
|
|
281
|
+
//#region src/autocomplete/catalog.ts
|
|
282
|
+
/** Methods allowed per receiver type. Must stay in sync with isAllowedReceiver in eval-ops.ts. */
|
|
283
|
+
const METHODS_BY_TYPE = {
|
|
284
|
+
string: [
|
|
285
|
+
"trim",
|
|
286
|
+
"trimStart",
|
|
287
|
+
"trimEnd",
|
|
288
|
+
"toLowerCase",
|
|
289
|
+
"toUpperCase",
|
|
290
|
+
"startsWith",
|
|
291
|
+
"endsWith",
|
|
292
|
+
"includes",
|
|
293
|
+
"indexOf",
|
|
294
|
+
"lastIndexOf",
|
|
295
|
+
"slice",
|
|
296
|
+
"substring",
|
|
297
|
+
"at",
|
|
298
|
+
"replace",
|
|
299
|
+
"replaceAll",
|
|
300
|
+
"split",
|
|
301
|
+
"padStart",
|
|
302
|
+
"padEnd",
|
|
303
|
+
"charAt",
|
|
304
|
+
"charCodeAt",
|
|
305
|
+
"repeat",
|
|
306
|
+
"concat",
|
|
307
|
+
"toString"
|
|
308
|
+
],
|
|
309
|
+
array: [
|
|
310
|
+
"filter",
|
|
311
|
+
"map",
|
|
312
|
+
"find",
|
|
313
|
+
"findIndex",
|
|
314
|
+
"some",
|
|
315
|
+
"every",
|
|
316
|
+
"flatMap",
|
|
317
|
+
"join",
|
|
318
|
+
"flat",
|
|
319
|
+
"includes",
|
|
320
|
+
"indexOf",
|
|
321
|
+
"lastIndexOf",
|
|
322
|
+
"slice",
|
|
323
|
+
"at",
|
|
324
|
+
"concat",
|
|
325
|
+
"toReversed",
|
|
326
|
+
"toSorted",
|
|
327
|
+
"toSpliced",
|
|
328
|
+
"with",
|
|
329
|
+
"toString"
|
|
330
|
+
],
|
|
331
|
+
number: ["toFixed", "toString"]
|
|
332
|
+
};
|
|
333
|
+
/** Return type keyed by "receiverType.method" to disambiguate shared method names. */
|
|
334
|
+
const RETURN_TYPES = {
|
|
335
|
+
"string.trim": "string",
|
|
336
|
+
"string.trimStart": "string",
|
|
337
|
+
"string.trimEnd": "string",
|
|
338
|
+
"string.toLowerCase": "string",
|
|
339
|
+
"string.toUpperCase": "string",
|
|
340
|
+
"string.slice": "string",
|
|
341
|
+
"string.substring": "string",
|
|
342
|
+
"string.replace": "string",
|
|
343
|
+
"string.replaceAll": "string",
|
|
344
|
+
"string.padStart": "string",
|
|
345
|
+
"string.padEnd": "string",
|
|
346
|
+
"string.charAt": "string",
|
|
347
|
+
"string.repeat": "string",
|
|
348
|
+
"string.concat": "string",
|
|
349
|
+
"string.split": "array",
|
|
350
|
+
"string.indexOf": "number",
|
|
351
|
+
"string.lastIndexOf": "number",
|
|
352
|
+
"string.charCodeAt": "number",
|
|
353
|
+
"string.startsWith": "boolean",
|
|
354
|
+
"string.endsWith": "boolean",
|
|
355
|
+
"string.includes": "boolean",
|
|
356
|
+
"string.at": "string",
|
|
357
|
+
"string.toString": "string",
|
|
358
|
+
"array.filter": "array",
|
|
359
|
+
"array.map": "array",
|
|
360
|
+
"array.flat": "array",
|
|
361
|
+
"array.flatMap": "array",
|
|
362
|
+
"array.concat": "array",
|
|
363
|
+
"array.slice": "array",
|
|
364
|
+
"array.toReversed": "array",
|
|
365
|
+
"array.toSorted": "array",
|
|
366
|
+
"array.toSpliced": "array",
|
|
367
|
+
"array.find": "unknown",
|
|
368
|
+
"array.findIndex": "number",
|
|
369
|
+
"array.some": "boolean",
|
|
370
|
+
"array.every": "boolean",
|
|
371
|
+
"array.join": "string",
|
|
372
|
+
"array.indexOf": "number",
|
|
373
|
+
"array.lastIndexOf": "number",
|
|
374
|
+
"array.includes": "boolean",
|
|
375
|
+
"array.at": "unknown",
|
|
376
|
+
"array.with": "array",
|
|
377
|
+
"array.toString": "string",
|
|
378
|
+
"number.toFixed": "string",
|
|
379
|
+
"number.toString": "string"
|
|
380
|
+
};
|
|
381
|
+
/** Look up the return type for a method call on a given receiver type. */
|
|
382
|
+
function getMethodReturnType(receiverType, method) {
|
|
383
|
+
const key = `${receiverType}.${method}`;
|
|
384
|
+
return key in RETURN_TYPES ? RETURN_TYPES[key] : void 0;
|
|
385
|
+
}
|
|
386
|
+
/** Type guard: narrows an InferredTypeName to MethodReceiverType (types that have method catalogs). */
|
|
387
|
+
function isMethodReceiverType(t) {
|
|
388
|
+
return t in METHODS_BY_TYPE;
|
|
389
|
+
}
|
|
390
|
+
/** Re-export shared blocked names for use in completion filtering. */
|
|
391
|
+
const BLOCKED_NAMES = BLOCKED_PROPERTIES;
|
|
392
|
+
const KEYWORDS = [
|
|
393
|
+
"true",
|
|
394
|
+
"false",
|
|
395
|
+
"null",
|
|
396
|
+
"undefined",
|
|
397
|
+
"in",
|
|
398
|
+
"not in"
|
|
399
|
+
];
|
|
400
|
+
//#endregion
|
|
401
|
+
//#region src/autocomplete/inference.ts
|
|
402
|
+
function inferType(value) {
|
|
403
|
+
if (value === null) return "null";
|
|
404
|
+
if (value === void 0) return "undefined";
|
|
405
|
+
if (Array.isArray(value)) return "array";
|
|
406
|
+
const t = typeof value;
|
|
407
|
+
if (t === "string" || t === "number" || t === "boolean") return t;
|
|
408
|
+
return "object";
|
|
409
|
+
}
|
|
410
|
+
function resolvePropertyChain(context, chain, policy) {
|
|
411
|
+
let current = context;
|
|
412
|
+
for (const key of chain) {
|
|
413
|
+
if (BLOCKED_PROPERTIES.has(key)) return {
|
|
414
|
+
found: false,
|
|
415
|
+
reason: "blocked"
|
|
416
|
+
};
|
|
417
|
+
if (policy?.allowedProperties && !policy.allowedProperties.has(key)) return {
|
|
418
|
+
found: false,
|
|
419
|
+
reason: "blocked"
|
|
420
|
+
};
|
|
421
|
+
if (policy?.deniedProperties?.has(key)) return {
|
|
422
|
+
found: false,
|
|
423
|
+
reason: "blocked"
|
|
424
|
+
};
|
|
425
|
+
if (current == null || typeof current !== "object") return {
|
|
426
|
+
found: false,
|
|
427
|
+
reason: "not-object"
|
|
428
|
+
};
|
|
429
|
+
const obj = current;
|
|
430
|
+
if (!(key in obj)) return {
|
|
431
|
+
found: false,
|
|
432
|
+
reason: "not-found"
|
|
433
|
+
};
|
|
434
|
+
current = obj[key];
|
|
435
|
+
}
|
|
436
|
+
return {
|
|
437
|
+
found: true,
|
|
438
|
+
value: current
|
|
439
|
+
};
|
|
440
|
+
}
|
|
441
|
+
function inferElementType(array) {
|
|
442
|
+
const first = array.find((el) => el != null);
|
|
443
|
+
if (first === void 0) return {
|
|
444
|
+
type: "unknown",
|
|
445
|
+
properties: [],
|
|
446
|
+
value: void 0
|
|
447
|
+
};
|
|
448
|
+
const type = inferType(first);
|
|
449
|
+
if (type === "object" && first !== null && typeof first === "object") return {
|
|
450
|
+
type: "object",
|
|
451
|
+
properties: Object.keys(first),
|
|
452
|
+
value: first
|
|
453
|
+
};
|
|
454
|
+
if (type === "array") return {
|
|
455
|
+
type: "array",
|
|
456
|
+
properties: [],
|
|
457
|
+
value: first
|
|
458
|
+
};
|
|
459
|
+
if (type === "string") return {
|
|
460
|
+
type: "string",
|
|
461
|
+
properties: [],
|
|
462
|
+
value: first
|
|
463
|
+
};
|
|
464
|
+
if (type === "number") return {
|
|
465
|
+
type: "number",
|
|
466
|
+
properties: [],
|
|
467
|
+
value: first
|
|
468
|
+
};
|
|
469
|
+
if (type === "boolean") return {
|
|
470
|
+
type: "boolean",
|
|
471
|
+
properties: [],
|
|
472
|
+
value: first
|
|
473
|
+
};
|
|
474
|
+
return {
|
|
475
|
+
type: "unknown",
|
|
476
|
+
properties: [],
|
|
477
|
+
value: void 0
|
|
478
|
+
};
|
|
479
|
+
}
|
|
480
|
+
function inferMethodReturnType(receiverType, method) {
|
|
481
|
+
return getMethodReturnType(receiverType, method) ?? "unknown";
|
|
482
|
+
}
|
|
483
|
+
//#endregion
|
|
484
|
+
//#region src/autocomplete/completions.ts
|
|
485
|
+
const LAMBDA_METHODS = new Set([
|
|
486
|
+
"filter",
|
|
487
|
+
"map",
|
|
488
|
+
"find",
|
|
489
|
+
"findIndex",
|
|
490
|
+
"some",
|
|
491
|
+
"every",
|
|
492
|
+
"flatMap"
|
|
493
|
+
]);
|
|
494
|
+
const PREVIEW_MAX_LENGTH = 20;
|
|
495
|
+
const METHOD_COMPLETIONS_BY_TYPE = {};
|
|
496
|
+
for (const [type, methods] of Object.entries(METHODS_BY_TYPE)) METHOD_COMPLETIONS_BY_TYPE[type] = methods.filter((m) => !BLOCKED_NAMES.has(m)).map((method) => {
|
|
497
|
+
const returnType = getMethodReturnType(type, method);
|
|
498
|
+
const detail = returnType ? `${type} → ${returnType}` : type;
|
|
499
|
+
const isLambda = LAMBDA_METHODS.has(method);
|
|
500
|
+
const insert = isLambda ? `${method}(.)` : `${method}()`;
|
|
501
|
+
return {
|
|
502
|
+
label: method,
|
|
503
|
+
kind: "method",
|
|
504
|
+
detail,
|
|
505
|
+
insertText: insert,
|
|
506
|
+
cursorOffset: isLambda ? insert.length - 2 : insert.length - 1,
|
|
507
|
+
sortPriority: 1
|
|
508
|
+
};
|
|
509
|
+
});
|
|
510
|
+
function generateCompletions(ctx, env) {
|
|
511
|
+
const results = [];
|
|
512
|
+
switch (ctx.kind) {
|
|
513
|
+
case "top-level-member":
|
|
514
|
+
case "lambda-member":
|
|
515
|
+
addPropertyCompletions(results, env);
|
|
516
|
+
addMethodCompletions(results, env);
|
|
517
|
+
break;
|
|
518
|
+
case "lambda-start":
|
|
519
|
+
addLambdaPropertyCompletions(results, env);
|
|
520
|
+
break;
|
|
521
|
+
case "pipe-transform":
|
|
522
|
+
for (const name of env.transforms) {
|
|
523
|
+
if (env.pipe?.inputType && env.pipe.transformTypes) {
|
|
524
|
+
const accepts = env.pipe.transformTypes[name];
|
|
525
|
+
if (Array.isArray(accepts) && !accepts.includes(env.pipe.inputType)) continue;
|
|
526
|
+
}
|
|
527
|
+
results.push({
|
|
528
|
+
label: name,
|
|
529
|
+
kind: "transform",
|
|
530
|
+
sortPriority: 0
|
|
531
|
+
});
|
|
532
|
+
}
|
|
533
|
+
break;
|
|
534
|
+
case "identifier":
|
|
535
|
+
addIdentifierCompletions(results, env);
|
|
536
|
+
break;
|
|
537
|
+
case "none": break;
|
|
538
|
+
}
|
|
539
|
+
return filterAndRank(results, getPrefix(ctx));
|
|
540
|
+
}
|
|
541
|
+
function addPropertyCompletions(results, env) {
|
|
542
|
+
const value = env.member?.resolvedValue;
|
|
543
|
+
if (value == null || typeof value !== "object") return;
|
|
544
|
+
const obj = value;
|
|
545
|
+
const keys = Object.keys(obj);
|
|
546
|
+
for (const key of keys) {
|
|
547
|
+
if (isBlocked(key, env.policy)) continue;
|
|
548
|
+
const propValue = obj[key];
|
|
549
|
+
const valueType = inferType(propValue);
|
|
550
|
+
results.push({
|
|
551
|
+
label: key,
|
|
552
|
+
kind: "property",
|
|
553
|
+
detail: valueType,
|
|
554
|
+
sortPriority: 0
|
|
555
|
+
});
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
function addMethodCompletions(results, env) {
|
|
559
|
+
const type = env.member?.resolvedType;
|
|
560
|
+
if (!type || !isMethodReceiverType(type)) return;
|
|
561
|
+
const cached = METHOD_COMPLETIONS_BY_TYPE[type];
|
|
562
|
+
if (!cached) return;
|
|
563
|
+
if (env.policy.deniedProperties) {
|
|
564
|
+
for (const c of cached) if (!env.policy.deniedProperties.has(c.label)) results.push(c);
|
|
565
|
+
} else for (const c of cached) results.push(c);
|
|
566
|
+
}
|
|
567
|
+
function addLambdaPropertyCompletions(results, env) {
|
|
568
|
+
if (!env.lambda?.elementProperties) return;
|
|
569
|
+
for (const prop of env.lambda.elementProperties) {
|
|
570
|
+
if (isBlocked(prop, env.policy)) continue;
|
|
571
|
+
const valueType = env.lambda.elementValue ? inferType(env.lambda.elementValue[prop]) : void 0;
|
|
572
|
+
results.push({
|
|
573
|
+
label: prop,
|
|
574
|
+
kind: "property",
|
|
575
|
+
detail: valueType,
|
|
576
|
+
sortPriority: 0
|
|
577
|
+
});
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
function addIdentifierCompletions(results, env) {
|
|
581
|
+
if (env.identifier) for (const key of env.identifier.contextKeys) {
|
|
582
|
+
const value = env.identifier.contextValues[key];
|
|
583
|
+
const valueType = inferType(value);
|
|
584
|
+
let detail = valueType;
|
|
585
|
+
if (valueType === "string" && typeof value === "string") detail = value.length > PREVIEW_MAX_LENGTH ? `"${value.slice(0, PREVIEW_MAX_LENGTH)}…"` : `"${value}"`;
|
|
586
|
+
else if (valueType === "number") detail = String(value);
|
|
587
|
+
else if (valueType === "boolean") detail = String(value);
|
|
588
|
+
else if (valueType === "array" && Array.isArray(value)) detail = `array(${value.length})`;
|
|
589
|
+
results.push({
|
|
590
|
+
label: key,
|
|
591
|
+
kind: "variable",
|
|
592
|
+
detail,
|
|
593
|
+
sortPriority: 0
|
|
594
|
+
});
|
|
595
|
+
}
|
|
596
|
+
for (const name of env.functions) {
|
|
597
|
+
const insert = `${name}()`;
|
|
598
|
+
results.push({
|
|
599
|
+
label: name,
|
|
600
|
+
kind: "function",
|
|
601
|
+
insertText: insert,
|
|
602
|
+
cursorOffset: insert.length - 1,
|
|
603
|
+
sortPriority: 1
|
|
604
|
+
});
|
|
605
|
+
}
|
|
606
|
+
for (const kw of KEYWORDS) results.push({
|
|
607
|
+
label: kw,
|
|
608
|
+
kind: "keyword",
|
|
609
|
+
sortPriority: 2
|
|
610
|
+
});
|
|
611
|
+
}
|
|
612
|
+
function isBlocked(name, policy) {
|
|
613
|
+
if (BLOCKED_NAMES.has(name)) return true;
|
|
614
|
+
if (policy.allowedProperties && !policy.allowedProperties.has(name)) return true;
|
|
615
|
+
if (policy.deniedProperties && policy.deniedProperties.has(name)) return true;
|
|
616
|
+
return false;
|
|
617
|
+
}
|
|
618
|
+
const EXACT_MATCH_BOOST = 20;
|
|
619
|
+
const PREFIX_MATCH_BOOST = 15;
|
|
620
|
+
const FUZZY_MATCH_BOOST = 5;
|
|
621
|
+
const SCORE_EXACT = 1e3;
|
|
622
|
+
const SCORE_PREFIX_BASE = 500;
|
|
623
|
+
const SCORE_PREFIX_PER_CHAR = 10;
|
|
624
|
+
const SCORE_CONTAINS_BASE = 200;
|
|
625
|
+
const SCORE_CONTAINS_PER_CHAR = 5;
|
|
626
|
+
const SCORE_CHAR_MATCH = 10;
|
|
627
|
+
const SCORE_CONSECUTIVE_BONUS = 15;
|
|
628
|
+
const SCORE_BOUNDARY_BONUS = 10;
|
|
629
|
+
/**
|
|
630
|
+
* Fuzzy match score. Returns 0 for no match, higher = better match.
|
|
631
|
+
* Rewards: consecutive matches, matches at word boundaries (camelCase), matches at start.
|
|
632
|
+
*/
|
|
633
|
+
function fuzzyScore(label, query) {
|
|
634
|
+
const labelLower = label.toLowerCase();
|
|
635
|
+
const queryLower = query.toLowerCase();
|
|
636
|
+
if (label === query) return SCORE_EXACT;
|
|
637
|
+
if (labelLower.startsWith(queryLower)) return SCORE_PREFIX_BASE + query.length * SCORE_PREFIX_PER_CHAR;
|
|
638
|
+
if (labelLower.includes(queryLower)) return SCORE_CONTAINS_BASE + query.length * SCORE_CONTAINS_PER_CHAR;
|
|
639
|
+
let qi = 0;
|
|
640
|
+
let score = 0;
|
|
641
|
+
let prevMatchIdx = -2;
|
|
642
|
+
for (let li = 0; li < label.length && qi < queryLower.length; li++) if (labelLower[li] === queryLower[qi]) {
|
|
643
|
+
qi++;
|
|
644
|
+
score += SCORE_CHAR_MATCH;
|
|
645
|
+
if (li === prevMatchIdx + 1) score += SCORE_CONSECUTIVE_BONUS;
|
|
646
|
+
if (li === 0 || label[li] === label[li].toUpperCase() && label[li] !== label[li].toLowerCase()) score += SCORE_BOUNDARY_BONUS;
|
|
647
|
+
prevMatchIdx = li;
|
|
648
|
+
}
|
|
649
|
+
if (qi < queryLower.length) return 0;
|
|
650
|
+
return score;
|
|
651
|
+
}
|
|
652
|
+
function filterAndRank(results, prefix) {
|
|
653
|
+
if (!prefix) return results.sort(comparator);
|
|
654
|
+
const scored = [];
|
|
655
|
+
for (const c of results) {
|
|
656
|
+
const score = fuzzyScore(c.label, prefix);
|
|
657
|
+
if (score === 0) continue;
|
|
658
|
+
let boost = 0;
|
|
659
|
+
if (score >= SCORE_EXACT) boost = EXACT_MATCH_BOOST;
|
|
660
|
+
else if (score >= SCORE_PREFIX_BASE) boost = PREFIX_MATCH_BOOST;
|
|
661
|
+
else if (score >= SCORE_CONTAINS_BASE) boost = FUZZY_MATCH_BOOST;
|
|
662
|
+
scored.push({
|
|
663
|
+
completion: {
|
|
664
|
+
...c,
|
|
665
|
+
sortPriority: c.sortPriority - boost
|
|
666
|
+
},
|
|
667
|
+
score
|
|
668
|
+
});
|
|
669
|
+
}
|
|
670
|
+
scored.sort((a, b) => {
|
|
671
|
+
if (a.score !== b.score) return b.score - a.score;
|
|
672
|
+
if (a.completion.sortPriority !== b.completion.sortPriority) return a.completion.sortPriority - b.completion.sortPriority;
|
|
673
|
+
if (a.completion.label.length !== b.completion.label.length) return a.completion.label.length - b.completion.label.length;
|
|
674
|
+
if (a.completion.label < b.completion.label) return -1;
|
|
675
|
+
if (a.completion.label > b.completion.label) return 1;
|
|
676
|
+
return 0;
|
|
677
|
+
});
|
|
678
|
+
return scored.map((s) => s.completion);
|
|
679
|
+
}
|
|
680
|
+
function comparator(a, b) {
|
|
681
|
+
if (a.sortPriority !== b.sortPriority) return a.sortPriority - b.sortPriority;
|
|
682
|
+
if (a.label.length !== b.label.length) return a.label.length - b.label.length;
|
|
683
|
+
if (a.label < b.label) return -1;
|
|
684
|
+
if (a.label > b.label) return 1;
|
|
685
|
+
return 0;
|
|
686
|
+
}
|
|
687
|
+
//#endregion
|
|
688
|
+
//#region src/autocomplete/index.ts
|
|
689
|
+
const VALID_IDENTIFIER = /^[a-zA-Z_$][\w$]*$/u;
|
|
690
|
+
/** Check if an error is an expected Bonsai error (syntax, security, type, or reference). */
|
|
691
|
+
function isExpectedError(err) {
|
|
692
|
+
return err instanceof ExpressionError || err instanceof BonsaiSecurityError || err instanceof BonsaiTypeError || err instanceof BonsaiReferenceError;
|
|
693
|
+
}
|
|
694
|
+
function createAutocomplete(instance, options = {}) {
|
|
695
|
+
let context = options.context ?? {};
|
|
696
|
+
const probeCache = /* @__PURE__ */ new Map();
|
|
697
|
+
const onError = options.onError;
|
|
698
|
+
const rawPolicy = instance.getPolicy();
|
|
699
|
+
const policy = {
|
|
700
|
+
allowedProperties: rawPolicy.allowedProperties ? new Set(rawPolicy.allowedProperties) : void 0,
|
|
701
|
+
deniedProperties: rawPolicy.deniedProperties ? new Set(rawPolicy.deniedProperties) : void 0
|
|
702
|
+
};
|
|
703
|
+
const resolvePolicy = {
|
|
704
|
+
allowedProperties: policy.allowedProperties,
|
|
705
|
+
deniedProperties: policy.deniedProperties
|
|
706
|
+
};
|
|
707
|
+
return {
|
|
708
|
+
complete(expression, cursor) {
|
|
709
|
+
try {
|
|
710
|
+
return completeInner(expression, cursor);
|
|
711
|
+
} catch (err) {
|
|
712
|
+
onError?.(err, "complete");
|
|
713
|
+
return [];
|
|
714
|
+
}
|
|
715
|
+
},
|
|
716
|
+
setContext(newContext) {
|
|
717
|
+
context = newContext ?? {};
|
|
718
|
+
}
|
|
719
|
+
};
|
|
720
|
+
function completeInner(expression, cursor) {
|
|
721
|
+
cursor = Math.max(0, Math.min(cursor, expression.length));
|
|
722
|
+
if (isCursorInsideString(expression, cursor)) return [];
|
|
723
|
+
const { tokens } = tolerantTokenize(expression, cursor, onError);
|
|
724
|
+
const cursorCtx = classifyCursor(tokens, cursor);
|
|
725
|
+
if (cursorCtx.kind === "none") return [];
|
|
726
|
+
const env = {
|
|
727
|
+
transforms: [],
|
|
728
|
+
functions: [],
|
|
729
|
+
policy
|
|
730
|
+
};
|
|
731
|
+
if (cursorCtx.kind === "pipe-transform") {
|
|
732
|
+
const transforms = instance.listTransforms();
|
|
733
|
+
env.transforms = transforms;
|
|
734
|
+
const inputType = inferPipeInputType(expression, cursor, instance, context, onError);
|
|
735
|
+
env.pipe = {
|
|
736
|
+
inputType,
|
|
737
|
+
transformTypes: options.transformTypes
|
|
738
|
+
};
|
|
739
|
+
if (inputType && !options.transformTypes) {
|
|
740
|
+
const accepted = probeAcceptedTransforms(instance, inputType, probeCache, transforms, onError);
|
|
741
|
+
env.transforms = transforms.filter((name) => accepted.has(name));
|
|
742
|
+
}
|
|
743
|
+
} else if (cursorCtx.kind === "top-level-member") {
|
|
744
|
+
const chain = extractChainFromTokens(cursorCtx.precedingTokens);
|
|
745
|
+
if (chain.length > 0) {
|
|
746
|
+
const result = resolvePropertyChain(context, chain, resolvePolicy);
|
|
747
|
+
if (result.found) env.member = {
|
|
748
|
+
resolvedValue: result.value,
|
|
749
|
+
resolvedType: inferType(result.value)
|
|
750
|
+
};
|
|
751
|
+
else if (cursorCtx.prefix === "") {
|
|
752
|
+
const evalResult = tryEvalPrefix(expression, cursor, instance, context, tokens, onError);
|
|
753
|
+
if (evalResult !== void 0) env.member = {
|
|
754
|
+
resolvedValue: evalResult,
|
|
755
|
+
resolvedType: inferType(evalResult)
|
|
756
|
+
};
|
|
757
|
+
} else {
|
|
758
|
+
const inferred = inferTypeFromTokenChain(cursorCtx.precedingTokens, context, resolvePolicy);
|
|
759
|
+
if (inferred) env.member = { resolvedType: inferred };
|
|
760
|
+
}
|
|
761
|
+
} else if (cursorCtx.prefix === "") {
|
|
762
|
+
const evalResult = tryEvalPrefix(expression, cursor, instance, context, tokens, onError);
|
|
763
|
+
if (evalResult !== void 0) env.member = {
|
|
764
|
+
resolvedValue: evalResult,
|
|
765
|
+
resolvedType: inferType(evalResult)
|
|
766
|
+
};
|
|
767
|
+
}
|
|
768
|
+
} else if (cursorCtx.kind === "lambda-member") {
|
|
769
|
+
const arrTokens = extractChainBeforeCall(tokens, cursor);
|
|
770
|
+
const arrResult = resolvePropertyChain(context, arrTokens, resolvePolicy);
|
|
771
|
+
if (arrResult.found && Array.isArray(arrResult.value)) {
|
|
772
|
+
const elemInfo = inferElementType(arrResult.value);
|
|
773
|
+
if (elemInfo.type === "object") {
|
|
774
|
+
const resolved = resolvePropertyChain(elemInfo.value, cursorCtx.chain, resolvePolicy);
|
|
775
|
+
if (resolved.found) env.member = {
|
|
776
|
+
resolvedValue: resolved.value,
|
|
777
|
+
resolvedType: inferType(resolved.value)
|
|
778
|
+
};
|
|
779
|
+
} else if (elemInfo.type !== "unknown") env.member = { resolvedType: inferType(elemInfo.value) };
|
|
780
|
+
}
|
|
781
|
+
} else if (cursorCtx.kind === "lambda-start") {
|
|
782
|
+
const arrTokens = extractChainBeforeCall(tokens, cursor);
|
|
783
|
+
let resolved = false;
|
|
784
|
+
if (arrTokens.length > 0) {
|
|
785
|
+
const arrResult = resolvePropertyChain(context, arrTokens, resolvePolicy);
|
|
786
|
+
if (arrResult.found && Array.isArray(arrResult.value)) {
|
|
787
|
+
const elemInfo = inferElementType(arrResult.value);
|
|
788
|
+
env.lambda = {
|
|
789
|
+
elementProperties: elemInfo.properties,
|
|
790
|
+
elementValue: elemInfo.type === "object" ? elemInfo.value : void 0
|
|
791
|
+
};
|
|
792
|
+
resolved = true;
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
if (!resolved) {
|
|
796
|
+
const nestedArr = tryResolveNestedLambdaArray(tokens, cursor, context, resolvePolicy);
|
|
797
|
+
if (nestedArr) {
|
|
798
|
+
const elemInfo = inferElementType(nestedArr);
|
|
799
|
+
env.lambda = {
|
|
800
|
+
elementProperties: elemInfo.properties,
|
|
801
|
+
elementValue: elemInfo.type === "object" ? elemInfo.value : void 0
|
|
802
|
+
};
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
} else if (cursorCtx.kind === "identifier") {
|
|
806
|
+
env.functions = instance.listFunctions();
|
|
807
|
+
env.identifier = {
|
|
808
|
+
contextKeys: Object.keys(context),
|
|
809
|
+
contextValues: context
|
|
810
|
+
};
|
|
811
|
+
}
|
|
812
|
+
return generateCompletions(cursorCtx, env);
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
/**
|
|
816
|
+
* Try to evaluate the expression up to the cursor to get the actual runtime type.
|
|
817
|
+
* This handles method chains like `user.name.trim().` where static resolution can't
|
|
818
|
+
* know that trim() returns a string.
|
|
819
|
+
*/
|
|
820
|
+
function tryEvalPrefix(expression, cursor, instance, context, tokens, onError) {
|
|
821
|
+
const before = tokens.filter((t) => t.end <= cursor);
|
|
822
|
+
if (before.length === 0) return void 0;
|
|
823
|
+
let endIdx = before.length - 1;
|
|
824
|
+
if (before[endIdx].type === "Punctuation" && before[endIdx].value === "." || before[endIdx].type === "OptionalChain") endIdx--;
|
|
825
|
+
if (endIdx < 0) return void 0;
|
|
826
|
+
const exprEnd = before[endIdx].end;
|
|
827
|
+
const exprText = expression.slice(0, exprEnd);
|
|
828
|
+
if (!exprText.trim()) return void 0;
|
|
829
|
+
try {
|
|
830
|
+
return instance.evaluateSync(exprText, context);
|
|
831
|
+
} catch (err) {
|
|
832
|
+
if (!isExpectedError(err)) onError?.(err, "tryEvalPrefix");
|
|
833
|
+
return;
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
/**
|
|
837
|
+
* Infer the type by walking the token chain and using the return type map.
|
|
838
|
+
* Handles: user.name.trim(). → string (because trim returns string)
|
|
839
|
+
*/
|
|
840
|
+
function inferTypeFromTokenChain(tokens, context, resolveOpts) {
|
|
841
|
+
let currentType;
|
|
842
|
+
const chain = [];
|
|
843
|
+
let i = 0;
|
|
844
|
+
for (; i < tokens.length; i++) if (tokens[i].type === "Identifier") chain.push(tokens[i].value);
|
|
845
|
+
else if (tokens[i].type === "Punctuation" && tokens[i].value === "." || tokens[i].type === "OptionalChain") continue;
|
|
846
|
+
else if (tokens[i].type === "Punctuation" && tokens[i].value === "(") {
|
|
847
|
+
const methodName = chain.pop();
|
|
848
|
+
if (!methodName) break;
|
|
849
|
+
if (currentType === void 0 && chain.length > 0) {
|
|
850
|
+
const result = resolvePropertyChain(context, chain, resolveOpts);
|
|
851
|
+
currentType = result.found ? inferType(result.value) : void 0;
|
|
852
|
+
} else if (currentType !== void 0 && chain.length > 0) currentType = void 0;
|
|
853
|
+
chain.length = 0;
|
|
854
|
+
if (currentType && isMethodReceiverType(currentType)) {
|
|
855
|
+
const returned = inferMethodReturnType(currentType, methodName);
|
|
856
|
+
currentType = returned === "unknown" ? void 0 : returned;
|
|
857
|
+
} else currentType = void 0;
|
|
858
|
+
let depth = 1;
|
|
859
|
+
i++;
|
|
860
|
+
while (i < tokens.length && depth > 0) {
|
|
861
|
+
if (tokens[i].type === "Punctuation" && tokens[i].value === "(") depth++;
|
|
862
|
+
if (tokens[i].type === "Punctuation" && tokens[i].value === ")") depth--;
|
|
863
|
+
i++;
|
|
864
|
+
}
|
|
865
|
+
i--;
|
|
866
|
+
continue;
|
|
867
|
+
} else break;
|
|
868
|
+
if (currentType !== void 0 && chain.length > 0) return;
|
|
869
|
+
if (currentType === void 0 && chain.length > 0) {
|
|
870
|
+
const result = resolvePropertyChain(context, chain, resolveOpts);
|
|
871
|
+
currentType = result.found ? inferType(result.value) : void 0;
|
|
872
|
+
}
|
|
873
|
+
return currentType;
|
|
874
|
+
}
|
|
875
|
+
/**
|
|
876
|
+
* For nested lambdas like `groups.map(.users.filter(.`, resolve the inner array
|
|
877
|
+
* by walking the call stack: find the outermost array from context, get its element,
|
|
878
|
+
* then follow the lambda property chain to find the nested array.
|
|
879
|
+
*/
|
|
880
|
+
function tryResolveNestedLambdaArray(tokens, cursor, context, resolveOpts) {
|
|
881
|
+
const before = tokens.filter((t) => t.start < cursor);
|
|
882
|
+
const lambdaChainsByDepth = /* @__PURE__ */ new Map();
|
|
883
|
+
let depth = 0;
|
|
884
|
+
let currentChain = [];
|
|
885
|
+
let inLambda = false;
|
|
886
|
+
for (let i = 0; i < before.length; i++) {
|
|
887
|
+
const t = before[i];
|
|
888
|
+
if (t.type === "Punctuation" && t.value === "(") {
|
|
889
|
+
if (inLambda && currentChain.length > 0) {
|
|
890
|
+
const chain = [...currentChain];
|
|
891
|
+
chain.pop();
|
|
892
|
+
if (chain.length > 0) lambdaChainsByDepth.set(depth, chain);
|
|
893
|
+
}
|
|
894
|
+
depth++;
|
|
895
|
+
inLambda = false;
|
|
896
|
+
currentChain = [];
|
|
897
|
+
} else if (t.type === "Punctuation" && t.value === ")") depth = Math.max(0, depth - 1);
|
|
898
|
+
else if (depth > 0 && t.type === "Punctuation" && t.value === ".") {
|
|
899
|
+
const prev = i > 0 ? before[i - 1] : null;
|
|
900
|
+
if (prev && prev.type === "Punctuation" && (prev.value === "(" || prev.value === ",")) {
|
|
901
|
+
inLambda = true;
|
|
902
|
+
currentChain = [];
|
|
903
|
+
}
|
|
904
|
+
} else if (inLambda && t.type === "Identifier") currentChain.push(t.value);
|
|
905
|
+
}
|
|
906
|
+
if (inLambda && currentChain.length > 0) lambdaChainsByDepth.set(depth, [...currentChain]);
|
|
907
|
+
let outerParenIdx = -1;
|
|
908
|
+
for (let i = 0; i < before.length; i++) if (before[i].type === "Punctuation" && before[i].value === "(" && i >= 2) {
|
|
909
|
+
const prev = before[i - 1];
|
|
910
|
+
const prevPrev = before[i - 2];
|
|
911
|
+
if (prev.type === "Identifier" && (prevPrev.type === "Punctuation" && prevPrev.value === "." || prevPrev.type === "OptionalChain" || prevPrev.type === "Pipe")) {
|
|
912
|
+
outerParenIdx = i;
|
|
913
|
+
break;
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
if (outerParenIdx <= 0) return void 0;
|
|
917
|
+
const chain = extractChainBeforeOuterCall(before, outerParenIdx);
|
|
918
|
+
if (chain.length === 0) return void 0;
|
|
919
|
+
const outerResult = resolvePropertyChain(context, chain, resolveOpts);
|
|
920
|
+
if (!outerResult.found || !Array.isArray(outerResult.value)) return void 0;
|
|
921
|
+
let currentValue = outerResult.value;
|
|
922
|
+
for (let d = 1; d <= depth; d++) {
|
|
923
|
+
const elemInfo = inferElementType(currentValue);
|
|
924
|
+
if (elemInfo.type !== "object") return void 0;
|
|
925
|
+
const lambdaChain = lambdaChainsByDepth.get(d);
|
|
926
|
+
if (!lambdaChain || lambdaChain.length === 0) return currentValue;
|
|
927
|
+
const nestedResult = resolvePropertyChain(elemInfo.value, lambdaChain, resolveOpts);
|
|
928
|
+
if (!nestedResult.found || !Array.isArray(nestedResult.value)) return void 0;
|
|
929
|
+
currentValue = nestedResult.value;
|
|
930
|
+
}
|
|
931
|
+
return Array.isArray(currentValue) ? currentValue : void 0;
|
|
932
|
+
}
|
|
933
|
+
const PROBE_CACHE_MAX = 32;
|
|
934
|
+
const TYPE_SAMPLES = {
|
|
935
|
+
string: "sample",
|
|
936
|
+
number: 42,
|
|
937
|
+
boolean: true,
|
|
938
|
+
array: [0],
|
|
939
|
+
object: {}
|
|
940
|
+
};
|
|
941
|
+
function probeAcceptedTransforms(instance, inputType, cache, currentTransforms, onError) {
|
|
942
|
+
const cacheKey = `${inputType}:${[...currentTransforms].sort().join(",")}`;
|
|
943
|
+
const cached = cache.get(cacheKey);
|
|
944
|
+
if (cached) return cached;
|
|
945
|
+
const sample = TYPE_SAMPLES[inputType];
|
|
946
|
+
if (sample === void 0) return new Set(currentTransforms);
|
|
947
|
+
const accepted = /* @__PURE__ */ new Set();
|
|
948
|
+
for (const name of currentTransforms) {
|
|
949
|
+
if (!VALID_IDENTIFIER.test(name)) continue;
|
|
950
|
+
try {
|
|
951
|
+
instance.evaluateSync(`x |> ${name}`, { x: sample });
|
|
952
|
+
accepted.add(name);
|
|
953
|
+
} catch (err) {
|
|
954
|
+
if (!isExpectedError(err)) onError?.(err, `probeTransform:${name}`);
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
if (cache.size >= PROBE_CACHE_MAX) {
|
|
958
|
+
const first = cache.keys().next();
|
|
959
|
+
if (!first.done) cache.delete(first.value);
|
|
960
|
+
}
|
|
961
|
+
cache.set(cacheKey, accepted);
|
|
962
|
+
return accepted;
|
|
963
|
+
}
|
|
964
|
+
function inferPipeInputType(expression, cursor, instance, context, onError) {
|
|
965
|
+
const pipeMatch = expression.slice(0, cursor).match(/^(?<input>.*)\|>\s*\w*\s*$/su);
|
|
966
|
+
if (!pipeMatch) return void 0;
|
|
967
|
+
const exprBefore = (pipeMatch.groups?.input ?? "").trim();
|
|
968
|
+
if (!exprBefore) return void 0;
|
|
969
|
+
try {
|
|
970
|
+
const result = instance.evaluateSync(exprBefore, context);
|
|
971
|
+
if (result === null || result === void 0) return void 0;
|
|
972
|
+
return inferType(result);
|
|
973
|
+
} catch (err) {
|
|
974
|
+
if (!isExpectedError(err)) onError?.(err, "inferPipeInputType");
|
|
975
|
+
return;
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
/** Extract trailing identifier.dot chain from a token array (walking backward). */
|
|
979
|
+
function extractChainFromTokens(tokens) {
|
|
980
|
+
const chain = [];
|
|
981
|
+
for (let i = tokens.length - 1; i >= 0; i--) {
|
|
982
|
+
const t = tokens[i];
|
|
983
|
+
if (t.type === "Identifier") chain.unshift(t.value);
|
|
984
|
+
else if (t.type === "Punctuation" && t.value === "." || t.type === "OptionalChain") continue;
|
|
985
|
+
else break;
|
|
986
|
+
}
|
|
987
|
+
return chain;
|
|
988
|
+
}
|
|
989
|
+
/** Extract the identifier chain before the outermost call's open paren. */
|
|
990
|
+
function extractChainBeforeOuterCall(before, outerParenIdx) {
|
|
991
|
+
const chain = [];
|
|
992
|
+
let walkIdx = outerParenIdx - 1;
|
|
993
|
+
if (walkIdx >= 0 && before[walkIdx].type === "Identifier") walkIdx--;
|
|
994
|
+
if (walkIdx >= 0) {
|
|
995
|
+
const sep = before[walkIdx];
|
|
996
|
+
if (sep.type === "Punctuation" && sep.value === "." || sep.type === "OptionalChain" || sep.type === "Pipe") walkIdx--;
|
|
997
|
+
}
|
|
998
|
+
if (walkIdx >= 0 && before[walkIdx].type === "Punctuation" && before[walkIdx].value === ")") {
|
|
999
|
+
let parenDepth = 1;
|
|
1000
|
+
walkIdx--;
|
|
1001
|
+
while (walkIdx >= 0 && parenDepth > 0) {
|
|
1002
|
+
if (before[walkIdx].type === "Punctuation" && before[walkIdx].value === ")") parenDepth++;
|
|
1003
|
+
if (before[walkIdx].type === "Punctuation" && before[walkIdx].value === "(") parenDepth--;
|
|
1004
|
+
walkIdx--;
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
1007
|
+
for (let i = walkIdx; i >= 0; i--) if (before[i].type === "Identifier") chain.unshift(before[i].value);
|
|
1008
|
+
else if (before[i].type === "Punctuation" && before[i].value === "." || before[i].type === "OptionalChain") continue;
|
|
1009
|
+
else break;
|
|
1010
|
+
return chain;
|
|
1011
|
+
}
|
|
1012
|
+
function extractChainBeforeCall(tokens, cursor) {
|
|
1013
|
+
const before = tokens.filter((t) => t.start < cursor);
|
|
1014
|
+
let parenIdx = -1;
|
|
1015
|
+
let depth = 0;
|
|
1016
|
+
for (let i = before.length - 1; i >= 0; i--) {
|
|
1017
|
+
if (before[i].type === "Punctuation" && before[i].value === ")") depth++;
|
|
1018
|
+
if (before[i].type === "Punctuation" && before[i].value === "(") {
|
|
1019
|
+
if (depth === 0) {
|
|
1020
|
+
parenIdx = i;
|
|
1021
|
+
break;
|
|
1022
|
+
}
|
|
1023
|
+
depth--;
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
if (parenIdx <= 0) return [];
|
|
1027
|
+
const methodToken = before[parenIdx - 1];
|
|
1028
|
+
if (!methodToken || methodToken.type !== "Identifier") return [];
|
|
1029
|
+
const preMethodIdx = parenIdx - 2;
|
|
1030
|
+
if (preMethodIdx < 0) return [];
|
|
1031
|
+
const preMethodToken = before[preMethodIdx];
|
|
1032
|
+
if (preMethodToken.type === "Punctuation" && preMethodToken.value === "." || preMethodToken.type === "OptionalChain" || preMethodToken.type === "Pipe") return extractChainFromTokens(before.slice(0, preMethodIdx));
|
|
1033
|
+
return [];
|
|
1034
|
+
}
|
|
1035
|
+
//#endregion
|
|
1036
|
+
export { createAutocomplete };
|
|
1037
|
+
|
|
1038
|
+
//# sourceMappingURL=index.mjs.map
|