pi-readseek 0.3.19 → 0.3.20
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/package.json +1 -1
- package/src/edit.ts +425 -409
- package/src/grep.ts +336 -474
- package/src/read.ts +358 -356
- package/src/readseek-value.ts +10 -2
- package/src/refs.ts +9 -37
- package/src/sg.ts +9 -39
- package/src/tui-render-utils.ts +55 -1
package/src/readseek-value.ts
CHANGED
|
@@ -55,8 +55,12 @@ export interface ReadseekEditResult {
|
|
|
55
55
|
semanticSummary?: SemanticSummary;
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
-
|
|
59
|
-
|
|
58
|
+
/**
|
|
59
|
+
* Build a {@link ReadseekLine} from an already-known hash. Use this when the
|
|
60
|
+
* hash is supplied by readseek (search, refs) rather than computed from `raw`;
|
|
61
|
+
* {@link buildReadseekLine} delegates here after hashing.
|
|
62
|
+
*/
|
|
63
|
+
export function buildReadseekLineWithHash(line: number, hash: string, raw: string): ReadseekLine {
|
|
60
64
|
return {
|
|
61
65
|
line,
|
|
62
66
|
hash,
|
|
@@ -66,6 +70,10 @@ export function buildReadseekLine(line: number, raw: string): ReadseekLine {
|
|
|
66
70
|
};
|
|
67
71
|
}
|
|
68
72
|
|
|
73
|
+
export function buildReadseekLine(line: number, raw: string): ReadseekLine {
|
|
74
|
+
return buildReadseekLineWithHash(line, computeLineHash(line, raw), raw);
|
|
75
|
+
}
|
|
76
|
+
|
|
69
77
|
export function buildReadseekLines(startLine: number, rawLines: string[]): ReadseekLine[] {
|
|
70
78
|
return rawLines.map((raw, index) => buildReadseekLine(startLine + index, raw));
|
|
71
79
|
}
|
package/src/refs.ts
CHANGED
|
@@ -4,13 +4,12 @@ import { Type } from "@sinclair/typebox";
|
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { stat as fsStat } from "node:fs/promises";
|
|
6
6
|
import { defineToolPromptMetadata } from "./tool-prompt-metadata.js";
|
|
7
|
-
import {
|
|
8
|
-
import { buildToolErrorResult } from "./readseek-value.js";
|
|
7
|
+
import { buildReadseekLineWithHash, buildToolErrorResult } from "./readseek-value.js";
|
|
9
8
|
import { resolveToCwd } from "./path-utils.js";
|
|
10
9
|
import { isReadseekAvailable, readseekRefs, type ReadseekReference } from "./readseek-client.js";
|
|
11
10
|
import { buildRefsOutput, type RefsOutputFile, type RefsOutputLine } from "./refs-output.js";
|
|
12
11
|
|
|
13
|
-
import { clampLineToWidth,
|
|
12
|
+
import { clampLineToWidth, renderAnchoredFilesResult, renderToolLabel } from "./tui-render-utils.js";
|
|
14
13
|
|
|
15
14
|
type RefsParams = {
|
|
16
15
|
name: string;
|
|
@@ -39,11 +38,7 @@ interface RefsToolOptions {
|
|
|
39
38
|
|
|
40
39
|
function refsLine(reference: ReadseekReference): RefsOutputLine {
|
|
41
40
|
return {
|
|
42
|
-
line
|
|
43
|
-
hash: reference.line_hash,
|
|
44
|
-
anchor: `${reference.line}:${reference.line_hash}`,
|
|
45
|
-
raw: reference.text,
|
|
46
|
-
display: escapeControlCharsForDisplay(reference.text),
|
|
41
|
+
...buildReadseekLineWithHash(reference.line, reference.line_hash, reference.text),
|
|
47
42
|
enclosingSymbol: reference.enclosingSymbol,
|
|
48
43
|
};
|
|
49
44
|
}
|
|
@@ -165,35 +160,12 @@ export function registerRefsTool(pi: ExtensionAPI, options: RefsToolOptions = {}
|
|
|
165
160
|
return new Text(clampLineToWidth(text, context.width), 0, 0);
|
|
166
161
|
},
|
|
167
162
|
renderResult(result: any, options: ToolRenderResultOptions, theme: any, ...rest: any[]) {
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
if (isError || result.isError) {
|
|
175
|
-
const firstLine = textContent.split("\n")[0] || "Error";
|
|
176
|
-
const body = expanded && textContent ? textContent : firstLine;
|
|
177
|
-
return new Text(clampLinesToWidth(summaryLine(body).split("\n"), width).join("\n"), 0, 0);
|
|
178
|
-
}
|
|
179
|
-
const readseekValue = (result.details as any)?.readseekValue as
|
|
180
|
-
| { tool: "refs"; files: Array<{ path: string; lines: any[] }> }
|
|
181
|
-
| undefined;
|
|
182
|
-
const files = readseekValue?.files ?? [];
|
|
183
|
-
if (files.length === 0) return new Text(summaryLine("no references"), 0, 0);
|
|
184
|
-
const fileCount = files.length;
|
|
185
|
-
const totalRefs = files.reduce((sum: number, f: any) => sum + f.lines.length, 0);
|
|
186
|
-
const refWord = totalRefs === 1 ? "reference" : "references";
|
|
187
|
-
const fileWord = fileCount === 1 ? "file" : "files";
|
|
188
|
-
let text = summaryLine(`${totalRefs} ${refWord} in ${fileCount} ${fileWord}`, { hidden: !expanded });
|
|
189
|
-
if (expanded) {
|
|
190
|
-
for (const file of files.slice(0, 20)) {
|
|
191
|
-
const display = path.relative(cwd, file.path) || file.path;
|
|
192
|
-
text += "\n" + theme.fg("dim", ` ${display} (${file.lines.length})`);
|
|
193
|
-
}
|
|
194
|
-
if (files.length > 20) text += "\n" + theme.fg("muted", ` … and ${files.length - 20} more files`);
|
|
195
|
-
}
|
|
196
|
-
return new Text(clampLinesToWidth(text.split("\n"), width).join("\n"), 0, 0);
|
|
163
|
+
return renderAnchoredFilesResult(result, options, theme, rest, {
|
|
164
|
+
pendingLabel: "pending refs",
|
|
165
|
+
emptyLabel: "no references",
|
|
166
|
+
unitSingular: "reference",
|
|
167
|
+
unitPlural: "references",
|
|
168
|
+
});
|
|
197
169
|
},
|
|
198
170
|
} satisfies Parameters<ExtensionAPI["registerTool"]>[0] & { ptc: typeof toolConfig };
|
|
199
171
|
|
package/src/sg.ts
CHANGED
|
@@ -4,13 +4,12 @@ import { Type } from "@sinclair/typebox";
|
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { stat as fsStat } from "node:fs/promises";
|
|
6
6
|
import { defineToolPromptMetadata } from "./tool-prompt-metadata.js";
|
|
7
|
-
import {
|
|
8
|
-
import { buildToolErrorResult, type ReadseekLine } from "./readseek-value.js";
|
|
7
|
+
import { buildReadseekLineWithHash, buildToolErrorResult, type ReadseekLine } from "./readseek-value.js";
|
|
9
8
|
import { resolveToCwd } from "./path-utils.js";
|
|
10
9
|
import { isReadseekAvailable, readseekSearch, type ReadseekHashline, type ReadseekSearchFileOutput } from "./readseek-client.js";
|
|
11
10
|
import { buildSgOutput } from "./sg-output.js";
|
|
12
11
|
|
|
13
|
-
import { clampLineToWidth,
|
|
12
|
+
import { clampLineToWidth, renderAnchoredFilesResult, renderToolLabel } from "./tui-render-utils.js";
|
|
14
13
|
|
|
15
14
|
type SgParams = { pattern: string; lang?: string; path?: string; cached?: boolean; others?: boolean; ignored?: boolean };
|
|
16
15
|
|
|
@@ -58,13 +57,7 @@ interface SgToolOptions {
|
|
|
58
57
|
}
|
|
59
58
|
|
|
60
59
|
function readseekLineFromSearch(line: ReadseekHashline): ReadseekLine {
|
|
61
|
-
return
|
|
62
|
-
line: line.line,
|
|
63
|
-
hash: line.hash,
|
|
64
|
-
anchor: `${line.line}:${line.hash}`,
|
|
65
|
-
raw: line.text,
|
|
66
|
-
display: escapeControlCharsForDisplay(line.text),
|
|
67
|
-
};
|
|
60
|
+
return buildReadseekLineWithHash(line.line, line.hash, line.text);
|
|
68
61
|
}
|
|
69
62
|
|
|
70
63
|
function linesFromSearchResult(result: ReadseekSearchFileOutput, ranges: SgRange[]): ReadseekLine[] {
|
|
@@ -230,35 +223,12 @@ export function registerSgTool(pi: ExtensionAPI, options: SgToolOptions = {}) {
|
|
|
230
223
|
return new Text(clampLineToWidth(text, context.width), 0, 0);
|
|
231
224
|
},
|
|
232
225
|
renderResult(result: any, options: ToolRenderResultOptions, theme: any, ...rest: any[]) {
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
if (isError || result.isError) {
|
|
240
|
-
const firstLine = textContent.split("\n")[0] || "Error";
|
|
241
|
-
const body = expanded && textContent ? textContent : firstLine;
|
|
242
|
-
return new Text(clampLinesToWidth(summaryLine(body).split("\n"), width).join("\n"), 0, 0);
|
|
243
|
-
}
|
|
244
|
-
const readseekValue = (result.details as any)?.readseekValue as
|
|
245
|
-
| { tool: "search"; files: Array<{ path: string; lines: any[] }> }
|
|
246
|
-
| undefined;
|
|
247
|
-
const files = readseekValue?.files ?? [];
|
|
248
|
-
if (files.length === 0) return new Text(summaryLine("no matches"), 0, 0);
|
|
249
|
-
const fileCount = files.length;
|
|
250
|
-
const totalMatches = files.reduce((sum: number, f: any) => sum + f.lines.length, 0);
|
|
251
|
-
const matchWord = totalMatches === 1 ? "match" : "matches";
|
|
252
|
-
const fileWord = fileCount === 1 ? "file" : "files";
|
|
253
|
-
let text = summaryLine(`${totalMatches} ${matchWord} in ${fileCount} ${fileWord}`, { hidden: files.length > 0 && !expanded });
|
|
254
|
-
if (expanded) {
|
|
255
|
-
for (const file of files.slice(0, 20)) {
|
|
256
|
-
const display = path.relative(cwd, file.path) || file.path;
|
|
257
|
-
text += "\n" + theme.fg("dim", ` ${display} (${file.lines.length})`);
|
|
258
|
-
}
|
|
259
|
-
if (files.length > 20) text += "\n" + theme.fg("muted", ` … and ${files.length - 20} more files`);
|
|
260
|
-
}
|
|
261
|
-
return new Text(clampLinesToWidth(text.split("\n"), width).join("\n"), 0, 0);
|
|
226
|
+
return renderAnchoredFilesResult(result, options, theme, rest, {
|
|
227
|
+
pendingLabel: "pending search",
|
|
228
|
+
emptyLabel: "no matches",
|
|
229
|
+
unitSingular: "match",
|
|
230
|
+
unitPlural: "matches",
|
|
231
|
+
});
|
|
262
232
|
},
|
|
263
233
|
} satisfies Parameters<ExtensionAPI["registerTool"]>[0] & { ptc: typeof toolConfig };
|
|
264
234
|
|
package/src/tui-render-utils.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { getCapabilities, hyperlink, truncateToWidth, visibleWidth, wrapTextWithAnsi } from "@earendil-works/pi-tui";
|
|
1
|
+
import { getCapabilities, hyperlink, Text, truncateToWidth, visibleWidth, wrapTextWithAnsi } from "@earendil-works/pi-tui";
|
|
2
|
+
import { relative } from "node:path";
|
|
2
3
|
import { pathToFileURL } from "node:url";
|
|
3
4
|
import { resolveToCwd } from "./path-utils.js";
|
|
4
5
|
|
|
@@ -154,3 +155,56 @@ export function wrapReadHashlinesForWidth(text: string, width: number | undefine
|
|
|
154
155
|
}
|
|
155
156
|
return output.join("\n");
|
|
156
157
|
}
|
|
158
|
+
|
|
159
|
+
export interface AnchoredFilesLabels {
|
|
160
|
+
pendingLabel: string;
|
|
161
|
+
emptyLabel: string;
|
|
162
|
+
unitSingular: string;
|
|
163
|
+
unitPlural: string;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Render the result summary shared by the anchored-files search tools (search,
|
|
168
|
+
* refs): a pending line, an error first-line/expanded body, an empty-result
|
|
169
|
+
* line, or a `<count> <unit> in <n> files` summary with an expandable per-file
|
|
170
|
+
* list. The four call-site differences are supplied via {@link labels}.
|
|
171
|
+
*/
|
|
172
|
+
export function renderAnchoredFilesResult(
|
|
173
|
+
result: any,
|
|
174
|
+
options: any,
|
|
175
|
+
theme: RendererTheme,
|
|
176
|
+
rest: any[],
|
|
177
|
+
labels: AnchoredFilesLabels,
|
|
178
|
+
): Text {
|
|
179
|
+
const { isPartial, isError, expanded, cwd, width } = resolveRenderResultContext(options, rest);
|
|
180
|
+
|
|
181
|
+
if (isPartial) return new Text(clampLinesToWidth([summaryLine(labels.pendingLabel)], width).join("\n"), 0, 0);
|
|
182
|
+
|
|
183
|
+
const content = result.content?.[0];
|
|
184
|
+
const textContent = content?.type === "text" ? content.text : "";
|
|
185
|
+
if (isError || result.isError) {
|
|
186
|
+
const firstLine = textContent.split("\n")[0] || "Error";
|
|
187
|
+
const body = expanded && textContent ? textContent : firstLine;
|
|
188
|
+
return new Text(clampLinesToWidth(summaryLine(body).split("\n"), width).join("\n"), 0, 0);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const readseekValue = (result.details as any)?.readseekValue as
|
|
192
|
+
| { files: Array<{ path: string; lines: any[] }> }
|
|
193
|
+
| undefined;
|
|
194
|
+
const files = readseekValue?.files ?? [];
|
|
195
|
+
if (files.length === 0) return new Text(summaryLine(labels.emptyLabel), 0, 0);
|
|
196
|
+
|
|
197
|
+
const fileCount = files.length;
|
|
198
|
+
const total = files.reduce((sum: number, f: any) => sum + f.lines.length, 0);
|
|
199
|
+
const unitWord = total === 1 ? labels.unitSingular : labels.unitPlural;
|
|
200
|
+
const fileWord = fileCount === 1 ? "file" : "files";
|
|
201
|
+
let text = summaryLine(`${total} ${unitWord} in ${fileCount} ${fileWord}`, { hidden: !expanded });
|
|
202
|
+
if (expanded) {
|
|
203
|
+
for (const file of files.slice(0, 20)) {
|
|
204
|
+
const display = relative(cwd, file.path) || file.path;
|
|
205
|
+
text += "\n" + theme.fg("dim", ` ${display} (${file.lines.length})`);
|
|
206
|
+
}
|
|
207
|
+
if (files.length > 20) text += "\n" + theme.fg("muted", ` … and ${files.length - 20} more files`);
|
|
208
|
+
}
|
|
209
|
+
return new Text(clampLinesToWidth(text.split("\n"), width).join("\n"), 0, 0);
|
|
210
|
+
}
|