@bendyline/docblocks 2.6.1 → 2.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/THIRD_PARTY_NOTICES.txt +33 -33
  2. package/dist/capabilities-CdQAc745.d.ts +1233 -0
  3. package/dist/catalog-BQK05tne.d.ts +122 -0
  4. package/dist/chunk-245W57QP.js +31 -0
  5. package/dist/{chunk-2WDJ27EH.js → chunk-46AGYHDY.js} +8 -0
  6. package/dist/{chunk-4437YWLE.js → chunk-6UPO2GE6.js} +4 -2
  7. package/dist/{chunk-UMWCTEXW.js → chunk-7SN7YQL4.js} +211 -40
  8. package/dist/{chunk-OQCPBOQA.js → chunk-DAPFSEKY.js} +35 -0
  9. package/dist/{chunk-M62ZGJ5I.js → chunk-HABG2JM3.js} +0 -17
  10. package/dist/{chunk-YM7O3PWO.js → chunk-HKOOHM67.js} +20 -12
  11. package/dist/chunk-IDYE73TU.js +77 -0
  12. package/dist/{chunk-KLQKU74Q.js → chunk-IGB2VSRE.js} +124 -330
  13. package/dist/{chunk-BVY7GGNV.js → chunk-L6PRMR7T.js} +137 -95
  14. package/dist/chunk-LPIJ6HKL.js +19 -0
  15. package/dist/{chunk-SJBB5OD7.js → chunk-QS5WTLUZ.js} +10 -8
  16. package/dist/chunk-R2JX5N3F.js +382 -0
  17. package/dist/{chunk-ZO7G53QP.js → chunk-UDEWLHQI.js} +4 -2
  18. package/dist/chunk-UK7R3RC7.js +514 -0
  19. package/dist/chunk-XTNWCNSK.js +464 -0
  20. package/dist/chunk-YHSFRPBS.js +42 -0
  21. package/dist/chunk-YNUQG4GF.js +1173 -0
  22. package/dist/document/index.d.ts +31 -1
  23. package/dist/document/index.js +7 -6
  24. package/dist/filesystem/electron.d.ts +8 -34
  25. package/dist/filesystem/electron.js +7 -4
  26. package/dist/filesystem/host.d.ts +28 -0
  27. package/dist/filesystem/host.js +103 -0
  28. package/dist/filesystem/index.d.ts +52 -6
  29. package/dist/filesystem/index.js +36 -15
  30. package/dist/filesystem/indexeddb.d.ts +8 -1
  31. package/dist/filesystem/indexeddb.js +7 -6
  32. package/dist/filesystem/memory.d.ts +2 -1
  33. package/dist/filesystem/memory.js +3 -2
  34. package/dist/filesystem/native.d.ts +2 -1
  35. package/dist/filesystem/native.js +3 -2
  36. package/dist/filesystem-v2-C8AdSXUN.d.ts +74 -0
  37. package/dist/host/index.d.ts +127 -5
  38. package/dist/host/index.js +100 -4
  39. package/dist/host/mobile.d.ts +389 -0
  40. package/dist/host/mobile.js +315 -0
  41. package/dist/host-provider-v2-CyLTRKJF.d.ts +51 -0
  42. package/dist/index.d.ts +8 -5
  43. package/dist/index.js +153 -34
  44. package/dist/mcp/index.d.ts +2 -2
  45. package/dist/mcp/zod.d.ts +23 -1
  46. package/dist/mcp/zod.js +1 -0
  47. package/dist/settings-HrrCikVX.d.ts +170 -0
  48. package/dist/types-BvDnr3YU.d.ts +88 -0
  49. package/dist/{types-Oy9U5FST.d.ts → types-d0yvHUJz.d.ts} +6 -0
  50. package/dist/{types-Bb3yccTH.d.ts → v2-BVnZZyd4.d.ts} +1 -86
  51. package/dist/vscode/index.d.ts +43 -1
  52. package/dist/vscode/index.js +67 -1
  53. package/dist/workspace/index.d.ts +10 -6
  54. package/dist/workspace/index.js +4 -2
  55. package/dist/workspace-index/index.d.ts +240 -0
  56. package/dist/workspace-index/index.js +1287 -0
  57. package/dist/workspace-settings/index.d.ts +301 -0
  58. package/dist/workspace-settings/index.js +820 -0
  59. package/package.json +22 -2
  60. package/dist/chunk-6XHOOBYF.js +0 -36
  61. package/dist/chunk-QI6YKTQV.js +0 -110
  62. package/dist/types-DBYraXxE.d.ts +0 -557
@@ -0,0 +1,1287 @@
1
+ import {
2
+ catalogEntryFromDocument,
3
+ chooseOutsideInMarkdownPath,
4
+ normalizeLineEndings,
5
+ resolveOutsideInLayout
6
+ } from "../chunk-R2JX5N3F.js";
7
+ import {
8
+ isCompanionDirectoryName,
9
+ isDiscoverableWorkspaceEntry,
10
+ isMarkdownFileName
11
+ } from "../chunk-YHSFRPBS.js";
12
+ import {
13
+ decodeUtf8Text
14
+ } from "../chunk-M5ZNQGR5.js";
15
+ import {
16
+ FsError,
17
+ WORKSPACE_ROOT,
18
+ parseWorkspacePath,
19
+ tryParseWorkspacePath,
20
+ workspacePathContains
21
+ } from "../chunk-AYWKIZLD.js";
22
+
23
+ // src/workspace-index/search-text.ts
24
+ var LineBuilder = class {
25
+ constructor() {
26
+ this.lines = [];
27
+ this.sourceLines = [];
28
+ this.kinds = [];
29
+ this.current = null;
30
+ this.currentLine = 0;
31
+ this.kind = "text";
32
+ }
33
+ /** Start a new block: whatever follows begins on a fresh line. */
34
+ block(kind) {
35
+ this.flush();
36
+ this.kind = kind;
37
+ }
38
+ /** Append text that starts on `line`, opening a new line when it advances. */
39
+ append(line, text) {
40
+ if (this.current === null || line > this.currentLine) {
41
+ this.flush();
42
+ this.current = "";
43
+ this.currentLine = line;
44
+ }
45
+ this.current += text;
46
+ }
47
+ /** Text whose embedded newlines are source line breaks (soft breaks, code). */
48
+ appendLines(firstLine, value) {
49
+ const parts = value.split("\n");
50
+ for (let index = 0; index < parts.length; index += 1) {
51
+ this.append(firstLine + index, parts[index]);
52
+ }
53
+ }
54
+ flush() {
55
+ if (this.current !== null && this.current.trim() !== "") {
56
+ this.lines.push(this.current.normalize("NFC"));
57
+ this.sourceLines.push(this.currentLine);
58
+ this.kinds.push(this.kind);
59
+ }
60
+ this.current = null;
61
+ }
62
+ build() {
63
+ this.flush();
64
+ let characters = 0;
65
+ for (const line of this.lines) characters += line.length;
66
+ return {
67
+ lines: this.lines,
68
+ sourceLines: this.sourceLines,
69
+ kinds: this.kinds,
70
+ characters
71
+ };
72
+ }
73
+ };
74
+ var UNSEARCHABLE_INLINE = /* @__PURE__ */ new Set([
75
+ "image",
76
+ "imageReference",
77
+ "inlineIcon",
78
+ "htmlInline",
79
+ "mention",
80
+ "footnoteReference"
81
+ ]);
82
+ function startLine(node) {
83
+ return node.position?.start.line;
84
+ }
85
+ function emitInline(nodes, out, fallback) {
86
+ for (const node of nodes) {
87
+ if (UNSEARCHABLE_INLINE.has(node.type)) continue;
88
+ const line = startLine(node) ?? fallback;
89
+ if ("children" in node && Array.isArray(node.children)) {
90
+ emitInline(node.children, out, line);
91
+ } else if ("value" in node && typeof node.value === "string") {
92
+ out.appendLines(line, node.value);
93
+ }
94
+ }
95
+ }
96
+ function emitTableRow(row, out, fallback) {
97
+ const line = startLine(row) ?? fallback;
98
+ row.children.forEach((cell, index) => {
99
+ if (index > 0) out.append(line, " | ");
100
+ emitInline(cell.children, out, startLine(cell) ?? line);
101
+ });
102
+ }
103
+ function contentStartLine(line, sourceLines, fences) {
104
+ return fences.test(sourceLines[line - 1] ?? "") ? line + 1 : line;
105
+ }
106
+ var CODE_FENCE = /^\s{0,3}(?:```|~~~)/u;
107
+ var MATH_FENCE = /^\s*\$\$/u;
108
+ function emitBlocks(blocks, out, sourceLines, fallback) {
109
+ for (const block of blocks) {
110
+ const line = startLine(block) ?? fallback;
111
+ switch (block.type) {
112
+ case "heading":
113
+ out.block("heading");
114
+ emitInline(block.children, out, line);
115
+ break;
116
+ case "paragraph":
117
+ out.block("text");
118
+ emitInline(block.children, out, line);
119
+ break;
120
+ case "leafDirective":
121
+ out.block("text");
122
+ emitInline(block.children, out, line);
123
+ break;
124
+ case "code":
125
+ out.block("code");
126
+ out.appendLines(contentStartLine(line, sourceLines, CODE_FENCE), block.value);
127
+ break;
128
+ case "math":
129
+ out.block("code");
130
+ out.appendLines(contentStartLine(line, sourceLines, MATH_FENCE), block.value);
131
+ break;
132
+ case "table":
133
+ for (const row of block.children) {
134
+ out.block("text");
135
+ emitTableRow(row, out, line);
136
+ }
137
+ break;
138
+ case "htmlBlock":
139
+ out.block("text");
140
+ out.appendLines(line, block.rawHtml.replace(/<[^>]*>/gu, " "));
141
+ break;
142
+ case "definitionList":
143
+ for (const item of block.children) {
144
+ if (item.type === "definitionTerm") {
145
+ out.block("text");
146
+ emitInline(item.children, out, startLine(item) ?? line);
147
+ } else {
148
+ emitBlocks(item.children, out, sourceLines, startLine(item) ?? line);
149
+ }
150
+ }
151
+ break;
152
+ case "blockquote":
153
+ case "list":
154
+ case "listItem":
155
+ case "footnoteDefinition":
156
+ case "containerDirective":
157
+ emitBlocks(block.children, out, sourceLines, line);
158
+ break;
159
+ case "thematicBreak":
160
+ case "definition":
161
+ break;
162
+ }
163
+ }
164
+ }
165
+ function emitFrontmatter(sourceLines, out) {
166
+ if (sourceLines[0]?.trim() !== "---") return;
167
+ for (let index = 1; index < sourceLines.length; index += 1) {
168
+ const text = sourceLines[index];
169
+ if (text.trim() === "---" || text.trim() === "...") return;
170
+ out.block("frontmatter");
171
+ out.append(index + 1, text);
172
+ }
173
+ }
174
+ function extractSearchText(document, source) {
175
+ const sourceLines = source.split("\n");
176
+ const out = new LineBuilder();
177
+ emitFrontmatter(sourceLines, out);
178
+ emitBlocks(document.children, out, sourceLines, 1);
179
+ return out.build();
180
+ }
181
+ function lineSearchText(source) {
182
+ const out = new LineBuilder();
183
+ source.split("\n").forEach((text, index) => {
184
+ out.block("text");
185
+ out.append(index + 1, text);
186
+ });
187
+ return out.build();
188
+ }
189
+
190
+ // src/workspace-index/analyze.ts
191
+ var markdownModule = null;
192
+ function loadMarkdown() {
193
+ markdownModule ?? (markdownModule = import("@bendyline/squisq/markdown"));
194
+ return markdownModule;
195
+ }
196
+ function stemOf(path) {
197
+ const name = path.slice(path.lastIndexOf("/") + 1);
198
+ const dot = name.lastIndexOf(".");
199
+ return dot > 0 ? name.slice(0, dot) : name;
200
+ }
201
+ function nameOnlyDocument(identity, issue) {
202
+ return {
203
+ record: {
204
+ path: identity.path,
205
+ sourcePath: identity.sourcePath,
206
+ format: identity.format,
207
+ lastModified: identity.lastModified,
208
+ title: stemOf(identity.path),
209
+ outline: [],
210
+ wordCount: 0,
211
+ content: "name-only",
212
+ issue
213
+ },
214
+ text: null
215
+ };
216
+ }
217
+ var ATX_HEADING = /^\s{0,3}#{1,6}\s+(.+?)\s*#*\s*$/u;
218
+ function linesDocument(identity, source, issue) {
219
+ const text = lineSearchText(source);
220
+ const heading = text.lines.map((line) => ATX_HEADING.exec(line)?.[1]).find(Boolean);
221
+ return {
222
+ record: {
223
+ path: identity.path,
224
+ sourcePath: identity.sourcePath,
225
+ format: identity.format,
226
+ lastModified: identity.lastModified,
227
+ title: heading ?? stemOf(identity.path),
228
+ outline: [],
229
+ wordCount: 0,
230
+ content: "lines",
231
+ issue
232
+ },
233
+ text
234
+ };
235
+ }
236
+ async function analyzeDocumentBytes(identity, data, options) {
237
+ let decoded;
238
+ try {
239
+ decoded = decodeUtf8Text(data, { path: identity.sourcePath ?? identity.path });
240
+ } catch (error) {
241
+ if (error instanceof FsError) return nameOnlyDocument(identity, "not-utf8");
242
+ throw error;
243
+ }
244
+ const source = normalizeLineEndings(decoded);
245
+ if (data.byteLength > options.maxParseBytes) {
246
+ return linesDocument(identity, source, "parse-limit");
247
+ }
248
+ const { MarkdownLimitError, parseMarkdown } = await loadMarkdown();
249
+ let document;
250
+ try {
251
+ document = parseMarkdown(source, {
252
+ limits: { maxSourceBytes: options.maxParseBytes },
253
+ signal: options.signal
254
+ });
255
+ } catch (error) {
256
+ if (error instanceof MarkdownLimitError) return linesDocument(identity, source, "parse-limit");
257
+ throw error;
258
+ }
259
+ const entry = await catalogEntryFromDocument(
260
+ {
261
+ path: identity.path,
262
+ ...identity.sourcePath && identity.sourcePath !== identity.path ? { source: identity.sourcePath } : {},
263
+ format: identity.format
264
+ },
265
+ document
266
+ );
267
+ return {
268
+ record: {
269
+ path: identity.path,
270
+ sourcePath: identity.sourcePath,
271
+ format: identity.format,
272
+ lastModified: identity.lastModified,
273
+ title: entry.title,
274
+ outline: entry.outline,
275
+ wordCount: entry.wordCount,
276
+ ...entry.excerpt ? { excerpt: entry.excerpt } : {},
277
+ ...entry.description ? { description: entry.description } : {},
278
+ ...entry.tags ? { tags: entry.tags } : {},
279
+ content: "full"
280
+ },
281
+ text: extractSearchText(document, source)
282
+ };
283
+ }
284
+
285
+ // src/workspace-index/types.ts
286
+ var WORKSPACE_INDEX_LIMITS = Object.freeze({
287
+ maxEntries: 5e4,
288
+ maxDirectories: 1e4,
289
+ maxDepth: 32,
290
+ maxDocuments: 1e4,
291
+ maxDocumentBytes: 4 * 1024 * 1024,
292
+ maxParseBytes: 1024 * 1024,
293
+ maxTextCharacters: 24e6
294
+ });
295
+ var WORKSPACE_SEARCH_LIMITS = Object.freeze({
296
+ maxQueryCharacters: 256,
297
+ maxDocuments: 200,
298
+ maxLinesPerDocument: 50,
299
+ maxMatchesPerDocument: 1e3,
300
+ maxTotalMatches: 1e5
301
+ });
302
+
303
+ // src/workspace-index/search.ts
304
+ function createCheckpoint(yieldControl, now, signal, sliceMs = 8) {
305
+ let sliceStart = now();
306
+ return async () => {
307
+ signal?.throwIfAborted();
308
+ if (now() - sliceStart < sliceMs) return;
309
+ await yieldControl();
310
+ signal?.throwIfAborted();
311
+ sliceStart = now();
312
+ };
313
+ }
314
+ var REGEX_SYNTAX = /[\\^$.*+?()[\]{}|/]/gu;
315
+ var WORD_CHARACTER = /^[\p{L}\p{N}\p{M}_]$/u;
316
+ function buildLiteralMatcher(query, maxQueryCharacters = WORKSPACE_SEARCH_LIMITS.maxQueryCharacters) {
317
+ const text = query.text.normalize("NFC");
318
+ if (!text.trim() || text.length > maxQueryCharacters) return null;
319
+ let source = text.replace(REGEX_SYNTAX, "\\$&");
320
+ if (query.wholeWord) {
321
+ const characters = [...text];
322
+ if (WORD_CHARACTER.test(characters[0])) source = `(?<![\\p{L}\\p{N}\\p{M}_])${source}`;
323
+ if (WORD_CHARACTER.test(characters[characters.length - 1])) {
324
+ source = `${source}(?![\\p{L}\\p{N}\\p{M}_])`;
325
+ }
326
+ }
327
+ return new RegExp(source, query.matchCase ? "gu" : "giu");
328
+ }
329
+ function rangesIn(matcher, text) {
330
+ const ranges = [];
331
+ for (const match of text.matchAll(matcher)) {
332
+ ranges.push([match.index, match.index + match[0].length]);
333
+ }
334
+ return ranges;
335
+ }
336
+ function countIn(matcher, text, cap) {
337
+ let count = 0;
338
+ for (const _match of text.matchAll(matcher)) {
339
+ count += 1;
340
+ if (count >= cap) break;
341
+ }
342
+ return count;
343
+ }
344
+ var SNIPPET_CHARACTERS = 200;
345
+ var SNIPPET_LEAD = 60;
346
+ function buildSnippet(line, ranges) {
347
+ if (line.length <= SNIPPET_CHARACTERS) {
348
+ const indent = line.length - line.trimStart().length;
349
+ return {
350
+ snippet: line.slice(indent),
351
+ ranges: ranges.map(([start2, end2]) => [start2 - indent, end2 - indent])
352
+ };
353
+ }
354
+ const first = ranges[0]?.[0] ?? 0;
355
+ const start = Math.max(0, Math.min(first - SNIPPET_LEAD, line.length - SNIPPET_CHARACTERS));
356
+ const end = Math.min(line.length, start + SNIPPET_CHARACTERS);
357
+ const prefix = start > 0 ? "\u2026" : "";
358
+ const suffix = end < line.length ? "\u2026" : "";
359
+ const shift = prefix.length - start;
360
+ const clipped = [];
361
+ for (const [rangeStart, rangeEnd] of ranges) {
362
+ if (rangeEnd <= start || rangeStart >= end) continue;
363
+ clipped.push([Math.max(rangeStart, start) + shift, Math.min(rangeEnd, end) + shift]);
364
+ }
365
+ return { snippet: `${prefix}${line.slice(start, end)}${suffix}`, ranges: clipped };
366
+ }
367
+ function stemOf2(path) {
368
+ const name = path.slice(path.lastIndexOf("/") + 1);
369
+ const dot = name.lastIndexOf(".");
370
+ return dot > 0 ? name.slice(0, dot) : name;
371
+ }
372
+ function compareCodeUnits(left, right) {
373
+ return left < right ? -1 : left > right ? 1 : 0;
374
+ }
375
+ async function searchDocuments(documents, query, checkpoint, limitOverrides = {}) {
376
+ const limits = { ...WORKSPACE_SEARCH_LIMITS, ...limitOverrides };
377
+ const matcher = buildLiteralMatcher(query, limits.maxQueryCharacters);
378
+ let pendingDocuments = 0;
379
+ let skippedDocuments = 0;
380
+ for (const document of documents) {
381
+ if (document.pending) pendingDocuments += 1;
382
+ else if (!document.text) skippedDocuments += 1;
383
+ }
384
+ if (!matcher) {
385
+ return {
386
+ documents: [],
387
+ totalDocuments: 0,
388
+ totalMatches: 0,
389
+ truncated: false,
390
+ pendingDocuments,
391
+ skippedDocuments
392
+ };
393
+ }
394
+ const tallies = [];
395
+ let totalMatches = 0;
396
+ let truncated = false;
397
+ for (const document of documents) {
398
+ await checkpoint();
399
+ const titleRanges = rangesIn(matcher, document.record.title.normalize("NFC"));
400
+ const titleMatch = titleRanges.length > 0 || countIn(matcher, stemOf2(document.record.path).normalize("NFC"), 1) > 0;
401
+ let matchCount = 0;
402
+ if (document.text && !truncated) {
403
+ for (const line of document.text.lines) {
404
+ const room = Math.min(
405
+ limits.maxMatchesPerDocument - matchCount,
406
+ limits.maxTotalMatches - totalMatches - matchCount
407
+ );
408
+ if (room <= 0) break;
409
+ matchCount += countIn(matcher, line, room);
410
+ }
411
+ if (matchCount >= limits.maxMatchesPerDocument) truncated = true;
412
+ totalMatches += matchCount;
413
+ if (totalMatches >= limits.maxTotalMatches) truncated = true;
414
+ }
415
+ if (titleMatch || matchCount > 0) {
416
+ tallies.push({ document, titleMatch, titleRanges, matchCount });
417
+ }
418
+ }
419
+ tallies.sort(
420
+ (left, right) => Number(right.titleMatch) - Number(left.titleMatch) || right.matchCount - left.matchCount || compareCodeUnits(right.document.record.lastModified, left.document.record.lastModified) || compareCodeUnits(left.document.record.path, right.document.record.path)
421
+ );
422
+ if (tallies.length > limits.maxDocuments) truncated = true;
423
+ const results = [];
424
+ for (const tally of tallies.slice(0, limits.maxDocuments)) {
425
+ await checkpoint();
426
+ const { document } = tally;
427
+ const matches = [];
428
+ let occurrence = 0;
429
+ let moreMatches = false;
430
+ const text = document.text;
431
+ if (text && tally.matchCount > 0) {
432
+ for (let index = 0; index < text.lines.length; index += 1) {
433
+ const line = text.lines[index];
434
+ const ranges = rangesIn(matcher, line);
435
+ if (ranges.length === 0) continue;
436
+ if (matches.length >= limits.maxLinesPerDocument) {
437
+ moreMatches = true;
438
+ break;
439
+ }
440
+ const snippet = buildSnippet(line, ranges);
441
+ matches.push({
442
+ line: text.sourceLines[index],
443
+ kind: text.kinds[index],
444
+ snippet: snippet.snippet,
445
+ ranges: snippet.ranges,
446
+ occurrence
447
+ });
448
+ occurrence += ranges.length;
449
+ }
450
+ }
451
+ results.push({
452
+ path: document.record.path,
453
+ title: document.record.title,
454
+ format: document.record.format,
455
+ titleMatch: tally.titleMatch,
456
+ titleRanges: tally.titleRanges,
457
+ matchCount: tally.matchCount,
458
+ matches,
459
+ moreMatches,
460
+ lastModified: document.record.lastModified,
461
+ pending: document.pending
462
+ });
463
+ }
464
+ return {
465
+ documents: results,
466
+ totalDocuments: tallies.length,
467
+ totalMatches,
468
+ truncated,
469
+ pendingDocuments,
470
+ skippedDocuments
471
+ };
472
+ }
473
+
474
+ // src/workspace-index/workspace-index.ts
475
+ function emptyTree() {
476
+ return { files: /* @__PURE__ */ new Map(), documents: /* @__PURE__ */ new Map(), directories: /* @__PURE__ */ new Set(), unreadable: /* @__PURE__ */ new Set() };
477
+ }
478
+ var IndexStopped = class extends Error {
479
+ };
480
+ var RootListingError = class extends Error {
481
+ };
482
+ function fsCode(error) {
483
+ return error instanceof FsError ? error.code : null;
484
+ }
485
+ function isVanished(error) {
486
+ const code = fsCode(error);
487
+ return code === "not-found" || code === "type-mismatch";
488
+ }
489
+ function classify(entry) {
490
+ if (isMarkdownFileName(entry.name)) return "markdown";
491
+ return resolveOutsideInLayout(entry.path) ? "outside-in" : "other";
492
+ }
493
+ function compareCodeUnits2(left, right) {
494
+ return left < right ? -1 : left > right ? 1 : 0;
495
+ }
496
+ function defaultYield() {
497
+ return new Promise((resolve) => setTimeout(resolve, 0));
498
+ }
499
+ function defaultNow() {
500
+ return typeof performance !== "undefined" ? performance.now() : Date.now();
501
+ }
502
+ var PUBLISH_INTERVAL_MS = 250;
503
+ var SLICE_MS = 8;
504
+ var WorkspaceIndexImpl = class {
505
+ constructor(options) {
506
+ this.abort = new AbortController();
507
+ this.tree = emptyTree();
508
+ this.truncation = null;
509
+ this.analysisTruncation = null;
510
+ this.rootError = null;
511
+ this.listedOnce = false;
512
+ this.analyzedCount = 0;
513
+ this.textCharacters = 0;
514
+ this.readIssues = 0;
515
+ this.started = false;
516
+ this.disposed = false;
517
+ this.paused = false;
518
+ this.pumping = false;
519
+ this.walkRequested = false;
520
+ this.lastWalkEnd = null;
521
+ this.pendingChanges = /* @__PURE__ */ new Set();
522
+ this.changesReady = false;
523
+ this.changeTimer = null;
524
+ this.priority = [];
525
+ this.queue = [];
526
+ this.queueIndex = 0;
527
+ this.queueDirty = true;
528
+ this.sliceStart = 0;
529
+ this.subscription = null;
530
+ this.watching = false;
531
+ this.listeners = /* @__PURE__ */ new Set();
532
+ this.listedWaiters = /* @__PURE__ */ new Set();
533
+ this.generation = 0;
534
+ this.lastPublish = 0;
535
+ this.filesCache = null;
536
+ this.documentsCache = null;
537
+ this.statusCache = null;
538
+ this.workspaceId = options.workspaceId;
539
+ this.source = options.source;
540
+ this.limits = { ...WORKSPACE_INDEX_LIMITS, ...options.limits };
541
+ this.yieldControl = options.yieldControl ?? defaultYield;
542
+ this.now = options.now ?? defaultNow;
543
+ this.minRefreshIntervalMs = options.minRefreshIntervalMs ?? 15e3;
544
+ this.changeCoalesceMs = options.changeCoalesceMs ?? 50;
545
+ }
546
+ // ── Public API ────────────────────────────────────────────────────────
547
+ getStatus() {
548
+ if (this.statusCache) return this.statusCache;
549
+ let pendingCount = 0;
550
+ for (const state of this.tree.documents.values()) {
551
+ if (state.analysis === null) pendingCount += 1;
552
+ }
553
+ const phase = this.disposed ? "disposed" : !this.started ? "idle" : this.rootError !== null ? "error" : !this.listedOnce ? "listing" : pendingCount > 0 ? "analyzing" : "ready";
554
+ this.statusCache = Object.freeze({
555
+ phase,
556
+ fileCount: this.tree.files.size,
557
+ documentCount: this.tree.documents.size,
558
+ analyzedCount: this.analyzedCount,
559
+ pendingCount,
560
+ watching: this.watching,
561
+ listed: this.listedOnce,
562
+ truncated: this.truncation ?? this.analysisTruncation,
563
+ issues: this.tree.unreadable.size + this.readIssues,
564
+ error: this.rootError,
565
+ generation: this.generation
566
+ });
567
+ return this.statusCache;
568
+ }
569
+ subscribe(listener) {
570
+ this.listeners.add(listener);
571
+ return () => this.listeners.delete(listener);
572
+ }
573
+ start() {
574
+ if (this.started || this.disposed) return;
575
+ this.started = true;
576
+ this.startWatching();
577
+ this.requestWalk(true);
578
+ }
579
+ getFiles() {
580
+ if (!this.filesCache) {
581
+ this.filesCache = Object.freeze(
582
+ [...this.tree.files.values()].map(({ version: _version, ...file }) => Object.freeze(file)).sort((left, right) => compareCodeUnits2(left.path, right.path))
583
+ );
584
+ }
585
+ return this.filesCache;
586
+ }
587
+ getDocuments() {
588
+ if (!this.documentsCache) {
589
+ this.documentsCache = Object.freeze(
590
+ [...this.tree.documents.values()].map((state) => this.recordOf(state)).sort((left, right) => compareCodeUnits2(left.path, right.path))
591
+ );
592
+ }
593
+ return this.documentsCache;
594
+ }
595
+ getDocument(path) {
596
+ const canonical = tryParseWorkspacePath(path);
597
+ const state = canonical === null ? void 0 : this.tree.documents.get(canonical);
598
+ return state ? this.recordOf(state) : null;
599
+ }
600
+ has(path) {
601
+ const canonical = tryParseWorkspacePath(path);
602
+ if (canonical === null) return "unknown";
603
+ if (this.tree.files.has(canonical) || this.tree.directories.has(canonical)) return "present";
604
+ if (!this.listedOnce || this.truncation !== null || this.rootError !== null) return "unknown";
605
+ const segments = canonical.split("/");
606
+ for (let index = 0; index < segments.length; index += 1) {
607
+ const kind = index < segments.length - 1 ? "directory" : "file";
608
+ if (!isDiscoverableWorkspaceEntry(segments[index], kind)) return "unknown";
609
+ }
610
+ for (const directory of this.tree.unreadable) {
611
+ if (workspacePathContains(directory, canonical)) return "unknown";
612
+ }
613
+ return "absent";
614
+ }
615
+ whenListed(options = {}) {
616
+ if (this.listedOnce) return Promise.resolve(true);
617
+ if (this.disposed) return Promise.resolve(false);
618
+ const { signal, timeoutMs } = options;
619
+ signal?.throwIfAborted();
620
+ return new Promise((resolve, reject) => {
621
+ let timer = null;
622
+ const cleanup = () => {
623
+ this.listedWaiters.delete(waiter);
624
+ if (timer !== null) clearTimeout(timer);
625
+ signal?.removeEventListener("abort", onAbort);
626
+ };
627
+ const waiter = (listed) => {
628
+ cleanup();
629
+ resolve(listed);
630
+ };
631
+ const onAbort = () => {
632
+ cleanup();
633
+ reject(signal?.reason ?? new DOMException("Aborted", "AbortError"));
634
+ };
635
+ this.listedWaiters.add(waiter);
636
+ if (timeoutMs !== void 0) timer = setTimeout(() => waiter(false), timeoutMs);
637
+ signal?.addEventListener("abort", onAbort, { once: true });
638
+ });
639
+ }
640
+ search(query, options = {}) {
641
+ const documents = [...this.tree.documents.values()].map((state) => ({
642
+ record: this.recordOf(state),
643
+ text: state.analysis?.text ?? null,
644
+ pending: state.analysis === null
645
+ }));
646
+ documents.sort((left, right) => compareCodeUnits2(left.record.path, right.record.path));
647
+ return searchDocuments(
648
+ documents,
649
+ query,
650
+ createCheckpoint(this.yieldControl, this.now, options.signal, SLICE_MS),
651
+ options.limits
652
+ );
653
+ }
654
+ refresh(options = {}) {
655
+ if (!options.force && this.watching) return;
656
+ this.requestWalk(options.force ?? false);
657
+ }
658
+ invalidate(path) {
659
+ const canonical = tryParseWorkspacePath(path);
660
+ if (canonical === null || !this.started || this.disposed) return;
661
+ this.enqueueChange(canonical);
662
+ }
663
+ prioritize(paths) {
664
+ const canonical = paths.map((path) => tryParseWorkspacePath(path)).filter((path) => path !== null);
665
+ if (canonical.length === 0) return;
666
+ this.priority = [...canonical, ...this.priority.filter((path) => !canonical.includes(path))];
667
+ void this.pump();
668
+ }
669
+ setPaused(paused) {
670
+ if (this.paused === paused) return;
671
+ this.paused = paused;
672
+ if (!paused) void this.pump();
673
+ }
674
+ dispose() {
675
+ if (this.disposed) return;
676
+ this.disposed = true;
677
+ this.abort.abort();
678
+ if (this.changeTimer !== null) clearTimeout(this.changeTimer);
679
+ this.changeTimer = null;
680
+ const subscription = this.subscription;
681
+ this.subscription = null;
682
+ this.watching = false;
683
+ if (subscription) void subscription.dispose().catch(() => void 0);
684
+ for (const waiter of [...this.listedWaiters]) waiter(false);
685
+ this.publish();
686
+ this.listeners.clear();
687
+ }
688
+ // ── Scheduling ────────────────────────────────────────────────────────
689
+ requestWalk(force) {
690
+ if (!this.started || this.disposed) return;
691
+ if (!force && this.lastWalkEnd !== null && this.now() - this.lastWalkEnd < this.minRefreshIntervalMs) {
692
+ return;
693
+ }
694
+ this.walkRequested = true;
695
+ void this.pump();
696
+ }
697
+ enqueueChange(path) {
698
+ this.pendingChanges.add(path);
699
+ if (this.changeTimer !== null) return;
700
+ this.changeTimer = setTimeout(() => {
701
+ this.changeTimer = null;
702
+ this.changesReady = true;
703
+ void this.pump();
704
+ }, this.changeCoalesceMs);
705
+ }
706
+ async pump() {
707
+ if (this.pumping || this.disposed || !this.started) return;
708
+ this.pumping = true;
709
+ this.sliceStart = this.now();
710
+ try {
711
+ while (!this.disposed && !this.paused) {
712
+ if (this.walkRequested) {
713
+ this.walkRequested = false;
714
+ await this.fullWalk();
715
+ continue;
716
+ }
717
+ if (this.changesReady && this.pendingChanges.size > 0) {
718
+ this.changesReady = false;
719
+ const paths = [...this.pendingChanges];
720
+ this.pendingChanges.clear();
721
+ await this.applyChanges(paths);
722
+ continue;
723
+ }
724
+ const next = this.nextPendingDocument();
725
+ if (!next) break;
726
+ await this.analyze(next);
727
+ this.maybePublish();
728
+ }
729
+ } catch (error) {
730
+ if (!(error instanceof IndexStopped)) {
731
+ this.rootError = error instanceof Error ? error.message : "Indexing failed.";
732
+ }
733
+ } finally {
734
+ this.pumping = false;
735
+ if (!this.disposed) this.publish();
736
+ }
737
+ }
738
+ /** Yield to the host between slices; stop once disposed. */
739
+ async checkpoint() {
740
+ if (this.disposed) throw new IndexStopped();
741
+ if (this.now() - this.sliceStart < SLICE_MS) return;
742
+ await this.yieldControl();
743
+ if (this.disposed) throw new IndexStopped();
744
+ this.sliceStart = this.now();
745
+ }
746
+ // ── Watching ──────────────────────────────────────────────────────────
747
+ startWatching() {
748
+ if (!this.source.watch || !this.source.capabilities?.watch) return;
749
+ let subscription;
750
+ try {
751
+ subscription = this.source.watch((event) => this.onWatchEvent(event), {
752
+ onError: () => this.onWatchFailure()
753
+ });
754
+ } catch {
755
+ return;
756
+ }
757
+ this.subscription = subscription;
758
+ this.watching = true;
759
+ subscription.ready.then(
760
+ () => {
761
+ if (this.subscription === subscription) this.requestWalk(true);
762
+ },
763
+ () => this.onWatchFailure()
764
+ );
765
+ }
766
+ onWatchEvent(event) {
767
+ if (this.disposed) return;
768
+ if (event.type === "overflow") {
769
+ this.requestWalk(true);
770
+ return;
771
+ }
772
+ this.enqueueChange(event.path);
773
+ if (event.type === "moved" && event.destinationPath !== null) {
774
+ this.enqueueChange(event.destinationPath);
775
+ }
776
+ }
777
+ /** Fall back to explicit refreshes; never resubscribe automatically. */
778
+ onWatchFailure() {
779
+ if (this.disposed || !this.watching) return;
780
+ const subscription = this.subscription;
781
+ this.subscription = null;
782
+ this.watching = false;
783
+ if (subscription) void subscription.dispose().catch(() => void 0);
784
+ this.requestWalk(true);
785
+ this.publish();
786
+ }
787
+ // ── Listing ───────────────────────────────────────────────────────────
788
+ async fullWalk() {
789
+ const progressive = !this.listedOnce;
790
+ const previous = this.tree.documents;
791
+ const target = emptyTree();
792
+ if (progressive) this.tree = target;
793
+ let truncation;
794
+ try {
795
+ truncation = await this.listTree(target, previous, WORKSPACE_ROOT, 0, progressive);
796
+ } catch (error) {
797
+ if (!(error instanceof RootListingError)) throw error;
798
+ this.rootError = error.message;
799
+ this.lastWalkEnd = this.now();
800
+ for (const waiter of [...this.listedWaiters]) waiter(false);
801
+ return;
802
+ }
803
+ this.tree = target;
804
+ this.truncation = truncation;
805
+ this.rootError = null;
806
+ this.lastWalkEnd = this.now();
807
+ this.afterStructuralChange();
808
+ if (!this.listedOnce) {
809
+ this.listedOnce = true;
810
+ for (const waiter of [...this.listedWaiters]) waiter(true);
811
+ }
812
+ this.publish();
813
+ }
814
+ /**
815
+ * Breadth-first listing of `root` into `target`, carrying analysis over
816
+ * from `previous` for documents whose source version is unchanged.
817
+ */
818
+ async listTree(target, previous, root, rootDepth, progressive) {
819
+ let truncation = null;
820
+ const queue = [
821
+ { path: root, depth: rootDepth }
822
+ ];
823
+ const companions = [];
824
+ let directories = 0;
825
+ let entries = 0;
826
+ walk: while (queue.length > 0) {
827
+ await this.checkpoint();
828
+ const { path, depth } = queue.shift();
829
+ directories += 1;
830
+ if (directories > this.limits.maxDirectories) {
831
+ truncation ?? (truncation = { reason: "directories", limit: this.limits.maxDirectories });
832
+ break;
833
+ }
834
+ let children;
835
+ try {
836
+ children = await this.source.readDirectory(path);
837
+ } catch (error) {
838
+ if (this.disposed) throw new IndexStopped();
839
+ if (path === WORKSPACE_ROOT) {
840
+ throw new RootListingError(
841
+ error instanceof Error ? error.message : "The workspace could not be listed."
842
+ );
843
+ }
844
+ if (!isVanished(error)) target.unreadable.add(path);
845
+ continue;
846
+ }
847
+ if (this.disposed) throw new IndexStopped();
848
+ target.directories.add(path);
849
+ const directoryNames = new Set(
850
+ children.filter((child) => child.kind === "directory").map((child) => child.name)
851
+ );
852
+ for (const child of children) {
853
+ if (!isDiscoverableWorkspaceEntry(child.name, child.kind)) continue;
854
+ entries += 1;
855
+ if (entries > this.limits.maxEntries) {
856
+ truncation ?? (truncation = { reason: "entries", limit: this.limits.maxEntries });
857
+ break walk;
858
+ }
859
+ if (child.kind === "directory") {
860
+ if (depth + 1 > this.limits.maxDepth) {
861
+ truncation ?? (truncation = { reason: "depth", limit: this.limits.maxDepth });
862
+ } else {
863
+ queue.push({ path: child.path, depth: depth + 1 });
864
+ }
865
+ continue;
866
+ }
867
+ const kind = classify(child);
868
+ target.files.set(child.path, this.fileOf(child, kind));
869
+ if (kind === "markdown") {
870
+ target.documents.set(child.path, this.markdownState(child, previous.get(child.path)));
871
+ } else if (kind === "outside-in") {
872
+ const layout = resolveOutsideInLayout(child.path);
873
+ if (directoryNames.has(layout.companionName)) {
874
+ companions.push({ file: child, layout });
875
+ } else {
876
+ target.documents.set(
877
+ child.path,
878
+ this.outsideInState(child, layout, null, previous.get(child.path))
879
+ );
880
+ }
881
+ }
882
+ }
883
+ if (progressive) this.maybePublish();
884
+ }
885
+ for (const { file, layout } of companions) {
886
+ await this.checkpoint();
887
+ const source = await this.companionSource(layout);
888
+ target.documents.set(
889
+ file.path,
890
+ this.outsideInState(file, layout, source, previous.get(file.path))
891
+ );
892
+ }
893
+ return truncation;
894
+ }
895
+ /** The Markdown companion an outside-in document is edited through. */
896
+ async companionSource(layout) {
897
+ let companion;
898
+ try {
899
+ companion = await this.source.readDirectory(parseWorkspacePath(layout.companionDirectory));
900
+ } catch (error) {
901
+ if (this.disposed) throw new IndexStopped();
902
+ if (!isVanished(error)) this.readIssues += 1;
903
+ return null;
904
+ }
905
+ const files = companion.filter((entry) => entry.kind === "file");
906
+ const chosen = chooseOutsideInMarkdownPath(
907
+ layout,
908
+ files.map((entry) => entry.path)
909
+ );
910
+ return chosen ? files.find((entry) => entry.path === chosen) ?? null : null;
911
+ }
912
+ fileOf(entry, kind) {
913
+ return {
914
+ path: entry.path,
915
+ name: entry.name,
916
+ size: entry.kind === "file" ? entry.size : 0,
917
+ lastModified: entry.lastModified,
918
+ kind,
919
+ version: entry.version
920
+ };
921
+ }
922
+ markdownState(entry, previous) {
923
+ return this.documentState(
924
+ {
925
+ path: entry.path,
926
+ sourcePath: entry.path,
927
+ format: "markdown",
928
+ lastModified: entry.lastModified
929
+ },
930
+ entry.version,
931
+ entry.kind === "file" ? entry.size : 0,
932
+ null,
933
+ previous
934
+ );
935
+ }
936
+ outsideInState(entry, layout, source, previous) {
937
+ return this.documentState(
938
+ {
939
+ path: entry.path,
940
+ sourcePath: source?.path ?? null,
941
+ format: layout.format,
942
+ lastModified: entry.lastModified
943
+ },
944
+ source?.version ?? null,
945
+ source?.kind === "file" ? source.size : 0,
946
+ layout,
947
+ previous
948
+ );
949
+ }
950
+ documentState(identity, sourceVersion, sourceSize, layout, previous) {
951
+ const state = {
952
+ identity,
953
+ sourceVersion,
954
+ sourceSize,
955
+ layout,
956
+ analysis: null,
957
+ analysisVersion: null
958
+ };
959
+ if (identity.sourcePath === null) {
960
+ state.analysis = nameOnlyDocument(identity, "no-companion");
961
+ } else if (sourceSize > this.limits.maxDocumentBytes) {
962
+ state.analysis = nameOnlyDocument(identity, "too-large");
963
+ } else if (previous?.analysis && previous.identity.sourcePath === identity.sourcePath && previous.analysisVersion !== null && previous.analysisVersion === sourceVersion) {
964
+ state.analysis = {
965
+ record: { ...previous.analysis.record, lastModified: identity.lastModified },
966
+ text: previous.analysis.text
967
+ };
968
+ state.analysisVersion = previous.analysisVersion;
969
+ }
970
+ return state;
971
+ }
972
+ // ── Incremental changes ───────────────────────────────────────────────
973
+ async applyChanges(paths) {
974
+ for (const path of paths) {
975
+ await this.checkpoint();
976
+ await this.applyChange(path);
977
+ }
978
+ this.afterStructuralChange();
979
+ this.publish();
980
+ }
981
+ async applyChange(path) {
982
+ if (path === WORKSPACE_ROOT) {
983
+ this.walkRequested = true;
984
+ return;
985
+ }
986
+ const segments = path.split("/");
987
+ for (let index = 0; index < segments.length - 1; index += 1) {
988
+ const segment = segments[index];
989
+ if (isCompanionDirectoryName(segment)) {
990
+ await this.refreshCompanion(segments.slice(0, index + 1).join("/"));
991
+ return;
992
+ }
993
+ if (!isDiscoverableWorkspaceEntry(segment, "directory")) return;
994
+ }
995
+ let entry;
996
+ try {
997
+ entry = await this.source.stat(path);
998
+ } catch (error) {
999
+ if (this.disposed) throw new IndexStopped();
1000
+ if (!isVanished(error)) {
1001
+ this.walkRequested = true;
1002
+ return;
1003
+ }
1004
+ entry = null;
1005
+ }
1006
+ if (this.disposed) throw new IndexStopped();
1007
+ const name = segments[segments.length - 1];
1008
+ if (entry === null) {
1009
+ this.removeSubtree(path);
1010
+ if (isCompanionDirectoryName(name)) await this.refreshCompanion(path);
1011
+ return;
1012
+ }
1013
+ if (entry.kind === "directory") {
1014
+ if (isCompanionDirectoryName(name)) {
1015
+ await this.refreshCompanion(path);
1016
+ return;
1017
+ }
1018
+ if (!isDiscoverableWorkspaceEntry(name, "directory")) return;
1019
+ const previous2 = new Map(this.tree.documents);
1020
+ this.removeSubtree(path);
1021
+ const truncation = await this.listTree(this.tree, previous2, path, segments.length, false);
1022
+ this.truncation ?? (this.truncation = truncation);
1023
+ return;
1024
+ }
1025
+ if (!isDiscoverableWorkspaceEntry(name, "file")) return;
1026
+ const kind = classify(entry);
1027
+ this.tree.files.set(path, this.fileOf(entry, kind));
1028
+ const previous = this.tree.documents.get(path);
1029
+ if (kind === "markdown") {
1030
+ this.tree.documents.set(path, this.markdownState(entry, previous));
1031
+ } else if (kind === "outside-in") {
1032
+ const layout = resolveOutsideInLayout(path);
1033
+ const source = await this.companionSource(layout);
1034
+ this.tree.documents.set(path, this.outsideInState(entry, layout, source, previous));
1035
+ }
1036
+ }
1037
+ /** Re-resolve the outside-in documents whose companion folder is `companion`. */
1038
+ async refreshCompanion(companion) {
1039
+ for (const state of [...this.tree.documents.values()]) {
1040
+ const layout = state.layout;
1041
+ if (!layout || layout.companionDirectory !== companion) continue;
1042
+ const file = this.tree.files.get(state.identity.path);
1043
+ if (!file) continue;
1044
+ const source = await this.companionSource(layout);
1045
+ const entry = {
1046
+ kind: "file",
1047
+ path: file.path,
1048
+ name: file.name,
1049
+ size: file.size,
1050
+ version: file.version,
1051
+ lastModified: file.lastModified
1052
+ };
1053
+ this.tree.documents.set(file.path, this.outsideInState(entry, layout, source, state));
1054
+ }
1055
+ }
1056
+ removeSubtree(path) {
1057
+ const within = (candidate) => workspacePathContains(path, candidate);
1058
+ for (const key of [...this.tree.files.keys()]) if (within(key)) this.tree.files.delete(key);
1059
+ for (const key of [...this.tree.documents.keys()]) {
1060
+ if (within(key)) this.tree.documents.delete(key);
1061
+ }
1062
+ for (const key of [...this.tree.directories])
1063
+ if (within(key)) this.tree.directories.delete(key);
1064
+ for (const key of [...this.tree.unreadable]) if (within(key)) this.tree.unreadable.delete(key);
1065
+ }
1066
+ /** Recount totals and requeue analysis after the tree changed shape. */
1067
+ afterStructuralChange() {
1068
+ let analyzed = 0;
1069
+ let characters = 0;
1070
+ for (const state of this.tree.documents.values()) {
1071
+ if (state.analysis && state.analysis.record.content !== "name-only") analyzed += 1;
1072
+ characters += state.analysis?.text?.characters ?? 0;
1073
+ }
1074
+ this.analyzedCount = analyzed;
1075
+ this.textCharacters = characters;
1076
+ this.queueDirty = true;
1077
+ }
1078
+ // ── Analysis ──────────────────────────────────────────────────────────
1079
+ nextPendingDocument() {
1080
+ while (this.priority.length > 0) {
1081
+ const path = this.priority.shift();
1082
+ const state = this.tree.documents.get(path);
1083
+ if (state && state.analysis === null) return state;
1084
+ }
1085
+ if (this.queueDirty) {
1086
+ this.queue = [...this.tree.documents.values()].filter((state) => state.analysis === null).sort(
1087
+ (left, right) => compareCodeUnits2(right.identity.lastModified, left.identity.lastModified) || compareCodeUnits2(left.identity.path, right.identity.path)
1088
+ );
1089
+ this.queueIndex = 0;
1090
+ this.queueDirty = false;
1091
+ }
1092
+ while (this.queueIndex < this.queue.length) {
1093
+ const state = this.queue[this.queueIndex];
1094
+ this.queueIndex += 1;
1095
+ if (this.tree.documents.get(state.identity.path) === state && state.analysis === null) {
1096
+ return state;
1097
+ }
1098
+ }
1099
+ return null;
1100
+ }
1101
+ async analyze(state) {
1102
+ const { identity } = state;
1103
+ if (this.analyzedCount >= this.limits.maxDocuments) {
1104
+ state.analysis = nameOnlyDocument(identity, "over-budget");
1105
+ this.analysisTruncation ?? (this.analysisTruncation = { reason: "documents", limit: this.limits.maxDocuments });
1106
+ return;
1107
+ }
1108
+ const sourcePath = identity.sourcePath;
1109
+ let read;
1110
+ try {
1111
+ read = await this.source.readFile(sourcePath);
1112
+ } catch (error) {
1113
+ if (this.disposed) throw new IndexStopped();
1114
+ if (isVanished(error)) {
1115
+ this.enqueueChange(identity.path);
1116
+ state.analysis = nameOnlyDocument(identity, "read-error");
1117
+ return;
1118
+ }
1119
+ this.readIssues += 1;
1120
+ state.analysis = nameOnlyDocument(identity, "read-error");
1121
+ return;
1122
+ }
1123
+ if (this.disposed) throw new IndexStopped();
1124
+ if (this.tree.documents.get(identity.path) !== state) return;
1125
+ if (read === null) {
1126
+ this.enqueueChange(identity.path);
1127
+ state.analysis = nameOnlyDocument(identity, "read-error");
1128
+ return;
1129
+ }
1130
+ await this.checkpoint();
1131
+ let analyzed;
1132
+ if (read.data.byteLength > this.limits.maxDocumentBytes) {
1133
+ analyzed = nameOnlyDocument(identity, "too-large");
1134
+ } else {
1135
+ try {
1136
+ analyzed = await analyzeDocumentBytes(identity, read.data, {
1137
+ maxParseBytes: this.limits.maxParseBytes,
1138
+ signal: this.abort.signal
1139
+ });
1140
+ } catch {
1141
+ if (this.disposed) throw new IndexStopped();
1142
+ analyzed = nameOnlyDocument(identity, "read-error");
1143
+ }
1144
+ }
1145
+ if (this.tree.documents.get(identity.path) !== state) return;
1146
+ const characters = analyzed.text?.characters ?? 0;
1147
+ if (characters > 0 && this.textCharacters + characters > this.limits.maxTextCharacters) {
1148
+ analyzed = {
1149
+ record: { ...analyzed.record, content: "name-only", issue: "over-budget" },
1150
+ text: null
1151
+ };
1152
+ this.analysisTruncation ?? (this.analysisTruncation = { reason: "text", limit: this.limits.maxTextCharacters });
1153
+ }
1154
+ state.analysis = analyzed;
1155
+ state.analysisVersion = read.entry.version;
1156
+ this.textCharacters += analyzed.text?.characters ?? 0;
1157
+ if (analyzed.record.content !== "name-only") this.analyzedCount += 1;
1158
+ }
1159
+ // ── Publishing ────────────────────────────────────────────────────────
1160
+ recordOf(state) {
1161
+ return state.analysis?.record ?? nameOnlyDocument(state.identity, "pending").record;
1162
+ }
1163
+ maybePublish() {
1164
+ if (this.now() - this.lastPublish >= PUBLISH_INTERVAL_MS) this.publish();
1165
+ }
1166
+ publish() {
1167
+ this.generation += 1;
1168
+ this.lastPublish = this.now();
1169
+ this.filesCache = null;
1170
+ this.documentsCache = null;
1171
+ this.statusCache = null;
1172
+ for (const listener of [...this.listeners]) {
1173
+ try {
1174
+ listener();
1175
+ } catch {
1176
+ }
1177
+ }
1178
+ }
1179
+ };
1180
+ function createWorkspaceIndex(options) {
1181
+ return new WorkspaceIndexImpl(options);
1182
+ }
1183
+
1184
+ // src/workspace-index/fuzzy.ts
1185
+ var SEPARATORS = /* @__PURE__ */ new Set(["/", "\\", "-", "_", ".", " "]);
1186
+ var SCORE_MATCH = 16;
1187
+ var BONUS_FIRST_CHARACTER = 24;
1188
+ var BONUS_WORD_START = 20;
1189
+ var BONUS_CONSECUTIVE = 12;
1190
+ var BONUS_SAME_CASE = 1;
1191
+ var PENALTY_GAP = 2;
1192
+ var PENALTY_GAP_CAP = 24;
1193
+ var PENALTY_LEADING = 1;
1194
+ function isWordStart(target, index) {
1195
+ if (index === 0) return true;
1196
+ const previous = target[index - 1];
1197
+ if (SEPARATORS.has(previous)) return true;
1198
+ const current = target[index];
1199
+ return previous === previous.toLowerCase() && current !== current.toLowerCase();
1200
+ }
1201
+ function toRanges(indices) {
1202
+ const ranges = [];
1203
+ for (const index of indices) {
1204
+ const last = ranges[ranges.length - 1];
1205
+ if (last && last[1] === index) ranges[ranges.length - 1] = [last[0], index + 1];
1206
+ else ranges.push([index, index + 1]);
1207
+ }
1208
+ return ranges;
1209
+ }
1210
+ function fuzzyMatch(query, target) {
1211
+ if (!query) return { score: 0, ranges: [] };
1212
+ const lowerQuery = query.toLowerCase();
1213
+ const lowerTarget = target.toLowerCase();
1214
+ if (lowerQuery.length > lowerTarget.length) return null;
1215
+ let queryIndex = 0;
1216
+ let end = -1;
1217
+ for (let index = 0; index < lowerTarget.length; index += 1) {
1218
+ if (lowerTarget[index] === lowerQuery[queryIndex]) {
1219
+ queryIndex += 1;
1220
+ if (queryIndex === lowerQuery.length) {
1221
+ end = index;
1222
+ break;
1223
+ }
1224
+ }
1225
+ }
1226
+ if (end < 0) return null;
1227
+ queryIndex = lowerQuery.length - 1;
1228
+ let start = end;
1229
+ for (let index = end; index >= 0; index -= 1) {
1230
+ if (lowerTarget[index] === lowerQuery[queryIndex]) {
1231
+ queryIndex -= 1;
1232
+ if (queryIndex < 0) {
1233
+ start = index;
1234
+ break;
1235
+ }
1236
+ }
1237
+ }
1238
+ const indices = [];
1239
+ queryIndex = 0;
1240
+ for (let index = start; index <= end && queryIndex < lowerQuery.length; index += 1) {
1241
+ if (lowerTarget[index] !== lowerQuery[queryIndex]) continue;
1242
+ indices.push(index);
1243
+ queryIndex += 1;
1244
+ }
1245
+ if (indices.length !== lowerQuery.length) return null;
1246
+ let score = 0;
1247
+ let previous = -2;
1248
+ for (let position = 0; position < indices.length; position += 1) {
1249
+ const index = indices[position];
1250
+ score += SCORE_MATCH;
1251
+ if (index === 0) score += BONUS_FIRST_CHARACTER;
1252
+ else if (isWordStart(target, index)) score += BONUS_WORD_START;
1253
+ if (index === previous + 1) score += BONUS_CONSECUTIVE;
1254
+ else if (previous >= 0)
1255
+ score -= Math.min(PENALTY_GAP * (index - previous - 1), PENALTY_GAP_CAP);
1256
+ if (target[index] === query[position]) score += BONUS_SAME_CASE;
1257
+ previous = index;
1258
+ }
1259
+ score -= Math.min(PENALTY_LEADING * indices[0], PENALTY_GAP_CAP);
1260
+ return { score, ranges: toRanges(indices) };
1261
+ }
1262
+ function fuzzyMatchTokens(tokens, target) {
1263
+ let score = 0;
1264
+ const marked = /* @__PURE__ */ new Set();
1265
+ for (const token of tokens) {
1266
+ const match = fuzzyMatch(token, target);
1267
+ if (!match) return null;
1268
+ score += match.score;
1269
+ for (const [rangeStart, rangeEnd] of match.ranges) {
1270
+ for (let index = rangeStart; index < rangeEnd; index += 1) marked.add(index);
1271
+ }
1272
+ }
1273
+ return { score, ranges: toRanges([...marked].sort((left, right) => left - right)) };
1274
+ }
1275
+ function fuzzyTokens(query) {
1276
+ return query.trim().split(/\s+/u).filter(Boolean);
1277
+ }
1278
+ export {
1279
+ WORKSPACE_INDEX_LIMITS,
1280
+ WORKSPACE_SEARCH_LIMITS,
1281
+ buildLiteralMatcher,
1282
+ buildSnippet,
1283
+ createWorkspaceIndex,
1284
+ fuzzyMatch,
1285
+ fuzzyMatchTokens,
1286
+ fuzzyTokens
1287
+ };