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:
@@ -1,6 +1,7 @@
|
||||
# Changelog
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Added
|
||||
|
||||
- Added whitespace normalization in line reference parsing to tolerate spaces around colons (e.g., `5 : ab` now parses as `5:ab`)
|
||||
@@ -8,24 +9,28 @@
|
||||
- Added warnings detection in `applyHashlineEdits` to alert users when edits affect significantly more lines than expected, indicating possible unintended reformatting
|
||||
- Added diagnostic output showing target line content when an edit produces no changes, helping users identify hash mismatches or incorrect replacement content
|
||||
- Added `{{hashline}}` Handlebars helper to compute accurate `LINE:HASH` references for prompt examples and documentation
|
||||
- Added `substr` edit variant to match and replace by unique substring when line-hash references are unavailable
|
||||
|
||||
### Changed
|
||||
|
||||
- Changed `applyHashlineEdits` return type to include optional `warnings` array for reporting suspicious edit patterns
|
||||
- Improved hash relocation logic to recompute touched lines after hash-based line number adjustments, preventing incorrect merge heuristics
|
||||
- Enhanced error messages for no-op edits to include preview of target lines with their current hashes and content
|
||||
- Changed hashline edit format from `src`/`dst` object structure to direct operation schemas (`replaceLine`, `replaceLines`, `insertAfter`, `insertBefore`, `substr`)
|
||||
- Changed hashline edit format from `src`/`dst` object structure to direct operation schemas (`replaceLine`, `replaceLines`, `insertAfter`)
|
||||
- Changed hash algorithm from 2-character hexadecimal to 3-character base-36 alphanumeric for improved readability and collision resistance
|
||||
- Improved hash mismatch handling to automatically relocate stale line references when the hash uniquely identifies a moved line
|
||||
- Changed `HashlineEdit` from `src`/`dst` format to direct operation schemas: `replaceLine`, `replaceLines`, `insertAfter`, `insertBefore`, `substr`
|
||||
- Changed `HashlineEdit` from `src`/`dst` format to direct operation schemas: `replaceLine`, `replaceLines`, `insertAfter`
|
||||
- Changed hash algorithm from hexadecimal (base-16) to base-36 alphanumeric for shorter, more readable line references
|
||||
- Increased maximum wrapped-line restoration from 6 to 10 lines to handle longer reflowed statements
|
||||
- Updated prompt examples to use `{{hashline}}` Handlebars helper for generating correct line references in tool instructions
|
||||
|
||||
### Removed
|
||||
|
||||
- Removed `insertBefore` hashline edit operation for inserting content before a line
|
||||
- Removed `substr` hashline edit operation for substring-based line replacement
|
||||
- Removed `insertBefore` and `substr` hashline edit variants
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed substr operation to resolve line numbers during validation phase rather than mutation phase, preventing ambiguity errors from being raised too late
|
||||
- Fixed multi-edit application to use original file state for all anchor references, preventing incorrect line numbers when earlier edits change file length
|
||||
|
||||
## [11.10.4] - 2026-02-10
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Edit (Replace lines)
|
||||
# Edit (Hash anchored)
|
||||
|
||||
Line-addressed edits using hash-verified line references. Read file with hashes first, then edit by referencing `LINE:HASH` pairs.
|
||||
|
||||
@@ -23,8 +23,6 @@ Line-addressed edits using hash-verified line references. Read file with hashes
|
||||
- `{ replaceLine: { loc: "LINE:HASH", content: "..." } }`
|
||||
- `{ replaceLines: { start: "LINE:HASH", end: "LINE:HASH", content: "..." } }`
|
||||
- `{ insertAfter: { loc: "LINE:HASH", content: "..." } }`
|
||||
- `{ insertBefore: { loc: "LINE:HASH", content: "..." } }`
|
||||
- `{ substr: { needle: "unique substring", content: "..." } }` — use when line hashes unavailable; needle must match exactly one line
|
||||
|
||||
`content: ""` means delete (for `replaceLine`/`replaceLines`).
|
||||
</instruction>
|
||||
@@ -75,10 +73,6 @@ edit {"path":"src/app.py","edits":[{"replaceLines":{"start":"{{hashline 5 'old_v
|
||||
edit {"path":"src/app.py","edits":[{"insertAfter":{"loc":"{{hashline 3 'def hello'}}","content":" # new comment"}}]}
|
||||
</example>
|
||||
|
||||
<example name="insert before">
|
||||
edit {"path":"src/app.py","edits":[{"insertBefore":{"loc":"{{hashline 3 'def hello'}}","content":" # new comment"}}]}
|
||||
</example>
|
||||
|
||||
<example name="multiple edits (bottom-up safe)">
|
||||
edit {"path":"src/app.py","edits":[{"replaceLine":{"loc":"{{hashline 10 'return True'}}","content":" return False"}},{"replaceLine":{"loc":"{{hashline 3 'def hello'}}","content":" x = 42"}}]}
|
||||
</example>
|
||||
@@ -364,24 +364,6 @@ describe("applyHashlineEdits — insert", () => {
|
||||
|
||||
expect(() => applyHashlineEdits(content, edits)).toThrow();
|
||||
});
|
||||
|
||||
test("inserts before a line", () => {
|
||||
const content = "aaa\nbbb\nccc";
|
||||
const edits: HashlineEdit[] = [{ insertBefore: { loc: makeRef(2, "bbb"), content: "NEW" } }];
|
||||
|
||||
const result = applyHashlineEdits(content, edits);
|
||||
expect(result.content).toBe("aaa\nNEW\nbbb\nccc");
|
||||
expect(result.firstChangedLine).toBe(2);
|
||||
});
|
||||
|
||||
test("inserts before first line", () => {
|
||||
const content = "aaa\nbbb";
|
||||
const edits: HashlineEdit[] = [{ insertBefore: { loc: makeRef(1, "aaa"), content: "TOP" } }];
|
||||
|
||||
const result = applyHashlineEdits(content, edits);
|
||||
expect(result.content).toBe("TOP\naaa\nbbb");
|
||||
expect(result.firstChangedLine).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
@@ -401,14 +383,6 @@ describe("applyHashlineEdits — heuristics", () => {
|
||||
expect(result.content).toBe("aaa\nbbb\nNEW\nccc");
|
||||
});
|
||||
|
||||
test("strips insert-before anchor echo", () => {
|
||||
const content = "aaa\nbbb\nccc";
|
||||
const edits: HashlineEdit[] = [{ insertBefore: { loc: makeRef(2, "bbb"), content: "NEW\nbbb" } }];
|
||||
|
||||
const result = applyHashlineEdits(content, edits);
|
||||
expect(result.content).toBe("aaa\nNEW\nbbb\nccc");
|
||||
});
|
||||
|
||||
test("strips range boundary echo and preserves whitespace on unchanged lines", () => {
|
||||
const content = [
|
||||
"import { foo } from 'x';",
|
||||
@@ -551,21 +525,6 @@ describe("applyHashlineEdits — heuristics", () => {
|
||||
expect(result.content).toBe("aaa\nBBB\nccc");
|
||||
});
|
||||
|
||||
test("substr replaces needle with content when it matches exactly one line", () => {
|
||||
const content = "aaa\ndevtools–unsupported-bridge-protocol\nccc";
|
||||
const edits: HashlineEdit[] = [
|
||||
{
|
||||
substr: {
|
||||
needle: "devtools–unsupported-bridge-protocol",
|
||||
content: "devtools-unsupported-bridge-protocol",
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const result = applyHashlineEdits(content, edits);
|
||||
expect(result.content).toBe("aaa\ndevtools-unsupported-bridge-protocol\nccc");
|
||||
});
|
||||
|
||||
test("treats same-line ranges as single-line replacements", () => {
|
||||
const content = "aaa\nbbb\nccc";
|
||||
const good = makeRef(2, "bbb");
|
||||
@@ -730,20 +689,6 @@ describe("applyHashlineEdits — errors", () => {
|
||||
expect(() => applyHashlineEdits(content, edits)).toThrow(HashlineMismatchError);
|
||||
});
|
||||
|
||||
test("rejects substr when needle not found", () => {
|
||||
const content = "aaa\nbbb";
|
||||
const edits: HashlineEdit[] = [{ substr: { needle: "garbage", content: "X" } }];
|
||||
|
||||
expect(() => applyHashlineEdits(content, edits)).toThrow(/Substr needle not found/);
|
||||
});
|
||||
|
||||
test("rejects substr when needle matches multiple lines", () => {
|
||||
const content = "aaa\ndup\nmid\ndup";
|
||||
const edits: HashlineEdit[] = [{ substr: { needle: "dup", content: "X" } }];
|
||||
|
||||
expect(() => applyHashlineEdits(content, edits)).toThrow(/Substr needle is ambiguous/);
|
||||
});
|
||||
|
||||
test("rejects out-of-range line", () => {
|
||||
const content = "aaa\nbbb";
|
||||
const edits: HashlineEdit[] = [{ replaceLine: { loc: "10:aa", content: "X" } }];
|
||||
|
||||
@@ -175,7 +175,7 @@ export function generateReport(result: BenchmarkResult): string {
|
||||
lines.push("");
|
||||
lines.push("| Operation | Count | % |");
|
||||
lines.push("|-----------|-------|---|");
|
||||
const order = ["replaceLine", "replaceLines", "insertAfter", "insertBefore", "substr"];
|
||||
const order = ["replaceLine", "replaceLines", "insertAfter"];
|
||||
for (const key of order) {
|
||||
const count = summary.hashlineEditSubtypes[key] ?? 0;
|
||||
const pct = formatPercent(count / total);
|
||||
|
||||
@@ -51,7 +51,7 @@ function getEditPathFromArgs(args: unknown): string | null {
|
||||
return typeof pathValue === "string" && pathValue.length > 0 ? pathValue : null;
|
||||
}
|
||||
|
||||
const HASHLINE_SUBTYPES = ["replaceLine", "replaceLines", "insertAfter", "insertBefore", "substr"] as const;
|
||||
const HASHLINE_SUBTYPES = ["replaceLine", "replaceLines", "insertAfter"] as const;
|
||||
|
||||
function countHashlineEditSubtypes(args: unknown): Record<string, number> {
|
||||
const counts: Record<string, number> = Object.fromEntries(HASHLINE_SUBTYPES.map((k) => [k, 0]));
|
||||
@@ -253,7 +253,10 @@ async function evaluateMutationIntent(
|
||||
};
|
||||
}
|
||||
|
||||
type GuidedHashlineEdit = { src: unknown; dst: string };
|
||||
type GuidedHashlineEdit =
|
||||
| { replaceLine: { loc: string; content: string } }
|
||||
| { replaceLines: { start: string; end: string; content: string } }
|
||||
| { insertAfter: { loc: string; content: string } };
|
||||
|
||||
function buildGuidedHashlineEdits(actual: string, expected: string): GuidedHashlineEdit[] {
|
||||
const changes = diffLines(actual, expected);
|
||||
@@ -273,19 +276,23 @@ function buildGuidedHashlineEdits(actual: string, expected: string): GuidedHashl
|
||||
if (pendingRemoved.length === 0) {
|
||||
const insertLine = pendingStart;
|
||||
if (pendingAdded.length === 0) return;
|
||||
if (insertLine <= actualLines.length) {
|
||||
const beforeLine = actualLines[insertLine - 1] ?? "";
|
||||
const beforeRef = `${insertLine}:${computeLineHash(insertLine, beforeLine)}`;
|
||||
if (insertLine === 1) {
|
||||
const firstLine = actualLines[0] ?? "";
|
||||
const firstRef = `1:${computeLineHash(1, firstLine)}`;
|
||||
edits.push({
|
||||
src: { kind: "insertBefore", before: beforeRef },
|
||||
dst: pendingAdded.join("\n"),
|
||||
replaceLine: { loc: firstRef, content: pendingAdded.join("\n") + "\n" + firstLine },
|
||||
});
|
||||
} else if (insertLine <= actualLines.length) {
|
||||
const afterLine = actualLines[insertLine - 2] ?? "";
|
||||
const afterRef = `${insertLine - 1}:${computeLineHash(insertLine - 1, afterLine)}`;
|
||||
edits.push({
|
||||
insertAfter: { loc: afterRef, content: pendingAdded.join("\n") },
|
||||
});
|
||||
} else if (insertLine === actualLines.length + 1 && actualLines.length > 0) {
|
||||
const afterLine = actualLines[actualLines.length - 1] ?? "";
|
||||
const afterRef = `${actualLines.length}:${computeLineHash(actualLines.length, afterLine)}`;
|
||||
edits.push({
|
||||
src: { kind: "insertAfter", after: afterRef },
|
||||
dst: pendingAdded.join("\n"),
|
||||
insertAfter: { loc: afterRef, content: pendingAdded.join("\n") },
|
||||
});
|
||||
}
|
||||
} else {
|
||||
@@ -294,13 +301,12 @@ function buildGuidedHashlineEdits(actual: string, expected: string): GuidedHashl
|
||||
const startContent = actualLines[startLine - 1] ?? "";
|
||||
const startRef = `${startLine}:${computeLineHash(startLine, startContent)}`;
|
||||
if (startLine === endLine) {
|
||||
edits.push({ src: { kind: "single", ref: startRef }, dst: pendingAdded.join("\n") });
|
||||
edits.push({ replaceLine: { loc: startRef, content: pendingAdded.join("\n") } });
|
||||
} else {
|
||||
const endContent = actualLines[endLine - 1] ?? "";
|
||||
const endRef = `${endLine}:${computeLineHash(endLine, endContent)}`;
|
||||
edits.push({
|
||||
src: { kind: "range", start: startRef, end: endRef },
|
||||
dst: pendingAdded.join("\n"),
|
||||
replaceLines: { start: startRef, end: endRef, content: pendingAdded.join("\n") },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user