pi-supernova 0.10.1 → 0.11.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.
Files changed (81) hide show
  1. package/README.md +376 -221
  2. package/docs/CHANGELOG.md +107 -5
  3. package/docs/TOKEN_COSTS.md +149 -324
  4. package/index.js +40 -11
  5. package/package.json +4 -3
  6. package/src/adapters/bash.js +40 -10
  7. package/src/adapters/edit.js +77 -23
  8. package/src/adapters/errors.js +4 -0
  9. package/src/adapters/index.js +1 -0
  10. package/src/adapters/list.js +6 -3
  11. package/src/adapters/read-focus.js +6 -0
  12. package/src/adapters/read-image.js +17 -9
  13. package/src/adapters/read-indexed.js +72 -0
  14. package/src/adapters/read-text.js +19 -2
  15. package/src/adapters/read.js +57 -13
  16. package/src/adapters/refs.js +17 -7
  17. package/src/adapters/write.js +14 -21
  18. package/src/bridge/host-bridge.js +152 -39
  19. package/src/bridge/invoke.js +3 -1
  20. package/src/bridge/tool-registry.js +132 -16
  21. package/src/context/evidence-graph.js +1 -0
  22. package/src/context/evidence-rank.js +1 -0
  23. package/src/context/evidence.js +4 -2
  24. package/src/context/fuzzy.js +31 -8
  25. package/src/context/outline.js +33 -9
  26. package/src/context/query.js +7 -2
  27. package/src/context/repo-index.js +8 -3
  28. package/src/context/search-files.js +20 -0
  29. package/src/context/search.js +25 -10
  30. package/src/context/snap-search.js +28 -6
  31. package/src/context/snap.js +8 -3
  32. package/src/context/source-entry.js +64 -28
  33. package/src/context/spans.js +1 -0
  34. package/src/contract/bash.js +16 -7
  35. package/src/contract/edit.js +26 -7
  36. package/src/contract/program.js +2 -1
  37. package/src/contract/read.js +46 -46
  38. package/src/fs/background.js +10 -2
  39. package/src/fs/check.js +844 -46
  40. package/src/fs/commit.js +89 -15
  41. package/src/fs/diff.js +12 -9
  42. package/src/fs/directory.js +14 -4
  43. package/src/fs/file-io.js +28 -7
  44. package/src/fs/glob.js +93 -11
  45. package/src/fs/json-read.js +3 -2
  46. package/src/fs/json-size.js +2 -1
  47. package/src/fs/lines.js +49 -2
  48. package/src/fs/patch.js +26 -10
  49. package/src/fs/process-tree.js +19 -1
  50. package/src/fs/read-window.js +36 -20
  51. package/src/fs/source-window.js +5 -10
  52. package/src/fs/text-ops.js +39 -19
  53. package/src/fs/vfs.js +296 -41
  54. package/src/fs/workspace.js +24 -6
  55. package/src/output/bottleneck.js +50 -25
  56. package/src/output/final.js +20 -0
  57. package/src/output/format.js +97 -36
  58. package/src/output/outcome.js +25 -9
  59. package/src/runtime/batch-input.js +17 -3
  60. package/src/runtime/guest-api.js +32 -7
  61. package/src/runtime/guest-worker.js +22 -4
  62. package/src/runtime/parallel.js +9 -49
  63. package/src/runtime/program-batch.js +53 -28
  64. package/src/runtime/program-file.js +2 -1
  65. package/src/runtime/program.js +94 -41
  66. package/src/runtime/reference.js +10 -9
  67. package/src/runtime/runtime.js +86 -17
  68. package/src/runtime/worker-pool.js +10 -1
  69. package/src/shared/decode.js +9 -8
  70. package/src/shared/image-worker.js +7 -0
  71. package/src/shared/image.js +58 -15
  72. package/src/shared/png.js +18 -0
  73. package/src/shared/result.js +30 -9
  74. package/src/shared/syntax-context.js +7 -1
  75. package/src/shared/utf8.js +11 -13
  76. package/src/ui/host-render.js +4 -0
  77. package/src/ui/omp-frame.js +2 -0
  78. package/src/ui/progress.js +2 -1
  79. package/src/ui/render-measure.js +11 -0
  80. package/src/ui/render.js +92 -31
  81. package/src/ui/trace.js +8 -5
package/src/fs/check.js CHANGED
@@ -6,24 +6,224 @@ const OPEN = { "{": "}", "[": "]", "(": ")" };
6
6
 
7
7
  const CLOSE = new Set(["}", "]", ")"]);
8
8
 
9
- const REGEX_PRECEDERS = new Set(["(", ",", "=", ":", "[", "!", "&", "|", "?", "{", "}", ";", "+", "-", "*", "%", "<", ">", "~", "^", "return", "typeof", "case", "do", "else", "in", "of"]);
9
+ const REGEX_PRECEDERS = new Set(["(", ",", "...", "=", "=>", ":", "[", "!", "&", "|", "?", "{", "}", ";", "+", "-", "*", "/", "%", "<", ">", "~", "^", "return", "throw", "typeof", "void", "delete", "await", "yield", "debugger", "new", "case", "do", "else", "in", "instanceof"]);
10
10
 
11
- function skipString(text, i, quote) {
12
- for (let j = i + 1; j < text.length; j++) {
11
+ const CONTROL_PARENS = new Set(["if", "while", "for", "with"]);
12
+
13
+ const RESTRICTED_PRODUCTIONS = new Set(["return", "yield", "break", "continue"]);
14
+
15
+ const JS_EXT = new Set([".js", ".mjs", ".cjs", ".jsx", ".ts", ".tsx", ".mts", ".cts"]);
16
+
17
+ const OBJECT_PRECEDERS = new Set(["(", "[", "=", ",", "...", "?", "+", "-", "*", "/", "%", "<", ">", "&", "|", "^", "!", "~", "return", "throw", "typeof", "void", "delete", "yield", "await", "case", "in", "instanceof"]);
18
+
19
+ function skipString(text, i, quote, multiline = false, spliced = false) {
20
+ for (let j = i + quote.length; j < text.length; j++) {
13
21
  if (text[j] === "\\") {
14
- j++;
22
+ let escaped = j + 1;
23
+
24
+ // C/C++ removes line splices before interpreting escapes. An escape can
25
+ // therefore reach a quote across one or more physical backslash-newlines.
26
+ if (spliced) {
27
+ while (text.startsWith("\\\n", escaped) || text.startsWith("\\\r\n", escaped)) escaped += text[escaped + 1] === "\r" ? 3 : 2;
28
+ }
29
+
30
+ // CRLF is one escaped line terminator, not an escaped CR followed by bare LF.
31
+ j = escaped + (text[escaped] === "\r" && text[escaped + 1] === "\n" ? 1 : 0);
15
32
  continue;
16
33
  }
17
34
 
18
- if (text[j] === quote) return j + 1;
35
+ if (text.startsWith(quote, j)) return j + quote.length;
19
36
 
20
- if (quote !== "`" && text[j] === "\n") return -1;
37
+ if (!multiline && quote !== "`" && text[j] === "\n") return -1;
21
38
  }
22
39
 
23
40
  return -1;
24
41
  }
25
42
 
26
- function skipTemplate(text, i, stack) {
43
+ // C++ raw strings carry their own delimiter. Phase-two splices may split the
44
+ // encoding/R prefix, but the quoted raw payload keeps its original bytes.
45
+ function consumeCppRaw(text, i) {
46
+ const prefix = /(?:(?:u(?:\\\r?\n)*8|[uUL])(?:\\\r?\n)*)?R(?:\\\r?\n)*"/y;
47
+ prefix.lastIndex = i;
48
+
49
+ if (!prefix.test(text)) return null;
50
+ const start = prefix.lastIndex;
51
+ const opener = /^([^\s()\\]{0,16})\(/.exec(text.slice(start, start + 17));
52
+
53
+ if (!opener) return null;
54
+ const end = text.indexOf(")" + opener[1] + '"', start + opener[0].length);
55
+
56
+ return end < 0 ? { error: "unterminated raw string", at: i } : { end: end + opener[1].length + 2, prev: "value" };
57
+ }
58
+
59
+ // Rust r#"..."#, br#"..."#, cr#"..."# (and r"...") treat quotes, braces, and
60
+ // backslashes as payload. r#ident is a raw identifier, not a string.
61
+ function consumeRustRaw(text, i) {
62
+ let j = i;
63
+
64
+ if (text[j] === "b" || text[j] === "c") j++;
65
+
66
+ if (text[j] !== "r") return null;
67
+ j++;
68
+ let hashes = 0;
69
+
70
+ while (text[j] === "#") {
71
+ hashes++;
72
+ j++;
73
+ }
74
+
75
+ if (text[j] !== '"') return null;
76
+ const closer = '"' + "#".repeat(hashes);
77
+ const end = text.indexOf(closer, j + 1);
78
+
79
+ return end < 0 ? { error: "unterminated raw string", at: i } : { end: end + closer.length, prev: "value" };
80
+ }
81
+
82
+ // Raw interpolation uses the dollar count as its brace delimiter. Nested C#
83
+ // expressions can contain quotes matching the enclosing raw-string delimiter.
84
+ function consumeCSharpRawInterpolation(text, i) {
85
+ const prefix = /(\$+)("{3,})/y;
86
+ prefix.lastIndex = i;
87
+ const opener = prefix.exec(text);
88
+
89
+ if (!opener) return null;
90
+ const width = opener[1].length;
91
+ const quote = opener[2];
92
+
93
+ for (let j = i + opener[0].length; j < text.length; j++) {
94
+ if (text.startsWith(quote, j)) return { end: j + quote.length, prev: "value" };
95
+
96
+ if (text[j] !== "{") continue;
97
+ let run = j + 1;
98
+
99
+ while (text[run] === "{") run++;
100
+
101
+ if (run - j < width) { j = run - 1; continue; }
102
+
103
+ const end = balancedEnd(text, run - 1, [], false, false, false, false, ".cs", true);
104
+
105
+ if (end < 0 || !text.startsWith("}".repeat(width - 1), end)) return { error: "unterminated string", at: i };
106
+ j = end + width - 2;
107
+ }
108
+
109
+ return { error: "unterminated string", at: i };
110
+ }
111
+
112
+ // C# interpolation expressions are nested code, not enclosing-string payload.
113
+ // Verbatim payload escapes quotes by doubling them and leaves backslashes intact.
114
+ function consumeCSharpString(text, i) {
115
+ const raw = consumeCSharpRawInterpolation(text, i);
116
+
117
+ if (raw) return raw;
118
+ const opener = /^(?:\$@"|@\$"|@"|\$")/.exec(text.slice(i, i + 3));
119
+
120
+ if (!opener) return null;
121
+ const verbatim = opener[0].includes("@");
122
+ const interpolated = opener[0].includes("$");
123
+
124
+ for (let j = i + opener[0].length; j < text.length; j++) {
125
+ if (!verbatim && text[j] === "\\") { j++; continue; }
126
+
127
+ if (interpolated && text[j] === "{") {
128
+ if (text[j + 1] === "{") { j++; continue; }
129
+
130
+ const end = balancedEnd(text, j, [], false, false, false, false, ".cs", true);
131
+
132
+ if (end < 0) return { error: "unterminated string", at: i };
133
+ j = end - 1;
134
+ continue;
135
+ }
136
+
137
+ if (text[j] === '"') {
138
+ if (verbatim && text[j + 1] === '"') { j++; continue; }
139
+
140
+ return { end: j + 1, prev: "value" };
141
+ }
142
+
143
+ if (!verbatim && text[j] === "\n") break;
144
+ }
145
+
146
+ return { error: "unterminated string", at: i };
147
+ }
148
+
149
+ // Kotlin templates contain nested Kotlin code. Raw payload has no backslash
150
+ // escapes, but both ordinary and raw strings interpolate ${...} expressions.
151
+ function consumeKotlinString(text, i) {
152
+ const quote = text.startsWith('"""', i) ? '"""' : '"';
153
+ const raw = quote.length === 3;
154
+
155
+ for (let j = i + quote.length; j < text.length; j++) {
156
+ if (!raw && text[j] === "\\") { j++; continue; }
157
+
158
+ if (text[j] === "$" && text[j + 1] === "{") {
159
+ const end = balancedEnd(text, j + 1, [], false, false, false, false, ".kt");
160
+
161
+ if (end < 0) return { error: "unterminated string", at: i };
162
+ j = end - 1;
163
+ continue;
164
+ }
165
+
166
+ if (text.startsWith(quote, j)) return { end: j + quote.length, prev: "value" };
167
+
168
+ if (!raw && text[j] === "\n") break;
169
+ }
170
+
171
+ return { error: "unterminated string", at: i };
172
+ }
173
+
174
+ // Swift strings use matching hash counts on delimiters and escapes. Interpolation
175
+ // is nested Swift code; its quoted payload must not close the enclosing string.
176
+ function consumeSwiftString(text, i) {
177
+ let start = i;
178
+
179
+ while (text[start] === "#") start++;
180
+
181
+ if (text[start] !== '"') return null;
182
+ const hashes = text.slice(i, start);
183
+ const quote = text.startsWith('"""', start) ? '"""' : '"';
184
+ const closer = quote + hashes;
185
+
186
+ for (let j = start + quote.length; j < text.length; j++) {
187
+ if (text[j] === "\\" && text.startsWith(hashes, j + 1)) {
188
+ const escaped = j + hashes.length + 1;
189
+
190
+ if (text[escaped] === "(") {
191
+ const end = balancedEnd(text, escaped, [], false, false, false, false, ".swift");
192
+
193
+ if (end < 0) return { error: "unterminated string", at: i };
194
+ j = end - 1;
195
+ } else j = escaped;
196
+ continue;
197
+ }
198
+
199
+ if (text.startsWith(closer, j)) return { end: j + closer.length, prev: "value" };
200
+
201
+ if (quote.length === 1 && text[j] === "\n") break;
202
+ }
203
+
204
+ return { error: "unterminated string", at: i };
205
+ }
206
+
207
+ // Swift extended regexes close on / plus the opener's hash count. Bare slashes,
208
+ // quotes, brackets and multiline payload are pattern text, not source tokens.
209
+ function consumeSwiftRegex(text, i) {
210
+ let start = i;
211
+
212
+ while (text[start] === "#") start++;
213
+
214
+ if (start === i || text[start] !== "/") return null;
215
+ const closer = "/" + text.slice(i, start);
216
+
217
+ for (let j = start + 1; j < text.length; j++) {
218
+ if (text[j] === "\\") { j++; continue; }
219
+
220
+ if (text.startsWith(closer, j)) return { end: j + closer.length, prev: "value" };
221
+ }
222
+
223
+ return { error: "unterminated regex", at: i };
224
+ }
225
+
226
+ function skipTemplate(text, i, stack, stringExt) {
27
227
  for (let j = i + 1; j < text.length; j++) {
28
228
  if (text[j] === "\\") {
29
229
  j++;
@@ -33,7 +233,7 @@ function skipTemplate(text, i, stack) {
33
233
  if (text[j] === "`") return j + 1;
34
234
 
35
235
  if (text[j] === "$" && text[j + 1] === "{") {
36
- const end = balancedEnd(text, j + 1, stack);
236
+ const end = balancedEnd(text, j + 1, stack, false, false, false, true, stringExt);
37
237
 
38
238
  if (end < 0) return -1;
39
239
  j = end - 1; // loop increment lands on the char after "}"
@@ -43,24 +243,63 @@ function skipTemplate(text, i, stack) {
43
243
  return -1;
44
244
  }
45
245
 
46
- /** Index just past the "}" matching the "{" at i, scanning nested code; −1 when unbalanced. */
47
- function balancedEnd(text, i, stack) {
246
+ /** Index just past the matching bracket at i, scanning nested code; −1 when unbalanced. */
247
+ function balancedEnd(text, i, stack, rust = false, cpp = false, go = false, js = true, stringExt = "", csharpInterpolation = false) {
48
248
  const depth = stack.length;
49
- const r = scan(text, i, stack, depth);
249
+ const r = scan(text, i, stack, depth, rust, cpp, go, js, stringExt, csharpInterpolation);
50
250
 
51
251
  return r.error ? -1 : r.end;
52
252
  }
53
253
 
54
- function skipComment(text, i) {
55
- if (text[i + 1] === "/") {
56
- const nl = text.indexOf("\n", i);
254
+ function skipComment(text, i, nested = false, js = false, stringExt = "") {
255
+ const spliced = [".c", ".cc", ".cpp", ".h", ".hpp"].includes(stringExt);
256
+ // Translation-phase line splices can split either comment opener. Keep the
257
+ // physical offsets so editable spans still include every original source line.
258
+ const opener = spliced ? /\/(?:\\\r?\n)*([/*])/y : /\/([/*])/y;
259
+ opener.lastIndex = i;
260
+ const matchOpener = opener.exec(text);
261
+
262
+ if (!matchOpener) return null;
263
+ const start = opener.lastIndex;
264
+
265
+ if (matchOpener[1] === "/") {
266
+ // Java admits CR/LF; C# additionally admits NEXT_LINE and Unicode separators.
267
+ const newline = js ? /[\r\n\u2028\u2029]/g
268
+ : stringExt === ".cs" ? /[\r\n\u0085\u2028\u2029]/g
269
+ : stringExt === ".java" ? /[\r\n]/g : /\n/g;
270
+
271
+ newline.lastIndex = start;
272
+ let match;
57
273
 
58
- return nl < 0 ? text.length : nl;
274
+ while ((match = newline.exec(text))) {
275
+ // C/C++ removes backslash-newline before comments are recognized. Even
276
+ // a pair of backslashes continues the comment; these are not string escapes.
277
+ const before = match.index - (text[match.index - 1] === "\r" ? 2 : 1);
278
+
279
+ if (!spliced || text[before] !== "\\") return match.index;
280
+ }
281
+
282
+ return text.length;
59
283
  }
60
284
 
61
- const end = text.indexOf("*/", i + 2);
285
+ // Rust, Kotlin and Swift block comments nest; other dialects close at the first */.
286
+ // C/C++ line splices can split that closer without changing source coordinates.
287
+ const closer = spliced ? /\*(?:\\\r?\n)*\//y : /\*\//y;
288
+ let depth = 1;
289
+
290
+ for (let j = start; j < text.length - 1; j++) {
291
+ if (nested && text[j] === "/" && text[j + 1] === "*") { depth++; j++; }
292
+ else if (text[j] === "*") {
293
+ closer.lastIndex = j;
62
294
 
63
- return end < 0 ? -1 : end + 2;
295
+ if (!closer.test(text)) continue;
296
+
297
+ if (--depth === 0) return closer.lastIndex;
298
+ j = closer.lastIndex - 1;
299
+ }
300
+ }
301
+
302
+ return -1;
64
303
  }
65
304
 
66
305
  function skipRegex(text, i) {
@@ -92,51 +331,198 @@ function lineOf(text, i) {
92
331
  return n;
93
332
  }
94
333
 
95
- const IDENT_START = /[A-Za-z_$]/;
96
-
97
- const IDENT_PART = /[\w$]/;
334
+ // Consume full identifiers, including Unicode escapes, so payload braces and
335
+ // keyword suffixes never become code tokens. Preserve the spelling for context.
336
+ const IDENTIFIER = /(?:[$_\p{ID_Start}]|\\u(?:[\da-fA-F]{4}|\{[\da-fA-F]+\}))(?:[$\u200C\u200D\p{ID_Continue}]|\\u(?:[\da-fA-F]{4}|\{[\da-fA-F]+\}))*/uy;
98
337
 
99
338
  function readIdentifier(text, i) {
100
- let j = i + 1;
339
+ IDENTIFIER.lastIndex = i;
340
+
341
+ return IDENTIFIER.test(text) ? IDENTIFIER.lastIndex : i;
342
+ }
343
+
344
+ function identifierContext(word, prev, stack, stringExt) {
345
+ // TypeScript assertion operators expect a type; a following object type closes
346
+ // the asserted operand, not a statement block. Bare identifiers stay operands.
347
+ if ([".ts", ".tsx", ".mts", ".cts"].includes(stringExt) && ["as", "satisfies"].includes(word)
348
+ && !REGEX_PRECEDERS.has(prev)
349
+ && !["", ".", "const", "let", "var", "function", "class", "interface", "enum", "namespace", "module", "type"].includes(prev)) return "=";
101
350
 
102
- while (j < text.length && IDENT_PART.test(text[j])) j++;
351
+ // `of` is an identifier except after the binding in a for-of header.
352
+ const forOf = word === "of" && stack.at(-1)?.forOf === true && prev !== "."
353
+ && !["", "const", "let", "var"].includes(prev) && (prev === "}" || !REGEX_PRECEDERS.has(prev));
103
354
 
104
- return j;
355
+ return prev === "." ? "value" : forOf ? "=" : word;
105
356
  }
106
357
 
107
- function consumeQuoted(text, i, stack) {
358
+ function punctuationContext(text, i, stack, conditionals, js) {
108
359
  const c = text[i];
109
360
 
361
+ // Property values and conditional alternatives begin expressions; labels and
362
+ // switch cases begin statements. Match a conditional colon at its own depth.
363
+ if (js && c === "?" && !["?", "."].includes(text[i + 1]) && text[i - 1] !== "?") conditionals.push(stack.length);
364
+
365
+ if (js && c === ":") {
366
+ const conditional = conditionals.at(-1) === stack.length;
367
+
368
+ if (conditional) conditionals.pop();
369
+
370
+ if (conditional || stack.at(-1)?.object) return "=";
371
+ }
372
+
373
+ return c;
374
+ }
375
+
376
+ function consumeQuoted(text, i, stack, go, rust, stringExt) {
377
+ const c = text[i];
378
+
379
+ // Go raw strings and Kotlin escaped identifiers have neither escapes nor
380
+ // JavaScript template interpolation.
381
+ if ((go || stringExt === ".kt") && c === "`") {
382
+ const end = text.indexOf("`", i + 1);
383
+
384
+ return end < 0 ? { error: go ? "unterminated raw string" : "unterminated escaped identifier", at: i } : { end: end + 1, prev: "value" };
385
+ }
386
+
110
387
  if (c === "`") {
111
- const end = skipTemplate(text, i, stack);
388
+ const end = skipTemplate(text, i, stack, stringExt);
112
389
 
113
390
  return end < 0 ? { error: "unterminated template literal", at: i } : { end, prev: "value" };
114
391
  }
115
392
 
116
- const end = skipString(text, i, c);
393
+ // C# raw strings have no backslash escapes; their closers match the opener's
394
+ // whole quote run, not merely three quotes.
395
+ if (stringExt === ".cs" && text.startsWith('"""', i)) {
396
+ const quote = /^"{3,}/.exec(text.slice(i))[0];
397
+ const end = text.indexOf(quote, i + quote.length);
398
+
399
+ return end < 0 ? { error: "unterminated string", at: i } : { end: end + quote.length, prev: "value" };
400
+ }
401
+
402
+ // Java text blocks close on an unescaped triple
403
+ // delimiter; individual quotes and physical newlines are payload.
404
+ const quote = stringExt === ".java" && text.startsWith('"""', i) ? '"""' : c;
405
+ const end = skipString(text, i, quote, quote.length === 3 || rust && c === '"', [".c", ".cc", ".cpp", ".h", ".hpp"].includes(stringExt));
117
406
 
118
407
  return end < 0 ? { error: "unterminated string", at: i } : { end, prev: "value" };
119
408
  }
120
409
 
121
- function consumeSlash(text, i, prev) {
122
- if (text[i + 1] === "/" || text[i + 1] === "*") {
123
- const end = skipComment(text, i);
410
+ function consumeSlash(text, i, prev, rust, js, stringExt) {
411
+ const commentEnd = skipComment(text, i, rust || stringExt === ".kt" || stringExt === ".swift", js, stringExt);
124
412
 
125
- if (end < 0) return { error: "unterminated comment", at: i };
413
+ if (commentEnd !== null) {
414
+ if (commentEnd < 0) return { error: "unterminated comment", at: i };
126
415
 
127
- return { end, prev };
416
+ // Restricted productions end at a line break, including one in a comment.
417
+ const restricted = RESTRICTED_PRODUCTIONS.has(prev);
418
+
419
+ return { end: commentEnd, prev: restricted && /[\r\n\u2028\u2029]/.test(text.slice(i, commentEnd)) ? ";" : prev, trivia: true };
128
420
  }
129
421
 
130
- if (prev !== "" && !REGEX_PRECEDERS.has(prev)) return null;
422
+ // Slash-delimited regex literals belong to JS/TS and Swift, not the other
423
+ // scanned dialects where JavaScript keywords can be ordinary identifiers.
424
+ if (!js && stringExt !== ".swift") return null;
425
+
426
+ // JS/TS class heritage and Swift try begin operands, not completed identifiers.
427
+ if (prev !== "" && !REGEX_PRECEDERS.has(prev) && !(js && prev === "extends") && !(stringExt === ".swift" && prev === "try")) return null;
131
428
  const end = skipRegex(text, i);
132
429
 
133
430
  return end > 0 ? { end, prev: "value" } : null;
134
431
  }
135
432
 
136
- /** Try to consume a comment, string, template, or regex at i. Returns { end, prev } | { error, at } | null. */
137
- function consumeLiteral(text, i, stack, prev, rust) {
433
+ function nextExpressionToken(text, i, allowNewline = true) {
434
+ while (i < text.length) {
435
+ if (/\s/.test(text[i])) {
436
+ if (!allowNewline && /[\r\n\u2028\u2029]/.test(text[i])) return -1;
437
+ i++;
438
+ continue;
439
+ }
440
+
441
+ if (!text.startsWith("/*", i) && !text.startsWith("//", i)) break;
442
+ const end = skipComment(text, i, false, true);
443
+
444
+ if (end < 0 || !allowNewline && /[\r\n\u2028\u2029]/.test(text.slice(i, end))) return -1;
445
+ i = end;
446
+ }
447
+
448
+ return i;
449
+ }
450
+
451
+ function expressionBlockKeyword(text, i) {
452
+ let end = readIdentifier(text, i);
453
+ let word = text.slice(i, end);
454
+
455
+ if (word === "async") {
456
+ const next = nextExpressionToken(text, end, false);
457
+
458
+ if (next < 0) return "";
459
+ end = readIdentifier(text, next);
460
+ word = text.slice(next, end);
461
+
462
+ if (word !== "function") return "";
463
+ }
464
+
465
+ if (word !== "function" && word !== "class") return "";
466
+ const next = nextExpressionToken(text, end);
467
+
468
+ // Keyword-named properties are not block expressions.
469
+ if (next < 0 || readIdentifier(text, next) === next && !(word === "function" ? ["(", "*"].includes(text[next]) : text[next] === "{")) return "";
470
+
471
+ return word;
472
+ }
473
+
474
+ function consumeExpressionBlock(text, i, stack, stringExt) {
475
+ const keyword = expressionBlockKeyword(text, i);
476
+
477
+ if (!keyword) return null;
478
+ const { open } = firstDeclarationOpen(text, i, text.length, false, false, false, true, keyword === "function", stringExt);
479
+
480
+ if (open < 0) return null;
481
+ const result = scan(text, open, stack, stack.length, false, false, false, true, stringExt);
482
+
483
+ return result.error ? result : { end: result.end, prev: "value" };
484
+ }
485
+
486
+ /** Consume a literal, expression object, comment, or update operator at i. Returns { end, prev } | { error, at } | null. */
487
+ function consumeLiteral(text, i, stack, prev, rust, cpp, go, js, prevEnd, stringExt) {
138
488
  const c = text[i];
139
489
 
490
+ if (cpp && ["R", "u", "U", "L"].includes(c)) {
491
+ const raw = consumeCppRaw(text, i);
492
+
493
+ if (raw) return raw;
494
+ }
495
+
496
+ // C++ numeric tokens can contain apostrophe digit separators, exponent signs
497
+ // and suffixes. Phase-two splices can split those tokens; retain their physical
498
+ // extent without treating a continued separator as a character-literal opener.
499
+ if (cpp && (/[0-9]/.test(c) || c === "." && /[0-9]/.test(text[i + 1]))) {
500
+ const number = /^(?:[0-9]|\.[0-9])(?:[eEpP](?:\\\r?\n)*[+-]|[\w.]|'(?:\\\r?\n)*[\w]|\\\r?\n)*/.exec(text.slice(i));
501
+
502
+ return { end: i + number[0].length, prev: "value" };
503
+ }
504
+
505
+ // Consume JS numeric operands atomically: a decimal's trailing dot is not a
506
+ // member operator, whereas the second dot in 42..valueOf() is.
507
+ if (js && (/[0-9]/.test(c) || c === "." && /[0-9]/.test(text[i + 1]))) {
508
+ const number = /^(?:0[xX][\da-fA-F_]+|0[bB][01_]+|0[oO][0-7_]+|(?:\d[\d_]*\.?[\d_]*|\.\d[\d_]*)(?:[eE][+-]?[\d_]+)?)n?/.exec(text.slice(i));
509
+
510
+ return { end: i + number[0].length, prev: "value" };
511
+ }
512
+
513
+ if (rust && ["r", "b", "c"].includes(c)) {
514
+ const raw = consumeRustRaw(text, i);
515
+
516
+ if (raw) return raw;
517
+
518
+ // A raw identifier is an operand, not the keyword after its r# prefix.
519
+ if (c === "r" && text[i + 1] === "#") {
520
+ const end = readIdentifier(text, i + 2);
521
+
522
+ if (end > i + 2) return { end, prev: "value" };
523
+ }
524
+ }
525
+
140
526
  if (rust && c === "'") {
141
527
  const lifetime = /^'[\p{ID_Start}_][\p{ID_Continue}]*/u.exec(text.slice(i));
142
528
  const end = i + (lifetime?.[0].length ?? 0);
@@ -145,17 +531,84 @@ function consumeLiteral(text, i, stack, prev, rust) {
145
531
  if (lifetime && text[end] !== "'") return { end, prev: "value" };
146
532
  }
147
533
 
148
- if (['"', "'", "`"].includes(c)) return consumeQuoted(text, i, stack);
534
+ if (stringExt === ".cs" && (c === "@" || c === "$")) {
535
+ const literal = consumeCSharpString(text, i);
536
+
537
+ if (literal) return literal;
538
+ }
539
+
540
+ // C# verbatim identifiers keep escaped keywords in operand context.
541
+ if (stringExt === ".cs" && c === "@") {
542
+ const end = readIdentifier(text, i + 1);
543
+
544
+ if (end > i + 1) return { end, prev: "value" };
545
+ }
546
+
547
+ if (stringExt === ".swift" && (c === "#" || c === '"')) {
548
+ const literal = consumeSwiftString(text, i) ?? consumeSwiftRegex(text, i);
549
+
550
+ if (literal) return literal;
551
+ }
552
+
553
+ if (stringExt === ".kt" && c === '"') return consumeKotlinString(text, i);
554
+
555
+ if (['"', "'", "`"].includes(c)) return consumeQuoted(text, i, stack, go, rust, stringExt);
556
+
557
+ // An arrow is not a comparison operator; its brace opens a body, not an object.
558
+ if (js && text.startsWith("=>", i)) return { end: i + 2, prev: "=>" };
559
+
560
+ // A private name is one operand token, even when its name is a keyword.
561
+ if (js && c === "#") {
562
+ const end = readIdentifier(text, i + 1);
563
+
564
+ if (end > i + 1) return { end, prev: "value" };
565
+ }
566
+
567
+ // Spread introduces an operand; its final dot is not member access.
568
+ if (js && c === "." && text.startsWith("...", i)) return { end: i + 3, prev: "..." };
569
+
570
+ // Postfix ! closes an operand. C# permits trivia; TypeScript excludes line
571
+ // breaks; Swift requires left adjacency and keeps try! in prefix context.
572
+ if (c === "!" && text[i + 1] !== "=" && prev !== "" && !REGEX_PRECEDERS.has(prev)
573
+ && (stringExt === ".cs" || js && !/[\r\n\u2028\u2029]/.test(text.slice(prevEnd, i))
574
+ || stringExt === ".swift" && prevEnd === i && prev !== "try")) return { end: i + 1, prev: "value" };
575
+
576
+ // Function/class expression bodies close a value; declarations still close a
577
+ // statement. Heritage constructors can themselves be function/class expressions.
578
+ if (js && (OBJECT_PRECEDERS.has(prev) || prev === "new" || prev === "extends")) {
579
+ const expression = consumeExpressionBlock(text, i, stack, stringExt);
580
+
581
+ if (expression) return expression;
582
+ }
583
+
584
+ // Expression aggregates close a value, not a statement block. Consume array
585
+ // bindings too so the declaration scanner can retain trailing continuations.
586
+ if (c === "{" && OBJECT_PRECEDERS.has(prev) && (js || prev !== "<" && prev !== ">")
587
+ || js && c === "[" && prev === "=") {
588
+ const depth = stack.length;
589
+ stack.push({ c, at: i, object: c === "{" });
590
+ const result = scan(text, i + 1, stack, depth, rust, cpp, go, js, stringExt);
591
+
592
+ return result.error ? result : { end: result.end, prev: "value" };
593
+ }
594
+
595
+ // Only postfix updates close a value. Prefix updates still expect an operand,
596
+ // and JavaScript postfix updates cannot cross a line terminator, even in a comment.
597
+ if ((c === "+" || c === "-") && text[i + 1] === c) {
598
+ const prefix = prev === "" || REGEX_PRECEDERS.has(prev) || js && /[\r\n\u2028\u2029]/.test(text.slice(prevEnd, i));
599
+
600
+ return { end: i + 2, prev: prefix ? c : "value" };
601
+ }
149
602
 
150
603
  if (c !== "/") return null;
151
604
 
152
- return consumeSlash(text, i, prev);
605
+ return consumeSlash(text, i, prev, rust, js, stringExt);
153
606
  }
154
607
 
155
608
  /** Push/pop a bracket; returns an error, a stop, or null to continue. */
156
- function bracket(c, i, stack, stopDepth) {
609
+ function bracket(c, i, stack, stopDepth, prev, js) {
157
610
  if (OPEN[c]) {
158
- stack.push({ c, at: i });
611
+ stack.push({ c, at: i, control: c === "(" && CONTROL_PARENS.has(prev), forOf: js && c === "(" && prev === "for" });
159
612
 
160
613
  return null;
161
614
  }
@@ -171,39 +624,384 @@ function bracket(c, i, stack, stopDepth) {
171
624
  }
172
625
 
173
626
  /** Skips comments, strings, templates and regex literals; `prev` is the last code token, which decides regex-vs-division. */
174
- function scan(text, start, stack, stopDepth, rust = false) {
627
+ function scan(text, start, stack, stopDepth, rust = false, cpp = false, go = false, js = true, stringExt = "", csharpInterpolation = false) {
628
+ const conditionals = [];
175
629
  let i = start;
176
630
  let prev = "";
631
+ let prevEnd = start;
177
632
 
178
633
  while (i < text.length) {
179
634
  const c = text[i];
180
- const literal = consumeLiteral(text, i, stack, prev, rust);
635
+
636
+ if (RESTRICTED_PRODUCTIONS.has(prev) && /[\r\n\u2028\u2029]/.test(c)) prev = ";";
637
+ const literal = consumeLiteral(text, i, stack, prev, rust, cpp, go, js, prevEnd, stringExt);
181
638
 
182
639
  if (literal) {
183
640
  if (literal.error) return literal;
184
641
  i = literal.end;
185
642
  prev = literal.prev;
643
+ prevEnd = literal.trivia ? prevEnd : i;
186
644
  continue;
187
645
  }
188
646
 
189
- if (IDENT_START.test(c)) {
190
- const j = readIdentifier(text, i);
191
- prev = text.slice(i, j);
647
+ // A top-level interpolation colon starts a format string, not C# code.
648
+ if (csharpInterpolation && c === ":" && stack.length === stopDepth + 1) {
649
+ const end = text.indexOf("}", i + 1);
650
+
651
+ if (end < 0) return { error: "unterminated string", at: i };
652
+ i = end;
653
+ continue;
654
+ }
655
+
656
+ const j = readIdentifier(text, i);
657
+
658
+ if (j > i) {
659
+ const word = text.slice(i, j);
660
+
661
+ // Preserve jump labels until their line break, and `for await` control headers.
662
+ if (prev !== "break" && prev !== "continue" && (prev !== "for" || word !== "await")) prev = identifierContext(word, prev, stack, stringExt);
663
+
192
664
  i = j;
665
+ prevEnd = i;
193
666
  continue;
194
667
  }
195
668
 
196
- const outcome = bracket(c, i, stack, stopDepth);
669
+ // Closing a control condition starts a statement, unlike closing a call or
670
+ // grouped expression. Nested parens retain their own division context.
671
+ const closesControl = c === ")" && stack.at(-1)?.control === true;
672
+ const outcome = bracket(c, i, stack, stopDepth, prev, js);
197
673
 
198
674
  if (outcome) return outcome;
199
675
 
200
- if (!/\s/.test(c)) prev = c;
676
+ if (!/\s/.test(c)) {
677
+ prev = closesControl ? ";" : punctuationContext(text, i, stack, conditionals, js);
678
+ prevEnd = i + 1;
679
+ }
680
+
201
681
  i++;
202
682
  }
203
683
 
204
684
  return { end: i };
205
685
  }
206
686
 
687
+ function offsetOfLine(text, line) {
688
+ if (line <= 1) return 0;
689
+ let n = 1;
690
+
691
+ for (let i = 0; i < text.length; i++) {
692
+ if (text[i] === "\n" && ++n === line) return i + 1;
693
+ }
694
+
695
+ return -1;
696
+ }
697
+
698
+ function firstDeclarationOpen(text, start, limit, rust, cpp, go, js, typedSignature = false, stringExt = "", bodyExpected = false) {
699
+ const stack = [];
700
+ const conditionals = [];
701
+ let i = start;
702
+ let prev = "";
703
+ let prevEnd = start;
704
+ let depth = 0;
705
+ let returnType = false;
706
+ let predicateTarget = false;
707
+ let genericDepth = 0;
708
+ let parametersStarted = false;
709
+ let heritageHeader = false;
710
+ let bindingType = false;
711
+ let typeAlias = false;
712
+
713
+ while (i < text.length) {
714
+ if (i >= limit) {
715
+ // Declaration names can put parameters or TypeScript generics after trivia.
716
+ // Completed overloads must not resume at a later expression opener.
717
+ const nextParameters = js && bodyExpected && !parametersStarted
718
+ && text[nextExpressionToken(text, i)] === "(";
719
+
720
+ const nextGenerics = typedSignature && !parametersStarted
721
+ && text[nextExpressionToken(text, i)] === "<";
722
+
723
+ // Wrapped parameters, generic headers and return types remain unfinished
724
+ // inside delimiters. A next-line union/intersection also continues a return
725
+ // type; completed overloads still stop here.
726
+ const nextTypeOperator = typedSignature && returnType && prev !== ";"
727
+ && ["|", "&"].includes(text[nextExpressionToken(text, i)]);
728
+
729
+ // `asserts` is a prefix only before an assertion target, not when an
730
+ // overload returns a type alias named asserts.
731
+ const target = prev === "asserts" ? nextExpressionToken(text, i) : -1;
732
+ const targetEnd = target < 0 ? -1 : readIdentifier(text, target);
733
+ const afterTarget = targetEnd > target ? nextExpressionToken(text, targetEnd, false) : -1;
734
+
735
+ const nextAssertionTarget = targetEnd > target
736
+ && !/^(?:export|function|class|const|var|let|import|enum|default)$/.test(text.slice(target, targetEnd))
737
+ && (afterTarget < 0 || afterTarget === text.length || ["{", ";"].includes(text[afterTarget])
738
+ || text.slice(afterTarget, readIdentifier(text, afterTarget)) === "is");
739
+
740
+ // Bindings remain unfinished after assignment, an arrow, a constructor
741
+ // prefix or an operand-introducing operator, including intervening trivia.
742
+ const nextInitializer = js && (prev === "=" || !bodyExpected && (prev === "=>" || prev === "new" || OBJECT_PRECEDERS.has(prev)));
743
+
744
+ // A leading member operator continues the previous initializer, unlike a
745
+ // leading decimal literal, which can start a separate statement after ASI.
746
+ const nextMember = js && !bodyExpected && prev !== ";"
747
+ && (prev === "." || /^(?:\.(?![\d.])|\?\.(?!\d))/.test(text.slice(nextExpressionToken(text, i))));
748
+
749
+ // Binary operators and declarator commas can lead continuation lines, but
750
+ // a next-line ++/-- starts a separate prefix-update statement under ASI.
751
+ const next = nextExpressionToken(text, i);
752
+
753
+ const nextBinary = js && !bodyExpected && prev !== ";"
754
+ && (/^(?:\+(?!\+)|-(?!-)|[*/%<>&|^,]|[=!]=|=(?!>)|\?\?)/.test(text.slice(next))
755
+ || ["in", "instanceof"].includes(text.slice(next, readIdentifier(text, next))));
756
+
757
+ // Conditional branches continue at their matching colon, not a label.
758
+ const nextConditional = js && !bodyExpected && prev !== ";"
759
+ && (text[next] === "?" || text[next] === ":" && conditionals.at(-1) === stack.length);
760
+
761
+ // A tagged template can follow its tag across trivia without ending the
762
+ // binding. A semicolon instead starts a separate untagged expression.
763
+ const nextTaggedTemplate = js && !bodyExpected && prev !== ";" && text[next] === "`";
764
+
765
+ // Calls may put arguments on later lines and continue after their closer.
766
+ // A semicolon instead starts a separate parenthesized statement.
767
+ const nextCall = js && !bodyExpected && prev !== ";" && (text[next] === "(" || depth > 0);
768
+
769
+ // Computed members continue across trivia, including wrapped keys. An
770
+ // explicit semicolon instead begins a standalone array expression.
771
+ const nextComputedMember = js && !bodyExpected && prev !== ";"
772
+ && (text[next] === "[" || stack.some(frame => frame.c === "["));
773
+
774
+ // Class/interface declarations cannot finish before their body, including
775
+ // wrapped heritage clauses. Keyword-named methods remain ordinary headers.
776
+ if (!heritageHeader && !nextInitializer && !nextMember && !nextBinary && !nextConditional && !nextTaggedTemplate && !nextCall && !nextComputedMember && !nextParameters && !nextGenerics && (!typedSignature || !(depth || genericDepth || returnType && (stack.length || nextTypeOperator || nextAssertionTarget
777
+ || [":", "|", "&", "<", ",", "?", "[", "=>", "keyof", "readonly", "typeof", "extends", "type-predicate"].includes(prev))))) break;
778
+ const lineEnd = text.indexOf("\n", i + 1);
779
+ limit = lineEnd < 0 ? text.length : lineEnd;
780
+ }
781
+
782
+ // A binding's block arrow is an initializer value. Keep scanning for later
783
+ // declarators instead of ending the binding at this function body.
784
+ if (js && !bodyExpected && text[i] === "{" && prev === "=>") {
785
+ const end = balancedEnd(text, i, [], rust, cpp, go, js, stringExt);
786
+
787
+ if (end < 0) return { open: -1, end: text.length };
788
+ i = end;
789
+ prev = "value";
790
+ prevEnd = i;
791
+ continue;
792
+ }
793
+
794
+ // A declaration signature ending in a generic > still opens its real body.
795
+ if (typedSignature && depth === 0 && text[i] === "{" && prev === ">") return { open: i, end: i };
796
+ const literal = consumeLiteral(text, i, stack, prev, rust, cpp, go, js, prevEnd, stringExt);
797
+
798
+ if (literal) {
799
+ if (literal.error) return { open: -1, end: limit };
800
+ i = literal.end;
801
+
802
+ // A multiline generic default is consumed as an expression object. Its
803
+ // closing line still contains the signature and possibly the body opener.
804
+ if (typedSignature && depth === 0 && i > limit) {
805
+ const lineEnd = text.indexOf("\n", i);
806
+ limit = lineEnd < 0 ? text.length : lineEnd;
807
+ }
808
+
809
+ prev = literal.prev;
810
+ prevEnd = literal.trivia ? prevEnd : i;
811
+ continue;
812
+ }
813
+
814
+ const j = readIdentifier(text, i);
815
+
816
+ if (j > i) {
817
+ const word = text.slice(i, j);
818
+
819
+ if (js && bodyExpected && depth === 0 && genericDepth === 0
820
+ && (word === "class" || typedSignature && word === "interface")) {
821
+ const next = nextExpressionToken(text, j);
822
+
823
+ if (word === "class" ? expressionBlockKeyword(text, i) === "class" : next >= 0 && readIdentifier(text, next) > next) heritageHeader = true;
824
+ }
825
+
826
+ if (js && depth === 0 && word === "type" && ["", "export", "declare"].includes(prev)
827
+ && [".ts", ".tsx", ".mts", ".cts"].includes(stringExt)) typeAlias = true;
828
+
829
+ const typePredicate = predicateTarget && word === "is";
830
+ // `is` is contextual: a return alias named is (including typeof is) is
831
+ // not a predicate. Only a leading target, optionally after asserts, is.
832
+ predicateTarget = returnType && [":", "asserts"].includes(prev)
833
+ && !["asserts", "typeof"].includes(word);
834
+ // Binding annotations and type aliases introduce a type operand; in
835
+ // value initializers these words stay identifiers.
836
+ prev = (bindingType || typeAlias) && ["keyof", "readonly", "unique", "infer", "abstract", "extends"].includes(word) ? "="
837
+ : typePredicate ? "type-predicate" : prev === "for" && word === "await" ? prev : identifierContext(word, prev, stack, stringExt);
838
+ i = j;
839
+ prevEnd = i;
840
+ continue;
841
+ }
842
+
843
+ if (typedSignature && depth === 0) {
844
+ if (text[i] === "<") genericDepth++;
845
+ else if (text[i] === ">") genericDepth = Math.max(0, genericDepth - 1);
846
+ }
847
+
848
+ if (text[i] === "(") {
849
+ if (depth === 0 && genericDepth === 0) parametersStarted = true;
850
+ depth++;
851
+ } else if (text[i] === ")") depth = Math.max(0, depth - 1);
852
+ else if (depth === 0 && typedSignature && text[i] === ":" && prev === ")") returnType = true;
853
+
854
+ // TypeScript generic constraints and return types can contain object types
855
+ // before the body. Consume wrapped members without ending the declaration.
856
+ if (depth === 0 && typedSignature && text[i] === "{" && (prev === "extends"
857
+ || prev === "readonly" && (genericDepth > 0 || returnType)
858
+ || genericDepth > 0 && ["keyof", "=>"].includes(prev)
859
+ || returnType && [":", "|", "&", "<", ",", "?", "[", "=>", "keyof", "type-predicate"].includes(prev))) {
860
+ const end = balancedEnd(text, i, [], rust, cpp, go, js, stringExt);
861
+
862
+ if (end < 0) return { open: -1, end: limit };
863
+ i = end;
864
+
865
+ // The type can cross the initial line bound. Scan the rest of its closing
866
+ // line too: a conditional-type suffix can precede the actual body there.
867
+ if (i > limit) {
868
+ const lineEnd = text.indexOf("\n", i);
869
+ limit = lineEnd < 0 ? text.length : lineEnd;
870
+ }
871
+
872
+ prev = "value";
873
+ prevEnd = i;
874
+ continue;
875
+ }
876
+
877
+ if (depth === 0 && (text[i] === "{" || (text[i] === "[" && prev === "="))) return { open: i, end: i };
878
+ bracket(text[i], i, stack, undefined, prev, js);
879
+
880
+ if (!/\s/.test(text[i])) {
881
+ // A top-level binding annotation introduces a type value, not a body.
882
+ // Its object brace must not end the declaration before the initializer.
883
+ const annotation = !bodyExpected && [".ts", ".tsx", ".mts", ".cts"].includes(stringExt)
884
+ && text[i] === ":" && stack.length === 0;
885
+
886
+ if (annotation) bindingType = true;
887
+ else if (stack.length === 0 && ["=", ";"].includes(text[i])) bindingType = false;
888
+ prev = annotation ? "=" : punctuationContext(text, i, stack, conditionals, js);
889
+ prevEnd = i + 1;
890
+ }
891
+
892
+ i++;
893
+ }
894
+
895
+ // An unfinished assignment may put its aggregate initializer on the next
896
+ // line. Use the last code token, not a textual '=', so comments cannot
897
+ // make an already completed binding absorb a separate block.
898
+ if (depth === 0 && prev === "=") {
899
+ const next = js ? nextExpressionToken(text, i) : limit + /^\s*/.exec(text.slice(limit))[0].length;
900
+
901
+ if (["{", "["].includes(text[next])) return { open: next, end: i };
902
+
903
+ // Literal and function/class bindings can start after the assignment's newline.
904
+ // Retain the complete expression, without absorbing a later declaration.
905
+ if (js) {
906
+ const expression = consumeLiteral(text, next, [], "=", rust, cpp, go, js, next, stringExt);
907
+
908
+ if (expression) return { open: -1, end: expression.error ? text.length : expression.end };
909
+ }
910
+ }
911
+
912
+ // Literal consumers can cross the first newline without exposing a bracket.
913
+ // Preserve that extent for bindings initialized by a multiline template/raw string.
914
+ return { open: -1, end: i };
915
+ }
916
+
917
+ function unmatchedOpenParen(text, start, limit, rust, cpp, go, js, stringExt) {
918
+ const stack = [];
919
+ const parens = [];
920
+ const conditionals = [];
921
+ let i = start;
922
+ let prev = "";
923
+ let prevEnd = start;
924
+
925
+ while (i < limit) {
926
+ const literal = consumeLiteral(text, i, stack, prev, rust, cpp, go, js, prevEnd, stringExt);
927
+
928
+ if (literal) {
929
+ if (literal.error) return -1;
930
+ i = literal.end;
931
+ prev = literal.prev;
932
+ prevEnd = literal.trivia ? prevEnd : i;
933
+ continue;
934
+ }
935
+
936
+ const j = readIdentifier(text, i);
937
+
938
+ if (j > i) {
939
+ prev = prev === "for" && text.slice(i, j) === "await" ? prev : identifierContext(text.slice(i, j), prev, stack, stringExt);
940
+ i = j;
941
+ prevEnd = i;
942
+ continue;
943
+ }
944
+
945
+ if (text[i] === "(") parens.push(i);
946
+ else if (text[i] === ")") parens.pop();
947
+ bracket(text[i], i, stack, undefined, prev, js);
948
+
949
+ if (!/\s/.test(text[i])) {
950
+ prev = punctuationContext(text, i, stack, conditionals, js);
951
+ prevEnd = i + 1;
952
+ }
953
+
954
+ i++;
955
+ }
956
+
957
+ return parens[0] ?? -1;
958
+ }
959
+
960
+ /** 1-indexed last line of a declaration block; bodyExpected permits a next-line opening brace. */
961
+ export function braceBlockEndLine(text, startLine, ext = "", bodyExpected = false) {
962
+ ext = String(ext ?? "").toLowerCase();
963
+ const rust = ext === ".rs";
964
+ const go = ext === ".go";
965
+ const js = JS_EXT.has(ext);
966
+ const cpp = [".cc", ".cpp", ".h", ".hpp"].includes(ext);
967
+ const start = offsetOfLine(text, startLine);
968
+
969
+ if (start < 0) return startLine;
970
+ const lineEnd = text.indexOf("\n", start);
971
+ const limit = lineEnd < 0 ? text.length : lineEnd;
972
+ const typedSignature = bodyExpected && [".ts", ".tsx", ".mts", ".cts"].includes(ext);
973
+ const declaration = firstDeclarationOpen(text, start, limit, rust, cpp, go, js, typedSignature, ext, bodyExpected);
974
+ let open = declaration.open;
975
+
976
+ // Wrapped signatures and call initializers both leave an unmatched `(`.
977
+ // A binding's call ends at its own `)`, not the next declaration's `{`.
978
+ if (open < 0) {
979
+ const paren = unmatchedOpenParen(text, start, Math.max(limit, declaration.end), rust, cpp, go, js, ext);
980
+
981
+ if (paren >= 0) {
982
+ const end = balancedEnd(text, paren, [], rust, cpp, go, js, ext);
983
+
984
+ // Function expressions and arrow signatures still need their body.
985
+ if (!bodyExpected && end >= 0 && !/^\s*(?:=>|\{)/.test(text.slice(end))) return lineOf(text, end - 1);
986
+ open = firstDeclarationOpen(text, start, text.length, rust, cpp, go, js, typedSignature, ext).open;
987
+ }
988
+ }
989
+
990
+ if (open < 0 && bodyExpected) {
991
+ // A completed signature may put its body on the next line. Do not apply
992
+ // this to bindings, where a following block can be a separate statement.
993
+ const nextStart = Math.max(limit, declaration.end);
994
+ const next = js ? nextExpressionToken(text, nextStart) : nextStart + /^\s*/.exec(text.slice(nextStart))[0].length;
995
+
996
+ if (text[next] === "{") open = next;
997
+ }
998
+
999
+ if (open < 0) return Math.max(startLine, lineOf(text, declaration.end - 1));
1000
+ const end = balancedEnd(text, open, [], rust, cpp, go, js, ext);
1001
+
1002
+ return end < 0 ? lineOf(text, Math.max(0, text.length - 1)) : lineOf(text, end - 1);
1003
+ }
1004
+
207
1005
  const CODE_EXT = new Set([".js", ".mjs", ".cjs", ".jsx", ".ts", ".tsx", ".mts", ".cts", ".java", ".kt", ".c", ".cc", ".cpp", ".h", ".hpp", ".cs", ".go", ".rs", ".swift", ".css", ".scss"]);
208
1006
 
209
1007
  /** { ok: true } | { ok: false, message }; message names the problem and line. */
@@ -223,7 +1021,7 @@ export function quickCheck(text, ext) {
223
1021
 
224
1022
  if (!CODE_EXT.has(ext)) return null;
225
1023
  const stack = [];
226
- const r = scan(text, 0, stack, undefined, ext === ".rs");
1024
+ const r = scan(text, 0, stack, undefined, ext === ".rs", [".cc", ".cpp", ".h", ".hpp"].includes(ext), ext === ".go", JS_EXT.has(ext), ext);
227
1025
 
228
1026
  if (r.error) return { ok: false, kind: "balance", message: r.error + " at line " + lineOf(text, r.at) };
229
1027