pi-hashline-edit-pro 6.2.0 → 6.3.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/README.md +28 -12
- package/package.json +1 -1
- package/src/anchor-registry.ts +22 -5
- package/src/auto-read-all.ts +1 -1
- package/src/batch.ts +127 -41
- package/src/commit.ts +1 -1
- package/src/copy-move.ts +57 -68
- package/src/edit-common.ts +6 -4
- package/src/grep.ts +50 -118
- package/src/hash-store.ts +13 -12
- package/src/hashline/resolve.ts +13 -2
- package/src/insert.ts +37 -31
- package/src/line-endings.ts +10 -1
- package/src/outline/index.ts +1 -0
- package/src/outline/render.ts +6 -2
- package/src/read.ts +17 -12
- package/src/replace-match.ts +39 -38
- package/src/replace-undo.ts +1 -1
- package/src/replace.ts +27 -16
- package/tool-prompts/read-guidelines.md +2 -2
package/src/insert.ts
CHANGED
|
@@ -3,9 +3,9 @@ import { Type } from "typebox";
|
|
|
3
3
|
import { constants } from "node:fs";
|
|
4
4
|
import { execPipeline, type ReplaceDetails, previewFromPipe, previewError } from "./replace";
|
|
5
5
|
import { commitEdit } from "./commit";
|
|
6
|
-
import { batchMemberFor,
|
|
6
|
+
import { batchMemberFor, executeBatchMember, noteBatchFailure, registerBatchPlanner, type BatchMemberPlan, type BatchPlannerInput } from "./batch";
|
|
7
7
|
import { readNormFile, type NormFile } from "./file-reader";
|
|
8
|
-
import { MAX_HASH_LINES, parseHashRef, parsePayloadText, resEdit, resolveAnchorLine, type Anchor, type
|
|
8
|
+
import { MAX_HASH_LINES, parseHashRef, parsePayloadText, planEdit, preserveDeletionSeparators, resEdit, resolveAnchorLine, type Anchor, type HTEdit, type StripWarningLocation } from "./hashline";
|
|
9
9
|
import type { LineEnding } from "./normalize";
|
|
10
10
|
import { stripAnchorRow } from "./hashline/resolve";
|
|
11
11
|
import { withAnchorSession } from "./anchor-registry";
|
|
@@ -97,6 +97,37 @@ function insertStripWarning(anchorLine: string | undefined, direction: "before"
|
|
|
97
97
|
return { label: "text", indexOffset: anchorLine !== undefined && direction === "after" ? -1 : 0 };
|
|
98
98
|
}
|
|
99
99
|
|
|
100
|
+
function planInsertBatchMember(input: BatchPlannerInput): BatchMemberPlan {
|
|
101
|
+
const req = normReq(input.member.args);
|
|
102
|
+
assertInsertReq(req);
|
|
103
|
+
const insertWarnings = literalEscapeHints([req.text], "text");
|
|
104
|
+
const { ref, warnings: anchorWarnings } = parseInsertAnchor(req.anchor);
|
|
105
|
+
const basePreload = { normalized: input.base.content, fileHashes: input.base.hashes } as NormFile;
|
|
106
|
+
const built = buildInsertEdit(req, basePreload, ref, input.displayPath);
|
|
107
|
+
const resWarnings: string[] = [];
|
|
108
|
+
const hedit = resEdit(built.editParams, resWarnings);
|
|
109
|
+
const stripWarning = insertStripWarning(built.anchorLine, req.direction);
|
|
110
|
+
const effectiveHedit = preserveDeletionSeparators(hedit, input.base.baseLines, input.base.hashes);
|
|
111
|
+
const planned = planEdit(input.base.content, effectiveHedit, input.base.hashes, {
|
|
112
|
+
filePath: input.displayPath,
|
|
113
|
+
servedHashes: input.served,
|
|
114
|
+
signal: input.signal,
|
|
115
|
+
baseFileLines: input.base.baseLines,
|
|
116
|
+
stripWarning,
|
|
117
|
+
});
|
|
118
|
+
return {
|
|
119
|
+
hedit,
|
|
120
|
+
warnings: [...anchorWarnings, ...insertWarnings, ...resWarnings],
|
|
121
|
+
planned,
|
|
122
|
+
foldedLines: built.anchorLine === undefined ? 0 : 1,
|
|
123
|
+
direction: req.direction,
|
|
124
|
+
stripWarning,
|
|
125
|
+
contentSeparators: built.contentSeparators,
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
registerBatchPlanner("insert", planInsertBatchMember);
|
|
130
|
+
|
|
100
131
|
export async function insertPreview(request: unknown, cwd: string, signal?: AbortSignal): Promise<RPreview> {
|
|
101
132
|
try {
|
|
102
133
|
const normalized = normReq(request);
|
|
@@ -180,12 +211,13 @@ export function buildInsertToolDef(flags: EditToolFlags = DEFAULT_EDIT_FLAGS): I
|
|
|
180
211
|
assertInsertReq(canonical);
|
|
181
212
|
const req = canonical;
|
|
182
213
|
const insertWarnings: string[] = [...literalEscapeHints([req.text], "text")];
|
|
214
|
+
const member = batchMemberFor(_toolCallId);
|
|
183
215
|
const targetPath = await resolveEditTargetWithRequirement({
|
|
184
216
|
anchor: req.anchor,
|
|
185
217
|
providedPath: req.path,
|
|
186
218
|
cwd: ctx.cwd,
|
|
219
|
+
anchorTarget: member?.target,
|
|
187
220
|
}).catch((error: unknown) => {
|
|
188
|
-
const member = batchMemberFor(_toolCallId);
|
|
189
221
|
if (member) noteBatchFailure(member, error);
|
|
190
222
|
throw error;
|
|
191
223
|
});
|
|
@@ -193,40 +225,14 @@ export function buildInsertToolDef(flags: EditToolFlags = DEFAULT_EDIT_FLAGS): I
|
|
|
193
225
|
let anchorWarnings: string[];
|
|
194
226
|
try {
|
|
195
227
|
({ ref, warnings: anchorWarnings } = parseInsertAnchor(req.anchor));
|
|
196
|
-
await throwIfStrictInput([...anchorWarnings, ...insertWarnings]);
|
|
228
|
+
if (member === undefined) await throwIfStrictInput([...anchorWarnings, ...insertWarnings]);
|
|
197
229
|
} catch (error) {
|
|
198
|
-
const member = batchMemberFor(_toolCallId);
|
|
199
230
|
if (member) noteBatchFailure(member, error);
|
|
200
231
|
throw error;
|
|
201
232
|
}
|
|
202
233
|
return queuedEdit(targetPath, ctx.cwd, signal, async (absolutePath, mutationTargetPath) => {
|
|
203
|
-
const member = batchMemberFor(_toolCallId);
|
|
204
234
|
if (member) {
|
|
205
|
-
|
|
206
|
-
const basePreload = { normalized: base.content, fileHashes: base.hashes } as NormFile;
|
|
207
|
-
const built = buildInsertEdit(req, basePreload, ref, targetPath);
|
|
208
|
-
let hedit: HEdit;
|
|
209
|
-
const resWarnings: string[] = [];
|
|
210
|
-
try {
|
|
211
|
-
hedit = resEdit(built.editParams, resWarnings);
|
|
212
|
-
} catch (error) {
|
|
213
|
-
noteBatchFailure(member, error);
|
|
214
|
-
throw error;
|
|
215
|
-
}
|
|
216
|
-
return executeBatchMember({
|
|
217
|
-
kind: "insert",
|
|
218
|
-
direction: req.direction,
|
|
219
|
-
member,
|
|
220
|
-
targetPath,
|
|
221
|
-
mutationTargetPath,
|
|
222
|
-
cwd: ctx.cwd,
|
|
223
|
-
signal,
|
|
224
|
-
hedit,
|
|
225
|
-
extraWarnings: [...anchorWarnings, ...insertWarnings, ...resWarnings],
|
|
226
|
-
foldedLines: built.anchorLine === undefined ? 0 : 1,
|
|
227
|
-
stripWarning: insertStripWarning(built.anchorLine, req.direction),
|
|
228
|
-
contentSeparators: built.contentSeparators,
|
|
229
|
-
});
|
|
235
|
+
return executeBatchMember({ member, targetPath, mutationTargetPath, cwd: ctx.cwd, signal });
|
|
230
236
|
}
|
|
231
237
|
const preload = await readNormFile(targetPath, ctx.cwd, {
|
|
232
238
|
signal,
|
package/src/line-endings.ts
CHANGED
|
@@ -75,9 +75,18 @@ export function separatorsForSpans(
|
|
|
75
75
|
spans: readonly EndingSpan[],
|
|
76
76
|
resultContent: string,
|
|
77
77
|
fallback: LineEnding,
|
|
78
|
+
baseLines?: readonly string[],
|
|
78
79
|
): LineEnding[] {
|
|
80
|
+
const consumed = new Set<number>();
|
|
81
|
+
if (baseLines !== undefined && baseSeparators.length < baseLineCount) {
|
|
82
|
+
for (const span of spans) {
|
|
83
|
+
if (span.replacementCount !== 0 || span.end !== baseLineCount - 1) continue;
|
|
84
|
+
const previous = baseLines[span.start - 1];
|
|
85
|
+
if (previous !== undefined && previous.trim().length > 0) consumed.add(span.start - 1);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
79
88
|
const mapped = sourceLineMap(baseLineCount, spans).map((source) =>
|
|
80
|
-
source >= 0 && source < baseSeparators.length ? baseSeparators[source]! : "",
|
|
89
|
+
source >= 0 && source < baseSeparators.length && !consumed.has(source) ? baseSeparators[source]! : "",
|
|
81
90
|
);
|
|
82
91
|
const needed = countNewlines(resultContent);
|
|
83
92
|
const result: LineEnding[] = [];
|
package/src/outline/index.ts
CHANGED
|
@@ -23,6 +23,7 @@ export async function buildFileOutline(input: FileOutlineInput): Promise<Outline
|
|
|
23
23
|
languageName: parsed.languageName,
|
|
24
24
|
totalLines: lines.length,
|
|
25
25
|
hashes: input.hashes,
|
|
26
|
+
lines,
|
|
26
27
|
symbols: parsed.symbols,
|
|
27
28
|
...(input.maxRows !== undefined ? { maxRows: input.maxRows } : {}),
|
|
28
29
|
...(input.maxDepth !== undefined ? { maxDepth: input.maxDepth } : {}),
|
package/src/outline/render.ts
CHANGED
|
@@ -27,6 +27,7 @@ export interface SymbolOutlineInput {
|
|
|
27
27
|
languageName?: string;
|
|
28
28
|
totalLines: number;
|
|
29
29
|
hashes: string[];
|
|
30
|
+
lines: string[];
|
|
30
31
|
symbols: OutlineSymbol[];
|
|
31
32
|
maxRows?: number;
|
|
32
33
|
maxDepth?: number;
|
|
@@ -62,7 +63,8 @@ function symbolLabel(symbol: OutlineSymbol, depth: number): string {
|
|
|
62
63
|
? ` ${symbol.detail.length > MAX_DETAIL_CHARS ? `${symbol.detail.slice(0, MAX_DETAIL_CHARS - 3)}...` : symbol.detail}`
|
|
63
64
|
: "";
|
|
64
65
|
const children = symbol.children !== undefined && symbol.children.length > 0 ? ` (${symbol.children.length} children)` : "";
|
|
65
|
-
|
|
66
|
+
const span = symbol.endLine - symbol.startLine + 1;
|
|
67
|
+
return `${indent}${symbol.type}${name}${detail}${children}${span > 1 ? ` [limit ${span}]` : ""}`;
|
|
66
68
|
}
|
|
67
69
|
|
|
68
70
|
export function renderSymbolOutline(input: SymbolOutlineInput): OutlineRender {
|
|
@@ -92,7 +94,9 @@ export function renderSymbolOutline(input: SymbolOutlineInput): OutlineRender {
|
|
|
92
94
|
|
|
93
95
|
const walk = (symbols: OutlineSymbol[], depth: number): boolean => {
|
|
94
96
|
for (const symbol of symbols) {
|
|
95
|
-
|
|
97
|
+
const contentLine = symbol.endLine === symbol.startLine ? input.lines[symbol.startLine - 1] : undefined;
|
|
98
|
+
const text = contentLine === undefined ? symbolLabel(symbol, depth) : clipLine(contentLine, PREVIEW_LINE_CHARS);
|
|
99
|
+
if (!addRow(symbol.startLine, text)) return false;
|
|
96
100
|
const children = symbol.children ?? [];
|
|
97
101
|
if (children.length === 0) continue;
|
|
98
102
|
if (depth >= maxDepth) {
|
package/src/read.ts
CHANGED
|
@@ -49,10 +49,11 @@ export function formatPaginationHint(
|
|
|
49
49
|
endLine: number,
|
|
50
50
|
totalLines: number,
|
|
51
51
|
nextOffset: number,
|
|
52
|
+
lastAnchor?: string,
|
|
52
53
|
byteLimit?: number,
|
|
53
54
|
): string {
|
|
54
55
|
const sizeSuffix = byteLimit !== undefined ? ` (${formatSize(byteLimit)} limit)` : "";
|
|
55
|
-
return `[Showing lines ${startLine}-${endLine} of ${totalLines}${sizeSuffix}. Use offset=${nextOffset} to continue.]`;
|
|
56
|
+
return `[Showing lines ${startLine}-${endLine} of ${totalLines}${sizeSuffix}. Use offset=${lastAnchor ?? nextOffset} to continue.]`;
|
|
56
57
|
}
|
|
57
58
|
|
|
58
59
|
export async function fmtReadPreview(
|
|
@@ -147,7 +148,7 @@ export async function fmtReadPreview(
|
|
|
147
148
|
let nextOffset: number | undefined;
|
|
148
149
|
if (shownRowCount > 0 && (skippedTruncation.truncated || lastShownLine < totalLines)) {
|
|
149
150
|
nextOffset = lastShownLine + 1;
|
|
150
|
-
preview += `\n\n${warning}\n${formatPaginationHint(startLine, lastShownLine, totalLines, nextOffset, skippedTruncation.truncated ? skippedTruncation.maxBytes : undefined)}`;
|
|
151
|
+
preview += `\n\n${warning}\n${formatPaginationHint(startLine, lastShownLine, totalLines, nextOffset, servedHashes.at(-1), skippedTruncation.truncated ? skippedTruncation.maxBytes : undefined)}`;
|
|
151
152
|
} else {
|
|
152
153
|
preview += `\n\n${warning}`;
|
|
153
154
|
}
|
|
@@ -175,13 +176,13 @@ export async function fmtReadPreview(
|
|
|
175
176
|
const endLineDisplay = startLine + truncation.outputLines - 1;
|
|
176
177
|
nextOffset = endLineDisplay + 1;
|
|
177
178
|
if (truncation.truncatedBy === "lines") {
|
|
178
|
-
preview += `\n\n${formatPaginationHint(startLine, endLineDisplay, totalLines, nextOffset)}`;
|
|
179
|
+
preview += `\n\n${formatPaginationHint(startLine, endLineDisplay, totalLines, nextOffset, servedHashes.at(-1))}`;
|
|
179
180
|
} else {
|
|
180
|
-
preview += `\n\n${formatPaginationHint(startLine, endLineDisplay, totalLines, nextOffset, truncation.maxBytes)}`;
|
|
181
|
+
preview += `\n\n${formatPaginationHint(startLine, endLineDisplay, totalLines, nextOffset, servedHashes.at(-1), truncation.maxBytes)}`;
|
|
181
182
|
}
|
|
182
183
|
} else if (endIdx < totalLines) {
|
|
183
184
|
nextOffset = endIdx + 1;
|
|
184
|
-
preview += `\n\n${formatPaginationHint(startLine, endIdx, totalLines, nextOffset)}`;
|
|
185
|
+
preview += `\n\n${formatPaginationHint(startLine, endIdx, totalLines, nextOffset, servedHashes.at(-1))}`;
|
|
185
186
|
}
|
|
186
187
|
|
|
187
188
|
return {
|
|
@@ -209,10 +210,10 @@ export function resolveReadOffset(
|
|
|
209
210
|
if (owner === undefined) {
|
|
210
211
|
const folded = ownersDifferingOnlyByCase(ref.hash);
|
|
211
212
|
const hint = folded.length > 0 ? ` Anchors are case-sensitive; ${folded.map((match) => `"${match.anchor}"`).join(", ")} differs only in case.` : "";
|
|
212
|
-
throw new Error(`[E_STALE_ANCHOR] "${ref.hash}" is not owned in this session.${hint}
|
|
213
|
+
throw new Error(`[E_STALE_ANCHOR] "${ref.hash}" is not owned in this session.${hint} Use a line number, or read again for fresh anchors.`);
|
|
213
214
|
}
|
|
214
215
|
if (owner.path !== resolvedPath) {
|
|
215
|
-
throw new Error(`[E_STALE_ANCHOR] "${ref.hash}" is owned by ${owner.path}.
|
|
216
|
+
throw new Error(`[E_STALE_ANCHOR] "${ref.hash}" is owned by ${owner.path}. Use a line number, or read ${owner.path} to use that anchor.`);
|
|
216
217
|
}
|
|
217
218
|
try {
|
|
218
219
|
return resolveAnchorLine(ref, fileLines, fileHashes, resolvedPath);
|
|
@@ -272,11 +273,15 @@ function renderReadCall(args: unknown, theme: any, context: any): Text {
|
|
|
272
273
|
const index = text.lastIndexOf(marker);
|
|
273
274
|
if (index < 0) return rendered;
|
|
274
275
|
const line = anchorLineOf(ref.anchor, ref.path, context?.cwd);
|
|
275
|
-
if (line === undefined
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
276
|
+
if (line === undefined) {
|
|
277
|
+
if (ref.limit === undefined) return rendered;
|
|
278
|
+
const suffix = theme.fg("warning", ` +${ref.limit}`);
|
|
279
|
+
rendered.setText(`${text.slice(0, index + marker.length)}${suffix}${text.slice(index + marker.length)}`);
|
|
280
|
+
return rendered;
|
|
281
|
+
}
|
|
282
|
+
const range = ref.limit === undefined ? `${line}` : `${line}-${line + ref.limit - 1}`;
|
|
283
|
+
const replacement = theme.fg("warning", `:${range} (${ref.anchor})`);
|
|
284
|
+
rendered.setText(`${text.slice(0, index)}${replacement}${text.slice(index + marker.length)}`);
|
|
280
285
|
return rendered;
|
|
281
286
|
}
|
|
282
287
|
|
package/src/replace-match.ts
CHANGED
|
@@ -3,9 +3,9 @@ import { constants } from "node:fs";
|
|
|
3
3
|
import { execPipeline, noteAnchorError, previewFromPipe, previewError, type ReplaceDetails } from "./replace";
|
|
4
4
|
import { commitEdit } from "./commit";
|
|
5
5
|
import { readNormFile, type NormFile } from "./file-reader";
|
|
6
|
-
import { fmtRegion, MAX_HASH_LINES, parseHashRef, resEdit, resolveAnchorLine, stripAnchorRow, type Anchor
|
|
6
|
+
import { fmtRegion, MAX_HASH_LINES, parseHashRef, planEdit, preserveDeletionSeparators, resEdit, resolveAnchorLine, stripAnchorRow, type Anchor } from "./hashline";
|
|
7
7
|
import { servedForPath, withAnchorSession } from "./anchor-registry";
|
|
8
|
-
import { batchMemberFor,
|
|
8
|
+
import { batchMemberFor, executeBatchMember, noteBatchFailure, registerBatchPlanner, type BatchMemberPlan, type BatchPlannerInput } from "./batch";
|
|
9
9
|
import { loadP, loadGuide } from "./prompts";
|
|
10
10
|
import {
|
|
11
11
|
assertReplaceMatchReq,
|
|
@@ -109,6 +109,33 @@ export function buildReplaceMatchEdit(
|
|
|
109
109
|
};
|
|
110
110
|
}
|
|
111
111
|
|
|
112
|
+
function planReplaceMatchBatchMember(input: BatchPlannerInput): BatchMemberPlan {
|
|
113
|
+
const req = normReq(input.member.args, "replace");
|
|
114
|
+
assertReplaceMatchReq(req);
|
|
115
|
+
const { refs, warnings } = parseMatchAnchors(req);
|
|
116
|
+
const hints = [...literalEscapeHints([req.old_string], "old_string"), ...literalEscapeHints([req.new_string], "new_string")];
|
|
117
|
+
const preload = { normalized: input.base.content, fileHashes: input.base.hashes } as NormFile;
|
|
118
|
+
const matchPlan = buildReplaceMatchEdit(req, refs, preload, input.displayPath, input.served);
|
|
119
|
+
const resWarnings: string[] = [];
|
|
120
|
+
const hedit = resEdit(matchPlan.editParams, resWarnings);
|
|
121
|
+
const effectiveHedit = preserveDeletionSeparators(hedit, input.base.baseLines, input.base.hashes);
|
|
122
|
+
const planned = planEdit(input.base.content, effectiveHedit, input.base.hashes, {
|
|
123
|
+
filePath: input.displayPath,
|
|
124
|
+
servedHashes: matchPlan.servedOverride ?? input.served,
|
|
125
|
+
signal: input.signal,
|
|
126
|
+
baseFileLines: input.base.baseLines,
|
|
127
|
+
});
|
|
128
|
+
return {
|
|
129
|
+
hedit,
|
|
130
|
+
warnings: [...warnings, ...hints, ...resWarnings],
|
|
131
|
+
planned,
|
|
132
|
+
foldedLines: 0,
|
|
133
|
+
...(matchPlan.servedOverride !== undefined ? { servedOverride: matchPlan.servedOverride } : {}),
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
registerBatchPlanner("replace_match", planReplaceMatchBatchMember);
|
|
138
|
+
|
|
112
139
|
export async function replaceMatchPreview(request: unknown, cwd: string, signal?: AbortSignal): Promise<RPreview> {
|
|
113
140
|
try {
|
|
114
141
|
const normalized = normReq(request, "replace");
|
|
@@ -173,13 +200,13 @@ export function buildReplaceMatchToolDef(flags: EditToolFlags = DEFAULT_EDIT_FLA
|
|
|
173
200
|
const normalized = normReq(params, "replace");
|
|
174
201
|
assertReplaceMatchReq(normalized);
|
|
175
202
|
const req = normalized;
|
|
203
|
+
const member = batchMemberFor(_toolCallId);
|
|
176
204
|
let refs: MatchRefs;
|
|
177
205
|
let warnings: string[];
|
|
178
206
|
try {
|
|
179
207
|
({ refs, warnings } = parseMatchAnchors(req));
|
|
180
|
-
await throwIfStrictInput(warnings);
|
|
208
|
+
if (member === undefined) await throwIfStrictInput(warnings);
|
|
181
209
|
} catch (error) {
|
|
182
|
-
const member = batchMemberFor(_toolCallId);
|
|
183
210
|
if (member) noteBatchFailure(member, error);
|
|
184
211
|
throw error;
|
|
185
212
|
}
|
|
@@ -189,53 +216,27 @@ export function buildReplaceMatchToolDef(flags: EditToolFlags = DEFAULT_EDIT_FLA
|
|
|
189
216
|
removeTo: req.replace_to,
|
|
190
217
|
providedPath: req.path,
|
|
191
218
|
cwd: ctx.cwd,
|
|
219
|
+
anchorTarget: member?.target,
|
|
192
220
|
}).catch((error: unknown) => {
|
|
193
|
-
const member = batchMemberFor(_toolCallId);
|
|
194
221
|
if (member) noteBatchFailure(member, error);
|
|
195
222
|
throw error;
|
|
196
223
|
});
|
|
197
224
|
return queuedEdit(targetPath, ctx.cwd, signal, async (absolutePath, mutationTargetPath) => {
|
|
198
|
-
const member = batchMemberFor(_toolCallId);
|
|
199
|
-
let preload: NormFile;
|
|
200
225
|
if (member) {
|
|
201
|
-
|
|
202
|
-
preload = { normalized: base.content, fileHashes: base.hashes } as NormFile;
|
|
203
|
-
} else {
|
|
204
|
-
preload = await readNormFile(targetPath, ctx.cwd, {
|
|
205
|
-
signal,
|
|
206
|
-
accessMode: constants.R_OK | constants.W_OK,
|
|
207
|
-
maxLines: MAX_HASH_LINES,
|
|
208
|
-
});
|
|
226
|
+
return executeBatchMember({ member, targetPath, mutationTargetPath, cwd: ctx.cwd, signal });
|
|
209
227
|
}
|
|
228
|
+
const preload = await readNormFile(targetPath, ctx.cwd, {
|
|
229
|
+
signal,
|
|
230
|
+
accessMode: constants.R_OK | constants.W_OK,
|
|
231
|
+
maxLines: MAX_HASH_LINES,
|
|
232
|
+
});
|
|
210
233
|
let plan: MatchPlan;
|
|
211
234
|
try {
|
|
212
|
-
plan = buildReplaceMatchEdit(req, refs, preload, targetPath,
|
|
235
|
+
plan = buildReplaceMatchEdit(req, refs, preload, targetPath, servedForPath(preload.absolutePath));
|
|
213
236
|
} catch (error) {
|
|
214
237
|
await noteAnchorError(mutationTargetPath, error);
|
|
215
|
-
if (member) noteBatchFailure(member, error);
|
|
216
238
|
throw error;
|
|
217
239
|
}
|
|
218
|
-
if (member) {
|
|
219
|
-
let hedit: HEdit;
|
|
220
|
-
const resWarnings: string[] = [];
|
|
221
|
-
try {
|
|
222
|
-
hedit = resEdit(plan.editParams, resWarnings);
|
|
223
|
-
} catch (error) {
|
|
224
|
-
noteBatchFailure(member, error);
|
|
225
|
-
throw error;
|
|
226
|
-
}
|
|
227
|
-
return executeBatchMember({
|
|
228
|
-
kind: "replace_match",
|
|
229
|
-
member,
|
|
230
|
-
targetPath,
|
|
231
|
-
mutationTargetPath,
|
|
232
|
-
cwd: ctx.cwd,
|
|
233
|
-
signal,
|
|
234
|
-
hedit,
|
|
235
|
-
extraWarnings: [...warnings, ...hints, ...resWarnings],
|
|
236
|
-
...(plan.servedOverride !== undefined ? { servedOverride: plan.servedOverride } : {}),
|
|
237
|
-
});
|
|
238
|
-
}
|
|
239
240
|
const pipe = await execPipeline(targetPath, plan.editParams, ctx.cwd, {
|
|
240
241
|
accessMode: constants.R_OK | constants.W_OK,
|
|
241
242
|
signal,
|
package/src/replace-undo.ts
CHANGED
|
@@ -184,7 +184,7 @@ export function regUndo(pi: ExtensionAPI, flags: EditToolFlags = DEFAULT_EDIT_FL
|
|
|
184
184
|
);
|
|
185
185
|
|
|
186
186
|
const currentNormalized = currentRaw === undefined ? "" : toLF(stripBOM(currentRaw).text);
|
|
187
|
-
const currentHashes = await lineHashes(currentNormalized, mutationTargetPath);
|
|
187
|
+
const currentHashes = await lineHashes(currentNormalized, mutationTargetPath, undefined, undefined, false, true);
|
|
188
188
|
const diffResult = genDiff(undo.content, undo.resultContent, 0, undefined, undo.hashes, { unlimited: true });
|
|
189
189
|
const linesAddedByReplace = cntDiff(diffResult.diff, "+");
|
|
190
190
|
const linesRemovedByReplace = cntDiff(diffResult.diff, "-");
|
package/src/replace.ts
CHANGED
|
@@ -16,6 +16,7 @@ import { loadP, loadGuide } from "./prompts";
|
|
|
16
16
|
import { type FileIdentity } from "./fs-write";
|
|
17
17
|
import { applyEdit,
|
|
18
18
|
lineHashes,
|
|
19
|
+
planEdit,
|
|
19
20
|
resEdit,
|
|
20
21
|
preserveDeletionSeparators,
|
|
21
22
|
MAX_HASH_LINES,
|
|
@@ -36,7 +37,7 @@ import { resolveTarget } from "./fs-write";
|
|
|
36
37
|
import { toCwd, toDisplayPath } from "./paths";
|
|
37
38
|
import { queuedEdit, editToolBase, editRenderCallWrapper, editRenderResultWrapper, resolveEditTargetWithRequirement, throwIfStrictInput, withReplacePrompts, DEFAULT_EDIT_FLAGS, type EditToolFlags } from "./edit-common";
|
|
38
39
|
import { commitEdit } from "./commit";
|
|
39
|
-
import { batchMemberFor, executeBatchMember, noteBatchFailure } from "./batch";
|
|
40
|
+
import { batchMemberFor, executeBatchMember, noteBatchFailure, registerBatchPlanner, type BatchMemberPlan, type BatchPlannerInput } from "./batch";
|
|
40
41
|
|
|
41
42
|
export { editToolSchema, type ReqParams, assertReq };
|
|
42
43
|
|
|
@@ -126,12 +127,32 @@ function countLineChanges(
|
|
|
126
127
|
export function buildReplaceHEdit(params: RawReqParams): { edit: HEdit; warnings: string[] } {
|
|
127
128
|
const editWarnings: string[] = [];
|
|
128
129
|
const anchors = { remove_from: params.remove_from, remove_to: params.remove_to };
|
|
129
|
-
const edit =
|
|
130
|
-
? resEdit({ ...anchors, text: params.text }, editWarnings)
|
|
131
|
-
: resEdit({ ...anchors, text: params.text }, editWarnings);
|
|
130
|
+
const edit = resEdit({ ...anchors, text: params.text }, editWarnings);
|
|
132
131
|
return { edit, warnings: editWarnings };
|
|
133
132
|
}
|
|
134
133
|
|
|
134
|
+
function planReplaceBatchMember(input: BatchPlannerInput): BatchMemberPlan {
|
|
135
|
+
const req = normReq(input.member.args, "remove");
|
|
136
|
+
assertReq(req);
|
|
137
|
+
const literalEscapes = literalEscapeHints([req.text], "text");
|
|
138
|
+
const built = buildReplaceHEdit(req);
|
|
139
|
+
const effectiveHedit = preserveDeletionSeparators(built.edit, input.base.baseLines, input.base.hashes);
|
|
140
|
+
const planned = planEdit(input.base.content, effectiveHedit, input.base.hashes, {
|
|
141
|
+
filePath: input.displayPath,
|
|
142
|
+
servedHashes: input.served,
|
|
143
|
+
signal: input.signal,
|
|
144
|
+
baseFileLines: input.base.baseLines,
|
|
145
|
+
});
|
|
146
|
+
return {
|
|
147
|
+
hedit: built.edit,
|
|
148
|
+
warnings: [...literalEscapes, ...built.warnings],
|
|
149
|
+
planned,
|
|
150
|
+
foldedLines: 0,
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
registerBatchPlanner("replace", planReplaceBatchMember);
|
|
155
|
+
|
|
135
156
|
function withEndingOverrides(edit: HEdit, overrides: (LineEnding | undefined)[] | undefined): HEdit {
|
|
136
157
|
if (overrides === undefined) return edit;
|
|
137
158
|
const length = Math.max(edit.content_separators?.length ?? 0, overrides.length);
|
|
@@ -292,18 +313,18 @@ export function buildToolDef(flags: EditToolFlags = DEFAULT_EDIT_FLAGS): ToolDef
|
|
|
292
313
|
assertReq(canonical);
|
|
293
314
|
const normalizedParams = canonical;
|
|
294
315
|
const literalEscapes = literalEscapeHints([normalizedParams.text], "text");
|
|
316
|
+
const member = batchMemberFor(_toolCallId);
|
|
295
317
|
const targetPath = await resolveEditTargetWithRequirement({
|
|
296
318
|
removeFrom: normalizedParams.remove_from,
|
|
297
319
|
removeTo: normalizedParams.remove_to,
|
|
298
320
|
providedPath: normalizedParams.path,
|
|
299
321
|
cwd: ctx.cwd,
|
|
322
|
+
anchorTarget: member?.target,
|
|
300
323
|
}).catch((error: unknown) => {
|
|
301
|
-
const member = batchMemberFor(_toolCallId);
|
|
302
324
|
if (member) noteBatchFailure(member, error);
|
|
303
325
|
throw error;
|
|
304
326
|
});
|
|
305
327
|
return queuedEdit(targetPath, ctx.cwd, signal, async (absolutePath, mutationTargetPath) => {
|
|
306
|
-
const member = batchMemberFor(_toolCallId);
|
|
307
328
|
if (!member) {
|
|
308
329
|
const pipe = await execPipeline(
|
|
309
330
|
targetPath,
|
|
@@ -320,22 +341,12 @@ export function buildToolDef(flags: EditToolFlags = DEFAULT_EDIT_FLAGS): ToolDef
|
|
|
320
341
|
signal,
|
|
321
342
|
});
|
|
322
343
|
}
|
|
323
|
-
let built: { edit: HEdit; warnings: string[] };
|
|
324
|
-
try {
|
|
325
|
-
built = buildReplaceHEdit(normalizedParams);
|
|
326
|
-
} catch (error) {
|
|
327
|
-
noteBatchFailure(member, error);
|
|
328
|
-
throw error;
|
|
329
|
-
}
|
|
330
344
|
return executeBatchMember({
|
|
331
|
-
kind: "replace",
|
|
332
345
|
member,
|
|
333
346
|
targetPath,
|
|
334
347
|
mutationTargetPath,
|
|
335
348
|
cwd: ctx.cwd,
|
|
336
349
|
signal,
|
|
337
|
-
hedit: built.edit,
|
|
338
|
-
extraWarnings: [...literalEscapes, ...built.warnings],
|
|
339
350
|
});
|
|
340
351
|
});
|
|
341
352
|
}));
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
- `read`: call again after an edit when you need anchors you lack — post-edit diff `+anchor│`/` anchor│` rows and any served `anchor│content` rows already carry fresh anchors for the changed range.
|
|
2
|
-
- `read`: pass a served anchor as `offset` with `limit` to read from it without line-number math; an outline row's `[limit N]` is the span to request.
|
|
3
|
-
- `read`: an outline row shows a symbol label,
|
|
2
|
+
- `read`: pass a served anchor as `offset` with `limit` to read from it without line-number math; an outline row's `[limit N]` is the span to request, and a row without one is a single line shown as its content.
|
|
3
|
+
- `read`: an outline row shows a symbol label, or the line content for a single-line symbol; edit by anchor, and read the anchor (or copy `old_string` from a served row) before `replace_match`.
|