pi-readseek 0.1.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 (67) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +41 -0
  3. package/index.ts +142 -0
  4. package/package.json +73 -0
  5. package/prompts/edit.md +113 -0
  6. package/prompts/find.md +19 -0
  7. package/prompts/grep.md +26 -0
  8. package/prompts/ls.md +11 -0
  9. package/prompts/read.md +33 -0
  10. package/prompts/sg.md +25 -0
  11. package/prompts/write.md +46 -0
  12. package/src/binary-detect.ts +22 -0
  13. package/src/binary-resolution.ts +77 -0
  14. package/src/coerce-obvious-int.ts +39 -0
  15. package/src/context-application.ts +70 -0
  16. package/src/context-hygiene.ts +503 -0
  17. package/src/diff-data.ts +303 -0
  18. package/src/doom-loop-suggestions.ts +42 -0
  19. package/src/doom-loop.ts +216 -0
  20. package/src/edit-classify.ts +190 -0
  21. package/src/edit-diff.ts +354 -0
  22. package/src/edit-output.ts +107 -0
  23. package/src/edit-render-helpers.ts +141 -0
  24. package/src/edit-syntax-validate.ts +120 -0
  25. package/src/edit.ts +725 -0
  26. package/src/find-parsers.ts +89 -0
  27. package/src/find-stat.ts +36 -0
  28. package/src/find.ts +613 -0
  29. package/src/grep-budget.ts +79 -0
  30. package/src/grep-output.ts +197 -0
  31. package/src/grep-render-helpers.ts +77 -0
  32. package/src/grep-symbol-scope.ts +197 -0
  33. package/src/grep.ts +792 -0
  34. package/src/hashline.ts +747 -0
  35. package/src/ls.ts +293 -0
  36. package/src/map-cache.ts +152 -0
  37. package/src/path-utils.ts +24 -0
  38. package/src/pending-diff-preview.ts +269 -0
  39. package/src/persistent-map-cache.ts +251 -0
  40. package/src/read-local-bundle.ts +87 -0
  41. package/src/read-output.ts +212 -0
  42. package/src/read-render-helpers.ts +104 -0
  43. package/src/read.ts +748 -0
  44. package/src/readseek/constants.ts +21 -0
  45. package/src/readseek/enums.ts +38 -0
  46. package/src/readseek/formatter.ts +431 -0
  47. package/src/readseek/language-detect.ts +29 -0
  48. package/src/readseek/mapper.ts +69 -0
  49. package/src/readseek/parser-errors.ts +22 -0
  50. package/src/readseek/parser-loader.ts +83 -0
  51. package/src/readseek/symbol-error-format.ts +18 -0
  52. package/src/readseek/symbol-lookup.ts +294 -0
  53. package/src/readseek/types.ts +79 -0
  54. package/src/readseek-client.ts +343 -0
  55. package/src/readseek-error-codes.ts +54 -0
  56. package/src/readseek-settings.ts +287 -0
  57. package/src/readseek-value.ts +144 -0
  58. package/src/replace-symbol.ts +74 -0
  59. package/src/runtime.ts +3 -0
  60. package/src/sg-output.ts +88 -0
  61. package/src/sg.ts +308 -0
  62. package/src/syntax-validate-mode.ts +25 -0
  63. package/src/tool-prompt-metadata.ts +76 -0
  64. package/src/tui-diff-component.ts +86 -0
  65. package/src/tui-diff-renderer.ts +92 -0
  66. package/src/tui-render-utils.ts +129 -0
  67. package/src/write.ts +532 -0
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Constants for thresholds.
3
+ */
4
+ export const THRESHOLDS = {
5
+ /** Maximum lines before truncation */
6
+ MAX_LINES: 2000,
7
+ /** Maximum bytes before truncation */
8
+ MAX_BYTES: 50 * 1024,
9
+ /** Maximum map size in bytes */
10
+ MAX_MAP_BYTES: 25 * 1024,
11
+ /** Target size for full detail */
12
+ FULL_TARGET_BYTES: 10 * 1024,
13
+ /** Target size for compact detail */
14
+ COMPACT_TARGET_BYTES: 20 * 1024,
15
+ /** Maximum size for outline level */
16
+ MAX_OUTLINE_BYTES: 50 * 1024,
17
+ /** Maximum size for truncated level (hard cap) */
18
+ MAX_TRUNCATED_BYTES: 100 * 1024,
19
+ /** Number of symbols to show at each end for truncated outline */
20
+ TRUNCATED_SYMBOLS_EACH: 50,
21
+ } as const;
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Symbol kinds supported by mappers.
3
+ */
4
+ export enum SymbolKind {
5
+ Class = "class",
6
+ Function = "function",
7
+ Method = "method",
8
+ Variable = "variable",
9
+ Constant = "constant",
10
+ Interface = "interface",
11
+ Type = "type",
12
+ Enum = "enum",
13
+ Struct = "struct",
14
+ Import = "import",
15
+ Module = "module",
16
+ Namespace = "namespace",
17
+ Property = "property",
18
+ Heading = "heading",
19
+ Table = "table",
20
+ View = "view",
21
+ Procedure = "procedure",
22
+ Trigger = "trigger",
23
+ Index = "index",
24
+ Schema = "schema",
25
+ Signal = "signal",
26
+ Unknown = "unknown",
27
+ }
28
+
29
+ /**
30
+ * Detail levels for map generation.
31
+ */
32
+ export enum DetailLevel {
33
+ Full = "full",
34
+ Compact = "compact",
35
+ Minimal = "minimal",
36
+ Outline = "outline",
37
+ Truncated = "truncated",
38
+ }
@@ -0,0 +1,431 @@
1
+ import { basename } from "node:path";
2
+
3
+ import type { FileMap, FileSymbol } from "./types.js";
4
+
5
+ import { THRESHOLDS } from "./constants.js";
6
+ import { DetailLevel } from "./enums.js";
7
+
8
+ const BOX_LINE = "───────────────────────────────────────";
9
+
10
+ /**
11
+ * Format a file size for display.
12
+ */
13
+ function formatSize(bytes: number): string {
14
+ if (bytes < 1024) {
15
+ return `${bytes} B`;
16
+ }
17
+ if (bytes < 1024 * 1024) {
18
+ return `${(bytes / 1024).toFixed(1)} KB`;
19
+ }
20
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
21
+ }
22
+
23
+ /**
24
+ * Format a number with comma separators.
25
+ */
26
+ function formatNumber(n: number): string {
27
+ return n.toLocaleString("en-US");
28
+ }
29
+
30
+ /**
31
+ * Format a symbol for display.
32
+ */
33
+ function formatSymbol(
34
+ symbol: FileSymbol,
35
+ level: DetailLevel,
36
+ indent = 0
37
+ ): string {
38
+ const prefix = " ".repeat(indent);
39
+ const lineRange =
40
+ symbol.startLine === symbol.endLine
41
+ ? `[${symbol.startLine}]`
42
+ : `[${symbol.startLine}-${symbol.endLine}]`;
43
+
44
+ let { name } = symbol;
45
+
46
+ if (level === DetailLevel.Full) {
47
+ if (symbol.signature) {
48
+ // Check whether the signature already contains the symbol name.
49
+ // Full-declaration signatures (e.g. Rust "pub fn foo(x: i32) -> bool")
50
+ // include the name; partial signatures (e.g. Python "(x, y) -> None")
51
+ // do not and should be appended.
52
+ if (symbol.signature.includes(name)) {
53
+ name = symbol.signature;
54
+ } else {
55
+ if (symbol.modifiers?.length) {
56
+ name = `${symbol.modifiers.join(" ")} ${name}`;
57
+ }
58
+ name = `${name}${symbol.signature}`;
59
+ }
60
+ } else if (symbol.modifiers?.length) {
61
+ name = `${symbol.modifiers.join(" ")} ${name}`;
62
+ }
63
+ }
64
+
65
+ // Format based on kind
66
+ let formatted: string;
67
+ switch (symbol.kind) {
68
+ case "class":
69
+ case "interface":
70
+ case "struct":
71
+ case "enum":
72
+ case "type": {
73
+ formatted = `${prefix}${symbol.kind} ${name}: ${lineRange}`;
74
+ break;
75
+ }
76
+ case "function":
77
+ case "method": {
78
+ formatted = `${prefix}${name}: ${lineRange}`;
79
+ break;
80
+ }
81
+ case "variable":
82
+ case "constant": {
83
+ formatted = `${prefix}${name} = ... ${lineRange}`;
84
+ break;
85
+ }
86
+ default: {
87
+ formatted = `${prefix}${name}: ${lineRange}`;
88
+ }
89
+ }
90
+
91
+ // Append docstring at Full detail level
92
+ if (level === DetailLevel.Full && symbol.docstring) {
93
+ formatted += ` — ${symbol.docstring}`;
94
+ }
95
+
96
+ return formatted;
97
+ }
98
+
99
+ /**
100
+ * Format symbols recursively.
101
+ */
102
+ function formatSymbols(
103
+ symbols: FileSymbol[],
104
+ level: DetailLevel,
105
+ indent = 0
106
+ ): string[] {
107
+ const lines: string[] = [];
108
+
109
+ for (const symbol of symbols) {
110
+ lines.push(formatSymbol(symbol, level, indent));
111
+
112
+ // Add children for full, compact, and minimal levels (not outline or truncated)
113
+ if (
114
+ level !== DetailLevel.Outline &&
115
+ level !== DetailLevel.Truncated &&
116
+ symbol.children?.length
117
+ ) {
118
+ // For minimal, flatten children
119
+ if (level === DetailLevel.Minimal) {
120
+ for (const child of symbol.children) {
121
+ lines.push(formatSymbol(child, level, indent + 1));
122
+ }
123
+ } else {
124
+ lines.push(...formatSymbols(symbol.children, level, indent + 1));
125
+ }
126
+ }
127
+ }
128
+
129
+ return lines;
130
+ }
131
+
132
+ /**
133
+ * Format a complete file map to a string.
134
+ */
135
+ export function formatFileMap(map: FileMap, level?: DetailLevel): string {
136
+ const effectiveLevel = level ?? map.detailLevel;
137
+ const fileName = basename(map.path);
138
+
139
+ const lines: string[] = [
140
+ "",
141
+ BOX_LINE,
142
+ `File Map: ${fileName}`,
143
+ `${formatNumber(map.totalLines)} lines │ ${formatSize(map.totalBytes)} │ ${map.language}`,
144
+ BOX_LINE,
145
+ "",
146
+ ];
147
+
148
+ // Add detail level notice if reduced from Full
149
+ if (map.truncatedInfo) {
150
+ const { shownSymbols, totalSymbols } = map.truncatedInfo;
151
+ lines.push(
152
+ `[Map ≤${formatSize(THRESHOLDS.MAX_TRUNCATED_BYTES)} | ${shownSymbols} of ${formatNumber(totalSymbols)} symbols]`
153
+ );
154
+ lines.push("");
155
+ } else if (effectiveLevel === DetailLevel.Outline) {
156
+ lines.push(`[Map ≤${formatSize(THRESHOLDS.MAX_OUTLINE_BYTES)} | outline]`);
157
+ lines.push("");
158
+ } else if (effectiveLevel === DetailLevel.Minimal) {
159
+ lines.push(`[Map ≤${formatSize(THRESHOLDS.MAX_MAP_BYTES)} | minimal]`);
160
+ lines.push("");
161
+ } else if (effectiveLevel === DetailLevel.Compact) {
162
+ lines.push(
163
+ `[Map ≤${formatSize(THRESHOLDS.COMPACT_TARGET_BYTES)} | compact]`
164
+ );
165
+ lines.push("");
166
+ }
167
+
168
+ // Add imports if present and not outline or truncated level
169
+ if (
170
+ effectiveLevel !== DetailLevel.Outline &&
171
+ effectiveLevel !== DetailLevel.Truncated &&
172
+ map.imports.length > 0
173
+ ) {
174
+ const importList =
175
+ map.imports.length > 10
176
+ ? [...map.imports.slice(0, 10), `...${map.imports.length - 10} more`]
177
+ : map.imports;
178
+ lines.push(`imports: ${importList.join(", ")}`);
179
+ lines.push("");
180
+ }
181
+
182
+ // Add symbols
183
+ if (map.truncatedInfo) {
184
+ // Truncated format: first half, separator, second half
185
+ const half = Math.floor(map.symbols.length / 2);
186
+ const firstSymbols = map.symbols.slice(0, half);
187
+ const lastSymbols = map.symbols.slice(half);
188
+
189
+ // Format first batch
190
+ const firstLines = formatSymbols(firstSymbols, effectiveLevel);
191
+ lines.push(...firstLines);
192
+
193
+ // Add separator
194
+ lines.push("");
195
+ lines.push(
196
+ ` ─ ─ ─ ${formatNumber(map.truncatedInfo.omittedSymbols)} more symbols ─ ─ ─`
197
+ );
198
+ lines.push("");
199
+
200
+ // Format last batch
201
+ const lastLines = formatSymbols(lastSymbols, effectiveLevel);
202
+ lines.push(...lastLines);
203
+ } else {
204
+ // Normal format
205
+ const symbolLines = formatSymbols(map.symbols, effectiveLevel);
206
+ lines.push(...symbolLines);
207
+ }
208
+
209
+ // Add footer with appropriate guidance
210
+ lines.push("");
211
+ lines.push(BOX_LINE);
212
+ if (map.truncatedInfo) {
213
+ // For truncated maps, provide specific guidance on finding omitted symbols
214
+ const firstShown = map.symbols.slice(0, Math.floor(map.symbols.length / 2));
215
+ const lastShown = map.symbols.slice(Math.floor(map.symbols.length / 2));
216
+ const lastFirst = firstShown.at(-1);
217
+ const firstLast = lastShown.at(0);
218
+ if (lastFirst && firstLast) {
219
+ const omitStart = lastFirst.endLine + 1;
220
+ const omitEnd = firstLast.startLine - 1;
221
+ lines.push(
222
+ `Omitted symbols are in lines ${formatNumber(omitStart)}-${formatNumber(omitEnd)}.`
223
+ );
224
+ }
225
+ lines.push(
226
+ "Use read(path, offset=LINE, limit=N) to view specific sections."
227
+ );
228
+ } else {
229
+ lines.push("Use read(path, offset=LINE, limit=N) for targeted reads.");
230
+ }
231
+ lines.push(BOX_LINE);
232
+
233
+ return lines.join("\n");
234
+ }
235
+
236
+ /**
237
+ * Get the appropriate detail level for a map based on size.
238
+ */
239
+ export function getDetailLevelForSize(currentSize: number): DetailLevel {
240
+ if (currentSize <= THRESHOLDS.FULL_TARGET_BYTES) {
241
+ return DetailLevel.Full;
242
+ }
243
+ if (currentSize <= THRESHOLDS.COMPACT_TARGET_BYTES) {
244
+ return DetailLevel.Compact;
245
+ }
246
+ if (currentSize <= THRESHOLDS.MAX_MAP_BYTES) {
247
+ return DetailLevel.Minimal;
248
+ }
249
+ return DetailLevel.Outline;
250
+ }
251
+
252
+ /**
253
+ * Reduce detail level of a file map.
254
+ */
255
+ export function reduceToLevel(map: FileMap, level: DetailLevel): FileMap {
256
+ if (level === DetailLevel.Outline) {
257
+ // Remove all children and signatures
258
+ return {
259
+ ...map,
260
+ detailLevel: DetailLevel.Outline,
261
+ imports: [],
262
+ symbols: map.symbols.map((s) => ({
263
+ name: s.name,
264
+ kind: s.kind,
265
+ startLine: s.startLine,
266
+ endLine: s.endLine,
267
+ })),
268
+ };
269
+ }
270
+
271
+ if (level === DetailLevel.Minimal) {
272
+ // Remove signatures and docstrings but keep children flattened
273
+ return {
274
+ ...map,
275
+ detailLevel: DetailLevel.Minimal,
276
+ symbols: map.symbols.map((s) => ({
277
+ name: s.name,
278
+ kind: s.kind,
279
+ startLine: s.startLine,
280
+ endLine: s.endLine,
281
+ isExported: s.isExported,
282
+ children: s.children?.map((c) => ({
283
+ name: c.name,
284
+ kind: c.kind,
285
+ startLine: c.startLine,
286
+ endLine: c.endLine,
287
+ isExported: c.isExported,
288
+ })),
289
+ })),
290
+ };
291
+ }
292
+
293
+ if (level === DetailLevel.Compact) {
294
+ // Remove signatures but keep structure
295
+ return {
296
+ ...map,
297
+ detailLevel: DetailLevel.Compact,
298
+ symbols: map.symbols.map((s) => stripSignatures(s)),
299
+ };
300
+ }
301
+
302
+ return { ...map, detailLevel: DetailLevel.Full };
303
+ }
304
+
305
+ function stripSignatures(symbol: FileSymbol): FileSymbol {
306
+ return {
307
+ name: symbol.name,
308
+ kind: symbol.kind,
309
+ startLine: symbol.startLine,
310
+ endLine: symbol.endLine,
311
+ modifiers: symbol.modifiers,
312
+ docstring: symbol.docstring,
313
+ isExported: symbol.isExported,
314
+ children: symbol.children?.map(stripSignatures),
315
+ };
316
+ }
317
+
318
+ /**
319
+ * Reduce a file map to truncated form: first N + last N symbols only.
320
+ * Used when even Outline level exceeds budget.
321
+ */
322
+ export function reduceToTruncated(
323
+ map: FileMap,
324
+ symbolsEach: number = THRESHOLDS.TRUNCATED_SYMBOLS_EACH
325
+ ): FileMap {
326
+ const { symbols } = map;
327
+ const total = symbols.length;
328
+
329
+ if (total <= symbolsEach * 2) {
330
+ // Not enough symbols to truncate, return as outline
331
+ return reduceToLevel(map, DetailLevel.Outline);
332
+ }
333
+
334
+ const firstSymbols = symbols.slice(0, symbolsEach).map((s) => ({
335
+ name: s.name,
336
+ kind: s.kind,
337
+ startLine: s.startLine,
338
+ endLine: s.endLine,
339
+ }));
340
+
341
+ const lastSymbols = symbols.slice(-symbolsEach).map((s) => ({
342
+ name: s.name,
343
+ kind: s.kind,
344
+ startLine: s.startLine,
345
+ endLine: s.endLine,
346
+ }));
347
+
348
+ return {
349
+ ...map,
350
+ symbols: [...firstSymbols, ...lastSymbols],
351
+ detailLevel: DetailLevel.Truncated,
352
+ imports: [],
353
+ truncatedInfo: {
354
+ totalSymbols: total,
355
+ shownSymbols: symbolsEach * 2,
356
+ omittedSymbols: total - symbolsEach * 2,
357
+ },
358
+ };
359
+ }
360
+
361
+ /**
362
+ * Format a file map with automatic budget enforcement.
363
+ * Reduces detail level until the map fits within the budget.
364
+ */
365
+ export function formatFileMapWithBudget(
366
+ map: FileMap,
367
+ maxBytes = THRESHOLDS.MAX_TRUNCATED_BYTES
368
+ ): string {
369
+ // Tiered budgets: progressively reduce detail level
370
+ const tiers: { level: DetailLevel; budget: number }[] = [
371
+ { level: DetailLevel.Full, budget: THRESHOLDS.FULL_TARGET_BYTES },
372
+ { level: DetailLevel.Compact, budget: THRESHOLDS.COMPACT_TARGET_BYTES },
373
+ { level: DetailLevel.Minimal, budget: THRESHOLDS.MAX_MAP_BYTES },
374
+ { level: DetailLevel.Outline, budget: THRESHOLDS.MAX_OUTLINE_BYTES },
375
+ ];
376
+
377
+ for (const { level, budget } of tiers) {
378
+ const reduced = reduceToLevel(map, level);
379
+ const formatted = formatFileMap(reduced, level);
380
+ const size = Buffer.byteLength(formatted, "utf8");
381
+
382
+ if (size <= budget && size <= maxBytes) {
383
+ return formatted;
384
+ }
385
+ }
386
+
387
+ // Outline exceeded budget - need to truncate symbols
388
+ // First check if full outline fits in maxBytes (just not in outline budget)
389
+ const outline = reduceToLevel(map, DetailLevel.Outline);
390
+ const outlineFormatted = formatFileMap(outline, DetailLevel.Outline);
391
+ const outlineSize = Buffer.byteLength(outlineFormatted, "utf8");
392
+
393
+ if (outlineSize <= maxBytes) {
394
+ // Outline fits in truncated budget, use it
395
+ return outlineFormatted;
396
+ }
397
+
398
+ // Need to truncate - binary search for maximum symbols that fit
399
+ const totalSymbols = map.symbols.length;
400
+ const minSymbols = 10; // Guaranteed minimum
401
+ const maxSymbolsEach = Math.floor(totalSymbols / 2); // Can't show more than half on each side
402
+
403
+ let low = minSymbols;
404
+ let high = maxSymbolsEach;
405
+ let bestResult: string | null = null;
406
+
407
+ while (low <= high) {
408
+ const mid = Math.floor((low + high) / 2);
409
+ const truncated = reduceToTruncated(map, mid);
410
+ const formatted = formatFileMap(truncated, DetailLevel.Truncated);
411
+ const size = Buffer.byteLength(formatted, "utf8");
412
+
413
+ if (size <= maxBytes) {
414
+ // This fits, try to show more
415
+ bestResult = formatted;
416
+ low = mid + 1;
417
+ } else {
418
+ // Too big, show fewer
419
+ high = mid - 1;
420
+ }
421
+ }
422
+
423
+ // Return best result or fallback to minimum
424
+ if (bestResult) {
425
+ return bestResult;
426
+ }
427
+
428
+ // Absolute fallback: minimum symbols (guaranteed to fit)
429
+ const minimal = reduceToTruncated(map, minSymbols);
430
+ return formatFileMap(minimal, DetailLevel.Truncated);
431
+ }
@@ -0,0 +1,29 @@
1
+ import type { LanguageInfo } from "./types.js";
2
+
3
+ const EXTENSION_LANGUAGE_MAP: Record<string, LanguageInfo> = {
4
+ ".rs": { id: "rust", name: "Rust" },
5
+ ".c": { id: "cpp", name: "C" },
6
+ ".cc": { id: "cpp", name: "C++" },
7
+ ".cpp": { id: "cpp", name: "C++" },
8
+ ".cxx": { id: "cpp", name: "C++" },
9
+ ".hpp": { id: "c-header", name: "C/C++ Header" },
10
+ ".h": { id: "c-header", name: "C/C++ Header" },
11
+ ".hxx": { id: "c-header", name: "C/C++ Header" },
12
+ ".java": { id: "java", name: "Java" },
13
+ };
14
+
15
+ export function detectLanguage(filePath: string): LanguageInfo | null {
16
+ const normalized = filePath.toLowerCase();
17
+ for (const [ext, language] of Object.entries(EXTENSION_LANGUAGE_MAP)) {
18
+ if (normalized.endsWith(ext)) return language;
19
+ }
20
+ return null;
21
+ }
22
+
23
+ export function isSupported(filePath: string): boolean {
24
+ return detectLanguage(filePath) !== null;
25
+ }
26
+
27
+ export function getSupportedExtensions(): string[] {
28
+ return Object.keys(EXTENSION_LANGUAGE_MAP);
29
+ }
@@ -0,0 +1,69 @@
1
+ import { stat } from "node:fs/promises";
2
+
3
+ import { readseekMap, readseekMapContent } from "../readseek-client.js";
4
+ import { THRESHOLDS } from "./constants.js";
5
+ import type { FileMap, MapOptions } from "./types.js";
6
+
7
+ export const READSEEK_MAPPER_NAME = "readseek";
8
+ export const READSEEK_MAPPER_VERSION = 1;
9
+
10
+ export interface MapperIdentity {
11
+ mapperName: string;
12
+ mapperVersion: number;
13
+ }
14
+
15
+ export interface MapResultWithIdentity extends MapperIdentity {
16
+ map: FileMap | null;
17
+ }
18
+
19
+ export const READSEEK_MAPPER_IDENTITY: MapperIdentity = {
20
+ mapperName: READSEEK_MAPPER_NAME,
21
+ mapperVersion: READSEEK_MAPPER_VERSION,
22
+ };
23
+
24
+ export const ALL_MAPPER_IDENTITIES: Record<string, MapperIdentity> = {
25
+ readseek: READSEEK_MAPPER_IDENTITY,
26
+ };
27
+
28
+ function throwIfAborted(signal?: AbortSignal): void {
29
+ if (!signal?.aborted) return;
30
+ const reason = signal.reason;
31
+ throw reason instanceof Error ? reason : new Error("aborted");
32
+ }
33
+
34
+ export async function generateMapWithIdentity(
35
+ filePath: string,
36
+ options: MapOptions = {},
37
+ ): Promise<MapResultWithIdentity> {
38
+ throwIfAborted(options.signal);
39
+ const fileStat = await stat(filePath);
40
+ throwIfAborted(options.signal);
41
+ const map = await readseekMap(filePath, fileStat.size);
42
+ throwIfAborted(options.signal);
43
+ return { map, ...READSEEK_MAPPER_IDENTITY };
44
+ }
45
+
46
+ export async function generateMap(
47
+ filePath: string,
48
+ options: MapOptions = {},
49
+ ): Promise<FileMap | null> {
50
+ return (await generateMapWithIdentity(filePath, options)).map;
51
+ }
52
+
53
+ export async function generateMapFromContent(
54
+ filePath: string,
55
+ content: string,
56
+ options: MapOptions = {},
57
+ ): Promise<FileMap | null> {
58
+ throwIfAborted(options.signal);
59
+ const map = await readseekMapContent(filePath, content);
60
+ throwIfAborted(options.signal);
61
+ return map;
62
+ }
63
+
64
+ export function shouldGenerateMap(
65
+ totalLines: number,
66
+ totalBytes: number,
67
+ ): boolean {
68
+ return totalLines > THRESHOLDS.MAX_LINES || totalBytes > THRESHOLDS.MAX_BYTES;
69
+ }
@@ -0,0 +1,22 @@
1
+ const emitted = new Set<string>();
2
+
3
+ function causeMessage(err: unknown): string {
4
+ if (err instanceof Error) return err.message;
5
+ return String(err);
6
+ }
7
+
8
+ export function reportParserError(
9
+ onceKey: string,
10
+ err: unknown,
11
+ options: { context?: string } = {},
12
+ ): void {
13
+ if (!process.env.PI_READSEEK_DEBUG) return;
14
+ if (emitted.has(onceKey)) return;
15
+ emitted.add(onceKey);
16
+ const context = options.context ?? onceKey;
17
+ console.error(`[readseek] ${context}: ${causeMessage(err)}`);
18
+ }
19
+
20
+ export function __resetParserErrorReporterForTests(): void {
21
+ emitted.clear();
22
+ }
@@ -0,0 +1,83 @@
1
+ import { createRequire } from "node:module";
2
+ import { dirname, join } from "node:path";
3
+ import { Language, Parser } from "web-tree-sitter";
4
+ import { reportParserError } from "./parser-errors.js";
5
+
6
+ export type WasmLanguageId = "rust" | "cpp" | "c-header" | "java";
7
+ export type WasmParser = Parser;
8
+
9
+ const require_ = createRequire(import.meta.url);
10
+ const wasmNames: Record<WasmLanguageId, string> = {
11
+ rust: "rust",
12
+ cpp: "cpp",
13
+ "c-header": "cpp",
14
+ java: "java",
15
+ };
16
+ let initPromise: Promise<void> | null = null;
17
+ const languages = new Map<WasmLanguageId, Language>();
18
+ const languageLoads = new Map<WasmLanguageId, Promise<Language | null>>();
19
+
20
+ function isBun(): boolean {
21
+ return typeof (globalThis as { Bun?: unknown }).Bun !== "undefined";
22
+ }
23
+
24
+ function wasmPath(langId: WasmLanguageId): string {
25
+ const pkg = require_.resolve("tree-sitter-wasms/package.json");
26
+ return join(dirname(pkg), "out", `tree-sitter-${wasmNames[langId]}.wasm`);
27
+ }
28
+
29
+ async function init(): Promise<void> {
30
+ initPromise ??= Parser.init().catch((err: unknown) => {
31
+ initPromise = null;
32
+ reportParserError("wasm:init", err, { context: "web-tree-sitter initialization failed" });
33
+ throw err;
34
+ });
35
+ return initPromise;
36
+ }
37
+
38
+ async function language(langId: WasmLanguageId): Promise<Language | null> {
39
+ const loaded = languages.get(langId);
40
+ if (loaded) return loaded;
41
+
42
+ const inFlight = languageLoads.get(langId);
43
+ if (inFlight) return inFlight;
44
+
45
+ const loadPromise = (async () => {
46
+ try {
47
+ await init();
48
+ const lang = await Language.load(wasmPath(langId));
49
+ languages.set(langId, lang);
50
+ return lang;
51
+ } catch (err) {
52
+ reportParserError(`wasm:load:${langId}`, err, {
53
+ context: `tree-sitter WASM grammar load failed for ${langId}`,
54
+ });
55
+ return null;
56
+ } finally {
57
+ languageLoads.delete(langId);
58
+ }
59
+ })();
60
+
61
+ languageLoads.set(langId, loadPromise);
62
+ return loadPromise;
63
+ }
64
+
65
+ export async function getWasmParser(langId: WasmLanguageId): Promise<WasmParser | null> {
66
+ if (isBun()) return null;
67
+ const lang = await language(langId);
68
+ if (!lang) return null;
69
+ try {
70
+ const parser = new Parser();
71
+ parser.setLanguage(lang);
72
+ return parser;
73
+ } catch (err) {
74
+ reportParserError(`wasm:parser:${langId}`, err, { context: `tree-sitter WASM parser creation failed for ${langId}` });
75
+ return null;
76
+ }
77
+ }
78
+
79
+ export function __resetWasmParserLoaderForTests(): void {
80
+ initPromise = null;
81
+ languages.clear();
82
+ languageLoads.clear();
83
+ }