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.
- package/LICENSE +22 -0
- package/README.md +41 -0
- package/index.ts +142 -0
- package/package.json +73 -0
- package/prompts/edit.md +113 -0
- package/prompts/find.md +19 -0
- package/prompts/grep.md +26 -0
- package/prompts/ls.md +11 -0
- package/prompts/read.md +33 -0
- package/prompts/sg.md +25 -0
- package/prompts/write.md +46 -0
- package/src/binary-detect.ts +22 -0
- package/src/binary-resolution.ts +77 -0
- package/src/coerce-obvious-int.ts +39 -0
- package/src/context-application.ts +70 -0
- package/src/context-hygiene.ts +503 -0
- package/src/diff-data.ts +303 -0
- package/src/doom-loop-suggestions.ts +42 -0
- package/src/doom-loop.ts +216 -0
- package/src/edit-classify.ts +190 -0
- package/src/edit-diff.ts +354 -0
- package/src/edit-output.ts +107 -0
- package/src/edit-render-helpers.ts +141 -0
- package/src/edit-syntax-validate.ts +120 -0
- package/src/edit.ts +725 -0
- package/src/find-parsers.ts +89 -0
- package/src/find-stat.ts +36 -0
- package/src/find.ts +613 -0
- package/src/grep-budget.ts +79 -0
- package/src/grep-output.ts +197 -0
- package/src/grep-render-helpers.ts +77 -0
- package/src/grep-symbol-scope.ts +197 -0
- package/src/grep.ts +792 -0
- package/src/hashline.ts +747 -0
- package/src/ls.ts +293 -0
- package/src/map-cache.ts +152 -0
- package/src/path-utils.ts +24 -0
- package/src/pending-diff-preview.ts +269 -0
- package/src/persistent-map-cache.ts +251 -0
- package/src/read-local-bundle.ts +87 -0
- package/src/read-output.ts +212 -0
- package/src/read-render-helpers.ts +104 -0
- package/src/read.ts +748 -0
- package/src/readseek/constants.ts +21 -0
- package/src/readseek/enums.ts +38 -0
- package/src/readseek/formatter.ts +431 -0
- package/src/readseek/language-detect.ts +29 -0
- package/src/readseek/mapper.ts +69 -0
- package/src/readseek/parser-errors.ts +22 -0
- package/src/readseek/parser-loader.ts +83 -0
- package/src/readseek/symbol-error-format.ts +18 -0
- package/src/readseek/symbol-lookup.ts +294 -0
- package/src/readseek/types.ts +79 -0
- package/src/readseek-client.ts +343 -0
- package/src/readseek-error-codes.ts +54 -0
- package/src/readseek-settings.ts +287 -0
- package/src/readseek-value.ts +144 -0
- package/src/replace-symbol.ts +74 -0
- package/src/runtime.ts +3 -0
- package/src/sg-output.ts +88 -0
- package/src/sg.ts +308 -0
- package/src/syntax-validate-mode.ts +25 -0
- package/src/tool-prompt-metadata.ts +76 -0
- package/src/tui-diff-component.ts +86 -0
- package/src/tui-diff-renderer.ts +92 -0
- package/src/tui-render-utils.ts +129 -0
- 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
|
+
}
|