pi-hashline-edit-pro 6.4.7 → 6.5.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 +9 -13
- package/package.json +9 -9
- package/src/anchor-registry.ts +33 -9
- package/src/batch.ts +2 -2
- package/src/copy-move.ts +8 -4
- package/src/edit-common.ts +12 -10
- package/src/edit-fidelity.ts +98 -41
- package/src/insert.ts +11 -4
- package/src/payload-contract.ts +41 -22
- package/src/read.ts +71 -35
- package/src/replace-diff.ts +38 -15
- package/src/replace-match.ts +67 -147
- package/src/replace.ts +6 -3
- package/src/utils.ts +22 -2
- package/tool-prompts/read-guidelines.md +1 -1
- package/tool-prompts/replace-match-guidelines.md +3 -3
- package/tool-prompts/replace-match-snippet.md +1 -1
- package/tool-prompts/replace-match.md +2 -2
package/src/insert.ts
CHANGED
|
@@ -12,7 +12,7 @@ import { stripAnchorRow } from "./hashline/resolve";
|
|
|
12
12
|
import { withAnchorSession } from "./anchor-registry";
|
|
13
13
|
import { loadP, loadGuidelines } from "./prompts";
|
|
14
14
|
import { assertInsertReq, normReq, type InsertReq } from "./payload-contract";
|
|
15
|
-
import { isRec, literalEscapeHints, splitLines } from "./utils";
|
|
15
|
+
import { anchorCarryIndex, isRec, literalEscapeHints, splitLines } from "./utils";
|
|
16
16
|
import { queuedEdit, editToolBase, editRenderCallWrapper, editRenderResultWrapper, resolveEditTargetWithRequirement, throwIfStrictInput, withInsertPrompts, DEFAULT_EDIT_FLAGS, type EditToolFlags } from "./edit-common";
|
|
17
17
|
import type { RPreview, RRState } from "./replace-render";
|
|
18
18
|
import { editResultSchema, withStructuredErrors } from "./structured";
|
|
@@ -98,6 +98,11 @@ function insertStripWarning(anchorLine: string | undefined, direction: "before"
|
|
|
98
98
|
return { label: "text", indexOffset: anchorLine !== undefined && direction === "after" ? -1 : 0 };
|
|
99
99
|
}
|
|
100
100
|
|
|
101
|
+
function insertAnchorCarry(direction: "before" | "after", editParams: HTEdit, anchorLine: string | undefined): number | undefined {
|
|
102
|
+
if (anchorLine === undefined) return undefined;
|
|
103
|
+
return anchorCarryIndex(direction, editParams.text.length - 1);
|
|
104
|
+
}
|
|
105
|
+
|
|
101
106
|
function planInsertBatchMember(input: BatchPlannerInput): BatchMemberPlan {
|
|
102
107
|
const req = normReq(input.member.args);
|
|
103
108
|
assertInsertReq(req);
|
|
@@ -149,6 +154,7 @@ export async function insertPreview(request: unknown, cwd: string, signal?: Abor
|
|
|
149
154
|
signal,
|
|
150
155
|
});
|
|
151
156
|
const { editParams, anchorLine, contentSeparators } = buildInsertEdit(normalized, preload, ref, targetPath);
|
|
157
|
+
const anchorCarry = insertAnchorCarry(normalized.direction, editParams, anchorLine);
|
|
152
158
|
const pipe = await execPipeline(targetPath, editParams, cwd, {
|
|
153
159
|
accessMode: constants.R_OK,
|
|
154
160
|
noPersist: true,
|
|
@@ -156,6 +162,7 @@ export async function insertPreview(request: unknown, cwd: string, signal?: Abor
|
|
|
156
162
|
signal,
|
|
157
163
|
stripWarning: insertStripWarning(anchorLine, normalized.direction),
|
|
158
164
|
endingOverrides: contentSeparators,
|
|
165
|
+
...(anchorCarry !== undefined ? { anchorCarry } : {}),
|
|
159
166
|
});
|
|
160
167
|
return previewFromPipe(pipe);
|
|
161
168
|
} catch (error: unknown) {
|
|
@@ -241,21 +248,21 @@ export function buildInsertToolDef(flags: EditToolFlags = DEFAULT_EDIT_FLAGS): I
|
|
|
241
248
|
maxLines: MAX_HASH_LINES,
|
|
242
249
|
});
|
|
243
250
|
const { editParams, anchorLine, contentSeparators } = buildInsertEdit(req, preload, ref, targetPath);
|
|
251
|
+
const anchorCarry = insertAnchorCarry(req.direction, editParams, anchorLine);
|
|
244
252
|
const pipe = await execPipeline(targetPath, editParams, ctx.cwd, {
|
|
245
253
|
accessMode: constants.R_OK | constants.W_OK,
|
|
246
254
|
signal,
|
|
247
255
|
preloadedNorm: preload,
|
|
248
256
|
stripWarning: insertStripWarning(anchorLine, req.direction),
|
|
249
257
|
endingOverrides: contentSeparators,
|
|
258
|
+
...(anchorCarry !== undefined ? { anchorCarry } : {}),
|
|
250
259
|
});
|
|
251
260
|
return commitEdit(pipe, {
|
|
252
261
|
path: pipe.path,
|
|
253
262
|
absolutePath,
|
|
254
263
|
mutationTargetPath,
|
|
255
264
|
editAnchors: [editParams.remove_from, editParams.remove_to],
|
|
256
|
-
...(
|
|
257
|
-
? {}
|
|
258
|
-
: { anchorCarry: req.direction === "after" ? 0 : editParams.text.length - 1 }),
|
|
265
|
+
...(anchorCarry !== undefined ? { anchorCarry } : {}),
|
|
259
266
|
signal,
|
|
260
267
|
verb: "inserted",
|
|
261
268
|
noopNoun: "Insertion",
|
package/src/payload-contract.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Type } from "typebox";
|
|
2
2
|
import { isRec, normalizeRequest, rejectUnknownFields, assertNoNul } from "./utils";
|
|
3
3
|
import { TEXT_ENDING_NOTE, TEXT_NOT_STRING_MSG, NEW_CONTENT_NOT_STRING_MSG } from "./constants";
|
|
4
|
+
import { toLF } from "./normalize";
|
|
4
5
|
|
|
5
6
|
const textSchema = Type.String({
|
|
6
7
|
description:
|
|
@@ -166,25 +167,44 @@ export function assertTransferReq(request: unknown): asserts request is Transfer
|
|
|
166
167
|
}
|
|
167
168
|
}
|
|
168
169
|
|
|
169
|
-
const MATCH_KS = new Set(["path", "replace_from", "replace_to", "
|
|
170
|
+
const MATCH_KS = new Set(["path", "replace_from", "replace_to", "match", "replacement"]);
|
|
170
171
|
|
|
171
172
|
export interface ReplaceMatchReq {
|
|
172
173
|
path?: string;
|
|
173
174
|
replace_from: string;
|
|
174
175
|
replace_to: string;
|
|
175
|
-
|
|
176
|
-
|
|
176
|
+
match: string;
|
|
177
|
+
replacement: string;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export function singleLineFragment(field: "match" | "replacement", raw: string): string {
|
|
181
|
+
const text = toLF(raw);
|
|
182
|
+
const fragment = text.endsWith("\n") ? text.slice(0, -1) : text;
|
|
183
|
+
const lineBreak = fragment.indexOf("\n");
|
|
184
|
+
if (lineBreak >= 0) {
|
|
185
|
+
const column = [...fragment.slice(0, lineBreak)].length + 1;
|
|
186
|
+
throw new Error(
|
|
187
|
+
`[E_BAD_SHAPE] ${field} must stay within one line, but the value contains a line break at column ${column}. A trailing line break is accepted as the line's own terminator; text on another line belongs to replace.`,
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
if (field === "match" && fragment.length === 0) {
|
|
191
|
+
throw new Error('[E_BAD_SHAPE] Replace-match request field "match" must be a non-empty string holding the exact text to find.');
|
|
192
|
+
}
|
|
193
|
+
return fragment;
|
|
177
194
|
}
|
|
178
195
|
|
|
179
196
|
export function assertReplaceMatchReq(request: unknown): asserts request is ReplaceMatchReq {
|
|
180
197
|
if (!isRec(request)) {
|
|
181
198
|
throw new Error("[E_BAD_SHAPE] Replace-match request must be an object.");
|
|
182
199
|
}
|
|
200
|
+
if ("old_string" in request || "new_string" in request) {
|
|
201
|
+
throw new Error('[E_BAD_SHAPE] Replace-match request no longer accepts "old_string"/"new_string"; use "match" for the text to find and "replacement" for the new text.');
|
|
202
|
+
}
|
|
183
203
|
rejectUnknownFields(request, MATCH_KS, "Replace-match request");
|
|
184
204
|
if (request.path !== undefined && typeof request.path !== "string") {
|
|
185
205
|
throw new Error('[E_BAD_SHAPE] Replace-match request field "path" must be a string when provided.');
|
|
186
206
|
}
|
|
187
|
-
for (const key of ["replace_from", "replace_to", "
|
|
207
|
+
for (const key of ["replace_from", "replace_to", "match", "replacement"] as const) {
|
|
188
208
|
if (typeof (request as Record<string, unknown>)[key] !== "string") {
|
|
189
209
|
throw new Error(`[E_BAD_SHAPE] Replace-match request requires a "${key}" string.`);
|
|
190
210
|
}
|
|
@@ -193,13 +213,12 @@ export function assertReplaceMatchReq(request: unknown): asserts request is Repl
|
|
|
193
213
|
if (within.replace_from.length === 0 || within.replace_to.length === 0) {
|
|
194
214
|
throw new Error('[E_BAD_SHAPE] Replace-match request requires non-empty "replace_from" and "replace_to" anchors.');
|
|
195
215
|
}
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
assertNoNul([within.new_string]);
|
|
216
|
+
singleLineFragment("match", within.match);
|
|
217
|
+
singleLineFragment("replacement", within.replacement);
|
|
218
|
+
assertNoNul([within.replacement]);
|
|
200
219
|
}
|
|
201
220
|
|
|
202
|
-
export function getReplaceMatchInput(args: unknown): { path?: string; replace_from: string; replace_to: string;
|
|
221
|
+
export function getReplaceMatchInput(args: unknown): { path?: string; replace_from: string; replace_to: string; match: string; replacement: string } | null {
|
|
203
222
|
let normalized: unknown;
|
|
204
223
|
try {
|
|
205
224
|
normalized = normalizeRequest(args, "replace");
|
|
@@ -210,8 +229,8 @@ export function getReplaceMatchInput(args: unknown): { path?: string; replace_fr
|
|
|
210
229
|
if (
|
|
211
230
|
typeof normalized.replace_from !== "string" ||
|
|
212
231
|
typeof normalized.replace_to !== "string" ||
|
|
213
|
-
typeof normalized.
|
|
214
|
-
typeof normalized.
|
|
232
|
+
typeof normalized.match !== "string" ||
|
|
233
|
+
typeof normalized.replacement !== "string"
|
|
215
234
|
) {
|
|
216
235
|
return null;
|
|
217
236
|
}
|
|
@@ -219,8 +238,8 @@ export function getReplaceMatchInput(args: unknown): { path?: string; replace_fr
|
|
|
219
238
|
...(typeof normalized.path === "string" ? { path: normalized.path } : {}),
|
|
220
239
|
replace_from: normalized.replace_from,
|
|
221
240
|
replace_to: normalized.replace_to,
|
|
222
|
-
|
|
223
|
-
|
|
241
|
+
match: normalized.match,
|
|
242
|
+
replacement: normalized.replacement,
|
|
224
243
|
};
|
|
225
244
|
}
|
|
226
245
|
|
|
@@ -230,15 +249,15 @@ const replaceMatchFromSchema = Type.String({
|
|
|
230
249
|
});
|
|
231
250
|
const replaceMatchToSchema = Type.String({
|
|
232
251
|
description:
|
|
233
|
-
"4-char anchor of the LAST line of the range searched; same as replace_from for a single line.",
|
|
252
|
+
"4-char anchor of the LAST line of the range searched; same as `replace_from` for a single line.",
|
|
234
253
|
});
|
|
235
|
-
const
|
|
254
|
+
const replaceMatchMatchSchema = Type.String({
|
|
236
255
|
description:
|
|
237
|
-
"The exact text to find inside the selected
|
|
256
|
+
"The exact text to find inside the selected lines, copied from the served row: only the fragment that changes, never text that `replacement` leaves unchanged. It must stay within one line and hold text: a single trailing line break is accepted as the line's own terminator, a line break anywhere else is refused, and a value that is only a line break leaves nothing to find and is refused. Every occurrence inside the range is replaced; the text around each match is left untouched. If `match` is an entire single-line range except indentation and trailing punctuation, that is a whole-line match: use `replace` instead.",
|
|
238
257
|
});
|
|
239
|
-
const
|
|
258
|
+
const replaceMatchReplacementSchema = Type.String({
|
|
240
259
|
description:
|
|
241
|
-
`The exact replacement for every matched occurrence. "" deletes the matches.
|
|
260
|
+
`The exact replacement for every matched occurrence within the range. "" deletes the matches. It must stay within one line: a single trailing line break is accepted and dropped (a value that is only a line break counts as ""), and a line break anywhere else is refused. The rest of each line is kept byte-for-byte.`,
|
|
242
261
|
});
|
|
243
262
|
const replaceMatchPathRequiredSchema = Type.String({
|
|
244
263
|
description:
|
|
@@ -249,8 +268,8 @@ export const replaceMatchToolSchema = Type.Object(
|
|
|
249
268
|
{
|
|
250
269
|
replace_from: replaceMatchFromSchema,
|
|
251
270
|
replace_to: replaceMatchToSchema,
|
|
252
|
-
|
|
253
|
-
|
|
271
|
+
match: replaceMatchMatchSchema,
|
|
272
|
+
replacement: replaceMatchReplacementSchema,
|
|
254
273
|
},
|
|
255
274
|
{ additionalProperties: true },
|
|
256
275
|
);
|
|
@@ -262,8 +281,8 @@ export function buildReplaceMatchToolSchema(requirePath: boolean): typeof replac
|
|
|
262
281
|
path: replaceMatchPathRequiredSchema,
|
|
263
282
|
replace_from: replaceMatchFromSchema,
|
|
264
283
|
replace_to: replaceMatchToSchema,
|
|
265
|
-
|
|
266
|
-
|
|
284
|
+
match: replaceMatchMatchSchema,
|
|
285
|
+
replacement: replaceMatchReplacementSchema,
|
|
267
286
|
},
|
|
268
287
|
{ additionalProperties: true },
|
|
269
288
|
) as typeof replaceMatchToolSchema;
|
package/src/read.ts
CHANGED
|
@@ -23,7 +23,7 @@ import { withAnchorSession, adoptAnchors, ownerOf, ownersDifferingOnlyByCase, fo
|
|
|
23
23
|
import { serveRows } from "./served";
|
|
24
24
|
import { snapshotCache } from "./hash-store/cache";
|
|
25
25
|
import { Text } from "@earendil-works/pi-tui";
|
|
26
|
-
import { anchoredLine, readResultSchema, withStructuredErrors, type AnchoredLine, type ReadResult } from "./structured";
|
|
26
|
+
import { anchoredLine, readResultSchema, withStructuredErrors, type AnchoredLine, type ReadResult, type ReadTextResult } from "./structured";
|
|
27
27
|
const R_DESC = loadP("../tool-prompts/read.md");
|
|
28
28
|
const R_SNIPPET = loadP("../tool-prompts/read-snippet.md");
|
|
29
29
|
function readGuide(): Guideline[] {
|
|
@@ -241,7 +241,6 @@ function anchorRefOf(args: unknown): { anchor: string; limit: number | undefined
|
|
|
241
241
|
const { offset, limit, path } = args;
|
|
242
242
|
if (typeof offset !== "string" || offset.length === 0 || /^\d+$/.test(offset)) return undefined;
|
|
243
243
|
const validLimit = typeof limit === "number" && Number.isInteger(limit) && limit >= 1 ? limit : undefined;
|
|
244
|
-
if (limit !== undefined && validLimit === undefined) return undefined;
|
|
245
244
|
return { anchor: offset, limit: validLimit, path: typeof path === "string" ? path : undefined };
|
|
246
245
|
}
|
|
247
246
|
|
|
@@ -263,28 +262,79 @@ function anchorLineOf(anchor: string, path: string | undefined, cwd: unknown): n
|
|
|
263
262
|
return index < 0 ? undefined : index + 1;
|
|
264
263
|
}
|
|
265
264
|
|
|
265
|
+
const ANSI_ESC = String.fromCharCode(27);
|
|
266
|
+
const CALL_ANSI_RE = new RegExp(`${ANSI_ESC}\\[[0-9;]*m`, "g");
|
|
267
|
+
const CALL_ANSI_AT_RE = new RegExp(`^${ANSI_ESC}\\[[0-9;]*m`);
|
|
268
|
+
|
|
269
|
+
function rawIndexForPlainIndex(text: string, plainIndex: number): number {
|
|
270
|
+
let raw = 0;
|
|
271
|
+
let plain = 0;
|
|
272
|
+
while (raw < text.length && plain < plainIndex) {
|
|
273
|
+
const escape = CALL_ANSI_AT_RE.exec(text.slice(raw));
|
|
274
|
+
if (escape !== null) {
|
|
275
|
+
raw += escape[0].length;
|
|
276
|
+
continue;
|
|
277
|
+
}
|
|
278
|
+
raw += 1;
|
|
279
|
+
plain += 1;
|
|
280
|
+
}
|
|
281
|
+
return raw;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
export function insertBeforeExpandHint(text: string, suffix: string): string {
|
|
285
|
+
const plain = text.replace(CALL_ANSI_RE, "");
|
|
286
|
+
if (!plain.endsWith(" to expand)")) return `${text}${suffix}`;
|
|
287
|
+
const plainIndex = plain.lastIndexOf(" (");
|
|
288
|
+
const insertAt = plainIndex < 0 ? text.length : rawIndexForPlainIndex(text, plainIndex);
|
|
289
|
+
return `${text.slice(0, insertAt)}${suffix}${text.slice(insertAt)}`;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function anchorRangeLabel(ref: { anchor: string; limit: number | undefined }, line: number | undefined): string {
|
|
293
|
+
if (line === undefined) return ref.limit === undefined ? `:${ref.anchor}` : `:${ref.anchor} +${ref.limit}`;
|
|
294
|
+
return ref.limit === undefined ? `:${line} (${ref.anchor})` : `:${line}-${line + ref.limit - 1} (${ref.anchor})`;
|
|
295
|
+
}
|
|
296
|
+
|
|
266
297
|
function renderReadCall(args: unknown, theme: any, context: any): Text {
|
|
267
298
|
const normalized = normalizeReadCallArgs(args);
|
|
268
|
-
const rendered = builtinReadRenderCall(normalized, theme, context);
|
|
269
299
|
const ref = anchorRefOf(normalized);
|
|
270
|
-
if (ref === undefined) return
|
|
271
|
-
const
|
|
272
|
-
const text = (rendered as unknown as { text: string }).text;
|
|
273
|
-
const index = text.lastIndexOf(marker);
|
|
274
|
-
if (index < 0) return rendered;
|
|
300
|
+
if (ref === undefined) return builtinReadRenderCall(normalized, theme, context);
|
|
301
|
+
const rendered = builtinReadRenderCall(isRec(normalized) ? { ...normalized, offset: undefined, limit: undefined } : normalized, theme, context);
|
|
275
302
|
const line = anchorLineOf(ref.anchor, ref.path, context?.cwd);
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
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)}`);
|
|
303
|
+
const suffix = theme.fg("warning", anchorRangeLabel(ref, line));
|
|
304
|
+
const text = (rendered as unknown as { text: string }).text;
|
|
305
|
+
rendered.setText(insertBeforeExpandHint(text, suffix));
|
|
285
306
|
return rendered;
|
|
286
307
|
}
|
|
287
308
|
|
|
309
|
+
function buildReadToolSchema(codemode: boolean) {
|
|
310
|
+
return Type.Object({
|
|
311
|
+
path: Type.String({
|
|
312
|
+
description: "Path to the file to read (relative or absolute)",
|
|
313
|
+
}),
|
|
314
|
+
offset: Type.Optional(
|
|
315
|
+
Type.Union([
|
|
316
|
+
Type.Integer({ minimum: 1 }),
|
|
317
|
+
Type.String(),
|
|
318
|
+
], { description: "Line number (1-indexed) or a served anchor to start reading from" }),
|
|
319
|
+
),
|
|
320
|
+
limit: Type.Optional(
|
|
321
|
+
Type.Integer({
|
|
322
|
+
minimum: 1,
|
|
323
|
+
description: "Maximum number of lines to read from offset",
|
|
324
|
+
}),
|
|
325
|
+
),
|
|
326
|
+
...(codemode
|
|
327
|
+
? {
|
|
328
|
+
anchors: Type.Optional(
|
|
329
|
+
Type.Boolean({
|
|
330
|
+
description: "Return { text, lines, notices } with editable anchors instead of the plain file text to a codemode script",
|
|
331
|
+
}),
|
|
332
|
+
),
|
|
333
|
+
}
|
|
334
|
+
: {}),
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
|
|
288
338
|
export function regRead(pi: ExtensionAPI, flags: EditToolFlags = DEFAULT_EDIT_FLAGS): void {
|
|
289
339
|
const prompted = withReadPrompts({ description: R_DESC, snippet: R_SNIPPET, guidelines: readGuide() }, flags);
|
|
290
340
|
pi.registerTool({
|
|
@@ -294,23 +344,7 @@ export function regRead(pi: ExtensionAPI, flags: EditToolFlags = DEFAULT_EDIT_FL
|
|
|
294
344
|
promptSnippet: prompted.snippet,
|
|
295
345
|
promptGuidelines: prompted.guidelines,
|
|
296
346
|
prepareArguments: makePrepareArguments(),
|
|
297
|
-
parameters:
|
|
298
|
-
path: Type.String({
|
|
299
|
-
description: "Path to the file to read (relative or absolute)",
|
|
300
|
-
}),
|
|
301
|
-
offset: Type.Optional(
|
|
302
|
-
Type.Union([
|
|
303
|
-
Type.Integer({ minimum: 1 }),
|
|
304
|
-
Type.String(),
|
|
305
|
-
], { description: "Line number (1-indexed) or a served anchor to start reading from" }),
|
|
306
|
-
),
|
|
307
|
-
limit: Type.Optional(
|
|
308
|
-
Type.Integer({
|
|
309
|
-
minimum: 1,
|
|
310
|
-
description: "Maximum number of lines to read from offset",
|
|
311
|
-
}),
|
|
312
|
-
),
|
|
313
|
-
}),
|
|
347
|
+
parameters: buildReadToolSchema(flags.codemode),
|
|
314
348
|
outputSchema: readResultSchema,
|
|
315
349
|
executionMode: "sequential",
|
|
316
350
|
renderCall: renderReadCall,
|
|
@@ -370,7 +404,7 @@ export function regRead(pi: ExtensionAPI, flags: EditToolFlags = DEFAULT_EDIT_FL
|
|
|
370
404
|
const servedRawText = preview.anchoredLines.map((row) => fileLines[row.line - 1] ?? "").join("\n");
|
|
371
405
|
const rawTruncation = servedRawText.length > 0 ? truncateHead(servedRawText, { maxBytes: DEFAULT_MAX_BYTES, maxLines: DEFAULT_MAX_LINES }) : undefined;
|
|
372
406
|
|
|
373
|
-
const
|
|
407
|
+
const readResult: ReadTextResult = {
|
|
374
408
|
ok: true,
|
|
375
409
|
kind: "read",
|
|
376
410
|
path: rawPath,
|
|
@@ -384,6 +418,8 @@ export function regRead(pi: ExtensionAPI, flags: EditToolFlags = DEFAULT_EDIT_FL
|
|
|
384
418
|
hadUtf8DecodeErrors,
|
|
385
419
|
notices,
|
|
386
420
|
};
|
|
421
|
+
const anchorsRequested = isRec(params) && params.anchors === true;
|
|
422
|
+
const structuredContent: ReadResult = flags.codemode && !anchorsRequested ? readResult.text : readResult;
|
|
387
423
|
return {
|
|
388
424
|
content: [{ type: "text", text: previewText }],
|
|
389
425
|
details: {
|
package/src/replace-diff.ts
CHANGED
|
@@ -159,6 +159,40 @@ function placeSpans(
|
|
|
159
159
|
return placed;
|
|
160
160
|
}
|
|
161
161
|
|
|
162
|
+
function splitPlacedSpan(
|
|
163
|
+
span: PlacedSpan,
|
|
164
|
+
oldLines: string[],
|
|
165
|
+
newLines: string[],
|
|
166
|
+
oldHashes: string[],
|
|
167
|
+
newIndexByHash: ReadonlyMap<string, number>,
|
|
168
|
+
): PlacedSpan[] {
|
|
169
|
+
if (span.oldStart > span.oldEnd || span.newStart > span.newEnd) return [span];
|
|
170
|
+
const pairs: Array<[number, number]> = [];
|
|
171
|
+
let lastNew = span.newStart - 1;
|
|
172
|
+
for (let oldIdx = span.oldStart; oldIdx <= span.oldEnd; oldIdx++) {
|
|
173
|
+
const newIdx = newIndexByHash.get(oldHashes[oldIdx]!);
|
|
174
|
+
if (newIdx === undefined || newIdx < span.newStart || newIdx > span.newEnd || newIdx <= lastNew) continue;
|
|
175
|
+
if (oldLines[oldIdx] !== newLines[newIdx]) continue;
|
|
176
|
+
pairs.push([oldIdx, newIdx]);
|
|
177
|
+
lastNew = newIdx;
|
|
178
|
+
}
|
|
179
|
+
if (pairs.length === 0) return [span];
|
|
180
|
+
const out: PlacedSpan[] = [];
|
|
181
|
+
let oldCursor = span.oldStart;
|
|
182
|
+
let newCursor = span.newStart;
|
|
183
|
+
for (const [oldIdx, newIdx] of pairs) {
|
|
184
|
+
if (oldCursor <= oldIdx - 1 || newCursor <= newIdx - 1) {
|
|
185
|
+
out.push({ oldStart: oldCursor, oldEnd: oldIdx - 1, newStart: newCursor, newEnd: newIdx - 1 });
|
|
186
|
+
}
|
|
187
|
+
oldCursor = oldIdx + 1;
|
|
188
|
+
newCursor = newIdx + 1;
|
|
189
|
+
}
|
|
190
|
+
if (oldCursor <= span.oldEnd || newCursor <= span.newEnd) {
|
|
191
|
+
out.push({ oldStart: oldCursor, oldEnd: span.oldEnd, newStart: newCursor, newEnd: span.newEnd });
|
|
192
|
+
}
|
|
193
|
+
return out;
|
|
194
|
+
}
|
|
195
|
+
|
|
162
196
|
function trimPlacedSpans(
|
|
163
197
|
placed: PlacedSpan[],
|
|
164
198
|
oldLines: string[],
|
|
@@ -166,22 +200,11 @@ function trimPlacedSpans(
|
|
|
166
200
|
oldHashes: string[],
|
|
167
201
|
newHashes: string[],
|
|
168
202
|
): PlacedSpan[] {
|
|
203
|
+
const newIndexByHash = new Map<string, number>();
|
|
204
|
+
for (let index = 0; index < newHashes.length; index++) newIndexByHash.set(newHashes[index]!, index);
|
|
169
205
|
const out: PlacedSpan[] = [];
|
|
170
206
|
for (const span of placed) {
|
|
171
|
-
|
|
172
|
-
let oldEnd = span.oldEnd;
|
|
173
|
-
let newStart = span.newStart;
|
|
174
|
-
let newEnd = span.newEnd;
|
|
175
|
-
while (oldStart <= oldEnd && newStart <= newEnd && oldLines[oldStart] === newLines[newStart] && oldHashes[oldStart] === newHashes[newStart]) {
|
|
176
|
-
oldStart += 1;
|
|
177
|
-
newStart += 1;
|
|
178
|
-
}
|
|
179
|
-
while (oldStart <= oldEnd && newStart <= newEnd && oldLines[oldEnd] === newLines[newEnd] && oldHashes[oldEnd] === newHashes[newEnd]) {
|
|
180
|
-
oldEnd -= 1;
|
|
181
|
-
newEnd -= 1;
|
|
182
|
-
}
|
|
183
|
-
if (oldStart > oldEnd && newStart > newEnd) continue;
|
|
184
|
-
out.push({ oldStart, oldEnd, newStart, newEnd });
|
|
207
|
+
for (const piece of splitPlacedSpan(span, oldLines, newLines, oldHashes, newIndexByHash)) out.push(piece);
|
|
185
208
|
}
|
|
186
209
|
return out;
|
|
187
210
|
}
|
|
@@ -364,7 +387,7 @@ export function genDiff(
|
|
|
364
387
|
): { diff: string; firstChangedLine: number | undefined; lineNumbers: (number|undefined)[] } {
|
|
365
388
|
const maxLineBytes = limits?.unlimited ? Number.POSITIVE_INFINITY : (limits?.maxLineBytes ?? DEFAULT_MAX_BYTES);
|
|
366
389
|
const maxBytes = limits?.unlimited ? Number.POSITIVE_INFINITY : (limits?.maxBytes ?? DEFAULT_MAX_BYTES);
|
|
367
|
-
if (spans && newContentHashes && oldContentHashes) {
|
|
390
|
+
if (spans && spans.length > 0 && newContentHashes && oldContentHashes) {
|
|
368
391
|
const anchored = genSpanDiff(oldContent, newContent, contextLines, newContentHashes, oldContentHashes, maxLineBytes, maxBytes, spans);
|
|
369
392
|
if (anchored) {
|
|
370
393
|
const diff = disambiguateDuplicateAnchors(anchored.diff);
|