token-goat 2.9.13 → 2.9.15

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 (52) hide show
  1. package/README.md +28 -1
  2. package/dist/token-goat-chunk-2X2EBBC6.mjs +277 -0
  3. package/dist/token-goat-chunk-3BTK54F3.mjs +1733 -0
  4. package/dist/token-goat-chunk-3NSDDTGL.mjs +34 -0
  5. package/dist/token-goat-chunk-4EXFN2AW.mjs +29 -0
  6. package/dist/token-goat-chunk-5V7DAC7V.mjs +123 -0
  7. package/dist/token-goat-chunk-6DLVZDB6.mjs +34 -0
  8. package/dist/token-goat-chunk-7ZYK25AO.mjs +24 -0
  9. package/dist/token-goat-chunk-A4JYKD5H.mjs +144 -0
  10. package/dist/token-goat-chunk-AH6QILZM.mjs +26 -0
  11. package/dist/token-goat-chunk-ASYEPR3S.mjs +212 -0
  12. package/dist/{token-goat-chunk-RDITECDL.mjs → token-goat-chunk-ATIFTMRC.mjs} +31 -13
  13. package/dist/{token-goat-chunk-ZZI3IDQZ.mjs → token-goat-chunk-BL5LNGBG.mjs} +3935 -11949
  14. package/dist/{token-goat-chunk-4NXUKV7D.mjs → token-goat-chunk-C5JIO6HK.mjs} +8 -4
  15. package/dist/{token-goat-chunk-FZU7GMUS.mjs → token-goat-chunk-DQ4J5AFF.mjs} +50 -18
  16. package/dist/token-goat-chunk-ERTXEKB6.mjs +228 -0
  17. package/dist/{token-goat-chunk-6B44WLIF.mjs → token-goat-chunk-GIIHUSZX.mjs} +142 -21
  18. package/dist/token-goat-chunk-GMOUBOX4.mjs +386 -0
  19. package/dist/{token-goat-chunk-3ESRORNM.mjs → token-goat-chunk-GMQQA7E4.mjs} +12576 -12326
  20. package/dist/token-goat-chunk-IVUQLQWN.mjs +2046 -0
  21. package/dist/token-goat-chunk-K7F2BFIK.mjs +2430 -0
  22. package/dist/{token-goat-chunk-U7X6LQD2.mjs → token-goat-chunk-LCZBPOIN.mjs} +10197 -9717
  23. package/dist/token-goat-chunk-LMFO66YD.mjs +331 -0
  24. package/dist/token-goat-chunk-LT7JRU6K.mjs +22 -0
  25. package/dist/token-goat-chunk-MZDIJJ3R.mjs +420 -0
  26. package/dist/token-goat-chunk-NDPO7GAH.mjs +177 -0
  27. package/dist/token-goat-chunk-NDRP4KJQ.mjs +4371 -0
  28. package/dist/token-goat-chunk-NEI4NC54.mjs +424 -0
  29. package/dist/token-goat-chunk-NU7TLMQK.mjs +585 -0
  30. package/dist/token-goat-chunk-OSUFN2FV.mjs +326 -0
  31. package/dist/token-goat-chunk-OUGNPMDA.mjs +959 -0
  32. package/dist/token-goat-chunk-PM76YS22.mjs +1341 -0
  33. package/dist/token-goat-chunk-S4XRY446.mjs +2637 -0
  34. package/dist/{token-goat-chunk-YOA4N6WA.mjs → token-goat-chunk-SFAS46RE.mjs} +5 -3
  35. package/dist/{token-goat-chunk-QWSUZWFP.mjs → token-goat-chunk-SZWYESBS.mjs} +793 -650
  36. package/dist/{token-goat-chunk-B3CTCQTH.mjs → token-goat-chunk-XEPXYDPI.mjs} +3 -2
  37. package/dist/token-goat-chunk-XTQAOTSO.mjs +89 -0
  38. package/dist/token-goat-chunk-Y4AFKTHK.mjs +22 -0
  39. package/dist/token-goat-chunk-YKG35VHC.mjs +228 -0
  40. package/dist/{token-goat-chunk-JOXLE672.mjs → token-goat-chunk-YQ7WI2CO.mjs} +990 -106
  41. package/dist/{token-goat-chunk-2WC4ZUXN.mjs → token-goat-chunk-YZX7EFG4.mjs} +1 -1
  42. package/dist/token-goat-chunk-Z6UXPYJA.mjs +62 -0
  43. package/dist/token-goat-hook.mjs +16 -8
  44. package/dist/token-goat.core.mjs +27 -10
  45. package/docs/cli.md +9 -6
  46. package/package.json +4 -2
  47. package/dist/token-goat-chunk-FQCNJV4V.mjs +0 -693
  48. package/dist/token-goat-chunk-JVNPCQB7.mjs +0 -31
  49. package/dist/token-goat-chunk-P2PU4CR5.mjs +0 -26
  50. package/dist/token-goat-chunk-QKXBGBQR.mjs +0 -3653
  51. package/dist/token-goat-chunk-UM47DRD3.mjs +0 -242
  52. package/dist/token-goat-chunk-UMXJN7DI.mjs +0 -5521
@@ -0,0 +1,2430 @@
1
+ import { createRequire as __cjsRequire } from 'node:module';
2
+ const require = __cjsRequire(import.meta.url);
3
+ import {
4
+ registerReset
5
+ } from "./token-goat-chunk-EEIDFMEM.mjs";
6
+ import {
7
+ Database
8
+ } from "./token-goat-chunk-OSUFN2FV.mjs";
9
+ import {
10
+ SYMBOL_BODY_CHAR_CAP,
11
+ countContentLines,
12
+ dataDir,
13
+ ensureDirSync,
14
+ escapeRegExp,
15
+ foldCase,
16
+ foldPath,
17
+ sleepSync
18
+ } from "./token-goat-chunk-PM76YS22.mjs";
19
+ import {
20
+ safeJoin
21
+ } from "./token-goat-chunk-ERTXEKB6.mjs";
22
+ import {
23
+ init_define_import_meta_env
24
+ } from "./token-goat-chunk-A37V4PBF.mjs";
25
+
26
+ // src/languages/common.ts
27
+ init_define_import_meta_env();
28
+
29
+ // src/doc_comment.ts
30
+ init_define_import_meta_env();
31
+ function precedingDocComment(lines, lineStart, style) {
32
+ const aboveIdx = lineStart - 2;
33
+ if (aboveIdx < 0 || aboveIdx >= lines.length) return "";
34
+ const aboveLine = lines[aboveIdx];
35
+ if (aboveLine === void 0) return "";
36
+ const aboveTrimmed = aboveLine.trim();
37
+ if (style === "vb") {
38
+ if (!aboveTrimmed.startsWith("'''")) return "";
39
+ const collected = [];
40
+ for (let i = aboveIdx; i >= 0; i--) {
41
+ const trimmed = (lines[i] ?? "").trim();
42
+ if (!trimmed.startsWith("'''")) break;
43
+ collected.unshift(trimmed.replace(/^'''\s?/, ""));
44
+ }
45
+ return collected.join("\n").trim();
46
+ }
47
+ if (style === "hash") {
48
+ if (!aboveTrimmed.startsWith("#")) return "";
49
+ const collected = [];
50
+ let i = aboveIdx;
51
+ while (i >= 0) {
52
+ const line = lines[i];
53
+ if (line === void 0) break;
54
+ const trimmed = line.trim();
55
+ if (!trimmed.startsWith("#")) break;
56
+ collected.unshift(trimmed.replace(/^#+\s?/, ""));
57
+ i--;
58
+ }
59
+ return collected.join("\n").trim();
60
+ }
61
+ if (aboveTrimmed.endsWith("*/")) {
62
+ let blockStart = aboveIdx;
63
+ while (blockStart >= 0) {
64
+ const l = lines[blockStart];
65
+ if (l === void 0) break;
66
+ if (l.trim().startsWith("/*")) break;
67
+ blockStart--;
68
+ }
69
+ if (blockStart < 0) return "";
70
+ const opener = lines[blockStart];
71
+ if (opener === void 0 || !opener.trim().startsWith("/*")) return "";
72
+ const region = lines.slice(blockStart, aboveIdx + 1);
73
+ const joined = region.join("\n").trimEnd();
74
+ if (joined.indexOf("*/") !== joined.length - 2) return "";
75
+ return region.map(
76
+ (l) => l.trim().replace(/^\/\*+/, "").replace(/\*+\/$/, "").replace(/^\*\s?/, "").trim()
77
+ ).filter((l) => l !== "").join("\n");
78
+ }
79
+ if (aboveTrimmed.startsWith("//")) {
80
+ const collected = [];
81
+ let i = aboveIdx;
82
+ while (i >= 0) {
83
+ const line = lines[i];
84
+ if (line === void 0) break;
85
+ const trimmed = line.trim();
86
+ if (!trimmed.startsWith("//")) break;
87
+ collected.unshift(trimmed.replace(/^\/\/[/!]?\s?/, ""));
88
+ i--;
89
+ }
90
+ return collected.join("\n").trim();
91
+ }
92
+ return "";
93
+ }
94
+
95
+ // src/languages/common.ts
96
+ function buildLineIndex(text) {
97
+ const idx = [0];
98
+ for (let i = 0; i < text.length; i++) {
99
+ if (text[i] === "\n") idx.push(i + 1);
100
+ }
101
+ return idx;
102
+ }
103
+ function offsetToLine(lineIndex, offset) {
104
+ let lo = 0;
105
+ let hi = lineIndex.length - 1;
106
+ while (lo < hi) {
107
+ const mid = lo + hi + 1 >> 1;
108
+ const lineStart = lineIndex[mid];
109
+ if (lineStart === void 0 || lineStart > offset) {
110
+ hi = mid - 1;
111
+ } else {
112
+ lo = mid;
113
+ }
114
+ }
115
+ return lo + 1;
116
+ }
117
+ function stripCstyleComments(text, lineCommentRe) {
118
+ const lines = text.split("\n");
119
+ let inComment = false;
120
+ const outLines = [];
121
+ for (const line of lines) {
122
+ const { code, inComment: nextInComment } = stripBlockCommentSpan(line, inComment);
123
+ outLines.push(code);
124
+ inComment = nextInComment;
125
+ }
126
+ let out = outLines.join("\n");
127
+ if (lineCommentRe !== void 0) {
128
+ out = out.replace(lineCommentRe, (m) => " ".repeat(m.length));
129
+ }
130
+ return out;
131
+ }
132
+ function stripXmlComments(text) {
133
+ const lines = text.split("\n");
134
+ let inComment = false;
135
+ const outLines = [];
136
+ for (const line of lines) {
137
+ let result = "";
138
+ let j = 0;
139
+ while (j < line.length) {
140
+ if (!inComment) {
141
+ const open = line.indexOf("<!--", j);
142
+ if (open === -1) {
143
+ result += line.slice(j);
144
+ break;
145
+ }
146
+ result += line.slice(j, open);
147
+ const close = line.indexOf("-->", open + 4);
148
+ if (close === -1) {
149
+ result += " ".repeat(line.length - open);
150
+ inComment = true;
151
+ break;
152
+ }
153
+ result += " ".repeat(close + 3 - open);
154
+ j = close + 3;
155
+ inComment = false;
156
+ } else {
157
+ const close = line.indexOf("-->", j);
158
+ if (close === -1) {
159
+ result += " ".repeat(line.length - j);
160
+ break;
161
+ }
162
+ result += " ".repeat(close + 3 - j);
163
+ j = close + 3;
164
+ inComment = false;
165
+ }
166
+ }
167
+ outLines.push(result);
168
+ }
169
+ return outLines.join("\n");
170
+ }
171
+ function scanQuotedStringEnd(content, start) {
172
+ const quote = content[start];
173
+ const n = content.length;
174
+ let j = start + 1;
175
+ while (j < n) {
176
+ if (content[j] === "\\" && j + 1 < n) {
177
+ j += 2;
178
+ continue;
179
+ }
180
+ if (content[j] === quote) {
181
+ j++;
182
+ break;
183
+ }
184
+ if (quote !== "`" && content[j] === "\n") {
185
+ j++;
186
+ break;
187
+ }
188
+ j++;
189
+ }
190
+ return j;
191
+ }
192
+ function stripJsComments(content) {
193
+ let out = "";
194
+ let i = 0;
195
+ const n = content.length;
196
+ while (i < n) {
197
+ const ch = content[i];
198
+ if (ch === "/" && content[i + 1] === "/") {
199
+ let j = i;
200
+ while (j < n && content[j] !== "\n") j++;
201
+ out += " ".repeat(j - i);
202
+ i = j;
203
+ continue;
204
+ }
205
+ if (ch === "/" && content[i + 1] === "*") {
206
+ let j = i + 2;
207
+ while (j < n && !(content[j] === "*" && content[j + 1] === "/")) j++;
208
+ const end = j < n ? j + 2 : n;
209
+ out += content.slice(i, end).replace(/[^\n]/g, " ");
210
+ i = end;
211
+ continue;
212
+ }
213
+ if (ch === '"' || ch === "'" || ch === "`") {
214
+ const j = scanQuotedStringEnd(content, i);
215
+ out += content.slice(i, j);
216
+ i = j;
217
+ continue;
218
+ }
219
+ out += ch;
220
+ i++;
221
+ }
222
+ return out;
223
+ }
224
+ function stripHashComments(text) {
225
+ return text.split("\n").map((line) => {
226
+ for (let i = 0; i < line.length; i++) {
227
+ if (line[i] === "#" && !isInsideStringLiteral(line, i)) {
228
+ return line.slice(0, i) + " ".repeat(line.length - i);
229
+ }
230
+ }
231
+ return line;
232
+ }).join("\n");
233
+ }
234
+ var HTML_COMMENT_RE = /<!--[\s\S]*?-->/g;
235
+ var HTML_SCRIPT_BODY_RE = /(<script\b[^>]*>)([\s\S]*?)(<\/script\s*>)/gi;
236
+ var HTML_CDATA_RE = /<!\[CDATA\[[\s\S]*?\]\]>/g;
237
+ var LIQUID_COMMENT_RE = /{%-?\s*comment\s*-?%}[\s\S]*?{%-?\s*endcomment\s*-?%}/gi;
238
+ function maskHtmlNoise(text) {
239
+ let out = text.replace(
240
+ HTML_SCRIPT_BODY_RE,
241
+ (_m, open, body, close) => open + body.replace(/[^\n]/g, " ") + close
242
+ );
243
+ out = out.replace(HTML_COMMENT_RE, (m) => m.replace(/[^\n]/g, " "));
244
+ out = out.replace(LIQUID_COMMENT_RE, (m) => m.replace(/[^\n]/g, " "));
245
+ out = out.replace(HTML_CDATA_RE, (m) => m.replace(/[^\n]/g, " "));
246
+ return out;
247
+ }
248
+ var HTML_HEADING_RE = /<h([1-6])[^>]*>(.*?)<\/h\1>/gis;
249
+ var HTML_HEADING_TAG_STRIP_RE = /<[^>]+>/g;
250
+ function findHtmlHeadingMatches(content) {
251
+ const masked = maskHtmlNoise(content);
252
+ const matches = [];
253
+ for (const m of masked.matchAll(HTML_HEADING_RE)) {
254
+ const level = parseInt(m[1] ?? "1", 10);
255
+ const raw = m[2] ?? "";
256
+ const heading = raw.replace(HTML_HEADING_TAG_STRIP_RE, "").trim();
257
+ matches.push({ level, heading, offset: m.index ?? 0, tag: m[0] ?? "" });
258
+ }
259
+ return matches;
260
+ }
261
+ function isInsideStringLiteral(line, index, from = 0, opts = {}) {
262
+ let openQuote = null;
263
+ let i = from;
264
+ while (i < index) {
265
+ const ch = line[i];
266
+ if (opts.symbolLiterals === true && openQuote === null && ch === "'") {
267
+ const len = scalaCharLiteralLength(line, i);
268
+ i += len === 0 ? 1 : len;
269
+ continue;
270
+ }
271
+ if (openQuote !== null && ch === "\\" && i + 1 < line.length) {
272
+ i += 2;
273
+ continue;
274
+ }
275
+ if (ch === '"' || ch === "'") {
276
+ if (openQuote === null) {
277
+ openQuote = ch;
278
+ } else if (openQuote === ch) {
279
+ openQuote = null;
280
+ }
281
+ }
282
+ i++;
283
+ }
284
+ return openQuote !== null;
285
+ }
286
+ function nextBlockCommentOpen(line, from) {
287
+ const lineCommentIdx = lineCommentStartIndex(line, ["//"], from);
288
+ let open = line.indexOf("/*", from);
289
+ while (open !== -1 && (isInsideStringLiteral(line, open, from) || lineCommentIdx !== -1 && open >= lineCommentIdx)) {
290
+ open = line.indexOf("/*", open + 1);
291
+ }
292
+ return open;
293
+ }
294
+ function stripBlockCommentSpan(line, inComment) {
295
+ let code = "";
296
+ let j = 0;
297
+ let comment = inComment;
298
+ while (j < line.length) {
299
+ if (!comment) {
300
+ const open = nextBlockCommentOpen(line, j);
301
+ if (open === -1) {
302
+ code += line.slice(j);
303
+ break;
304
+ }
305
+ code += line.slice(j, open);
306
+ const close = line.indexOf("*/", open + 2);
307
+ if (close === -1) {
308
+ code += " ".repeat(line.length - open);
309
+ comment = true;
310
+ break;
311
+ }
312
+ code += " ".repeat(close + 2 - open);
313
+ j = close + 2;
314
+ comment = false;
315
+ } else {
316
+ const close = line.indexOf("*/", j);
317
+ if (close === -1) {
318
+ code += " ".repeat(line.length - j);
319
+ break;
320
+ }
321
+ code += " ".repeat(close + 2 - j);
322
+ j = close + 2;
323
+ comment = false;
324
+ }
325
+ }
326
+ return { code, inComment: comment };
327
+ }
328
+ function stripNestedBlockCommentSpan(line, depth) {
329
+ let code = "";
330
+ let j = 0;
331
+ let d = depth;
332
+ while (j < line.length) {
333
+ if (d === 0) {
334
+ const open = nextBlockCommentOpen(line, j);
335
+ if (open === -1) {
336
+ code += line.slice(j);
337
+ break;
338
+ }
339
+ code += line.slice(j, open);
340
+ code += " ";
341
+ j = open + 2;
342
+ d = 1;
343
+ } else {
344
+ const open = line.indexOf("/*", j);
345
+ const close = line.indexOf("*/", j);
346
+ if (close === -1 && open === -1) {
347
+ code += " ".repeat(line.length - j);
348
+ break;
349
+ }
350
+ const next = close === -1 ? open : open === -1 ? close : Math.min(open, close);
351
+ code += " ".repeat(next + 2 - j);
352
+ d = Math.max(0, d + (next === open ? 1 : -1));
353
+ j = next + 2;
354
+ }
355
+ }
356
+ return { code, depth: d };
357
+ }
358
+ function stripLineComment(line, markers = ["//"], opts = {}) {
359
+ const cutIdx = lineCommentStartIndex(line, markers, 0, opts);
360
+ return cutIdx === -1 ? line : line.slice(0, cutIdx);
361
+ }
362
+ function lineCommentStartIndex(line, markers, from = 0, opts = {}) {
363
+ let cutIdx = -1;
364
+ for (const marker of markers) {
365
+ let idx = line.indexOf(marker, from);
366
+ while (idx !== -1 && isInsideStringLiteral(line, idx, from, opts)) {
367
+ idx = line.indexOf(marker, idx + 1);
368
+ }
369
+ if (idx !== -1 && (cutIdx === -1 || idx < cutIdx)) cutIdx = idx;
370
+ }
371
+ return cutIdx;
372
+ }
373
+ function scalaCharLiteralLength(line, index) {
374
+ if (line[index] !== "'") return 0;
375
+ if (line[index + 1] === "\\") {
376
+ if (line[index + 2] === "u") {
377
+ let i = index + 3;
378
+ while (line[i] === "u") i++;
379
+ return /^[0-9a-fA-F]{4}$/.test(line.slice(i, i + 4)) && line[i + 4] === "'" ? i + 5 - index : 0;
380
+ }
381
+ return line[index + 2] !== void 0 && line[index + 3] === "'" ? 4 : 0;
382
+ }
383
+ return line[index + 1] !== void 0 && line[index + 1] !== "'" && line[index + 2] === "'" ? 3 : 0;
384
+ }
385
+ function stripStringLiterals(line, opts = {}) {
386
+ const openDelim = (idx) => {
387
+ const q = line[idx];
388
+ if (opts.tripleQuotes && line[idx + 1] === q && line[idx + 2] === q) return q + q + q;
389
+ return q;
390
+ };
391
+ let out = "";
392
+ let i = 0;
393
+ const stack = [];
394
+ while (i < line.length) {
395
+ const ch = line[i];
396
+ if (ch === "\n") {
397
+ out += ch;
398
+ i++;
399
+ stack.length = 0;
400
+ continue;
401
+ }
402
+ const top = stack[stack.length - 1];
403
+ if (top === void 0) {
404
+ if (opts.symbolLiterals === true && ch === "'" && scalaCharLiteralLength(line, i) === 0) {
405
+ out += ch;
406
+ i++;
407
+ continue;
408
+ }
409
+ if (ch === '"' || ch === "'") {
410
+ const bareBraceHole = ch === '"' && i > 0 && line[i - 1] === "$";
411
+ const delim = openDelim(i);
412
+ stack.push({ kind: "string", quote: delim, bareBraceHole });
413
+ out += delim;
414
+ i += delim.length;
415
+ continue;
416
+ }
417
+ out += ch;
418
+ i++;
419
+ continue;
420
+ }
421
+ if (top.kind === "hole") {
422
+ if (opts.symbolLiterals === true && ch === "'" && scalaCharLiteralLength(line, i) === 0) {
423
+ out += ch;
424
+ i++;
425
+ continue;
426
+ }
427
+ if (ch === '"' || ch === "'") {
428
+ const bareBraceHole = ch === '"' && i > 0 && line[i - 1] === "$";
429
+ const delim = openDelim(i);
430
+ stack.push({ kind: "string", quote: delim, bareBraceHole });
431
+ out += delim;
432
+ i += delim.length;
433
+ continue;
434
+ }
435
+ if (ch === "{") {
436
+ top.depth++;
437
+ out += ch;
438
+ i++;
439
+ continue;
440
+ }
441
+ if (ch === "}") {
442
+ if (top.depth > 0) {
443
+ top.depth--;
444
+ } else {
445
+ stack.pop();
446
+ }
447
+ out += ch;
448
+ i++;
449
+ continue;
450
+ }
451
+ out += ch;
452
+ i++;
453
+ continue;
454
+ }
455
+ if (ch === "\\" && i + 1 < line.length && line[i + 1] !== "\n") {
456
+ out += " ";
457
+ i += 2;
458
+ continue;
459
+ }
460
+ if (ch === top.quote[0] && (top.quote.length === 1 || line.startsWith(top.quote, i))) {
461
+ stack.pop();
462
+ out += top.quote;
463
+ i += top.quote.length;
464
+ continue;
465
+ }
466
+ if (top.quote === '"') {
467
+ if (top.bareBraceHole && ch === "{") {
468
+ if (line[i + 1] === "{") {
469
+ out += " ";
470
+ i += 2;
471
+ continue;
472
+ }
473
+ stack.push({ kind: "hole", depth: 0 });
474
+ out += ch;
475
+ i++;
476
+ continue;
477
+ }
478
+ if (!top.bareBraceHole && ch === "$" && line[i + 1] === "{") {
479
+ stack.push({ kind: "hole", depth: 0 });
480
+ out += line.slice(i, i + 2);
481
+ i += 2;
482
+ continue;
483
+ }
484
+ if (!top.bareBraceHole && ch === "{" && line[i + 1] === "$") {
485
+ stack.push({ kind: "hole", depth: 0 });
486
+ out += ch;
487
+ i++;
488
+ continue;
489
+ }
490
+ }
491
+ out += " ";
492
+ i++;
493
+ }
494
+ return out;
495
+ }
496
+ function closingQuoteRunEnd(text, from, quote, len, runClose) {
497
+ for (let i = from; i < text.length; i++) {
498
+ if (text[i] !== quote) continue;
499
+ let run = 0;
500
+ while (text[i + run] === quote) run++;
501
+ if (run >= len) return runClose === "last" ? i + run : i + len;
502
+ i += run - 1;
503
+ }
504
+ return -1;
505
+ }
506
+ var MULTILINE_CLOSER_ANCHOR = {
507
+ // An Elixir heredoc is terminated only by a delimiter run that begins its own line, preceded by nothing but the whitespace that also sets the indentation stripped from the body (Elixir syntax reference, section "Strings", heredocs). A run appearing mid-line inside the body is content.
508
+ elixir: "lineStart",
509
+ // The closing delimiter of a Swift multi-line string literal has to appear on a line of its own, and its indentation sets what is stripped from each body line (The Swift Programming Language, "Strings and Characters", section "Multiline String Literals"). The same section's rules apply to the extended-delimiter multi-line form (`#"""`), whose only reachable state here is the cross-line one.
510
+ swift: "lineStart",
511
+ // A PHP heredoc or nowdoc closes on its identifier at the start of a line; PHP 7.3 allowed that identifier to be indented and to be followed by other characters (PHP language reference, "Strings", section "Heredoc text"). Read by the heredoc/nowdoc branch, which builds its anchor regex from this value.
512
+ php: "lineStart",
513
+ // A PowerShell here-string terminator (`"@` / `'@`) must be the first characters on its line, with no leading whitespace at all, which is the one language here that is stricter than `'lineStart'` (about_Quoting_Rules, "Here-Strings").
514
+ powershell: "column0",
515
+ // Kotlin's raw string is terminated by the next `"""` wherever it falls, so `val s = """a"""` on one line is a complete literal (Kotlin language specification, "Expressions", section "String literals").
516
+ kotlin: "anywhere",
517
+ // Scala's multi-line string literal is likewise terminated by the next run of three quotes on any line (Scala language specification, section 1.3.5 "Character Literals and String Literals").
518
+ scala: "anywhere",
519
+ // Dart's multi-line string production is a delimiter, content, then the delimiter, with no rule about where on a line the closing delimiter falls (Dart Programming Language Specification, section "Strings").
520
+ dart: "anywhere",
521
+ // A GraphQL block string closes at the next `"""` wherever it sits: the spec's BlockString production places no positional rule on the closing delimiter, and a one-line description such as `"""A user."""` is the ordinary form (GraphQL specification, section 2.9.4 "String Value").
522
+ graphql: "anywhere",
523
+ // C# has two cross-line forms and they disagree, so the language-level value is the permissive one. A verbatim string (`@"..."`) closes on the next non-doubled quote wherever it sits and has no positional rule (C# language reference, "String literals"). A multi-line raw string literal does require its closing run to begin its own line (C# language reference, "Raw string literals"), but that rule is unreachable as a mis-pairing here: the same section forbids the content from holding a quote run as long as the delimiter, so no run inside the body can be mistaken for the closer in the first place, and the run-length rule already in `closingQuoteRunEnd` is what enforces it.
524
+ csharp: "anywhere",
525
+ // R has no fixed-delimiter multi-line form and no positional rule: an ordinary character constant runs to its next unescaped quote (R Language Definition, section 10.3.1 "Literal constants") and a raw constant to its mirrored closing punctuation (R base help page `?Quotes`, "Raw character constants"), on whatever line either lands.
526
+ r: "anywhere"
527
+ };
528
+ function closerSearchStart(line, from, anchor) {
529
+ if (anchor === "anywhere") return from;
530
+ const indent = anchor === "column0" ? 0 : /^[ \t]*/.exec(line)?.[0].length ?? 0;
531
+ return from <= indent ? indent : -1;
532
+ }
533
+ var TRIPLE_QUOTE_RUN_CLOSE = {
534
+ kotlin: "last",
535
+ scala: "last",
536
+ // A GraphQL block string ends at the first `"""` run, because its grammar excludes that sequence from the body outright rather than letting the longest run win (GraphQL specification, section 2.9.4 "String Value", the BlockString production).
537
+ graphql: "first",
538
+ // Swift's closing delimiter has to stand alone on its own line (The Swift Programming Language, "Strings and Characters", section "Multiline String Literals"), so no run longer than three ever terminates a valid Swift literal; it is grouped with Kotlin and Scala so that the same-line and cross-line paths agree, not to assert a rule Swift itself defines.
539
+ swift: "last",
540
+ csharp: "last",
541
+ // An Elixir heredoc cannot open and close on one line, because its opening `"""` must be followed by a newline, so only the cross-line closer is reachable for valid Elixir and that path has always consumed the whole run.
542
+ elixir: "last",
543
+ dart: "first",
544
+ // No triple-quoted literal at all: PHP uses heredoc/nowdoc, PowerShell `@"`/`@'` here-strings, R ordinary and raw character constants. Present only because the Record is exhaustive; the value is never read for these three.
545
+ php: "last",
546
+ powershell: "last",
547
+ r: "last"
548
+ };
549
+ function quoteRunCloser(line, from, quote, len, state) {
550
+ const anchor = state.closerAnchor ?? "anywhere";
551
+ const start = closerSearchStart(line, from, anchor);
552
+ if (start === -1) return null;
553
+ if (anchor !== "anywhere") {
554
+ let run = 0;
555
+ while (line[start + run] === quote) run++;
556
+ if (run < len) return null;
557
+ }
558
+ const end = closingQuoteRunEnd(line, start, quote, len, state.runClose ?? "last");
559
+ return end === -1 ? null : { maskEnd: end };
560
+ }
561
+ function findMultilineCloser(line, from, state) {
562
+ switch (state.kind) {
563
+ case "heredoc":
564
+ case "nowdoc": {
565
+ const indent = (state.closerAnchor ?? "anywhere") === "column0" ? "" : "[ \\t]*";
566
+ const re = new RegExp(`^${indent}${escapeRegExp(state.identifier)}\\b`);
567
+ const m = re.exec(line);
568
+ return m ? { maskEnd: m[0].length } : null;
569
+ }
570
+ case "sigil": {
571
+ const end = elixirSigilEnd(line, from, state);
572
+ return end === -1 ? null : { maskEnd: end };
573
+ }
574
+ case "tripleSingleQuote": {
575
+ return quoteRunCloser(line, from, "'", 3, state);
576
+ }
577
+ case "tripleQuote": {
578
+ const n = state.identifier !== "" ? parseInt(state.identifier, 10) : 3;
579
+ return quoteRunCloser(line, from, '"', n, state);
580
+ }
581
+ case "rRaw":
582
+ case "swiftExtended": {
583
+ const anchor = state.closerAnchor ?? "anywhere";
584
+ if (anchor !== "anywhere") {
585
+ const start = closerSearchStart(line, from, anchor);
586
+ if (start === -1 || !line.startsWith(state.identifier, start)) return null;
587
+ return { maskEnd: start + state.identifier.length };
588
+ }
589
+ const idx = line.indexOf(state.identifier, from);
590
+ return idx === -1 ? null : { maskEnd: idx + state.identifier.length };
591
+ }
592
+ case "rString": {
593
+ const idx = findRQuoteEnd(line, from, state.identifier);
594
+ return idx === -1 ? null : { maskEnd: idx + 1 };
595
+ }
596
+ case "verbatim": {
597
+ if (!state.interpolated) {
598
+ let j2 = from;
599
+ while (j2 < line.length) {
600
+ if (line[j2] === '"') {
601
+ if (line[j2 + 1] === '"') {
602
+ j2 += 2;
603
+ continue;
604
+ }
605
+ return { maskEnd: j2 + 1 };
606
+ }
607
+ j2++;
608
+ }
609
+ return null;
610
+ }
611
+ let j = from;
612
+ let holeDepth = 0;
613
+ let nestedQuote = null;
614
+ while (j < line.length) {
615
+ const c = line[j];
616
+ if (nestedQuote !== null) {
617
+ if (c === "\\" && j + 1 < line.length) {
618
+ j += 2;
619
+ continue;
620
+ }
621
+ if (c === nestedQuote) {
622
+ nestedQuote = null;
623
+ }
624
+ j++;
625
+ continue;
626
+ }
627
+ if (holeDepth > 0) {
628
+ if (c === '"' || c === "'") {
629
+ nestedQuote = c;
630
+ j++;
631
+ continue;
632
+ }
633
+ if (c === "{") holeDepth++;
634
+ else if (c === "}") holeDepth--;
635
+ j++;
636
+ continue;
637
+ }
638
+ if (c === "{") {
639
+ holeDepth = 1;
640
+ j++;
641
+ continue;
642
+ }
643
+ if (c === '"') {
644
+ if (line[j + 1] === '"') {
645
+ j += 2;
646
+ continue;
647
+ }
648
+ return { maskEnd: j + 1 };
649
+ }
650
+ j++;
651
+ }
652
+ return null;
653
+ }
654
+ case "psHereDouble":
655
+ case "psHereSingle": {
656
+ const start = closerSearchStart(line, from, state.closerAnchor ?? "anywhere");
657
+ const terminator = state.kind === "psHereDouble" ? '"@' : "'@";
658
+ if (start === -1 || !line.startsWith(terminator, start)) return null;
659
+ return { maskEnd: start + terminator.length };
660
+ }
661
+ default:
662
+ return null;
663
+ }
664
+ }
665
+ var MULTILINE_OPENER_COMMENT_MARKERS = {
666
+ php: ["//", "#"],
667
+ kotlin: ["//"],
668
+ graphql: ["#"],
669
+ csharp: ["//"],
670
+ powershell: ["#"],
671
+ swift: ["//"],
672
+ elixir: ["#"],
673
+ scala: ["//"],
674
+ dart: ["//"],
675
+ r: ["#"]
676
+ };
677
+ var MULTILINE_OPENER_BLOCK_COMMENT_LANGS = /* @__PURE__ */ new Set(["php", "kotlin", "csharp", "swift", "scala", "dart"]);
678
+ var R_RAW_OPENER_RE = /[rR](["'])(-*)([([{])/y;
679
+ var R_RAW_CLOSE_BRACKET = { "(": ")", "[": "]", "{": "}" };
680
+ var R_IDENT_CHAR_RE = /[A-Za-z0-9._]/;
681
+ function matchRRawOpener(text, index) {
682
+ const ch = text[index];
683
+ if (ch !== "r" && ch !== "R") return null;
684
+ if (R_IDENT_CHAR_RE.test(text[index - 1] ?? "")) return null;
685
+ R_RAW_OPENER_RE.lastIndex = index;
686
+ const m = R_RAW_OPENER_RE.exec(text);
687
+ if (m === null) return null;
688
+ const close = R_RAW_CLOSE_BRACKET[m[3] ?? ""];
689
+ if (close === void 0) return null;
690
+ return { openerEnd: index + m[0].length, closer: `${close}${m[2] ?? ""}${m[1] ?? ""}` };
691
+ }
692
+ var GROOVY_OPERAND_BEFORE = /* @__PURE__ */ new Set([..."([{,;=+-*/%<>!&|^~?:"]);
693
+ var MAX_GROOVY_SLASHY_BODY = 1024;
694
+ var GROOVY_OPERAND_KEYWORDS = /* @__PURE__ */ new Set(["return", "case", "in", "new", "assert", "instanceof"]);
695
+ function groovyOperandPosition(text, index) {
696
+ let i = index - 1;
697
+ while (i >= 0 && (text[i] === " " || text[i] === " ")) i--;
698
+ if (i < 0) return true;
699
+ const ch = text[i];
700
+ if (ch === "\n" || ch === "\r") return true;
701
+ if (GROOVY_OPERAND_BEFORE.has(ch)) return true;
702
+ if (!/[\w$]/.test(ch)) return false;
703
+ let j = i;
704
+ while (j >= 0 && /[\w$]/.test(text[j])) j--;
705
+ return GROOVY_OPERAND_KEYWORDS.has(text.slice(j + 1, i + 1));
706
+ }
707
+ function matchGroovySlashy(text, index) {
708
+ const dollar = text[index] === "$" && text[index + 1] === "/";
709
+ if (!dollar) {
710
+ if (text[index] !== "/") return null;
711
+ const next = text[index + 1];
712
+ if (next === "/" || next === "*") return null;
713
+ if (!groovyOperandPosition(text, index)) return null;
714
+ }
715
+ const bodyStart = index + (dollar ? 2 : 1);
716
+ const limit = Math.min(text.length, bodyStart + MAX_GROOVY_SLASHY_BODY);
717
+ for (let i = bodyStart; i < limit; i++) {
718
+ const ch = text[i];
719
+ if (dollar) {
720
+ if (ch === "$" && (text[i + 1] === "$" || text[i + 1] === "/")) {
721
+ i++;
722
+ continue;
723
+ }
724
+ if (ch === "/" && text[i + 1] === "$") return { bodyStart, bodyEnd: i, end: i + 2 };
725
+ continue;
726
+ }
727
+ if (ch === "\\" && text[i + 1] === "/") {
728
+ i++;
729
+ continue;
730
+ }
731
+ if (ch === "\n") return null;
732
+ if (ch === "/") return { bodyStart, bodyEnd: i, end: i + 1 };
733
+ }
734
+ return null;
735
+ }
736
+ var ELIXIR_SIGIL_CLOSER = { "(": ")", "[": "]", "{": "}", "<": ">", "/": "/", "|": "|", '"': '"', "'": "'" };
737
+ var ELIXIR_SIGIL_OPENER = { ")": "(", "]": "[", "}": "{", ">": "<" };
738
+ function matchElixirSigilOpener(line, index) {
739
+ if (line[index] !== "~") return null;
740
+ let i = index + 1;
741
+ const first = line[i] ?? "";
742
+ let escapes;
743
+ if (first >= "a" && first <= "z") {
744
+ escapes = true;
745
+ i++;
746
+ } else if (first >= "A" && first <= "Z") {
747
+ escapes = false;
748
+ while (i < line.length && (line[i] ?? "") >= "A" && (line[i] ?? "") <= "Z") i++;
749
+ } else {
750
+ return null;
751
+ }
752
+ const delim = line[i] ?? "";
753
+ const closer = ELIXIR_SIGIL_CLOSER[delim];
754
+ if (closer === void 0) return null;
755
+ if ((delim === '"' || delim === "'") && line[i + 1] === delim && line[i + 2] === delim) return null;
756
+ return { openerEnd: i + 1, closer, escapes };
757
+ }
758
+ function elixirSigilEnd(line, from, state) {
759
+ const closer = state.identifier;
760
+ const opener = ELIXIR_SIGIL_OPENER[closer];
761
+ for (let i = from; i < line.length; i++) {
762
+ const ch = line[i];
763
+ if (state.escapes === true && ch === "\\") {
764
+ i++;
765
+ continue;
766
+ }
767
+ if (opener !== void 0 && ch === opener) {
768
+ state.depth = (state.depth ?? 0) + 1;
769
+ continue;
770
+ }
771
+ if (ch === closer) {
772
+ if ((state.depth ?? 0) > 0) {
773
+ state.depth = (state.depth ?? 0) - 1;
774
+ continue;
775
+ }
776
+ return i + 1;
777
+ }
778
+ }
779
+ return -1;
780
+ }
781
+ var SWIFT_EXTENDED_OPENER_RE = /(#+)("""|")/y;
782
+ function matchSwiftExtendedOpener(text, index) {
783
+ if (text[index] !== "#") return null;
784
+ SWIFT_EXTENDED_OPENER_RE.lastIndex = index;
785
+ const m = SWIFT_EXTENDED_OPENER_RE.exec(text);
786
+ if (m === null) return null;
787
+ const hashes = m[1] ?? "";
788
+ const multiline = m[2] === '"""';
789
+ return { openerEnd: index + m[0].length, multiline, closer: `${multiline ? '"""' : '"'}${hashes}` };
790
+ }
791
+ var BASH_WORD_TERMINATORS = /* @__PURE__ */ new Set([" ", " ", "\n", "|", "&", ";", "(", ")", "<", ">"]);
792
+ function matchBashHeredocOpener(text, index) {
793
+ if (text[index] !== "<" || text[index + 1] !== "<") return null;
794
+ if (text[index - 1] === "<" || text[index + 2] === "<") return null;
795
+ let i = index + 2;
796
+ if (text[i] === "-") i++;
797
+ while (text[i] === " " || text[i] === " ") i++;
798
+ let terminator = "";
799
+ while (i < text.length) {
800
+ const ch = text[i];
801
+ if (ch === "'") {
802
+ const close = text.indexOf("'", i + 1);
803
+ if (close === -1) return null;
804
+ terminator += text.slice(i + 1, close);
805
+ i = close + 1;
806
+ continue;
807
+ }
808
+ if (ch === '"') {
809
+ let j = i + 1;
810
+ for (; j < text.length; j++) {
811
+ if (text[j] === "\\") {
812
+ terminator += text[j + 1] ?? "";
813
+ j++;
814
+ continue;
815
+ }
816
+ if (text[j] === '"') break;
817
+ terminator += text[j];
818
+ }
819
+ if (j >= text.length) return null;
820
+ i = j + 1;
821
+ continue;
822
+ }
823
+ if (ch === "\\") {
824
+ if (i + 1 >= text.length) break;
825
+ terminator += text[i + 1];
826
+ i += 2;
827
+ continue;
828
+ }
829
+ if (ch === void 0 || BASH_WORD_TERMINATORS.has(ch)) break;
830
+ terminator += ch;
831
+ i++;
832
+ }
833
+ if (terminator === "") return null;
834
+ return { openerEnd: i, terminator };
835
+ }
836
+ function findRQuoteEnd(line, start, quote) {
837
+ for (let j = start; j < line.length; j++) {
838
+ const c = line[j];
839
+ if (c === "\\") {
840
+ j++;
841
+ continue;
842
+ }
843
+ if (c === quote) return j;
844
+ }
845
+ return -1;
846
+ }
847
+ function isInsideSameLineBlockComment(line, idx, from = 0) {
848
+ let comment = false;
849
+ let j = from;
850
+ while (j < idx) {
851
+ if (!comment) {
852
+ const open = line.indexOf("/*", j);
853
+ if (open === -1 || open >= idx) return false;
854
+ if (isInsideStringLiteral(line, open, from)) {
855
+ j = open + 2;
856
+ continue;
857
+ }
858
+ comment = true;
859
+ j = open + 2;
860
+ } else {
861
+ const close = line.indexOf("*/", j);
862
+ if (close === -1 || close >= idx) return true;
863
+ comment = false;
864
+ j = close + 2;
865
+ }
866
+ }
867
+ return comment;
868
+ }
869
+ function findMultilineOpener(line, from, lang) {
870
+ const commentIdx = lineCommentStartIndex(line, MULTILINE_OPENER_COMMENT_MARKERS[lang], from);
871
+ const isCommented = (idx) => commentIdx !== -1 && idx >= commentIdx || MULTILINE_OPENER_BLOCK_COMMENT_LANGS.has(lang) && isInsideSameLineBlockComment(line, idx, from);
872
+ if (lang === "php") {
873
+ const re = /<<<\s*(['"]?)([A-Za-z_]\w*)\1/g;
874
+ re.lastIndex = from;
875
+ const m = re.exec(line);
876
+ if (!m || isInsideStringLiteral(line, m.index, from) || isCommented(m.index)) return null;
877
+ const identifier = m[2] ?? "";
878
+ const kind = m[1] === "'" ? "nowdoc" : "heredoc";
879
+ return { openStart: m.index, closesSameLine: null, state: { kind, identifier, closerAnchor: MULTILINE_CLOSER_ANCHOR[lang] } };
880
+ }
881
+ if (lang === "kotlin" || lang === "swift" || lang === "scala" || lang === "graphql") {
882
+ let swiftExt = null;
883
+ if (lang === "swift") {
884
+ for (let h = line.indexOf("#", from); h !== -1; h = line.indexOf("#", h + 1)) {
885
+ const ext = matchSwiftExtendedOpener(line, h);
886
+ if (ext === null || isInsideStringLiteral(line, h, from) || isCommented(h)) continue;
887
+ const extClose = line.indexOf(ext.closer, ext.openerEnd);
888
+ const extState = { kind: "swiftExtended", identifier: ext.closer, closerAnchor: MULTILINE_CLOSER_ANCHOR[lang] };
889
+ if (extClose !== -1) {
890
+ swiftExt = { openStart: h, closesSameLine: extClose + ext.closer.length, state: extState };
891
+ break;
892
+ }
893
+ if (ext.multiline) {
894
+ swiftExt = { openStart: h, closesSameLine: null, state: extState };
895
+ break;
896
+ }
897
+ }
898
+ }
899
+ const idx = line.indexOf('"""', from);
900
+ if (swiftExt !== null && (idx === -1 || swiftExt.openStart < idx)) return swiftExt;
901
+ if (idx === -1 || isInsideStringLiteral(line, idx, from) || isCommented(idx)) return null;
902
+ const runClose = TRIPLE_QUOTE_RUN_CLOSE[lang];
903
+ const tripleState = { kind: "tripleQuote", identifier: "3", runClose, closerAnchor: MULTILINE_CLOSER_ANCHOR[lang] };
904
+ const closeEnd = closingQuoteRunEnd(line, idx + 3, '"', 3, runClose);
905
+ if (closeEnd !== -1) {
906
+ return { openStart: idx, closesSameLine: closeEnd, state: tripleState };
907
+ }
908
+ return { openStart: idx, closesSameLine: null, state: tripleState };
909
+ }
910
+ if (lang === "elixir" || lang === "dart") {
911
+ const candidates = [];
912
+ const dq = line.indexOf('"""', from);
913
+ if (dq !== -1) candidates.push([dq, "tripleQuote", '"""']);
914
+ const sq = line.indexOf("'''", from);
915
+ if (sq !== -1) candidates.push([sq, "tripleSingleQuote", "'''"]);
916
+ candidates.sort((a, b) => a[0] - b[0]);
917
+ const first = candidates[0];
918
+ if (lang === "elixir") {
919
+ for (let t = line.indexOf("~", from); t !== -1; t = line.indexOf("~", t + 1)) {
920
+ if (first !== void 0 && first[0] < t) break;
921
+ const open = matchElixirSigilOpener(line, t);
922
+ if (open === null || isInsideStringLiteral(line, t, from) || isCommented(t)) continue;
923
+ const state2 = { kind: "sigil", identifier: open.closer, depth: 0, escapes: open.escapes };
924
+ const end = elixirSigilEnd(line, open.openerEnd, state2);
925
+ return { openStart: t, closesSameLine: end === -1 ? null : end, state: state2 };
926
+ }
927
+ }
928
+ if (first === void 0) return null;
929
+ const [idx, kind, delim] = first;
930
+ if (isInsideStringLiteral(line, idx, from) || isCommented(idx)) return null;
931
+ const runClose = TRIPLE_QUOTE_RUN_CLOSE[lang];
932
+ const state = { kind, identifier: "3", runClose, closerAnchor: MULTILINE_CLOSER_ANCHOR[lang] };
933
+ const closeEnd = closingQuoteRunEnd(line, idx + 3, delim[0] ?? '"', 3, runClose);
934
+ if (closeEnd !== -1) return { openStart: idx, closesSameLine: closeEnd, state };
935
+ return { openStart: idx, closesSameLine: null, state };
936
+ }
937
+ if (lang === "r") {
938
+ const limit = commentIdx === -1 ? line.length : commentIdx;
939
+ let i = from;
940
+ while (i < limit) {
941
+ const ch = line[i] ?? "";
942
+ if (ch === "`") {
943
+ const end = line.indexOf("`", i + 1);
944
+ if (end === -1 || end >= limit) return null;
945
+ i = end + 1;
946
+ continue;
947
+ }
948
+ const raw = matchRRawOpener(line, i);
949
+ if (raw !== null) {
950
+ const closeIdx = line.indexOf(raw.closer, raw.openerEnd);
951
+ if (closeIdx === -1) return { openStart: i, closesSameLine: null, state: { kind: "rRaw", identifier: raw.closer, closerAnchor: MULTILINE_CLOSER_ANCHOR[lang] } };
952
+ i = closeIdx + raw.closer.length;
953
+ continue;
954
+ }
955
+ if (ch === '"' || ch === "'") {
956
+ const closeIdx = findRQuoteEnd(line, i + 1, ch);
957
+ if (closeIdx === -1) return { openStart: i, closesSameLine: null, state: { kind: "rString", identifier: ch, closerAnchor: MULTILINE_CLOSER_ANCHOR[lang] } };
958
+ i = closeIdx + 1;
959
+ continue;
960
+ }
961
+ i++;
962
+ }
963
+ return null;
964
+ }
965
+ if (lang === "csharp") {
966
+ const tripleRe = /"{3,}/g;
967
+ tripleRe.lastIndex = from;
968
+ const tripleM = tripleRe.exec(line);
969
+ let tripleIdx = tripleM ? tripleM.index : -1;
970
+ const tripleLen = tripleM ? tripleM[0].length : 0;
971
+ if (tripleIdx !== -1 && (isInsideStringLiteral(line, tripleIdx, from) || isCommented(tripleIdx))) tripleIdx = -1;
972
+ const verbRe = /\$?@\$?"/g;
973
+ verbRe.lastIndex = from;
974
+ const verbM = verbRe.exec(line);
975
+ let verbIdx = verbM ? verbM.index : -1;
976
+ if (verbIdx !== -1 && isCommented(verbIdx)) verbIdx = -1;
977
+ if (verbIdx !== -1 && isInsideStringLiteral(line, verbIdx, from)) verbIdx = -1;
978
+ if (tripleIdx === -1 && verbIdx === -1) return null;
979
+ const useTriple = tripleIdx !== -1 && (verbIdx === -1 || tripleIdx < verbIdx);
980
+ if (useTriple) {
981
+ const rawState = { kind: "tripleQuote", identifier: String(tripleLen), runClose: TRIPLE_QUOTE_RUN_CLOSE.csharp, closerAnchor: MULTILINE_CLOSER_ANCHOR[lang] };
982
+ const closeEnd = closingQuoteRunEnd(line, tripleIdx + tripleLen, '"', tripleLen, rawState.runClose ?? "last");
983
+ if (closeEnd !== -1) {
984
+ return { openStart: tripleIdx, closesSameLine: closeEnd, state: rawState };
985
+ }
986
+ return { openStart: tripleIdx, closesSameLine: null, state: rawState };
987
+ }
988
+ const quoteIdx = verbIdx + (verbM?.[0].length ?? 1) - 1;
989
+ const interpolated = (verbM?.[0] ?? "").includes("$");
990
+ const closer = findMultilineCloser(line, quoteIdx + 1, { kind: "verbatim", identifier: "", interpolated });
991
+ if (closer !== null) {
992
+ return { openStart: verbIdx, closesSameLine: closer.maskEnd, state: { kind: "verbatim", identifier: "", interpolated } };
993
+ }
994
+ return { openStart: verbIdx, closesSameLine: null, state: { kind: "verbatim", identifier: "", interpolated } };
995
+ }
996
+ if (lang === "powershell") {
997
+ const re = /@("|')\s*$/;
998
+ const tail = line.slice(from);
999
+ const m = re.exec(tail);
1000
+ if (!m) return null;
1001
+ const openStart = from + m.index;
1002
+ if (isCommented(openStart)) return null;
1003
+ if (isInsideStringLiteral(line, openStart, from)) return null;
1004
+ const kind = m[1] === '"' ? "psHereDouble" : "psHereSingle";
1005
+ return { openStart, closesSameLine: null, state: { kind, identifier: "", closerAnchor: MULTILINE_CLOSER_ANCHOR[lang] } };
1006
+ }
1007
+ return null;
1008
+ }
1009
+ function stripMultilineStringSpan(line, state, lang) {
1010
+ let code = "";
1011
+ let i = 0;
1012
+ let cur = state;
1013
+ while (i < line.length) {
1014
+ if (cur !== null) {
1015
+ const closed = findMultilineCloser(line, i, cur);
1016
+ if (closed === null) {
1017
+ code += " ".repeat(line.length - i);
1018
+ i = line.length;
1019
+ continue;
1020
+ }
1021
+ code += " ".repeat(closed.maskEnd - i);
1022
+ i = closed.maskEnd;
1023
+ cur = null;
1024
+ continue;
1025
+ }
1026
+ const opened = findMultilineOpener(line, i, lang);
1027
+ if (opened === null) {
1028
+ code += line.slice(i);
1029
+ break;
1030
+ }
1031
+ code += line.slice(i, opened.openStart);
1032
+ if (opened.closesSameLine !== null) {
1033
+ code += " ".repeat(opened.closesSameLine - opened.openStart);
1034
+ i = opened.closesSameLine;
1035
+ cur = null;
1036
+ } else {
1037
+ code += " ".repeat(line.length - opened.openStart);
1038
+ i = line.length;
1039
+ cur = opened.state;
1040
+ }
1041
+ }
1042
+ return { code, state: cur };
1043
+ }
1044
+ function makeSpanSymbol(filePath, name, kind, span, parent = "", lines, style) {
1045
+ return {
1046
+ filePath,
1047
+ name,
1048
+ kind,
1049
+ lineStart: span.startLine,
1050
+ lineEnd: span.endLine,
1051
+ body: span.body,
1052
+ docstring: lines !== void 0 && style !== void 0 ? precedingDocComment(lines, span.startLine, style) : "",
1053
+ parent
1054
+ };
1055
+ }
1056
+ function makeLineSymbol(filePath, name, kind, line, sig, parent, lines, style) {
1057
+ return {
1058
+ filePath,
1059
+ name,
1060
+ kind,
1061
+ lineStart: line,
1062
+ lineEnd: line,
1063
+ body: sig ?? "",
1064
+ docstring: lines !== void 0 && style !== void 0 ? precedingDocComment(lines, line, style) : "",
1065
+ parent: parent ?? ""
1066
+ };
1067
+ }
1068
+ function makeSymbolEmitter(symbols, sections, seen, filePath, maxSymbols = 1e4, maxHeadingLen = 120) {
1069
+ return function emit(name, kind, line) {
1070
+ if (!name || name.length > maxHeadingLen) return;
1071
+ if (symbols.length >= maxSymbols) return;
1072
+ const key = `${name}\0${kind}\0${line}`;
1073
+ if (seen.has(key)) return;
1074
+ seen.add(key);
1075
+ symbols.push({
1076
+ filePath,
1077
+ name,
1078
+ kind,
1079
+ lineStart: line,
1080
+ lineEnd: line,
1081
+ body: "",
1082
+ docstring: "",
1083
+ parent: ""
1084
+ });
1085
+ sections.push({ heading: name, level: 1, line, endLine: line });
1086
+ };
1087
+ }
1088
+ function assignFlatEndLines(sections, totalLines) {
1089
+ for (let i = 0; i < sections.length; i++) {
1090
+ const s = sections[i];
1091
+ if (s === void 0) continue;
1092
+ let j = i + 1;
1093
+ while (j < sections.length && (sections[j]?.line ?? 0) <= s.line) j++;
1094
+ const next = sections[j];
1095
+ const end = next !== void 0 ? next.line - 1 : totalLines;
1096
+ s.endLine = end < s.line ? s.line : end;
1097
+ }
1098
+ }
1099
+ function propagateEndLinesToSymbols(symbols, sections) {
1100
+ const sectionMap = /* @__PURE__ */ new Map();
1101
+ for (const sec of sections) {
1102
+ const key = `${sec.heading}\0${sec.line}`;
1103
+ const queue = sectionMap.get(key);
1104
+ if (queue !== void 0) queue.push(sec.endLine);
1105
+ else sectionMap.set(key, [sec.endLine]);
1106
+ }
1107
+ return symbols.map((sym) => {
1108
+ const key = `${sym.name}\0${sym.lineStart}`;
1109
+ const endLine = sectionMap.get(key)?.shift();
1110
+ if (endLine !== void 0 && endLine !== sym.lineEnd) {
1111
+ return { ...sym, lineEnd: endLine };
1112
+ }
1113
+ return sym;
1114
+ });
1115
+ }
1116
+ function tripleQuoteDelimiters(opts) {
1117
+ const delims = [];
1118
+ if (opts?.tripleQuote === true) delims.push('"""');
1119
+ if (opts?.tripleSingleQuote === true) delims.push("'''");
1120
+ return delims;
1121
+ }
1122
+ function quoteRunLength(content, i) {
1123
+ let n = 0;
1124
+ while (content[i + n] === '"') n++;
1125
+ return n;
1126
+ }
1127
+ function skipRawStringQuotes(content, from, min) {
1128
+ return closingQuoteRunEnd(content, from, '"', min, "last");
1129
+ }
1130
+ function stepPowershellString(content, i, quote) {
1131
+ const ch = content[i];
1132
+ if (quote === '"' && ch === "`") return { next: i + 1, open: true };
1133
+ return { next: i, open: ch !== quote };
1134
+ }
1135
+ function opensCsharpVerbatimString(content, i) {
1136
+ const prev = content[i - 1];
1137
+ return prev === "@" || prev === "$" && content[i - 2] === "@";
1138
+ }
1139
+ function toLineCommentPrefixes(prefix) {
1140
+ if (prefix === void 0) return [];
1141
+ return typeof prefix === "string" ? [prefix] : prefix;
1142
+ }
1143
+ function atLineComment(content, i, prefixes, exceptions) {
1144
+ if (exceptions.some((e) => content.startsWith(e, i))) return false;
1145
+ return prefixes.some((prefix) => content.startsWith(prefix, i));
1146
+ }
1147
+ function skipBlockComment(content, start, block, nested) {
1148
+ let i = start + block[0].length;
1149
+ let depth = 1;
1150
+ while (i < content.length) {
1151
+ if (nested && content.startsWith(block[0], i)) {
1152
+ depth++;
1153
+ i += block[0].length;
1154
+ continue;
1155
+ }
1156
+ if (content.startsWith(block[1], i)) {
1157
+ depth--;
1158
+ i += block[1].length;
1159
+ if (depth === 0) return i;
1160
+ continue;
1161
+ }
1162
+ i++;
1163
+ }
1164
+ return -1;
1165
+ }
1166
+ function findMatchingBraceEndLine(content, openBraceIndex, totalLines, lineIndex, lineCommentPrefix, opts) {
1167
+ const linePrefixes = toLineCommentPrefixes(lineCommentPrefix);
1168
+ const lineExceptions = opts?.lineCommentExceptions ?? [];
1169
+ const block = opts?.blockComment;
1170
+ const backtick = opts?.backtickQuote === true;
1171
+ const escapes = opts?.stringEscapes ?? "backslash";
1172
+ const nestedBlock = opts?.nestedBlockComments === true;
1173
+ const rawString = opts?.rawStringQuotes === true;
1174
+ const rRaw = opts?.rRawStrings === true;
1175
+ const tripleDelims = tripleQuoteDelimiters(opts);
1176
+ const lineString = opts?.lineStringPrefix;
1177
+ let depth = 0;
1178
+ let quote = null;
1179
+ let verbatim = false;
1180
+ for (let i = openBraceIndex; i < content.length; i++) {
1181
+ const ch = content[i];
1182
+ if (quote !== null) {
1183
+ if (verbatim) {
1184
+ if (ch === quote) {
1185
+ if (content[i + 1] === quote) {
1186
+ i++;
1187
+ continue;
1188
+ }
1189
+ quote = null;
1190
+ verbatim = false;
1191
+ }
1192
+ continue;
1193
+ }
1194
+ if (escapes === "powershell") {
1195
+ const step = stepPowershellString(content, i, quote);
1196
+ i = step.next;
1197
+ if (!step.open) quote = null;
1198
+ continue;
1199
+ }
1200
+ if (ch === "\\") {
1201
+ i++;
1202
+ continue;
1203
+ }
1204
+ if (ch === quote) quote = null;
1205
+ continue;
1206
+ }
1207
+ if (block !== void 0 && content.startsWith(block[0], i)) {
1208
+ const end = skipBlockComment(content, i, block, nestedBlock);
1209
+ i = end === -1 ? content.length : end - 1;
1210
+ continue;
1211
+ }
1212
+ if (atLineComment(content, i, linePrefixes, lineExceptions)) {
1213
+ while (i < content.length && content[i] !== "\n") i++;
1214
+ continue;
1215
+ }
1216
+ if (lineString !== void 0 && content.startsWith(lineString, i)) {
1217
+ while (i < content.length && content[i] !== "\n") i++;
1218
+ continue;
1219
+ }
1220
+ if (rRaw) {
1221
+ const rOpen = matchRRawOpener(content, i);
1222
+ if (rOpen !== null) {
1223
+ const end = content.indexOf(rOpen.closer, rOpen.openerEnd);
1224
+ i = end === -1 ? content.length : end + rOpen.closer.length - 1;
1225
+ continue;
1226
+ }
1227
+ }
1228
+ if (rawString && ch === '"') {
1229
+ const run = quoteRunLength(content, i);
1230
+ if (run >= 3) {
1231
+ const end = skipRawStringQuotes(content, i + run, run);
1232
+ i = end === -1 ? content.length : end - 1;
1233
+ continue;
1234
+ }
1235
+ }
1236
+ const tripleAt = tripleDelims.find((t) => content.startsWith(t, i));
1237
+ if (tripleAt !== void 0) {
1238
+ const end = closingQuoteRunEnd(content, i + 3, tripleAt[0] ?? '"', 3, opts?.tripleQuoteRunClose ?? "last");
1239
+ i = end === -1 ? content.length : end - 1;
1240
+ continue;
1241
+ }
1242
+ if (ch === '"' || ch === "'" || backtick && ch === "`") {
1243
+ quote = ch;
1244
+ verbatim = escapes === "csharp" && ch === '"' && opensCsharpVerbatimString(content, i);
1245
+ continue;
1246
+ }
1247
+ if (ch === "{") depth++;
1248
+ else if (ch === "}") {
1249
+ depth--;
1250
+ if (depth === 0) {
1251
+ return offsetToLine(lineIndex, i);
1252
+ }
1253
+ }
1254
+ }
1255
+ return opts?.noMatchValue ?? totalLines;
1256
+ }
1257
+ function assignBraceBlockSpans(symbols, content, opts = {}) {
1258
+ const lineCommentPrefix = opts.lineComment;
1259
+ const stringEscapes = opts.stringEscapes ?? "backslash";
1260
+ const nestedBlockComments = opts.nestedBlockComments ?? false;
1261
+ const tripleQuote = opts.tripleQuote ?? false;
1262
+ const lineStringPrefix = opts.lineStringPrefix;
1263
+ const firstLinePrefix = toLineCommentPrefixes(lineCommentPrefix)[0];
1264
+ if (symbols.length === 0) return [...symbols];
1265
+ const lines = content.split("\n");
1266
+ const totalLines = lines.length;
1267
+ const lineIndex = buildLineIndex(content);
1268
+ const starts = [...new Set(symbols.map((s) => s.lineStart))].sort((a, b) => a - b);
1269
+ const scanContent = opts.multilineLang === void 0 ? content : maskMultilineStrings(content, opts.multilineLang);
1270
+ const blockComment = opts.blockComment !== void 0 ? opts.blockComment ?? void 0 : firstLinePrefix === "//" ? ["/*", "*/"] : firstLinePrefix === "#" ? ["<#", "#>"] : void 0;
1271
+ const scanOpts = {
1272
+ noMatchValue: -1,
1273
+ stringEscapes,
1274
+ tripleQuote,
1275
+ rawStringQuotes: opts.rawStringQuotes ?? false,
1276
+ tripleSingleQuote: opts.tripleSingleQuote ?? false,
1277
+ tripleQuoteRunClose: opts.tripleQuoteRunClose ?? "last",
1278
+ ...blockComment === void 0 ? {} : { blockComment, nestedBlockComments },
1279
+ ...lineStringPrefix === void 0 ? {} : { lineStringPrefix },
1280
+ ...opts.lineCommentExceptions === void 0 ? {} : { lineCommentExceptions: opts.lineCommentExceptions }
1281
+ };
1282
+ return symbols.map((sym) => {
1283
+ if (sym.lineEnd !== sym.lineStart) return sym;
1284
+ const nextStart = starts.find((s) => s > sym.lineStart);
1285
+ const lastSearchLine = Math.min(nextStart !== void 0 ? nextStart - 1 : totalLines, sym.lineStart + BRACE_SEARCH_MAX_LINES);
1286
+ const openIndex = findBlockOpenBrace(scanContent, lineIndex, sym.lineStart, lastSearchLine, lineCommentPrefix, scanOpts);
1287
+ if (openIndex === null) return sym;
1288
+ const endLine = findMatchingBraceEndLine(
1289
+ scanContent,
1290
+ openIndex,
1291
+ totalLines,
1292
+ lineIndex,
1293
+ lineCommentPrefix,
1294
+ scanOpts
1295
+ );
1296
+ if (endLine <= sym.lineStart) return sym;
1297
+ return { ...sym, lineEnd: endLine, body: lines.slice(sym.lineStart - 1, endLine).join("\n") };
1298
+ });
1299
+ }
1300
+ var BRACE_SEARCH_MAX_LINES = 10;
1301
+ function maskMultilineStrings(content, lang) {
1302
+ let state = null;
1303
+ const parts = [];
1304
+ let pos = 0;
1305
+ for (; ; ) {
1306
+ const nl = content.indexOf("\n", pos);
1307
+ const end = nl === -1 ? content.length : nl;
1308
+ const lineEnd = end > pos && content[end - 1] === "\r" ? end - 1 : end;
1309
+ const masked = stripMultilineStringSpan(content.slice(pos, lineEnd), state, lang);
1310
+ state = masked.state;
1311
+ parts.push(masked.code, content.slice(lineEnd, end));
1312
+ if (nl === -1) break;
1313
+ parts.push("\n");
1314
+ pos = nl + 1;
1315
+ }
1316
+ return parts.join("");
1317
+ }
1318
+ function findBlockOpenBrace(content, lineIndex, startLine, lastSearchLine, lineCommentPrefix, opts) {
1319
+ const blockComment = opts?.blockComment;
1320
+ const stringEscapes = opts?.stringEscapes ?? "backslash";
1321
+ const nestedBlockComments = opts?.nestedBlockComments === true;
1322
+ const rawString = opts?.rawStringQuotes === true;
1323
+ const tripleDelims = tripleQuoteDelimiters(opts);
1324
+ const lineString = opts?.lineStringPrefix;
1325
+ const linePrefixes = toLineCommentPrefixes(lineCommentPrefix);
1326
+ const lineExceptions = opts?.lineCommentExceptions ?? [];
1327
+ const from = lineIndex[startLine - 1];
1328
+ if (from === void 0) return null;
1329
+ const to = lineIndex[lastSearchLine] ?? content.length;
1330
+ let quote = null;
1331
+ let parenDepth = 0;
1332
+ let linesSeen = 0;
1333
+ let atLineStart = false;
1334
+ let verbatim = false;
1335
+ for (let i = from; i < to; i++) {
1336
+ const ch = content[i];
1337
+ if (ch === void 0) break;
1338
+ if (quote !== null) {
1339
+ if (verbatim) {
1340
+ if (ch === quote) {
1341
+ if (content[i + 1] === quote) {
1342
+ i++;
1343
+ continue;
1344
+ }
1345
+ quote = null;
1346
+ verbatim = false;
1347
+ }
1348
+ continue;
1349
+ }
1350
+ if (stringEscapes === "powershell") {
1351
+ const step = stepPowershellString(content, i, quote);
1352
+ i = step.next;
1353
+ if (!step.open) quote = null;
1354
+ continue;
1355
+ }
1356
+ if (ch === "\\") {
1357
+ i++;
1358
+ continue;
1359
+ }
1360
+ if (ch === quote) quote = null;
1361
+ continue;
1362
+ }
1363
+ if (ch === "\n") {
1364
+ linesSeen++;
1365
+ atLineStart = true;
1366
+ continue;
1367
+ }
1368
+ if (blockComment !== void 0 && content.startsWith(blockComment[0], i)) {
1369
+ const end = skipBlockComment(content, i, blockComment, nestedBlockComments);
1370
+ if (end === -1) return null;
1371
+ i = end - 1;
1372
+ continue;
1373
+ }
1374
+ if (atLineComment(content, i, linePrefixes, lineExceptions)) {
1375
+ while (i + 1 < to && content[i + 1] !== "\n") i++;
1376
+ continue;
1377
+ }
1378
+ if (atLineStart && !/\s/.test(ch)) {
1379
+ atLineStart = false;
1380
+ if (parenDepth === 0 && linesSeen >= 1 && startsWithBlockKeyword(content, i, to)) return null;
1381
+ }
1382
+ if (lineString !== void 0 && content.startsWith(lineString, i)) {
1383
+ while (i + 1 < to && content[i + 1] !== "\n") i++;
1384
+ continue;
1385
+ }
1386
+ if (rawString && ch === '"') {
1387
+ const run = quoteRunLength(content, i);
1388
+ if (run >= 3) {
1389
+ const end = skipRawStringQuotes(content, i + run, run);
1390
+ if (end === -1) return null;
1391
+ i = end - 1;
1392
+ continue;
1393
+ }
1394
+ }
1395
+ const tripleAt = tripleDelims.find((t) => content.startsWith(t, i));
1396
+ if (tripleAt !== void 0) {
1397
+ const end = closingQuoteRunEnd(content, i + 3, tripleAt[0] ?? '"', 3, opts?.tripleQuoteRunClose ?? "last");
1398
+ if (end === -1) return null;
1399
+ i = end - 1;
1400
+ continue;
1401
+ }
1402
+ if (ch === '"' || ch === "'") {
1403
+ quote = ch;
1404
+ verbatim = stringEscapes === "csharp" && ch === '"' && opensCsharpVerbatimString(content, i);
1405
+ continue;
1406
+ }
1407
+ if (ch === "(" || ch === "[") parenDepth++;
1408
+ else if (ch === ")" || ch === "]") {
1409
+ if (parenDepth > 0) parenDepth--;
1410
+ } else if (ch === ";") return null;
1411
+ else if (ch === "}" && parenDepth === 0) return null;
1412
+ else if (ch === "{") return i;
1413
+ }
1414
+ return null;
1415
+ }
1416
+ var BLOCK_OPENING_KEYWORDS = /* @__PURE__ */ new Set([
1417
+ "if",
1418
+ "for",
1419
+ "while",
1420
+ "do",
1421
+ "switch",
1422
+ "when",
1423
+ "match",
1424
+ "try",
1425
+ "foreach",
1426
+ "loop",
1427
+ "guard",
1428
+ "repeat",
1429
+ "unless",
1430
+ "until"
1431
+ ]);
1432
+ function startsWithBlockKeyword(content, i, to) {
1433
+ const first = content[i];
1434
+ if (first === void 0 || !/[A-Za-z_]/.test(first)) return false;
1435
+ let j = i + 1;
1436
+ while (j < to && /[A-Za-z0-9_]/.test(content[j])) j++;
1437
+ return BLOCK_OPENING_KEYWORDS.has(content.slice(i, j));
1438
+ }
1439
+ function lastContentLine(rawLines) {
1440
+ let n = rawLines.length;
1441
+ while (n > 0 && rawLines[n - 1].trim() === "") n--;
1442
+ return Math.max(1, n);
1443
+ }
1444
+ function nestedAngleClause(depth) {
1445
+ let inner = "[^<>]*";
1446
+ for (let i = 0; i < depth; i++) inner = "[^<>]*(?:<" + inner + ">[^<>]*)*";
1447
+ return "<" + inner + ">";
1448
+ }
1449
+ var GENERIC_CLAUSE = nestedAngleClause(4);
1450
+
1451
+ // src/languages/ini_idx.ts
1452
+ init_define_import_meta_env();
1453
+ var MAX_SECTIONS = 200;
1454
+ var MAX_HEADING_LEN = 200;
1455
+ var MAX_ENV_KEYS = 200;
1456
+ var HEADER_RE = /^\[([^\]\r\n]+)\]\s*(?:[;#].*)?$/;
1457
+ var ENV_KEY_RE = /^(?:export\s+)?([A-Za-z_][\w.-]*)\s*(?:=|:(?!\/\/))/;
1458
+ function extractIni(content, filePath) {
1459
+ const symbols = [];
1460
+ const sections = [];
1461
+ const seen = /* @__PURE__ */ new Set();
1462
+ const lines = content.split(/\r?\n/);
1463
+ const totalLines = countContentLines(content);
1464
+ for (let i = 0; i < lines.length; i++) {
1465
+ if (symbols.length >= MAX_SECTIONS) break;
1466
+ const line = lines[i] ?? "";
1467
+ if (!line || line[0] !== "[") continue;
1468
+ const m = HEADER_RE.exec(line);
1469
+ if (m === null) continue;
1470
+ const name = m[1]?.trim() ?? "";
1471
+ if (!name || name.length > MAX_HEADING_LEN) continue;
1472
+ const key = `${name}\0${i + 1}`;
1473
+ if (seen.has(key)) continue;
1474
+ seen.add(key);
1475
+ symbols.push(makeLineSymbol(filePath, name, "ini_section", i + 1));
1476
+ sections.push({ heading: name, level: 1, line: i + 1, endLine: i + 1 });
1477
+ }
1478
+ sections.sort((a, b) => a.line - b.line);
1479
+ assignFlatEndLines(sections, totalLines);
1480
+ return propagateEndLinesToSymbols(symbols, sections);
1481
+ }
1482
+ function _isEscapedQuote(line, i, q) {
1483
+ if (q === "'") return false;
1484
+ let backslashes = 0;
1485
+ let j = i - 1;
1486
+ while (j >= 0 && line[j] === "\\") {
1487
+ backslashes++;
1488
+ j--;
1489
+ }
1490
+ return backslashes % 2 === 1;
1491
+ }
1492
+ function _lineClosesQuote(line, q) {
1493
+ for (let i = 0; i < line.length; i++) {
1494
+ if (line[i] === q && !_isEscapedQuote(line, i, q)) return true;
1495
+ }
1496
+ return false;
1497
+ }
1498
+ function _detectOpenQuote(value) {
1499
+ const trimmed = value.replace(/^\s+/, "");
1500
+ const q = trimmed[0];
1501
+ if (q !== '"' && q !== "'") return null;
1502
+ for (let i = 1; i < trimmed.length; i++) {
1503
+ if (trimmed[i] === q && !_isEscapedQuote(trimmed, i, q)) return null;
1504
+ }
1505
+ return q;
1506
+ }
1507
+ function extractEnv(content, filePath) {
1508
+ const symbols = [];
1509
+ const seen = /* @__PURE__ */ new Set();
1510
+ const lines = content.split(/\r?\n/);
1511
+ let openQuote = null;
1512
+ for (let i = 0; i < lines.length; i++) {
1513
+ if (symbols.length >= MAX_ENV_KEYS) break;
1514
+ const line = lines[i] ?? "";
1515
+ if (openQuote !== null) {
1516
+ if (_lineClosesQuote(line, openQuote)) openQuote = null;
1517
+ continue;
1518
+ }
1519
+ const trimmed = line.replace(/^[ \t]+/, "");
1520
+ if (!trimmed || trimmed[0] === "#" || trimmed[0] === ";") continue;
1521
+ const m = ENV_KEY_RE.exec(trimmed);
1522
+ if (m === null) continue;
1523
+ const name = m[1]?.trim() ?? "";
1524
+ if (!name || name.length > MAX_HEADING_LEN) continue;
1525
+ const key = `${name}\0${i + 1}`;
1526
+ if (seen.has(key)) continue;
1527
+ seen.add(key);
1528
+ symbols.push(makeLineSymbol(filePath, name, "env_key", i + 1));
1529
+ openQuote = _detectOpenQuote(trimmed.slice(m[0].length));
1530
+ }
1531
+ return symbols;
1532
+ }
1533
+
1534
+ // src/parser_types.ts
1535
+ init_define_import_meta_env();
1536
+ import * as fs from "node:fs";
1537
+ import * as path from "node:path";
1538
+
1539
+ // src/languages/sniff.ts
1540
+ init_define_import_meta_env();
1541
+ var SNIFF_CHARS = 8192;
1542
+ var ABL_MARKER_RES = [
1543
+ /^&ANALYZE-SUSPEND\b/i,
1544
+ /^&(?:SCOPED|GLOBAL)-DEFINE\s/i,
1545
+ /^(?:ROUTINE|BLOCK)-LEVEL\s+ON\s+ERROR\s+UNDO\b/i,
1546
+ /^DEF(?:INE)?\s+(?:NEW\s+)?(?:GLOBAL\s+)?(?:SHARED\s+)?(?:VAR|VARIABLE|TEMP-TABLE|BUFFER|QUERY|STREAM|DATASET|FRAME|(?:INPUT|OUTPUT|INPUT-OUTPUT)\s+PARAM(?:ETER)?)\s/i,
1547
+ /^FUNCTION\s+[\w-]+\s+RETURNS\s/i,
1548
+ /^FOR\s+EACH\s/i,
1549
+ /^END\s+(?:PROCEDURE|FUNCTION|CLASS|INTERFACE|METHOD|CONSTRUCTOR)\s*\.$/i,
1550
+ /^USING\s+[\w.*-]+(?:\s+FROM\s+(?:ASSEMBLY|PROPATH))?\s*\.$/i
1551
+ ];
1552
+ var ABL_HEADER_RE = /^(?:PROCEDURE\s+[\w-]+|(?:CLASS|INTERFACE)\s+[\w.-]+)(?:\s|:)/i;
1553
+ var ABL_SNIFF_CHARS = SNIFF_CHARS;
1554
+ function isAblSource(content) {
1555
+ const head = content.slice(0, ABL_SNIFF_CHARS);
1556
+ if (head.includes("\0")) return false;
1557
+ for (const raw of head.split(/\r?\n/)) {
1558
+ const line = raw.trim();
1559
+ if (line === "") continue;
1560
+ if (ABL_MARKER_RES.some((re) => re.test(line))) return true;
1561
+ if (ABL_HEADER_RE.test(line) && line.endsWith(":")) return true;
1562
+ }
1563
+ return false;
1564
+ }
1565
+ var SNIFF_FUNCTION_RE = /^function\b\s*(?:(?:\[[\w\s,~]*\]|[A-Za-z]\w*)\s*=\s*)?[A-Za-z]\w*(?:\.[A-Za-z]\w*)?\s*(?:\(|[%#;,]|$)/;
1566
+ var SNIFF_CLASSDEF_RE = /^classdef\b\s*(?:\([^)]*\)\s*)?[A-Za-z]\w*\s*(?:<|[%#]|$)/;
1567
+ function isMatlabSource(content) {
1568
+ for (const raw of content.slice(0, SNIFF_CHARS).split("\n")) {
1569
+ const line = raw.trim();
1570
+ if (SNIFF_FUNCTION_RE.test(line) || SNIFF_CLASSDEF_RE.test(line)) return true;
1571
+ }
1572
+ return false;
1573
+ }
1574
+ function isObjcSource(content) {
1575
+ return /^[ \t]*(?:#[ \t]*import[ \t]*[<"]|@(?:interface|implementation|protocol)\b)/m.test(content.slice(0, SNIFF_CHARS));
1576
+ }
1577
+ function isObjcHeader(content) {
1578
+ return /^[ \t]*@(?:interface|protocol)\b/m.test(content.slice(0, SNIFF_CHARS));
1579
+ }
1580
+ function isPascalSource(content) {
1581
+ const head = content.slice(0, SNIFF_CHARS);
1582
+ let i = 0;
1583
+ for (; ; ) {
1584
+ while (i < head.length && /\s/.test(head[i])) i++;
1585
+ if (head[i] === "{") {
1586
+ const end = head.indexOf("}", i);
1587
+ if (end < 0) return false;
1588
+ i = end + 1;
1589
+ } else if (head.startsWith("(*", i)) {
1590
+ const end = head.indexOf("*)", i);
1591
+ if (end < 0) return false;
1592
+ i = end + 2;
1593
+ } else if (head.startsWith("//", i)) {
1594
+ const end = head.indexOf("\n", i);
1595
+ if (end < 0) return false;
1596
+ i = end + 1;
1597
+ } else {
1598
+ break;
1599
+ }
1600
+ }
1601
+ return /^(?:unit|program|library)[ \t\r\n]+[A-Za-z_][\w.]*[ \t\r\n]*[;(]/i.test(head.slice(i, i + 300));
1602
+ }
1603
+ var PERL_MARKER_RE = /^(?:#!.*\bperl\b|[ \t]*use[ \t]+(?:strict|warnings|Test::More|Test2::V0|Test::Simple)\b|[ \t]*package[ \t]+[\w:]+[ \t]*;|[ \t]*my[ \t]+[$@%]|[ \t]*sub[ \t]+\w+[ \t]*\{)/m;
1604
+ function isPerlSource(content) {
1605
+ const head = content.slice(0, SNIFF_CHARS);
1606
+ return PERL_MARKER_RE.test(head) && !/^[ \t]*use[ \t]+v6\b/m.test(head);
1607
+ }
1608
+ function isPrologSource(content) {
1609
+ const head = content.slice(0, SNIFF_CHARS);
1610
+ if (PERL_MARKER_RE.test(head)) return false;
1611
+ return /^:-/m.test(head) || /^[a-z]\w*(?:\([^()\n]*\))?[ \t]*:-/m.test(head);
1612
+ }
1613
+ var LATEX_CLASS_MARKER_RE = /\\(?:ProvidesClass|documentclass)\b/;
1614
+ function isLatexClassFile(content) {
1615
+ return LATEX_CLASS_MARKER_RE.test(content.slice(0, SNIFF_CHARS));
1616
+ }
1617
+
1618
+ // src/language_specs.ts
1619
+ init_define_import_meta_env();
1620
+ var CODE = { symbolBearing: true, sourceHints: true, grepSource: true, diffable: true };
1621
+ var DATA = { symbolBearing: false, sourceHints: false, grepSource: false, diffable: false };
1622
+ var LANGUAGE_SPECS = [
1623
+ { id: "typescript", extraction: "tree-sitter", extensions: [".ts", ".tsx", ".mts", ".cts"], ...CODE, fence: "typescript", fenceByExtension: { ".tsx": "tsx" } },
1624
+ { id: "javascript", extraction: "tree-sitter", extensions: [".js", ".jsx", ".mjs", ".cjs"], ...CODE, fence: "javascript", fenceByExtension: { ".jsx": "jsx" } },
1625
+ // Bazel and Starlark (`.bzl`, `.star`, BUILD/WORKSPACE/MODULE.bazel) are a Python dialect: `def` and top-level calls such as `load(...)` parse with the Python grammar (https://github.com/bazelbuild/starlark/blob/master/spec.md).
1626
+ { id: "python", extraction: "tree-sitter", extensions: [".py", ".pyi", ".bzl", ".star"], basenames: ["build.bazel", "workspace.bazel", "module.bazel"], exactBasenames: ["BUILD", "WORKSPACE"], ...CODE, fence: "python" },
1627
+ { id: "go", extraction: "tree-sitter", extensions: [".go"], ...CODE, fence: "go" },
1628
+ { id: "rust", extraction: "tree-sitter", extensions: [".rs"], ...CODE, fence: "rust" },
1629
+ // Rake task files, Gemfile, Rakefile and the other extensionless Ruby DSL files are plain Ruby syntax.
1630
+ { id: "ruby", extraction: "tree-sitter", extensions: [".rb", ".ruby", ".rake"], basenames: ["gemfile", "rakefile", "vagrantfile", "guardfile", "podfile", "capfile", "fastfile", "brewfile"], ...CODE, fence: "ruby" },
1631
+ { id: "java", extraction: "tree-sitter", extensions: [".java"], ...CODE, fence: "java" },
1632
+ // `.h` is C unless it declares an Objective-C `@interface` or `@protocol` (refineLanguageByContent); a C++ header parses with the C grammar.
1633
+ { id: "c", extraction: "tree-sitter", extensions: [".c", ".h"], label: "C", ...CODE, fence: "c" },
1634
+ { id: "cpp", extraction: "tree-sitter", extensions: [".cpp", ".cc", ".cxx", ".hpp", ".hxx"], label: "C++", ...CODE, fence: "cpp" },
1635
+ // zsh, ksh and bats scripts share the POSIX `name() {` and `function name {` function forms the bash adapter reads.
1636
+ { id: "bash", extraction: "regex", extensions: [".sh", ".bash", ".zsh", ".ksh", ".bats"], ...DATA, symbolBearing: true, fence: "bash" },
1637
+ // MDX headings are plain ATX; `.rst` needs an underline heading parser this adapter lacks, so it stays unmapped.
1638
+ { id: "markdown", extraction: "regex", extensions: [".md", ".markdown", ".mdx"], ...DATA, fence: "markdown" },
1639
+ { id: "toml", extraction: "regex", extensions: [".toml"], basenames: ["cargo.toml", "pyproject.toml"], label: "TOML", ...DATA, diffable: true, fence: "toml" },
1640
+ // JSON with comments (`.jsonc`) and Avro schemas (`.avsc`) read with the JSON adapter, which skips `//` and `/* */` comments.
1641
+ { id: "json", extraction: "regex", extensions: [".json", ".jsonc", ".avsc"], basenames: ["package.json", "tsconfig.json"], label: "JSON", ...DATA, diffable: true, fence: "json" },
1642
+ { id: "yaml", extraction: "regex", extensions: [".yaml", ".yml"], label: "YAML", ...DATA, diffable: true, fence: "yaml" },
1643
+ { id: "css", extraction: "regex", extensions: [".css", ".scss", ".sass", ".less"], label: "CSS", ...DATA, grepSource: true, diffable: true, fence: "css", fenceByExtension: { ".scss": "scss", ".sass": "sass", ".less": "less" } },
1644
+ { id: "dockerfile", extraction: "regex", extensions: [], basenames: ["dockerfile"], ...DATA, fence: "dockerfile" },
1645
+ { id: "csharp", extraction: "regex", extensions: [".cs"], label: "C#", ...CODE, fence: "csharp" },
1646
+ { id: "php", extraction: "regex", extensions: [".php"], label: "PHP", ...CODE, fence: "php" },
1647
+ { id: "html", extraction: "regex", extensions: [".html", ".htm"], label: "HTML", ...DATA, fence: "html" },
1648
+ { id: "liquid", extraction: "regex", extensions: [".liquid"], ...DATA, fence: "liquid" },
1649
+ // Six template dialects, each masking its own delimiters out then handing off to the HTML
1650
+ // extractor (src/languages/templates_idx.ts) -- markup formats, so DATA defaults like html/liquid.
1651
+ { id: "jinja2", extraction: "regex", extensions: [".j2", ".jinja", ".jinja2"], label: "Jinja2", ...DATA, fence: "jinja" },
1652
+ { id: "handlebars", extraction: "regex", extensions: [".hbs", ".handlebars"], label: "Handlebars", ...DATA, fence: "handlebars" },
1653
+ { id: "erb", extraction: "regex", extensions: [".erb"], label: "ERB", ...DATA, fence: "erb" },
1654
+ { id: "ejs", extraction: "regex", extensions: [".ejs"], label: "EJS", ...DATA, fence: "ejs" },
1655
+ { id: "nunjucks", extraction: "regex", extensions: [".njk"], label: "Nunjucks", ...DATA, fence: "html" },
1656
+ { id: "twig", extraction: "regex", extensions: [".twig"], label: "Twig", ...DATA, fence: "twig" },
1657
+ { id: "kotlin", extraction: "regex", extensions: [".kt", ".kts"], ...CODE, fence: "kotlin" },
1658
+ { id: "swift", extraction: "regex", extensions: [".swift"], ...CODE, fence: "swift" },
1659
+ { id: "scala", extraction: "regex", extensions: [".scala", ".sc"], ...CODE, fence: "scala" },
1660
+ { id: "lua", extraction: "regex", extensions: [".lua"], ...CODE, fence: "lua" },
1661
+ { id: "elixir", extraction: "regex", extensions: [".ex", ".exs"], ...CODE, fence: "elixir" },
1662
+ { id: "dart", extraction: "regex", extensions: [".dart"], ...CODE, fence: "dart" },
1663
+ { id: "zig", extraction: "regex", extensions: [".zig"], ...CODE, fence: "zig" },
1664
+ { id: "r", extraction: "regex", extensions: [".r"], ...CODE, fence: "r" },
1665
+ { id: "graphql", extraction: "regex", extensions: [".graphql", ".gql"], label: "GraphQL", ...DATA, symbolBearing: true, fence: "graphql" },
1666
+ // Oracle PL/SQL sources: package spec and body, standalone procedure/function, trigger, and object type spec and body.
1667
+ { id: "sql", extraction: "regex", extensions: [".sql", ".pks", ".pkb", ".pls", ".plsql", ".pck", ".prc", ".fnc", ".trg", ".tps", ".tpb"], label: "SQL", ...DATA, symbolBearing: true, diffable: true, fence: "sql" },
1668
+ { id: "ini", extraction: "regex", extensions: [".ini", ".cfg", ".conf"], label: "INI", ...DATA, fence: "ini" },
1669
+ // `.mk` fragments (config.mk, rules.mk) share a bare Makefile's syntax.
1670
+ { id: "makefile", extraction: "regex", extensions: [".mk"], basenames: ["makefile", "gnumakefile", "bsdmakefile"], ...DATA, fence: "makefile", basenameImportsExtension: ".mk" },
1671
+ { id: "proto", extraction: "regex", extensions: [".proto"], label: "Protocol Buffers", ...DATA, symbolBearing: true, fence: "protobuf" },
1672
+ { id: "terraform", extraction: "regex", extensions: [".tf", ".tfvars", ".hcl"], ...DATA, symbolBearing: true, fence: "hcl" },
1673
+ // `.env` and `.env.<suffix>` are matched by parser_types.ts's DOTENV_VARIANT_RE before this table.
1674
+ { id: "env_file", extraction: "regex", extensions: [".env"], basenames: [".envrc"], label: "env file", ...DATA },
1675
+ { id: "powershell", extraction: "regex", extensions: [".ps1", ".psm1"], label: "PowerShell", ...CODE, fence: "powershell" },
1676
+ // VB.NET, VB6/VBA standard modules, VBScript and VB6 forms. A VB6 class module shares `.cls` with Apex (and LaTeX) and is told apart by content in refineLanguageByContent.
1677
+ { id: "vb", extraction: "regex", extensions: [".vb", ".bas", ".vbs", ".frm"], label: "Visual Basic", ...CODE, fence: "vb", fenceByExtension: { ".vb": "vbnet", ".vbs": "vbscript" } },
1678
+ {
1679
+ id: "cobol",
1680
+ extraction: "own-result",
1681
+ extensions: [".cbl", ".cob", ".cobol", ".cpy"],
1682
+ label: "COBOL",
1683
+ ...CODE,
1684
+ fence: "cobol",
1685
+ partialRefsReason: "PERFORM, GO TO and CALL 'literal' are recorded as references, but a paragraph also runs by falling through from the one above it and a program can be called through a data item holding its name, so `dead` skips COBOL"
1686
+ },
1687
+ // Natural object sources as NaturalONE and SYSOBJH export them. Maps (.nsm) and DDMs (.nsd) are layouts, not code, and stay unmapped.
1688
+ {
1689
+ id: "natural",
1690
+ extraction: "own-result",
1691
+ extensions: [".nsp", ".nsn", ".nss", ".nsa", ".nsl", ".nsg", ".nsc", ".nsh"],
1692
+ label: "Natural",
1693
+ ...CODE,
1694
+ fence: "natural",
1695
+ partialRefsReason: "PERFORM, CALLNAT 'literal' and FETCH 'literal' are recorded as references, but an object can also be called through a variable holding its name, so `dead` skips Natural"
1696
+ },
1697
+ { id: "abap", extraction: "regex", extensions: [".abap"], label: "ABAP", ...CODE, fence: "abap" },
1698
+ { id: "sas", extraction: "regex", extensions: [".sas"], label: "SAS", ...CODE, fence: "sas" },
1699
+ { id: "pli", extraction: "regex", extensions: [".pli", ".pl1"], label: "PL/I", ...CODE, fence: "pli" },
1700
+ // `.rpg` stays unmapped: RPG II and RPG III sources use it, and their fixed layout predates the ILE RPG forms this adapter reads.
1701
+ { id: "rpg", extraction: "regex", extensions: [".rpgle", ".sqlrpgle"], label: "RPG", ...CODE, fence: "rpgle" },
1702
+ { id: "jcl", extraction: "regex", extensions: [".jcl"], label: "JCL", ...CODE, fence: "jcl" },
1703
+ // `.mm` is always Objective-C++. A `.m` (MATLAB uses it too) is Objective-C only on an `#import`, `@interface`, `@implementation` or `@protocol` line, and a `.h` only on `@interface` or `@protocol`: refineLanguageByContent in parser_types.ts decides.
1704
+ { id: "objc", extraction: "regex", extensions: [".mm"], label: "Objective-C", ...CODE, fence: "objectivec" },
1705
+ // Gradle build scripts and Jenkinsfiles are Groovy.
1706
+ { id: "groovy", extraction: "regex", extensions: [".groovy", ".gvy", ".gradle"], basenames: ["jenkinsfile"], label: "Groovy", ...CODE, fence: "groovy", basenameImportsExtension: ".groovy" },
1707
+ // A Prolog `.pl` stays unknown (refineLanguageByContent), and a `.t` is Perl only on a Perl marker line.
1708
+ { id: "perl", extraction: "regex", extensions: [".pl", ".pm"], label: "Perl", ...CODE, fence: "perl" },
1709
+ { id: "solidity", extraction: "regex", extensions: [".sol"], label: "Solidity", ...CODE, fence: "solidity" },
1710
+ { id: "thrift", extraction: "regex", extensions: [".thrift"], label: "Thrift", ...DATA, symbolBearing: true, fence: "thrift" },
1711
+ { id: "glsl", extraction: "regex", extensions: [".glsl", ".vert", ".frag", ".comp", ".geom", ".tesc", ".tese"], label: "GLSL", ...CODE, fence: "glsl" },
1712
+ // `.fx` stays unmapped: other languages use it too.
1713
+ { id: "hlsl", extraction: "regex", extensions: [".hlsl", ".hlsli"], label: "HLSL", ...CODE, fence: "hlsl" },
1714
+ { id: "wgsl", extraction: "regex", extensions: [".wgsl"], label: "WGSL", ...CODE, fence: "wgsl" },
1715
+ { id: "metal", extraction: "regex", extensions: [".metal"], label: "Metal", ...CODE, fence: "metal" },
1716
+ // `.f`, `.for` and `.f77` are read as fixed form unless code starts in column 1. `.fpp` stays unmapped: it is used for both forms.
1717
+ { id: "fortran", extraction: "regex", extensions: [".f", ".for", ".f77", ".f90", ".f95", ".f03", ".f08"], label: "Fortran", ...CODE, fence: "fortran" },
1718
+ // `.inc` stays unmapped (many languages use it), and a `.pp` (Puppet uses it too) is Pascal only on a unit, program or library header: refineLanguageByContent in parser_types.ts decides. Text-form `.dfm` forms list their components.
1719
+ { id: "pascal", extraction: "regex", extensions: [".pas", ".dpr", ".dpk", ".lpr", ".dfm"], label: "Pascal", ...CODE, fence: "pascal" },
1720
+ // MATLAB has no extension of its own: a `.m` that is not Objective-C is MATLAB only on a `function` or `classdef` header line, which refineLanguageByContent in parser_types.ts checks, so a Mathematica or Mercury `.m` stays unknown.
1721
+ { id: "matlab", extraction: "regex", extensions: [], label: "MATLAB", ...CODE, fence: "matlab" },
1722
+ { id: "cmake", extraction: "regex", extensions: [".cmake"], basenames: ["cmakelists.txt"], label: "CMake", ...CODE, fence: "cmake", basenameImportsExtension: ".cmake" },
1723
+ // One adapter for the three assembly dialects that share these extensions: GNU as (`.s`, and `.S` through the lowercase lookup), NASM (`.asm`, `.nasm`) and IBM High Level Assembler (`.asm`), which the adapter tells apart by content. `.inc` stays unmapped: many languages use it.
1724
+ { id: "asm", extraction: "regex", extensions: [".s", ".asm", ".nasm"], label: "Assembly", ...CODE, fence: "asm" },
1725
+ { id: "batch", extraction: "regex", extensions: [".bat", ".cmd"], label: "Windows batch", ...CODE, fence: "batch" },
1726
+ { id: "erlang", extraction: "regex", extensions: [".erl", ".hrl"], label: "Erlang", ...CODE, fence: "erlang" },
1727
+ { id: "vhdl", extraction: "regex", extensions: [".vhd", ".vhdl"], label: "VHDL", ...CODE, fence: "vhdl" },
1728
+ // Five Lisp-family dialects, each its own row: none shares an extractor (see common_lisp.ts's
1729
+ // module doc for why their lexical rules stay separate rather than a single Lisp masker).
1730
+ { id: "common_lisp", extraction: "regex", extensions: [".lisp", ".lsp", ".cl"], label: "Common Lisp", ...CODE, fence: "lisp" },
1731
+ { id: "scheme", extraction: "regex", extensions: [".scm", ".ss"], label: "Scheme", ...CODE, fence: "scheme" },
1732
+ { id: "racket", extraction: "regex", extensions: [".rkt", ".rktl"], label: "Racket", ...CODE, fence: "racket" },
1733
+ { id: "clojure", extraction: "regex", extensions: [".clj", ".cljs", ".cljc"], label: "Clojure", ...CODE, fence: "clojure" },
1734
+ { id: "emacs_lisp", extraction: "regex", extensions: [".el"], label: "Emacs Lisp", ...CODE, fence: "lisp" },
1735
+ // `.lhs` (literate Haskell) is deliberately not claimed here -- see haskell.ts's module doc.
1736
+ { id: "haskell", extraction: "regex", extensions: [".hs"], label: "Haskell", ...CODE, fence: "haskell" },
1737
+ // `.mli` interface files share `.ml`'s lexical rules (comments, strings, quoted strings) and are read with the same extractor -- see ocaml.ts's module doc.
1738
+ { id: "ocaml", extraction: "regex", extensions: [".ml", ".mli"], label: "OCaml", ...CODE, fence: "ocaml" },
1739
+ // `.fsi` signature files and `.fsx` scripts share `.fs`'s lexical rules and are read with the same extractor -- see fsharp.ts's module doc.
1740
+ { id: "fsharp", extraction: "regex", extensions: [".fs", ".fsi", ".fsx"], label: "F#", ...CODE, fence: "fsharp" },
1741
+ { id: "nix", extraction: "regex", extensions: [".nix"], label: "Nix", ...CODE, fence: "nix" },
1742
+ // OpenEdge ABL has no extension of its own: a `.p` or `.w` (Pascal and CWEB use them too) or a `.cls` (Apex, VB6, LaTeX) is ABL only when its head carries an ABL marker, which refineLanguageByContent in parser_types.ts checks. The path-only hooks see a `.p` or `.w` as unknown and a `.cls` as Apex.
1743
+ { id: "abl", extraction: "regex", extensions: [], label: "OpenEdge ABL", ...CODE, fence: "abl" },
1744
+ { id: "apex", extraction: "regex", extensions: [".cls", ".trigger"], label: "Apex", ...CODE, fence: "apex" },
1745
+ // Matched by the `-meta.xml` suffix in detectLanguage, not by an extension.
1746
+ { id: "salesforce_metadata", extraction: "regex", extensions: [], label: "Salesforce metadata", ...DATA, symbolBearing: true, sourceHints: true, grepSource: true, fence: "xml" },
1747
+ { id: "salesforce_markup", extraction: "own-result", extensions: [".cmp", ".app", ".evt", ".intf", ".design", ".auradoc", ".tokens", ".page", ".component", ".email"], label: "Salesforce markup", ...DATA, symbolBearing: true, sourceHints: true, grepSource: true, fence: "xml" },
1748
+ { id: "vue", extraction: "own-result", extensions: [".vue"], ...DATA, fence: "vue" },
1749
+ { id: "svelte", extraction: "own-result", extensions: [".svelte"], ...DATA, fence: "svelte" },
1750
+ { id: "astro", extraction: "own-result", extensions: [".astro"], ...DATA, fence: "astro" },
1751
+ // Notebooks index through their code cells as Python, in parser.ts's ipynb branch.
1752
+ { id: "ipynb", extraction: "own-result", extensions: [".ipynb"], label: "Jupyter notebook", ...DATA, fence: "json" }
1753
+ ];
1754
+ var SPEC_BY_ID = new Map(LANGUAGE_SPECS.map((s) => [s.id, s]));
1755
+ function rows() {
1756
+ return LANGUAGE_SPECS;
1757
+ }
1758
+ var EXTENSION_LANGUAGE = new Map(
1759
+ LANGUAGE_SPECS.flatMap((s) => s.extensions.map((e) => [e, s.id]))
1760
+ );
1761
+ var FILENAME_LANGUAGE = new Map(
1762
+ rows().flatMap((s) => (s.basenames ?? []).map((b) => [b, s.id]))
1763
+ );
1764
+ var EXACT_FILENAME_LANGUAGE = new Map(
1765
+ rows().flatMap((s) => (s.exactBasenames ?? []).map((b) => [b, s.id]))
1766
+ );
1767
+ var TREE_SITTER_LANGUAGES = LANGUAGE_SPECS.filter((s) => s.extraction === "tree-sitter").map((s) => s.id);
1768
+ function languageHasFlag(language, flag) {
1769
+ return SPEC_BY_ID.get(language)?.[flag] === true;
1770
+ }
1771
+ function languageLabel(language) {
1772
+ if (language === "unknown") return "this file type";
1773
+ return SPEC_BY_ID.get(language)?.label ?? language;
1774
+ }
1775
+ function fenceFor(language, ext) {
1776
+ const spec = SPEC_BY_ID.get(language);
1777
+ if (spec === void 0) return "";
1778
+ return spec.fenceByExtension?.[ext] ?? spec.fence ?? "";
1779
+ }
1780
+ function basenameImportsExtension(language) {
1781
+ return SPEC_BY_ID.get(language)?.basenameImportsExtension;
1782
+ }
1783
+ function partialRefsReason(language) {
1784
+ return SPEC_BY_ID.get(language)?.partialRefsReason;
1785
+ }
1786
+
1787
+ // src/parser_types.ts
1788
+ var DOTENV_VARIANT_RE = /^\.env(\..+)?$/;
1789
+ var VB6_HEADER_SCAN_LINES = 40;
1790
+ function isVb6ClassModule(content) {
1791
+ if (content.includes("\0")) return false;
1792
+ const lines = (content.charCodeAt(0) === 65279 ? content.slice(1) : content).split(/\r?\n/, VB6_HEADER_SCAN_LINES);
1793
+ const first = lines.find((l) => l.trim() !== "");
1794
+ if (first !== void 0 && /^VERSION\s+1\.0\s+CLASS\b/i.test(first.trim())) return true;
1795
+ return lines.some((l) => /^Attribute\s+VB_Name\s*=\s*"/i.test(l.trim()));
1796
+ }
1797
+ function refineLanguageByContent(filePath, language, content) {
1798
+ const sniff = CONTENT_SNIFFS.get(path.extname(filePath).toLowerCase());
1799
+ if (sniff === void 0 || sniff.from !== language) return language;
1800
+ return sniff.refine(content) ?? language;
1801
+ }
1802
+ var LANGUAGE_SNIFF_BYTES = 8192;
1803
+ function sniffHead(content) {
1804
+ const head = content.slice(0, LANGUAGE_SNIFF_BYTES);
1805
+ return Buffer.byteLength(head, "utf8") <= LANGUAGE_SNIFF_BYTES ? head : Buffer.from(head, "utf8").subarray(0, LANGUAGE_SNIFF_BYTES).toString("utf8");
1806
+ }
1807
+ var ablOrUnknown = (c) => isAblSource(c) ? "abl" : void 0;
1808
+ var clsRefine = (c) => {
1809
+ if (isVb6ClassModule(c)) return "vb";
1810
+ const abl = ablOrUnknown(c);
1811
+ if (abl !== void 0) return abl;
1812
+ if (isLatexClassFile(c)) return "unknown";
1813
+ return void 0;
1814
+ };
1815
+ var CONTENT_SNIFFS = /* @__PURE__ */ new Map([
1816
+ [".cls", { from: "apex", refine: clsRefine }],
1817
+ [".p", { from: "unknown", refine: ablOrUnknown }],
1818
+ [".w", { from: "unknown", refine: ablOrUnknown }],
1819
+ [".m", { from: "unknown", refine: (c) => isObjcSource(sniffHead(c)) ? "objc" : isMatlabSource(sniffHead(c)) ? "matlab" : void 0 }],
1820
+ [".pp", { from: "unknown", refine: (c) => isPascalSource(sniffHead(c)) ? "pascal" : void 0 }],
1821
+ [".h", { from: "c", refine: (c) => isObjcHeader(sniffHead(c)) ? "objc" : void 0 }],
1822
+ [".pl", { from: "perl", refine: (c) => isPrologSource(sniffHead(c)) ? "unknown" : void 0 }],
1823
+ [".t", { from: "unknown", refine: (c) => isPerlSource(sniffHead(c)) ? "perl" : void 0 }]
1824
+ ]);
1825
+ function needsContentSniff(filePath, language) {
1826
+ return CONTENT_SNIFFS.get(path.extname(filePath).toLowerCase())?.from === language;
1827
+ }
1828
+ function detectLanguageOfFile(filePath) {
1829
+ const language = detectLanguage(filePath);
1830
+ if (!needsContentSniff(filePath, language)) return language;
1831
+ try {
1832
+ const fd = fs.openSync(filePath, "r");
1833
+ try {
1834
+ const buf = Buffer.alloc(LANGUAGE_SNIFF_BYTES);
1835
+ const n = fs.readSync(fd, buf, 0, buf.length, 0);
1836
+ return refineLanguageByContent(filePath, language, buf.subarray(0, n).toString("utf8"));
1837
+ } finally {
1838
+ fs.closeSync(fd);
1839
+ }
1840
+ } catch {
1841
+ return language;
1842
+ }
1843
+ }
1844
+ function detectLanguage(filePath) {
1845
+ const exactBase = path.basename(filePath);
1846
+ const base = exactBase.toLowerCase();
1847
+ if (DOTENV_VARIANT_RE.test(base)) return "env_file";
1848
+ const byName = EXACT_FILENAME_LANGUAGE.get(exactBase) ?? FILENAME_LANGUAGE.get(base);
1849
+ if (byName !== void 0) return byName;
1850
+ if (base.endsWith("-meta.xml")) {
1851
+ return "salesforce_metadata";
1852
+ }
1853
+ const ext = path.extname(base).toLowerCase();
1854
+ return EXTENSION_LANGUAGE.get(ext) ?? "unknown";
1855
+ }
1856
+ var UNSUPPORTED_LANGUAGE_EXTENSIONS = /* @__PURE__ */ new Map([
1857
+ [".rpg", "RPG II or RPG III"],
1858
+ [".nsm", "Natural map"],
1859
+ [".nsd", "Natural DDM"]
1860
+ ]);
1861
+ function nonTreeSitterLanguageCount() {
1862
+ return LANGUAGE_SPECS.filter((s) => s.extraction !== "tree-sitter" && s.id !== "ipynb").length;
1863
+ }
1864
+ function unsupportedLanguageName(filePath) {
1865
+ const ext = path.extname(filePath).toLowerCase();
1866
+ return UNSUPPORTED_LANGUAGE_EXTENSIONS.get(ext);
1867
+ }
1868
+
1869
+ // src/dotenv_redact.ts
1870
+ init_define_import_meta_env();
1871
+ var DOTENV_VALUE_PLACEHOLDER = "[REDACTED:dotenv_value]";
1872
+ var ASSIGNMENT_RE = /^(\s*(?:export\s+)?[A-Za-z_][\w.-]*\s*(?:\+?=|:(?!\/\/)))/;
1873
+ var SAFE_LINE_RE = /^\s*(?:[#;].*)?$/;
1874
+ function isDotenvPath(filePath) {
1875
+ return detectLanguage(filePath) === "env_file";
1876
+ }
1877
+ function redactDotenvValues(text) {
1878
+ const lines = text.split("\n");
1879
+ const out = [];
1880
+ let openQuote = null;
1881
+ for (const raw of lines) {
1882
+ const hasCr = raw.endsWith("\r");
1883
+ const line = hasCr ? raw.slice(0, -1) : raw;
1884
+ const eol = hasCr ? "\r" : "";
1885
+ const emit = (s) => {
1886
+ out.push(`${s}${eol}`);
1887
+ };
1888
+ if (openQuote !== null) {
1889
+ if (_lineClosesQuote(line, openQuote)) openQuote = null;
1890
+ emit(DOTENV_VALUE_PLACEHOLDER);
1891
+ continue;
1892
+ }
1893
+ if (SAFE_LINE_RE.test(line)) {
1894
+ emit(line);
1895
+ continue;
1896
+ }
1897
+ const m = ASSIGNMENT_RE.exec(line);
1898
+ if (m === null || m[1] === void 0) {
1899
+ emit(DOTENV_VALUE_PLACEHOLDER);
1900
+ continue;
1901
+ }
1902
+ const prefix = m[1];
1903
+ openQuote = _detectOpenQuote(line.slice(prefix.length));
1904
+ emit(`${prefix}${DOTENV_VALUE_PLACEHOLDER}`);
1905
+ }
1906
+ return out.join("\n");
1907
+ }
1908
+ function redactIfDotenv(filePath, text) {
1909
+ return isDotenvPath(filePath) ? redactDotenvValues(text) : text;
1910
+ }
1911
+
1912
+ // src/db.ts
1913
+ init_define_import_meta_env();
1914
+ import * as fs2 from "node:fs";
1915
+ import { createRequire } from "node:module";
1916
+ import * as path2 from "node:path";
1917
+ var _require = createRequire(import.meta.url);
1918
+ var _connections = /* @__PURE__ */ new Map();
1919
+ var SCHEMA_SQL = `
1920
+ CREATE TABLE IF NOT EXISTS files (
1921
+ path TEXT PRIMARY KEY,
1922
+ sha TEXT,
1923
+ mtime REAL,
1924
+ language TEXT,
1925
+ indexed_at REAL,
1926
+ embed_sha TEXT,
1927
+ retry_count INTEGER NOT NULL DEFAULT 0,
1928
+ parser_sha TEXT
1929
+ );
1930
+ -- Expression index on TG_LOWER(path) -- see pathEqClause (sql_path.ts) and TG_LOWER's
1931
+ -- registration above. TG_LOWER is registered { deterministic: true }, which is required for
1932
+ -- SQLite to index an expression at all; without it CREATE INDEX on a function call throws
1933
+ -- "non-deterministic functions prohibited in index expressions". Because pathEqClause emits
1934
+ -- this exact 'TG_LOWER(path) = ?' text for every case-insensitive-filesystem query, the planner
1935
+ -- matches it against this index and uses SEARCH instead of a full table SCAN, without requiring
1936
+ -- any writer to populate a separate folded column (verified via EXPLAIN QUERY PLAN in
1937
+ -- db.test.ts / sql_path.test.ts). CREATE INDEX IF NOT EXISTS is purely additive and safe to run
1938
+ -- against an already-populated table on every connection open, unlike an ALTER TABLE column add
1939
+ -- -- no MIGRATIONS entry or SCHEMA_VERSION bump is needed for this index.
1940
+ CREATE INDEX IF NOT EXISTS idx_files_path_folded ON files(TG_LOWER(path));
1941
+
1942
+ CREATE TABLE IF NOT EXISTS symbols (
1943
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
1944
+ file_path TEXT,
1945
+ name TEXT,
1946
+ kind TEXT,
1947
+ line_start INTEGER,
1948
+ line_end INTEGER,
1949
+ body TEXT,
1950
+ docstring TEXT,
1951
+ parent TEXT NOT NULL DEFAULT ''
1952
+ );
1953
+ CREATE INDEX IF NOT EXISTS idx_symbols_name ON symbols(name);
1954
+ CREATE INDEX IF NOT EXISTS idx_symbols_file ON symbols(file_path);
1955
+ CREATE INDEX IF NOT EXISTS idx_symbols_name_kind ON symbols(name, kind);
1956
+ CREATE INDEX IF NOT EXISTS idx_symbols_file_folded ON symbols(TG_LOWER(file_path));
1957
+ CREATE INDEX IF NOT EXISTS idx_symbols_file_name_folded ON symbols(TG_LOWER(file_path), name);
1958
+ -- Partial index backing checkSymbolBodySize (cli_doctor.ts), which every SessionStart hook runs.
1959
+ -- Its predicate cannot be served by any index above, so the check had to read the whole symbols
1960
+ -- table -- 226 MB / 231324 rows here, 229 ms per session start, and the early-exit LIMIT 1 never
1961
+ -- fires on a healthy index because there is nothing to find. Indexing the *violating* rows only
1962
+ -- makes the check a lookup into a b-tree that is empty on a healthy index: measured 229 ms -> 0.0
1963
+ -- ms, 4 KB on disk, and no measurable insert cost (-0.2%, within noise, over 40000 real rows),
1964
+ -- because SQLite evaluates the predicate and skips the b-tree write for every row under the cap.
1965
+ -- SQLite uses a partial index only where the query's WHERE implies the index's, so the probe in
1966
+ -- cli_doctor.ts spells its comparison the same way and against the same constant. That makes the
1967
+ -- threshold part of the stored schema -- see SYMBOL_BODY_CHAR_CAP in constants.ts for what
1968
+ -- changing it requires. A query with a lower threshold correctly gets a full scan instead, so no
1969
+ -- other reader can be served stale rows by this index.
1970
+ CREATE INDEX IF NOT EXISTS idx_symbols_oversized_body ON symbols(id) WHERE LENGTH(body) > ${SYMBOL_BODY_CHAR_CAP};
1971
+
1972
+ CREATE TABLE IF NOT EXISTS refs (
1973
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
1974
+ file_path TEXT,
1975
+ name TEXT,
1976
+ line INTEGER,
1977
+ col INTEGER,
1978
+ context TEXT
1979
+ );
1980
+ CREATE INDEX IF NOT EXISTS idx_refs_name ON refs(name);
1981
+ CREATE INDEX IF NOT EXISTS idx_refs_file ON refs(file_path);
1982
+ CREATE INDEX IF NOT EXISTS idx_refs_file_folded ON refs(TG_LOWER(file_path));
1983
+ CREATE INDEX IF NOT EXISTS idx_refs_file_name_folded ON refs(TG_LOWER(file_path), name);
1984
+
1985
+ CREATE TABLE IF NOT EXISTS chunks (
1986
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
1987
+ file_path TEXT,
1988
+ start_line INTEGER,
1989
+ end_line INTEGER,
1990
+ text TEXT,
1991
+ kind TEXT
1992
+ );
1993
+ CREATE INDEX IF NOT EXISTS idx_chunks_file ON chunks(file_path);
1994
+ CREATE INDEX IF NOT EXISTS idx_chunks_file_folded ON chunks(TG_LOWER(file_path));
1995
+ CREATE INDEX IF NOT EXISTS idx_chunks_file_kind_folded ON chunks(TG_LOWER(file_path), kind);
1996
+
1997
+ -- Tracks every project root a hook has ever seen an edit for, so the worker's periodic sweep
1998
+ -- (sweepKnownRoots in index_prune.ts) knows which roots to auto-prune without scanning the
1999
+ -- entire shared files table for distinct top-level directories on every cycle. Purely additive
2000
+ -- (no SCHEMA_VERSION bump needed): last_seen_ms is refreshed on every observed edit,
2001
+ -- first_missing_ms is set the first sweep that finds the root unreachable and cleared the
2002
+ -- moment it's seen reachable again -- see sweepKnownRoots' grace-period logic.
2003
+ CREATE TABLE IF NOT EXISTS known_roots (
2004
+ root TEXT PRIMARY KEY,
2005
+ last_seen_ms REAL NOT NULL,
2006
+ first_missing_ms REAL
2007
+ );
2008
+
2009
+ -- Resume point for a budget-truncated reconcile sweep (reconcile.ts), one row per project root. A
2010
+ -- project too large to finish a sweep inside DEFAULT_RECONCILE_BUDGET_MS would otherwise scan the
2011
+ -- same deterministic (git ls-files) prefix every session forever, leaving every file after the
2012
+ -- budget cutoff permanently unchecked. last_scanned_path is the last tracked file the sweep
2013
+ -- finished examining before its budget ran out; the next sweep rotates its scan order to resume
2014
+ -- right after that file, wrapping back to the start, so repeated truncated sweeps eventually cover
2015
+ -- the whole project. Cleared (row deleted) the moment a sweep completes a full lap without running
2016
+ -- out of budget. Purely additive (no SCHEMA_VERSION bump needed): a missing row just means "start
2017
+ -- from the beginning", the same as a fresh database.
2018
+ CREATE TABLE IF NOT EXISTS reconcile_cursor (
2019
+ root TEXT PRIMARY KEY,
2020
+ last_scanned_path TEXT NOT NULL,
2021
+ updated_at REAL NOT NULL
2022
+ );
2023
+
2024
+ -- Cross-cache full-text search index for 'token-goat recall' (recall_index.ts). One row
2025
+ -- per bash-output/web-output/mcp-output blob-store entry (see disk_cache.ts), refreshed
2026
+ -- in place (ON CONFLICT DO UPDATE) whenever storeBashOutput/storeWebOutput/storeMcpOutput
2027
+ -- write that entry, so recall never needs a separate rebuild step. row_id is a plain
2028
+ -- surrogate integer key -- entry_id is the real blob-store id (bash/mcp ids are hex,
2029
+ -- web ids are the cache's own scheme) and is not unique on its own since bash-output and
2030
+ -- mcp-output ids share one namespace (BASH_OUTPUT_SUBDIR) while web-output ids are a
2031
+ -- separate namespace; cache_type disambiguates.
2032
+ CREATE TABLE IF NOT EXISTS cache_recall (
2033
+ row_id INTEGER PRIMARY KEY AUTOINCREMENT,
2034
+ cache_type TEXT NOT NULL,
2035
+ entry_id TEXT NOT NULL,
2036
+ label TEXT,
2037
+ content TEXT,
2038
+ stored_at REAL,
2039
+ UNIQUE(cache_type, entry_id)
2040
+ );
2041
+ CREATE INDEX IF NOT EXISTS idx_cache_recall_type ON cache_recall(cache_type);
2042
+
2043
+ -- Per-emission ledger for 'token-goat hint-stats' (hint_stats.ts). One row per hint
2044
+ -- emission event (a hook returning a 'context' HookOutput classified as a discretionary
2045
+ -- efficiency nudge, as opposed to a mandatory informational injection -- see hint_stats.ts's
2046
+ -- doc comment for the exact category list and what is deliberately excluded). correlator is a
2047
+ -- best-effort file-path/output-id substring extracted from the hint's own text, used to check
2048
+ -- whether a later Bash tool call in the same session actually followed the hint's specific
2049
+ -- pointer (see resolvePendingHintsForEvent) -- NULL when no such pointer could be extracted,
2050
+ -- in which case the row is inserted already resolved with acted_on=0 (counted as emitted, never
2051
+ -- eligible for auto-detected credit). calls_remaining is the countdown of subsequent tool-use
2052
+ -- events still eligible to resolve this row before it is considered timed out.
2053
+ CREATE TABLE IF NOT EXISTS hint_emissions (
2054
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
2055
+ category TEXT NOT NULL,
2056
+ session_id TEXT NOT NULL,
2057
+ harness TEXT NOT NULL,
2058
+ correlator TEXT,
2059
+ emitted_at REAL NOT NULL,
2060
+ resolved INTEGER NOT NULL DEFAULT 0,
2061
+ acted_on INTEGER NOT NULL DEFAULT 0,
2062
+ calls_remaining INTEGER NOT NULL DEFAULT 0,
2063
+ bytes_emitted INTEGER
2064
+ );
2065
+ CREATE INDEX IF NOT EXISTS idx_hint_emissions_category ON hint_emissions(category);
2066
+ CREATE INDEX IF NOT EXISTS idx_hint_emissions_session_pending ON hint_emissions(session_id, resolved);
2067
+
2068
+ -- Manual efficacy votes for a hint category (token-goat hint-stats --mark-effective/--mark-ineffective),
2069
+ -- kept separate from hint_emissions' automatic acted_on signal so the two are never silently
2070
+ -- blended -- see hint_stats.ts's doc comment on why some categories only support this manual signal.
2071
+ CREATE TABLE IF NOT EXISTS hint_manual_marks (
2072
+ category TEXT PRIMARY KEY,
2073
+ effective_count INTEGER NOT NULL DEFAULT 0,
2074
+ ineffective_count INTEGER NOT NULL DEFAULT 0
2075
+ );
2076
+
2077
+ -- Durable counter backing hint_stats.ts's backoff-threshold probe-recovery schedule: how many
2078
+ -- CONSECUTIVE suppressed occasions have elapsed for (category, harness) since a hint in this
2079
+ -- category was last actually shown (either organically, because shouldSuppress no longer holds,
2080
+ -- or via a prior probe). shouldSuppress itself stays a pure function of hint_emissions -- this
2081
+ -- table exists only because a suppressed occasion is deliberately never written to
2082
+ -- hint_emissions (see that table's own comment), so without a separate durable counter here
2083
+ -- there would be no way to know "how many suppressed occasions have we seen" across the
2084
+ -- short-lived hook CLI processes that call applyHintTracking. Keyed by (category, harness), not
2085
+ -- category alone, to match shouldSuppress/categoryStats' own per-harness scoping -- unlike
2086
+ -- hint_manual_marks (a human-entered vote, deliberately not harness-split).
2087
+ CREATE TABLE IF NOT EXISTS hint_suppression_probes (
2088
+ category TEXT NOT NULL,
2089
+ harness TEXT NOT NULL,
2090
+ streak INTEGER NOT NULL DEFAULT 0,
2091
+ PRIMARY KEY (category, harness)
2092
+ );
2093
+
2094
+ -- Free-text architecture/rationale notes (the "why" layer -- see notes.ts), attached either to
2095
+ -- a whole file (symbol = '') or to one specific indexed symbol within it (symbol = that
2096
+ -- symbol's name). '' rather than NULL for the whole-file case because SQLite's UNIQUE treats
2097
+ -- NULLs as pairwise-distinct (never conflicting with each other), which would let note-add
2098
+ -- accumulate unlimited duplicate whole-file notes for the same file instead of upserting one;
2099
+ -- '' is a real, comparable value so UNIQUE(file_path, symbol) enforces "at most one note per
2100
+ -- attachment point" for both cases identically. 'fingerprint' is a SHA-256 digest (see
2101
+ -- fingerprintContent in fingerprint.ts) captured at write time of exactly what the note
2102
+ -- describes -- the resolved symbol's current body text for a symbol-scoped note, or a stable
2103
+ -- digest of the file's current top-level symbol manifest (name:kind:line-range per symbol,
2104
+ -- sorted) for a file-scoped note -- so 'token-goat note-list --stale-only' can recompute the
2105
+ -- same fingerprint against the live index later and flag a mismatch (see notes.ts's
2106
+ -- isNoteStale). Staleness detection is purely advisory: nothing here ever auto-rewrites or
2107
+ -- deletes a note's content, only flags that the code it describes has moved since it was
2108
+ -- written -- a human/agent re-review decides what to do with a stale note.
2109
+ CREATE TABLE IF NOT EXISTS notes (
2110
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
2111
+ file_path TEXT NOT NULL,
2112
+ symbol TEXT NOT NULL DEFAULT '',
2113
+ content TEXT NOT NULL,
2114
+ fingerprint TEXT NOT NULL,
2115
+ created_at REAL NOT NULL,
2116
+ updated_at REAL NOT NULL,
2117
+ UNIQUE(file_path, symbol)
2118
+ );
2119
+ CREATE INDEX IF NOT EXISTS idx_notes_file_folded ON notes(TG_LOWER(file_path));
2120
+
2121
+ -- Baseline for skill_version_drift.ts's one-shot nudge: the token-goat CLI version (and its
2122
+ -- flat command-name set, JSON-encoded) active the moment the token-goat skill's body was
2123
+ -- last (re)loaded into this session -- see hooks_skill.ts's postSkillHandler. A session that
2124
+ -- keeps running after the CLI is upgraded has no other way to learn that new surgical-read
2125
+ -- commands now exist (the skill only re-announces itself on an explicit reload), so
2126
+ -- checkSkillVersionDrift compares this snapshot against the live command set on each user turn
2127
+ -- and fires the nudge exactly once (notified_at) per (re)load. session_id is the primary key
2128
+ -- because only one skill (token-goat) is ever tracked here.
2129
+ CREATE TABLE IF NOT EXISTS skill_version_snapshots (
2130
+ session_id TEXT PRIMARY KEY,
2131
+ skill_name TEXT NOT NULL,
2132
+ loaded_version TEXT NOT NULL,
2133
+ loaded_commands_json TEXT NOT NULL,
2134
+ notified_at REAL
2135
+ );
2136
+
2137
+ -- Which embedding stack produced the vectors currently in chunk_vectors -- the model, its
2138
+ -- pinned revision, and the inference runtime (see embeddingProvenance in embeddings.ts). The
2139
+ -- vector table itself is vec0(rowid, embedding) and has nowhere to record this, so without
2140
+ -- this row a database that was embedded by one stack and then added to by another holds two
2141
+ -- incomparable sets of vectors under one index, with nothing able to tell them apart. That is
2142
+ -- not hypothetical: global.db is machine-wide across every project on the machine (see
2143
+ -- constants.ts), so upgrading the runtime, or changing the model or its pinned revision, mixes
2144
+ -- old and new vectors for as long as the old files go untouched. Measured drift between two
2145
+ -- runtime versions of the same quantized model is 0.9925-0.9978 cosine on the final vector --
2146
+ -- small, but enough to reorder near-ties, and invisible to every existing check.
2147
+ --
2148
+ -- Single-row by construction (the CHECK pins the key), because there is exactly one vector
2149
+ -- table per database. An EMPTY table on a database that already holds chunks means the vectors
2150
+ -- predate this stamp and their provenance is unknowable -- see ensureEmbeddingProvenance, which
2151
+ -- treats that exactly like a mismatch. That is what makes this work without a migration step.
2152
+ CREATE TABLE IF NOT EXISTS embedding_provenance (
2153
+ id INTEGER PRIMARY KEY CHECK (id = 1),
2154
+ provenance TEXT NOT NULL
2155
+ );
2156
+ `;
2157
+ var FTS_TOKENIZER = "unicode61 remove_diacritics 2";
2158
+ var FTS_SQL = `
2159
+ CREATE VIRTUAL TABLE IF NOT EXISTS symbols_fts USING fts5(
2160
+ name,
2161
+ body,
2162
+ docstring,
2163
+ content='symbols',
2164
+ content_rowid='id',
2165
+ tokenize='${FTS_TOKENIZER}'
2166
+ );
2167
+ CREATE TRIGGER IF NOT EXISTS symbols_ai AFTER INSERT ON symbols BEGIN
2168
+ INSERT INTO symbols_fts(rowid, name, body, docstring)
2169
+ VALUES (new.id, new.name, new.body, new.docstring);
2170
+ END;
2171
+ CREATE TRIGGER IF NOT EXISTS symbols_ad AFTER DELETE ON symbols BEGIN
2172
+ INSERT INTO symbols_fts(symbols_fts, rowid, name, body, docstring)
2173
+ VALUES ('delete', old.id, old.name, old.body, old.docstring);
2174
+ END;
2175
+ CREATE TRIGGER IF NOT EXISTS symbols_au AFTER UPDATE ON symbols BEGIN
2176
+ INSERT INTO symbols_fts(symbols_fts, rowid, name, body, docstring)
2177
+ VALUES ('delete', old.id, old.name, old.body, old.docstring);
2178
+ INSERT INTO symbols_fts(rowid, name, body, docstring)
2179
+ VALUES (new.id, new.name, new.body, new.docstring);
2180
+ END;
2181
+
2182
+ -- Content-linked FTS5 mirror of cache_recall (recall_index.ts), same shape as symbols_fts
2183
+ -- above. An INSERT ... ON CONFLICT DO UPDATE against cache_recall fires the AFTER UPDATE
2184
+ -- trigger (not AFTER INSERT) on the conflicting row, same as any other SQLite upsert, so the
2185
+ -- delete+reinsert pattern below keeps the fts index correct on a re-indexed (overwritten)
2186
+ -- entry, not just a brand-new one.
2187
+ CREATE VIRTUAL TABLE IF NOT EXISTS cache_recall_fts USING fts5(
2188
+ label,
2189
+ content,
2190
+ content='cache_recall',
2191
+ content_rowid='row_id',
2192
+ tokenize='${FTS_TOKENIZER}'
2193
+ );
2194
+ CREATE TRIGGER IF NOT EXISTS cache_recall_ai AFTER INSERT ON cache_recall BEGIN
2195
+ INSERT INTO cache_recall_fts(rowid, label, content)
2196
+ VALUES (new.row_id, new.label, new.content);
2197
+ END;
2198
+ CREATE TRIGGER IF NOT EXISTS cache_recall_ad AFTER DELETE ON cache_recall BEGIN
2199
+ INSERT INTO cache_recall_fts(cache_recall_fts, rowid, label, content)
2200
+ VALUES ('delete', old.row_id, old.label, old.content);
2201
+ END;
2202
+ CREATE TRIGGER IF NOT EXISTS cache_recall_au AFTER UPDATE ON cache_recall BEGIN
2203
+ INSERT INTO cache_recall_fts(cache_recall_fts, rowid, label, content)
2204
+ VALUES ('delete', old.row_id, old.label, old.content);
2205
+ INSERT INTO cache_recall_fts(rowid, label, content)
2206
+ VALUES (new.row_id, new.label, new.content);
2207
+ END;
2208
+ `;
2209
+ var SCHEMA_VERSION = 14;
2210
+ function alterTableIdempotent(conn, sql) {
2211
+ try {
2212
+ conn.exec(sql);
2213
+ } catch (err) {
2214
+ if (!(err instanceof Error) || !/duplicate column/i.test(err.message)) throw err;
2215
+ }
2216
+ }
2217
+ function purgeDotenvEmbeddings(conn) {
2218
+ let paths;
2219
+ try {
2220
+ paths = conn.prepare("SELECT DISTINCT file_path FROM chunks").all().map((r) => r.file_path).filter(isDotenvPath);
2221
+ } catch {
2222
+ return;
2223
+ }
2224
+ if (paths.length === 0) return;
2225
+ for (const p of paths) {
2226
+ try {
2227
+ conn.prepare("DELETE FROM chunk_vectors WHERE rowid IN (SELECT id FROM chunks WHERE file_path = ?)").run(p);
2228
+ } catch {
2229
+ }
2230
+ conn.prepare("DELETE FROM chunks WHERE file_path = ?").run(p);
2231
+ try {
2232
+ conn.prepare("UPDATE files SET embed_sha = NULL WHERE path = ?").run(p);
2233
+ } catch {
2234
+ }
2235
+ }
2236
+ }
2237
+ function rebuildFtsAtCurrentTokenizer(conn) {
2238
+ try {
2239
+ const declarations = conn.prepare("SELECT sql FROM sqlite_master WHERE name IN ('symbols_fts','cache_recall_fts')").all();
2240
+ const current = declarations.map((d) => /tokenize\s*=\s*'([^']*)'/.exec(d.sql ?? "")?.[1] ?? "");
2241
+ if (current.length === 2 && current.every((t) => t === FTS_TOKENIZER)) return;
2242
+ conn.transaction(() => {
2243
+ conn.exec("DROP TABLE IF EXISTS symbols_fts; DROP TABLE IF EXISTS cache_recall_fts;");
2244
+ conn.exec(FTS_SQL);
2245
+ conn.exec("INSERT INTO symbols_fts(symbols_fts) VALUES('rebuild');");
2246
+ conn.exec("INSERT INTO cache_recall_fts(cache_recall_fts) VALUES('rebuild');");
2247
+ }).immediate();
2248
+ } catch {
2249
+ }
2250
+ }
2251
+ var MIGRATIONS = {
2252
+ // v1 -> v2: adds files.embed_sha, tracked separately from files.sha so embedding freshness can be gated independently of parse freshness (see makeIndexer in worker.ts). A pre-existing v1 database's `files` table predates the column, so it needs an explicit ALTER TABLE here; a brand-new database already has the column from SCHEMA_SQL's CREATE TABLE above, so the ALTER TABLE would fail with "duplicate column name" there -- swallow exactly that error and rethrow anything else, so a genuine ALTER TABLE failure is never silently lost.
2253
+ 1: (conn) => alterTableIdempotent(conn, "ALTER TABLE files ADD COLUMN embed_sha TEXT"),
2254
+ // v2 -> v3: adds files.retry_count, a durable per-path counter for consecutive transient-read-failure requeues (see MAX_TRANSIENT_RETRIES / requeueDirtyPath / clearRetryCount in worker.ts). Previously this counter lived only in an in-memory Map inside worker.ts, which meant the retry-count reset -- run at the time in the short-lived hook CLI process -- could never actually reach the long-lived detached daemon process's own copy of that Map: they are different Node processes with no shared memory, so the reset was a silent no-op in the real deployed topology. Persisting the counter in `files` makes it visible to both processes via the one thing they do share: the index DB. Same swallow-duplicate-column pattern as v1 -> v2 above.
2255
+ 2: (conn) => alterTableIdempotent(conn, "ALTER TABLE files ADD COLUMN retry_count INTEGER NOT NULL DEFAULT 0"),
2256
+ // v8 -> v9: adds symbols.parent (see SCHEMA_VERSION comment above for why). A pre-existing v8 database's `symbols` table predates the column, so it needs an explicit ALTER TABLE here; a brand-new database already has the column from SCHEMA_SQL's CREATE TABLE above, so the ALTER TABLE would fail with "duplicate column name" there -- swallow exactly that error and rethrow anything else, same pattern as v1 -> v2 / v2 -> v3 above.
2257
+ 8: (conn) => alterTableIdempotent(conn, "ALTER TABLE symbols ADD COLUMN parent TEXT NOT NULL DEFAULT ''"),
2258
+ // v9 -> v10: adds hint_emissions.bytes_emitted (see SCHEMA_VERSION comment above for why). A pre-existing v9 database's `hint_emissions` table predates the column, so it needs an explicit ALTER TABLE here; a brand-new database already has the column from SCHEMA_SQL's CREATE TABLE above, so the ALTER TABLE would fail with "duplicate column name" there -- swallow exactly that error and rethrow anything else, same pattern as v1 -> v2 / v2 -> v3 / v8 -> v9 above.
2259
+ 9: (conn) => alterTableIdempotent(conn, "ALTER TABLE hint_emissions ADD COLUMN bytes_emitted INTEGER"),
2260
+ // v10 -> v11: purge chunks (and their vectors) for dotenv files. Until this version, a tracked `.env` was chunked and embedded verbatim on the git path, so `semantic` returned its values -- see dotenv_redact.ts. Redacting from now on is not enough on its own: the embed-freshness gate (isEmbedFresh in parser.ts) skips a file whose bytes have not changed, so an already-indexed .env would have kept serving its pre-fix chunks indefinitely. Deleting the rows here both removes the stored secrets and, by clearing embed_sha, makes the next drain re-embed the file through the redacting path.
2261
+ 10: purgeDotenvEmbeddings,
2262
+ // v12 -> v13: adds files.parser_sha, the digest of the extraction logic that produced this file's rows, tracked separately from files.sha for the same reason embed_sha is -- content freshness and parse freshness are different questions, and the content sha alone could only ever answer the first. A pre-existing v12 database's `files` table predates the column, so it needs an explicit ALTER TABLE here; a brand-new database already has it from SCHEMA_SQL's CREATE TABLE above, so the ALTER TABLE would fail with "duplicate column name" there -- swallow exactly that error and rethrow anything else, same pattern as v1 -> v2 / v2 -> v3 / v8 -> v9 / v9 -> v10 above. Deliberately left NULL for every existing row rather than backfilled with the current fingerprint: NULL is the truthful answer (nobody recorded which parser wrote those rows), and it is also the answer that makes the freshness gates reparse them once, which is exactly what a database indexed by an older parser needs. v13 -> v14: changes both FTS5 tables' tokenizer to `unicode61 remove_diacritics 2`, so a search for `Noi` or `Viet` finds `Hà Nội` and `Việt Nam` -- combining marks that `remove_diacritics 1`, FTS5's default, leaves in place. This is the first schema change that `CREATE VIRTUAL TABLE IF NOT EXISTS` cannot express at all rather than merely cannot express on a populated table: against an existing virtual table that statement is a silent no-op, so without MIGRATIONS[13] the new tokenizer would reach only databases created after this release. The step drops both tables, re-runs FTS_SQL to re-create them at the current declaration, and rebuilds each from its content table.
2263
+ 12: (conn) => alterTableIdempotent(conn, "ALTER TABLE files ADD COLUMN parser_sha TEXT"),
2264
+ // v13 -> v14: re-creates both FTS5 tables at the tokenizer FTS_SQL currently declares (see the SCHEMA_VERSION comment above for why no `IF NOT EXISTS` form can do this).
2265
+ 13: rebuildFtsAtCurrentTokenizer
2266
+ };
2267
+ function runMigrations(conn, fromVersion, toVersion) {
2268
+ for (let v = fromVersion; v < toVersion; v++) {
2269
+ MIGRATIONS[v]?.(conn);
2270
+ }
2271
+ }
2272
+ var WAL_SWITCH_DEADLINE_MS = 15e3;
2273
+ function enableWalWithRetry(conn, budgetMs = WAL_SWITCH_DEADLINE_MS) {
2274
+ const deadline = Date.now() + budgetMs;
2275
+ let lastError;
2276
+ for (; ; ) {
2277
+ try {
2278
+ const mode = conn.pragma("journal_mode = WAL", { simple: true });
2279
+ if (String(mode).toLowerCase() === "wal") return;
2280
+ lastError = new Error(`got: ${String(mode)}`);
2281
+ } catch (e) {
2282
+ lastError = e;
2283
+ }
2284
+ try {
2285
+ if (String(conn.pragma("journal_mode", { simple: true })).toLowerCase() === "wal") return;
2286
+ } catch {
2287
+ }
2288
+ if (Date.now() >= deadline) {
2289
+ throw new Error(`db: failed to enable WAL mode (${lastError instanceof Error ? lastError.message : String(lastError)})`);
2290
+ }
2291
+ sleepSync(25);
2292
+ }
2293
+ }
2294
+ function initConnection(conn) {
2295
+ conn.pragma("busy_timeout = 15000");
2296
+ enableWalWithRetry(conn);
2297
+ conn.pragma("synchronous = NORMAL");
2298
+ conn.pragma("cache_size = -32000");
2299
+ conn.pragma("temp_store = MEMORY");
2300
+ conn.pragma("mmap_size = 134217728");
2301
+ conn.function(
2302
+ "TG_LOWER",
2303
+ { deterministic: true },
2304
+ (value) => value === null ? null : foldCase(String(value))
2305
+ );
2306
+ const storedVersion = Number(conn.pragma("user_version", { simple: true }));
2307
+ if (storedVersion > SCHEMA_VERSION) {
2308
+ throw new Error(
2309
+ `db: index schema version ${storedVersion} is newer than this token-goat build supports (expected ${SCHEMA_VERSION}). Update token-goat, or delete the stale index database and let it rebuild.`
2310
+ );
2311
+ }
2312
+ conn.exec(SCHEMA_SQL);
2313
+ try {
2314
+ conn.exec(FTS_SQL);
2315
+ } catch {
2316
+ }
2317
+ try {
2318
+ const sqliteVec = _require("sqlite-vec");
2319
+ sqliteVec.load(conn);
2320
+ conn.exec(
2321
+ `CREATE VIRTUAL TABLE IF NOT EXISTS chunk_vectors USING vec0(
2322
+ embedding float[384]
2323
+ );`
2324
+ );
2325
+ } catch {
2326
+ }
2327
+ if (storedVersion < SCHEMA_VERSION) {
2328
+ runMigrations(conn, storedVersion, SCHEMA_VERSION);
2329
+ conn.pragma(`user_version = ${SCHEMA_VERSION}`);
2330
+ }
2331
+ }
2332
+ function resolveDbPath(dbPath) {
2333
+ if (path2.isAbsolute(dbPath)) return dbPath;
2334
+ if (dbPath.includes("/") || dbPath.includes("\\")) return path2.resolve(dbPath);
2335
+ return safeJoin(dataDir(), dbPath);
2336
+ }
2337
+ function connectionKey(dbPath) {
2338
+ const resolved = resolveDbPath(dbPath);
2339
+ return { resolved, key: foldPath(resolved) };
2340
+ }
2341
+ function getDb(dbPath) {
2342
+ const { resolved, key } = connectionKey(dbPath);
2343
+ const existing = _connections.get(key);
2344
+ if (existing !== void 0) return existing;
2345
+ const dir = path2.dirname(resolved);
2346
+ try {
2347
+ ensureDirSync(dir);
2348
+ } catch (e) {
2349
+ if (e.code !== "EEXIST" || !fs2.existsSync(dir)) throw e;
2350
+ }
2351
+ const conn = new Database(resolved);
2352
+ try {
2353
+ initConnection(conn);
2354
+ } catch (e) {
2355
+ try {
2356
+ conn.close();
2357
+ } catch {
2358
+ }
2359
+ throw e;
2360
+ }
2361
+ _connections.set(key, conn);
2362
+ return conn;
2363
+ }
2364
+ function closeAllDbs() {
2365
+ for (const conn of _connections.values()) {
2366
+ try {
2367
+ conn.close();
2368
+ } catch {
2369
+ }
2370
+ }
2371
+ _connections.clear();
2372
+ }
2373
+ registerReset(closeAllDbs);
2374
+
2375
+ export {
2376
+ precedingDocComment,
2377
+ buildLineIndex,
2378
+ offsetToLine,
2379
+ stripCstyleComments,
2380
+ stripXmlComments,
2381
+ scanQuotedStringEnd,
2382
+ stripJsComments,
2383
+ stripHashComments,
2384
+ maskHtmlNoise,
2385
+ findHtmlHeadingMatches,
2386
+ isInsideStringLiteral,
2387
+ stripBlockCommentSpan,
2388
+ stripNestedBlockCommentSpan,
2389
+ stripLineComment,
2390
+ stripStringLiterals,
2391
+ matchRRawOpener,
2392
+ matchGroovySlashy,
2393
+ matchBashHeredocOpener,
2394
+ stripMultilineStringSpan,
2395
+ makeSpanSymbol,
2396
+ makeLineSymbol,
2397
+ makeSymbolEmitter,
2398
+ assignFlatEndLines,
2399
+ propagateEndLinesToSymbols,
2400
+ findMatchingBraceEndLine,
2401
+ assignBraceBlockSpans,
2402
+ lastContentLine,
2403
+ GENERIC_CLAUSE,
2404
+ extractIni,
2405
+ _lineClosesQuote,
2406
+ _detectOpenQuote,
2407
+ extractEnv,
2408
+ isAblSource,
2409
+ isMatlabSource,
2410
+ isObjcSource,
2411
+ isObjcHeader,
2412
+ isPascalSource,
2413
+ isPerlSource,
2414
+ isPrologSource,
2415
+ FILENAME_LANGUAGE,
2416
+ TREE_SITTER_LANGUAGES,
2417
+ languageHasFlag,
2418
+ languageLabel,
2419
+ fenceFor,
2420
+ basenameImportsExtension,
2421
+ partialRefsReason,
2422
+ refineLanguageByContent,
2423
+ detectLanguageOfFile,
2424
+ detectLanguage,
2425
+ nonTreeSitterLanguageCount,
2426
+ unsupportedLanguageName,
2427
+ isDotenvPath,
2428
+ redactIfDotenv,
2429
+ getDb
2430
+ };