feat(coding-agent): removed insertBefore and substr hashline edit operations
- Removed insertBefore and substr hashline edit operations, simplifying the edit API to support only replaceLine, replaceLines, and insertAfter operations. - Updated hashline edit schema and type definitions to remove insertBefore and substr operation types from the ParsedRefs union and edit validation logic. - Removed insertBefore and substr test cases from hashline test suite, including tests for insert-before functionality, anchor echo stripping, and substring matching. - Updated benchmark runner to refactor edit operations from src/dst format to discriminated union types (replaceLine, replaceLines, insertAfter) and adjusted insertAfter line references.
This commit is contained in:
@@ -18,9 +18,7 @@ import type { HashMismatch } from "./types";
|
||||
type ParsedRefs =
|
||||
| { kind: "single"; ref: { line: number; hash: string } }
|
||||
| { kind: "range"; start: { line: number; hash: string }; end: { line: number; hash: string } }
|
||||
| { kind: "insertAfter"; after: { line: number; hash: string } }
|
||||
| { kind: "insertBefore"; before: { line: number; hash: string } }
|
||||
| { kind: "substr"; needle: string; resolvedLine?: number };
|
||||
| { kind: "insertAfter"; after: { line: number; hash: string } };
|
||||
|
||||
function parseHashlineEdit(edit: HashlineEdit): { spec: ParsedRefs; dst: string } {
|
||||
if ("replaceLine" in edit) {
|
||||
@@ -37,21 +35,9 @@ function parseHashlineEdit(edit: HashlineEdit): { spec: ParsedRefs; dst: string
|
||||
dst: edit.replaceLines.content,
|
||||
};
|
||||
}
|
||||
if ("insertAfter" in edit) {
|
||||
return {
|
||||
spec: { kind: "insertAfter", after: parseLineRef(edit.insertAfter.loc) },
|
||||
dst: edit.insertAfter.content,
|
||||
};
|
||||
}
|
||||
if ("substr" in edit) {
|
||||
return {
|
||||
spec: { kind: "substr", needle: edit.substr.needle },
|
||||
dst: edit.substr.content,
|
||||
};
|
||||
}
|
||||
return {
|
||||
spec: { kind: "insertBefore", before: parseLineRef(edit.insertBefore.loc) },
|
||||
dst: edit.insertBefore.content,
|
||||
spec: { kind: "insertAfter", after: parseLineRef(edit.insertAfter.loc) },
|
||||
dst: edit.insertAfter.content,
|
||||
};
|
||||
}
|
||||
/** Split dst into lines; empty string means delete (no lines). */
|
||||
@@ -237,14 +223,6 @@ function stripInsertAnchorEchoAfter(anchorLine: string, dstLines: string[]): str
|
||||
return dstLines;
|
||||
}
|
||||
|
||||
function stripInsertAnchorEchoBefore(anchorLine: string, dstLines: string[]): string[] {
|
||||
if (dstLines.length <= 1) return dstLines;
|
||||
if (equalsIgnoringWhitespace(dstLines[dstLines.length - 1], anchorLine)) {
|
||||
return dstLines.slice(0, -1);
|
||||
}
|
||||
return dstLines;
|
||||
}
|
||||
|
||||
function stripRangeBoundaryEcho(fileLines: string[], startLine: number, endLine: number, dstLines: string[]): string[] {
|
||||
// Only strip when the model replaced with multiple lines and grew the edit.
|
||||
// This avoids turning a single-line replacement into a deletion.
|
||||
@@ -699,7 +677,7 @@ export function validateLineRef(ref: { line: number; hash: string }, fileLines:
|
||||
* Apply an array of hashline edits to file content.
|
||||
*
|
||||
* Each edit operation identifies target lines directly (`replaceLine`, `replaceLines`,
|
||||
* `insertAfter`, `insertBefore`). Line references are resolved via {@link parseLineRef}
|
||||
* `insertAfter`). Line references are resolved via {@link parseLineRef}
|
||||
* and hashes validated before any mutation.
|
||||
*
|
||||
* Edits are sorted bottom-up (highest effective line first) so earlier
|
||||
@@ -728,29 +706,6 @@ export function applyHashlineEdits(
|
||||
};
|
||||
});
|
||||
|
||||
// Resolve substr specs to line numbers
|
||||
for (const p of parsed) {
|
||||
if (p.spec.kind !== "substr") continue;
|
||||
const indices: number[] = [];
|
||||
for (let i = 0; i < fileLines.length; i++) {
|
||||
if (fileLines[i].includes(p.spec.needle)) indices.push(i);
|
||||
}
|
||||
if (indices.length === 0) {
|
||||
throw new Error(`Substr needle not found in file: "${p.spec.needle}"`);
|
||||
}
|
||||
if (indices.length > 1) {
|
||||
const previews = indices
|
||||
.slice(0, 5)
|
||||
.map(i => `${i + 1}: ${fileLines[i]}`)
|
||||
.join("\n");
|
||||
const more = indices.length > 5 ? `\n... (${indices.length - 5} more)` : "";
|
||||
throw new Error(
|
||||
`Substr needle is ambiguous (found ${indices.length} matches): "${p.spec.needle}"\n${previews}${more}`,
|
||||
);
|
||||
}
|
||||
(p.spec as { resolvedLine?: number }).resolvedLine = indices[0] + 1;
|
||||
}
|
||||
|
||||
function collectExplicitlyTouchedLines(): Set<number> {
|
||||
const touched = new Set<number>();
|
||||
for (const { spec } of parsed) {
|
||||
@@ -764,12 +719,6 @@ export function applyHashlineEdits(
|
||||
case "insertAfter":
|
||||
touched.add(spec.after.line);
|
||||
break;
|
||||
case "insertBefore":
|
||||
touched.add(spec.before.line);
|
||||
break;
|
||||
case "substr":
|
||||
touched.add(spec.resolvedLine!);
|
||||
break;
|
||||
}
|
||||
}
|
||||
return touched;
|
||||
@@ -810,17 +759,6 @@ export function applyHashlineEdits(
|
||||
}
|
||||
refsToValidate.push(spec.after);
|
||||
break;
|
||||
case "insertBefore":
|
||||
if (dstLines.length === 0) {
|
||||
throw new Error('Insert-before edit (src "..N:HH") requires non-empty dst');
|
||||
}
|
||||
refsToValidate.push(spec.before);
|
||||
break;
|
||||
case "substr":
|
||||
if (dstLines.length !== 1) {
|
||||
throw new Error(`Substr edit requires single-line replacement (got ${dstLines.length} lines)`);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
for (const ref of refsToValidate) {
|
||||
@@ -867,14 +805,6 @@ export function applyHashlineEdits(
|
||||
sortLine = p.spec.after.line;
|
||||
precedence = 1;
|
||||
break;
|
||||
case "insertBefore":
|
||||
sortLine = p.spec.before.line;
|
||||
precedence = 2;
|
||||
break;
|
||||
case "substr":
|
||||
sortLine = p.spec.resolvedLine ?? 0;
|
||||
precedence = 3;
|
||||
break;
|
||||
}
|
||||
return { ...p, idx, sortLine, precedence };
|
||||
});
|
||||
@@ -945,21 +875,6 @@ export function applyHashlineEdits(
|
||||
trackFirstChanged(spec.after.line + 1);
|
||||
break;
|
||||
}
|
||||
case "insertBefore": {
|
||||
const anchorLine = originalFileLines[spec.before.line - 1];
|
||||
const inserted = stripInsertAnchorEchoBefore(anchorLine, dstLines);
|
||||
fileLines.splice(spec.before.line - 1, 0, ...inserted);
|
||||
trackFirstChanged(spec.before.line);
|
||||
break;
|
||||
}
|
||||
case "substr": {
|
||||
const lineIdx = spec.resolvedLine! - 1;
|
||||
const original = fileLines[lineIdx];
|
||||
const replaced = original.replace(spec.needle, dstLines[0]);
|
||||
fileLines.splice(lineIdx, 1, replaced);
|
||||
trackFirstChanged(spec.resolvedLine!);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -144,24 +144,10 @@ const hashlineInsertAfterSchema = Type.Object({
|
||||
content: Type.String({ description: "Content to insert (\\n-separated); must be non-empty" }),
|
||||
}),
|
||||
});
|
||||
const hashlineInsertBeforeSchema = Type.Object({
|
||||
insertBefore: Type.Object({
|
||||
loc: Type.String({ description: 'Insert before this line "LINE:HASH"' }),
|
||||
content: Type.String({ description: "Content to insert (\\n-separated); must be non-empty" }),
|
||||
}),
|
||||
});
|
||||
const hashlineSubstrSchema = Type.Object({
|
||||
substr: Type.Object({
|
||||
needle: Type.String({ description: "Unique substring to find in the target line" }),
|
||||
content: Type.String({ description: "Replacement for the needle (single-line only)" }),
|
||||
}),
|
||||
});
|
||||
const hashlineEditItemSchema = Type.Union([
|
||||
hashlineReplaceLineSchema,
|
||||
hashlineReplaceLinesSchema,
|
||||
hashlineInsertAfterSchema,
|
||||
hashlineInsertBeforeSchema,
|
||||
hashlineSubstrSchema,
|
||||
]);
|
||||
const hashlineEditSchema = Type.Object({
|
||||
path: Type.String({ description: "File path (relative or absolute)" }),
|
||||
@@ -428,7 +414,6 @@ export class EditTool implements AgentTool<TInput> {
|
||||
if ("replaceLine" in edit) refs.push(edit.replaceLine.loc);
|
||||
else if ("replaceLines" in edit) refs.push(edit.replaceLines.start, edit.replaceLines.end);
|
||||
else if ("insertAfter" in edit) refs.push(edit.insertAfter.loc);
|
||||
else if ("insertBefore" in edit) refs.push(edit.insertBefore.loc);
|
||||
for (const ref of refs) {
|
||||
const parsed = parseLineRef(ref);
|
||||
if (parsed.line >= 1 && parsed.line <= lines.length) {
|
||||
|
||||
@@ -88,9 +88,7 @@ interface EditRenderArgs {
|
||||
type HashlineEditPreview =
|
||||
| { replaceLine: { loc: string; content: string } }
|
||||
| { replaceLines: { start: string; end: string; content: string } }
|
||||
| { insertAfter: { loc: string; content: string } }
|
||||
| { insertBefore: { loc: string; content: string } }
|
||||
| { substr: { needle: string; content: string } };
|
||||
| { insertAfter: { loc: string; content: string } };
|
||||
|
||||
/** Extended context for edit tool rendering */
|
||||
export interface EditRenderContext {
|
||||
@@ -170,21 +168,9 @@ function formatStreamingHashlineEdits(edits: HashlineEditPreview[], uiTheme: The
|
||||
dst: edit.replaceLines.content,
|
||||
};
|
||||
}
|
||||
if ("insertAfter" in edit) {
|
||||
return {
|
||||
srcLabel: `• insertAfter ${edit.insertAfter.loc}..`,
|
||||
dst: edit.insertAfter.content,
|
||||
};
|
||||
}
|
||||
if ("substr" in edit) {
|
||||
return {
|
||||
srcLabel: `• substr "${edit.substr.needle}"`,
|
||||
dst: edit.substr.content,
|
||||
};
|
||||
}
|
||||
return {
|
||||
srcLabel: `• insertBefore ..${edit.insertBefore.loc}`,
|
||||
dst: edit.insertBefore.content,
|
||||
srcLabel: `• insertAfter ${edit.insertAfter.loc}..`,
|
||||
dst: edit.insertAfter.content,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user