refactor(hashline-edit): enforce three-op edit model
Unify internal hashline edit handling around replace/append/prepend to remove legacy operation shapes. This keeps normalization, ordering, deduplication, execution, and tests aligned with the new op/pos/end/lines contract.
This commit is contained in:
parent
6ec0ff732b
commit
08b663df86
@ -6,23 +6,13 @@ function normalizeEditPayload(payload: string | string[]): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function buildDedupeKey(edit: HashlineEdit): string {
|
function buildDedupeKey(edit: HashlineEdit): string {
|
||||||
switch (edit.type) {
|
switch (edit.op) {
|
||||||
case "set_line":
|
|
||||||
return `set_line|${edit.line}|${normalizeEditPayload(edit.text)}`
|
|
||||||
case "replace_lines":
|
|
||||||
return `replace_lines|${edit.start_line}|${edit.end_line}|${normalizeEditPayload(edit.text)}`
|
|
||||||
case "insert_after":
|
|
||||||
return `insert_after|${edit.line}|${normalizeEditPayload(edit.text)}`
|
|
||||||
case "insert_before":
|
|
||||||
return `insert_before|${edit.line}|${normalizeEditPayload(edit.text)}`
|
|
||||||
case "insert_between":
|
|
||||||
return `insert_between|${edit.after_line}|${edit.before_line}|${normalizeEditPayload(edit.text)}`
|
|
||||||
case "replace":
|
case "replace":
|
||||||
return `replace|${edit.old_text}|${normalizeEditPayload(edit.new_text)}`
|
return `replace|${edit.pos}|${edit.end ?? ""}|${normalizeEditPayload(edit.lines)}`
|
||||||
case "append":
|
case "append":
|
||||||
return `append|${normalizeEditPayload(edit.text)}`
|
return `append|${edit.pos ?? ""}|${normalizeEditPayload(edit.lines)}`
|
||||||
case "prepend":
|
case "prepend":
|
||||||
return `prepend|${normalizeEditPayload(edit.text)}`
|
return `prepend|${edit.pos ?? ""}|${normalizeEditPayload(edit.lines)}`
|
||||||
default:
|
default:
|
||||||
return JSON.stringify(edit)
|
return JSON.stringify(edit)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -150,11 +150,3 @@ export function applyPrepend(lines: string[], text: string | string[]): string[]
|
|||||||
}
|
}
|
||||||
return [...normalized, ...lines]
|
return [...normalized, ...lines]
|
||||||
}
|
}
|
||||||
|
|
||||||
export function applyReplace(content: string, oldText: string, newText: string | string[]): string {
|
|
||||||
if (!content.includes(oldText)) {
|
|
||||||
throw new Error(`Text not found: "${oldText}"`)
|
|
||||||
}
|
|
||||||
const replacement = Array.isArray(newText) ? newText.join("\n") : newText
|
|
||||||
return content.replaceAll(oldText, replacement)
|
|
||||||
}
|
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { describe, expect, it } from "bun:test"
|
import { describe, expect, it } from "bun:test"
|
||||||
import { applyHashlineEdits, applyInsertAfter, applyReplace, applyReplaceLines, applySetLine } from "./edit-operations"
|
import { applyHashlineEdits, applyInsertAfter, applyReplaceLines, applySetLine } from "./edit-operations"
|
||||||
import { applyAppend, applyPrepend } from "./edit-operation-primitives"
|
import { applyAppend, applyInsertBetween, applyPrepend } from "./edit-operation-primitives"
|
||||||
import { computeLineHash } from "./hash-computation"
|
import { computeLineHash } from "./hash-computation"
|
||||||
import type { HashlineEdit } from "./types"
|
import type { HashlineEdit } from "./types"
|
||||||
|
|
||||||
@ -49,7 +49,7 @@ describe("hashline edit operations", () => {
|
|||||||
//#when
|
//#when
|
||||||
const result = applyHashlineEdits(
|
const result = applyHashlineEdits(
|
||||||
lines.join("\n"),
|
lines.join("\n"),
|
||||||
[{ type: "insert_before", line: anchorFor(lines, 2), text: "before 2" }]
|
[{ op: "prepend", pos: anchorFor(lines, 2), lines: "before 2" }]
|
||||||
)
|
)
|
||||||
|
|
||||||
//#then
|
//#then
|
||||||
@ -61,15 +61,7 @@ describe("hashline edit operations", () => {
|
|||||||
const lines = ["line 1", "line 2", "line 3"]
|
const lines = ["line 1", "line 2", "line 3"]
|
||||||
|
|
||||||
//#when
|
//#when
|
||||||
const result = applyHashlineEdits(
|
const result = applyInsertBetween(lines, anchorFor(lines, 1), anchorFor(lines, 2), ["between"]).join("\n")
|
||||||
lines.join("\n"),
|
|
||||||
[{
|
|
||||||
type: "insert_between",
|
|
||||||
after_line: anchorFor(lines, 1),
|
|
||||||
before_line: anchorFor(lines, 2),
|
|
||||||
text: ["between"],
|
|
||||||
}]
|
|
||||||
)
|
|
||||||
|
|
||||||
//#then
|
//#then
|
||||||
expect(result).toEqual("line 1\nbetween\nline 2\nline 3")
|
expect(result).toEqual("line 1\nbetween\nline 2\nline 3")
|
||||||
@ -89,7 +81,7 @@ describe("hashline edit operations", () => {
|
|||||||
|
|
||||||
//#when / #then
|
//#when / #then
|
||||||
expect(() =>
|
expect(() =>
|
||||||
applyHashlineEdits(lines.join("\n"), [{ type: "insert_before", line: anchorFor(lines, 1), text: [] }])
|
applyHashlineEdits(lines.join("\n"), [{ op: "prepend", pos: anchorFor(lines, 1), lines: [] }])
|
||||||
).toThrow(/non-empty/i)
|
).toThrow(/non-empty/i)
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -98,28 +90,7 @@ describe("hashline edit operations", () => {
|
|||||||
const lines = ["line 1", "line 2"]
|
const lines = ["line 1", "line 2"]
|
||||||
|
|
||||||
//#when / #then
|
//#when / #then
|
||||||
expect(() =>
|
expect(() => applyInsertBetween(lines, anchorFor(lines, 1), anchorFor(lines, 2), [])).toThrow(/non-empty/i)
|
||||||
applyHashlineEdits(
|
|
||||||
lines.join("\n"),
|
|
||||||
[{
|
|
||||||
type: "insert_between",
|
|
||||||
after_line: anchorFor(lines, 1),
|
|
||||||
before_line: anchorFor(lines, 2),
|
|
||||||
text: [],
|
|
||||||
}]
|
|
||||||
)
|
|
||||||
).toThrow(/non-empty/i)
|
|
||||||
})
|
|
||||||
|
|
||||||
it("applies replace operation", () => {
|
|
||||||
//#given
|
|
||||||
const content = "hello world foo"
|
|
||||||
|
|
||||||
//#when
|
|
||||||
const result = applyReplace(content, "world", "universe")
|
|
||||||
|
|
||||||
//#then
|
|
||||||
expect(result).toEqual("hello universe foo")
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it("applies mixed edits in one pass", () => {
|
it("applies mixed edits in one pass", () => {
|
||||||
@ -127,8 +98,8 @@ describe("hashline edit operations", () => {
|
|||||||
const content = "line 1\nline 2\nline 3"
|
const content = "line 1\nline 2\nline 3"
|
||||||
const lines = content.split("\n")
|
const lines = content.split("\n")
|
||||||
const edits: HashlineEdit[] = [
|
const edits: HashlineEdit[] = [
|
||||||
{ type: "insert_after", line: anchorFor(lines, 1), text: "inserted" },
|
{ op: "append", pos: anchorFor(lines, 1), lines: "inserted" },
|
||||||
{ type: "set_line", line: anchorFor(lines, 3), text: "modified" },
|
{ op: "replace", pos: anchorFor(lines, 3), lines: "modified" },
|
||||||
]
|
]
|
||||||
|
|
||||||
//#when
|
//#when
|
||||||
@ -143,8 +114,8 @@ describe("hashline edit operations", () => {
|
|||||||
const content = "line 1\nline 2"
|
const content = "line 1\nline 2"
|
||||||
const lines = content.split("\n")
|
const lines = content.split("\n")
|
||||||
const edits: HashlineEdit[] = [
|
const edits: HashlineEdit[] = [
|
||||||
{ type: "insert_after", line: anchorFor(lines, 1), text: "inserted" },
|
{ op: "append", pos: anchorFor(lines, 1), lines: "inserted" },
|
||||||
{ type: "insert_after", line: anchorFor(lines, 1), text: "inserted" },
|
{ op: "append", pos: anchorFor(lines, 1), lines: "inserted" },
|
||||||
]
|
]
|
||||||
|
|
||||||
//#when
|
//#when
|
||||||
@ -227,16 +198,9 @@ describe("hashline edit operations", () => {
|
|||||||
const lines = ["line 1", "line 2", "line 3"]
|
const lines = ["line 1", "line 2", "line 3"]
|
||||||
|
|
||||||
//#when / #then
|
//#when / #then
|
||||||
expect(() =>
|
expect(() => applyInsertBetween(lines, anchorFor(lines, 1), anchorFor(lines, 2), ["line 1", "line 2"])).toThrow(
|
||||||
applyHashlineEdits(lines.join("\n"), [
|
/non-empty/i
|
||||||
{
|
)
|
||||||
type: "insert_between",
|
|
||||||
after_line: anchorFor(lines, 1),
|
|
||||||
before_line: anchorFor(lines, 2),
|
|
||||||
text: ["line 1", "line 2"],
|
|
||||||
},
|
|
||||||
])
|
|
||||||
).toThrow(/non-empty/i)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it("restores indentation for first replace_lines entry", () => {
|
it("restores indentation for first replace_lines entry", () => {
|
||||||
@ -322,8 +286,8 @@ describe("hashline edit operations", () => {
|
|||||||
|
|
||||||
//#when
|
//#when
|
||||||
const result = applyHashlineEdits(content, [
|
const result = applyHashlineEdits(content, [
|
||||||
{ type: "append", text: ["line 3"] },
|
{ op: "append", lines: ["line 3"] },
|
||||||
{ type: "prepend", text: ["line 0"] },
|
{ op: "prepend", lines: ["line 0"] },
|
||||||
])
|
])
|
||||||
|
|
||||||
//#then
|
//#then
|
||||||
|
|||||||
@ -5,9 +5,7 @@ import {
|
|||||||
applyAppend,
|
applyAppend,
|
||||||
applyInsertAfter,
|
applyInsertAfter,
|
||||||
applyInsertBefore,
|
applyInsertBefore,
|
||||||
applyInsertBetween,
|
|
||||||
applyPrepend,
|
applyPrepend,
|
||||||
applyReplace,
|
|
||||||
applyReplaceLines,
|
applyReplaceLines,
|
||||||
applySetLine,
|
applySetLine,
|
||||||
} from "./edit-operation-primitives"
|
} from "./edit-operation-primitives"
|
||||||
@ -33,42 +31,17 @@ export function applyHashlineEditsWithReport(content: string, edits: HashlineEdi
|
|||||||
|
|
||||||
let noopEdits = 0
|
let noopEdits = 0
|
||||||
|
|
||||||
let result = content
|
let lines = content.length === 0 ? [] : content.split("\n")
|
||||||
let lines = result.length === 0 ? [] : result.split("\n")
|
|
||||||
|
|
||||||
const refs = collectLineRefs(sortedEdits)
|
const refs = collectLineRefs(sortedEdits)
|
||||||
validateLineRefs(lines, refs)
|
validateLineRefs(lines, refs)
|
||||||
|
|
||||||
for (const edit of sortedEdits) {
|
for (const edit of sortedEdits) {
|
||||||
switch (edit.type) {
|
switch (edit.op) {
|
||||||
case "set_line": {
|
case "replace": {
|
||||||
lines = applySetLine(lines, edit.line, edit.text, { skipValidation: true })
|
const next = edit.end
|
||||||
break
|
? applyReplaceLines(lines, edit.pos, edit.end, edit.lines, { skipValidation: true })
|
||||||
}
|
: applySetLine(lines, edit.pos, edit.lines, { skipValidation: true })
|
||||||
case "replace_lines": {
|
|
||||||
lines = applyReplaceLines(lines, edit.start_line, edit.end_line, edit.text, { skipValidation: true })
|
|
||||||
break
|
|
||||||
}
|
|
||||||
case "insert_after": {
|
|
||||||
const next = applyInsertAfter(lines, edit.line, edit.text, { skipValidation: true })
|
|
||||||
if (next.join("\n") === lines.join("\n")) {
|
|
||||||
noopEdits += 1
|
|
||||||
break
|
|
||||||
}
|
|
||||||
lines = next
|
|
||||||
break
|
|
||||||
}
|
|
||||||
case "insert_before": {
|
|
||||||
const next = applyInsertBefore(lines, edit.line, edit.text, { skipValidation: true })
|
|
||||||
if (next.join("\n") === lines.join("\n")) {
|
|
||||||
noopEdits += 1
|
|
||||||
break
|
|
||||||
}
|
|
||||||
lines = next
|
|
||||||
break
|
|
||||||
}
|
|
||||||
case "insert_between": {
|
|
||||||
const next = applyInsertBetween(lines, edit.after_line, edit.before_line, edit.text, { skipValidation: true })
|
|
||||||
if (next.join("\n") === lines.join("\n")) {
|
if (next.join("\n") === lines.join("\n")) {
|
||||||
noopEdits += 1
|
noopEdits += 1
|
||||||
break
|
break
|
||||||
@ -77,7 +50,9 @@ export function applyHashlineEditsWithReport(content: string, edits: HashlineEdi
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
case "append": {
|
case "append": {
|
||||||
const next = applyAppend(lines, edit.text)
|
const next = edit.pos
|
||||||
|
? applyInsertAfter(lines, edit.pos, edit.lines, { skipValidation: true })
|
||||||
|
: applyAppend(lines, edit.lines)
|
||||||
if (next.join("\n") === lines.join("\n")) {
|
if (next.join("\n") === lines.join("\n")) {
|
||||||
noopEdits += 1
|
noopEdits += 1
|
||||||
break
|
break
|
||||||
@ -86,7 +61,9 @@ export function applyHashlineEditsWithReport(content: string, edits: HashlineEdi
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
case "prepend": {
|
case "prepend": {
|
||||||
const next = applyPrepend(lines, edit.text)
|
const next = edit.pos
|
||||||
|
? applyInsertBefore(lines, edit.pos, edit.lines, { skipValidation: true })
|
||||||
|
: applyPrepend(lines, edit.lines)
|
||||||
if (next.join("\n") === lines.join("\n")) {
|
if (next.join("\n") === lines.join("\n")) {
|
||||||
noopEdits += 1
|
noopEdits += 1
|
||||||
break
|
break
|
||||||
@ -94,17 +71,6 @@ export function applyHashlineEditsWithReport(content: string, edits: HashlineEdi
|
|||||||
lines = next
|
lines = next
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
case "replace": {
|
|
||||||
result = lines.join("\n")
|
|
||||||
const replaced = applyReplace(result, edit.old_text, edit.new_text)
|
|
||||||
if (replaced === result) {
|
|
||||||
noopEdits += 1
|
|
||||||
break
|
|
||||||
}
|
|
||||||
result = replaced
|
|
||||||
lines = result.split("\n")
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -124,6 +90,4 @@ export {
|
|||||||
applyReplaceLines,
|
applyReplaceLines,
|
||||||
applyInsertAfter,
|
applyInsertAfter,
|
||||||
applyInsertBefore,
|
applyInsertBefore,
|
||||||
applyInsertBetween,
|
|
||||||
applyReplace,
|
|
||||||
} from "./edit-operation-primitives"
|
} from "./edit-operation-primitives"
|
||||||
|
|||||||
@ -2,23 +2,13 @@ import { parseLineRef } from "./validation"
|
|||||||
import type { HashlineEdit } from "./types"
|
import type { HashlineEdit } from "./types"
|
||||||
|
|
||||||
export function getEditLineNumber(edit: HashlineEdit): number {
|
export function getEditLineNumber(edit: HashlineEdit): number {
|
||||||
switch (edit.type) {
|
switch (edit.op) {
|
||||||
case "set_line":
|
|
||||||
return parseLineRef(edit.line).line
|
|
||||||
case "replace_lines":
|
|
||||||
return parseLineRef(edit.end_line).line
|
|
||||||
case "insert_after":
|
|
||||||
return parseLineRef(edit.line).line
|
|
||||||
case "insert_before":
|
|
||||||
return parseLineRef(edit.line).line
|
|
||||||
case "insert_between":
|
|
||||||
return parseLineRef(edit.before_line).line
|
|
||||||
case "append":
|
|
||||||
return Number.NEGATIVE_INFINITY
|
|
||||||
case "prepend":
|
|
||||||
return Number.NEGATIVE_INFINITY
|
|
||||||
case "replace":
|
case "replace":
|
||||||
return Number.NEGATIVE_INFINITY
|
return parseLineRef(edit.end ?? edit.pos).line
|
||||||
|
case "append":
|
||||||
|
return edit.pos ? parseLineRef(edit.pos).line : Number.NEGATIVE_INFINITY
|
||||||
|
case "prepend":
|
||||||
|
return edit.pos ? parseLineRef(edit.pos).line : Number.NEGATIVE_INFINITY
|
||||||
default:
|
default:
|
||||||
return Number.POSITIVE_INFINITY
|
return Number.POSITIVE_INFINITY
|
||||||
}
|
}
|
||||||
@ -26,21 +16,12 @@ export function getEditLineNumber(edit: HashlineEdit): number {
|
|||||||
|
|
||||||
export function collectLineRefs(edits: HashlineEdit[]): string[] {
|
export function collectLineRefs(edits: HashlineEdit[]): string[] {
|
||||||
return edits.flatMap((edit) => {
|
return edits.flatMap((edit) => {
|
||||||
switch (edit.type) {
|
switch (edit.op) {
|
||||||
case "set_line":
|
case "replace":
|
||||||
return [edit.line]
|
return edit.end ? [edit.pos, edit.end] : [edit.pos]
|
||||||
case "replace_lines":
|
|
||||||
return [edit.start_line, edit.end_line]
|
|
||||||
case "insert_after":
|
|
||||||
return [edit.line]
|
|
||||||
case "insert_before":
|
|
||||||
return [edit.line]
|
|
||||||
case "insert_between":
|
|
||||||
return [edit.after_line, edit.before_line]
|
|
||||||
case "append":
|
case "append":
|
||||||
case "prepend":
|
case "prepend":
|
||||||
case "replace":
|
return edit.pos ? [edit.pos] : []
|
||||||
return []
|
|
||||||
default:
|
default:
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
|
|||||||
@ -32,7 +32,7 @@ function resolveToolCallID(ctx: ToolContextWithCallID): string | undefined {
|
|||||||
|
|
||||||
function canCreateFromMissingFile(edits: HashlineEdit[]): boolean {
|
function canCreateFromMissingFile(edits: HashlineEdit[]): boolean {
|
||||||
if (edits.length === 0) return false
|
if (edits.length === 0) return false
|
||||||
return edits.every((edit) => edit.type === "append" || edit.type === "prepend")
|
return edits.every((edit) => edit.op === "append" || edit.op === "prepend")
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildSuccessMeta(
|
function buildSuccessMeta(
|
||||||
|
|||||||
@ -8,14 +8,9 @@ export {
|
|||||||
export { parseLineRef, validateLineRef } from "./validation"
|
export { parseLineRef, validateLineRef } from "./validation"
|
||||||
export type { LineRef } from "./validation"
|
export type { LineRef } from "./validation"
|
||||||
export type {
|
export type {
|
||||||
SetLine,
|
ReplaceEdit,
|
||||||
ReplaceLines,
|
AppendEdit,
|
||||||
InsertAfter,
|
PrependEdit,
|
||||||
InsertBefore,
|
|
||||||
InsertBetween,
|
|
||||||
Replace,
|
|
||||||
Append,
|
|
||||||
Prepend,
|
|
||||||
HashlineEdit,
|
HashlineEdit,
|
||||||
} from "./types"
|
} from "./types"
|
||||||
export { NIBBLE_STR, HASHLINE_DICT, HASHLINE_REF_PATTERN, HASHLINE_OUTPUT_PATTERN } from "./constants"
|
export { NIBBLE_STR, HASHLINE_DICT, HASHLINE_REF_PATTERN, HASHLINE_OUTPUT_PATTERN } from "./constants"
|
||||||
@ -23,8 +18,6 @@ export {
|
|||||||
applyHashlineEdits,
|
applyHashlineEdits,
|
||||||
applyInsertAfter,
|
applyInsertAfter,
|
||||||
applyInsertBefore,
|
applyInsertBefore,
|
||||||
applyInsertBetween,
|
|
||||||
applyReplace,
|
|
||||||
applyReplaceLines,
|
applyReplaceLines,
|
||||||
applySetLine,
|
applySetLine,
|
||||||
} from "./edit-operations"
|
} from "./edit-operations"
|
||||||
|
|||||||
@ -2,7 +2,7 @@ import { describe, expect, it } from "bun:test"
|
|||||||
import { normalizeHashlineEdits, type RawHashlineEdit } from "./normalize-edits"
|
import { normalizeHashlineEdits, type RawHashlineEdit } from "./normalize-edits"
|
||||||
|
|
||||||
describe("normalizeHashlineEdits", () => {
|
describe("normalizeHashlineEdits", () => {
|
||||||
it("maps replace with pos to set_line", () => {
|
it("maps replace with pos to replace", () => {
|
||||||
//#given
|
//#given
|
||||||
const input: RawHashlineEdit[] = [{ op: "replace", pos: "2#VK", lines: "updated" }]
|
const input: RawHashlineEdit[] = [{ op: "replace", pos: "2#VK", lines: "updated" }]
|
||||||
|
|
||||||
@ -10,10 +10,10 @@ describe("normalizeHashlineEdits", () => {
|
|||||||
const result = normalizeHashlineEdits(input)
|
const result = normalizeHashlineEdits(input)
|
||||||
|
|
||||||
//#then
|
//#then
|
||||||
expect(result).toEqual([{ type: "set_line", line: "2#VK", text: "updated" }])
|
expect(result).toEqual([{ op: "replace", pos: "2#VK", lines: "updated" }])
|
||||||
})
|
})
|
||||||
|
|
||||||
it("maps replace with pos and end to replace_lines", () => {
|
it("maps replace with pos and end to replace", () => {
|
||||||
//#given
|
//#given
|
||||||
const input: RawHashlineEdit[] = [{ op: "replace", pos: "2#VK", end: "4#MB", lines: ["a", "b"] }]
|
const input: RawHashlineEdit[] = [{ op: "replace", pos: "2#VK", end: "4#MB", lines: ["a", "b"] }]
|
||||||
|
|
||||||
@ -21,10 +21,10 @@ describe("normalizeHashlineEdits", () => {
|
|||||||
const result = normalizeHashlineEdits(input)
|
const result = normalizeHashlineEdits(input)
|
||||||
|
|
||||||
//#then
|
//#then
|
||||||
expect(result).toEqual([{ type: "replace_lines", start_line: "2#VK", end_line: "4#MB", text: ["a", "b"] }])
|
expect(result).toEqual([{ op: "replace", pos: "2#VK", end: "4#MB", lines: ["a", "b"] }])
|
||||||
})
|
})
|
||||||
|
|
||||||
it("maps anchored append and prepend to insert operations", () => {
|
it("maps anchored append and prepend preserving op", () => {
|
||||||
//#given
|
//#given
|
||||||
const input: RawHashlineEdit[] = [
|
const input: RawHashlineEdit[] = [
|
||||||
{ op: "append", pos: "2#VK", lines: ["after"] },
|
{ op: "append", pos: "2#VK", lines: ["after"] },
|
||||||
@ -35,10 +35,7 @@ describe("normalizeHashlineEdits", () => {
|
|||||||
const result = normalizeHashlineEdits(input)
|
const result = normalizeHashlineEdits(input)
|
||||||
|
|
||||||
//#then
|
//#then
|
||||||
expect(result).toEqual([
|
expect(result).toEqual([{ op: "append", pos: "2#VK", lines: ["after"] }, { op: "prepend", pos: "4#MB", lines: ["before"] }])
|
||||||
{ type: "insert_after", line: "2#VK", text: ["after"] },
|
|
||||||
{ type: "insert_before", line: "4#MB", text: ["before"] },
|
|
||||||
])
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it("prefers pos over end for prepend anchors", () => {
|
it("prefers pos over end for prepend anchors", () => {
|
||||||
@ -49,7 +46,7 @@ describe("normalizeHashlineEdits", () => {
|
|||||||
const result = normalizeHashlineEdits(input)
|
const result = normalizeHashlineEdits(input)
|
||||||
|
|
||||||
//#then
|
//#then
|
||||||
expect(result).toEqual([{ type: "insert_before", line: "3#AA", text: ["before"] }])
|
expect(result).toEqual([{ op: "prepend", pos: "3#AA", lines: ["before"] }])
|
||||||
})
|
})
|
||||||
|
|
||||||
it("rejects legacy payload without op", () => {
|
it("rejects legacy payload without op", () => {
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import type { HashlineEdit } from "./types"
|
import type { AppendEdit, HashlineEdit, PrependEdit, ReplaceEdit } from "./types"
|
||||||
|
|
||||||
type HashlineToolOp = "replace" | "append" | "prepend"
|
type HashlineToolOp = "replace" | "append" | "prepend"
|
||||||
|
|
||||||
@ -36,62 +36,43 @@ function normalizeReplaceEdit(edit: RawHashlineEdit, index: number): HashlineEdi
|
|||||||
const pos = normalizeAnchor(edit.pos)
|
const pos = normalizeAnchor(edit.pos)
|
||||||
const end = normalizeAnchor(edit.end)
|
const end = normalizeAnchor(edit.end)
|
||||||
const anchor = requireLine(pos ?? end, index, "replace")
|
const anchor = requireLine(pos ?? end, index, "replace")
|
||||||
const text = requireLines(edit, index)
|
const lines = requireLines(edit, index)
|
||||||
|
|
||||||
if (pos && end) {
|
const normalized: ReplaceEdit = {
|
||||||
return {
|
op: "replace",
|
||||||
type: "replace_lines",
|
pos: anchor,
|
||||||
start_line: pos,
|
lines,
|
||||||
end_line: end,
|
|
||||||
text,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
type: "set_line",
|
|
||||||
line: anchor,
|
|
||||||
text,
|
|
||||||
}
|
}
|
||||||
|
if (end) normalized.end = end
|
||||||
|
return normalized
|
||||||
}
|
}
|
||||||
|
|
||||||
function normalizeAppendEdit(edit: RawHashlineEdit, index: number): HashlineEdit {
|
function normalizeAppendEdit(edit: RawHashlineEdit, index: number): HashlineEdit {
|
||||||
const pos = normalizeAnchor(edit.pos)
|
const pos = normalizeAnchor(edit.pos)
|
||||||
const end = normalizeAnchor(edit.end)
|
const end = normalizeAnchor(edit.end)
|
||||||
const anchor = pos ?? end
|
const anchor = pos ?? end
|
||||||
const text = requireLines(edit, index)
|
const lines = requireLines(edit, index)
|
||||||
|
|
||||||
if (!anchor) {
|
const normalized: AppendEdit = {
|
||||||
return {
|
op: "append",
|
||||||
type: "append",
|
lines,
|
||||||
text,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
type: "insert_after",
|
|
||||||
line: anchor,
|
|
||||||
text,
|
|
||||||
}
|
}
|
||||||
|
if (anchor) normalized.pos = anchor
|
||||||
|
return normalized
|
||||||
}
|
}
|
||||||
|
|
||||||
function normalizePrependEdit(edit: RawHashlineEdit, index: number): HashlineEdit {
|
function normalizePrependEdit(edit: RawHashlineEdit, index: number): HashlineEdit {
|
||||||
const pos = normalizeAnchor(edit.pos)
|
const pos = normalizeAnchor(edit.pos)
|
||||||
const end = normalizeAnchor(edit.end)
|
const end = normalizeAnchor(edit.end)
|
||||||
const anchor = pos ?? end
|
const anchor = pos ?? end
|
||||||
const text = requireLines(edit, index)
|
const lines = requireLines(edit, index)
|
||||||
|
|
||||||
if (!anchor) {
|
const normalized: PrependEdit = {
|
||||||
return {
|
op: "prepend",
|
||||||
type: "prepend",
|
lines,
|
||||||
text,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
type: "insert_before",
|
|
||||||
line: anchor,
|
|
||||||
text,
|
|
||||||
}
|
}
|
||||||
|
if (anchor) normalized.pos = anchor
|
||||||
|
return normalized
|
||||||
}
|
}
|
||||||
|
|
||||||
export function normalizeHashlineEdits(rawEdits: RawHashlineEdit[]): HashlineEdit[] {
|
export function normalizeHashlineEdits(rawEdits: RawHashlineEdit[]): HashlineEdit[] {
|
||||||
|
|||||||
@ -1,57 +1,20 @@
|
|||||||
export interface SetLine {
|
export interface ReplaceEdit {
|
||||||
type: "set_line"
|
op: "replace"
|
||||||
line: string
|
pos: string
|
||||||
text: string | string[]
|
end?: string
|
||||||
|
lines: string | string[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ReplaceLines {
|
export interface AppendEdit {
|
||||||
type: "replace_lines"
|
op: "append"
|
||||||
start_line: string
|
pos?: string
|
||||||
end_line: string
|
lines: string | string[]
|
||||||
text: string | string[]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface InsertAfter {
|
export interface PrependEdit {
|
||||||
type: "insert_after"
|
op: "prepend"
|
||||||
line: string
|
pos?: string
|
||||||
text: string | string[]
|
lines: string | string[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface InsertBefore {
|
export type HashlineEdit = ReplaceEdit | AppendEdit | PrependEdit
|
||||||
type: "insert_before"
|
|
||||||
line: string
|
|
||||||
text: string | string[]
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface InsertBetween {
|
|
||||||
type: "insert_between"
|
|
||||||
after_line: string
|
|
||||||
before_line: string
|
|
||||||
text: string | string[]
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Replace {
|
|
||||||
type: "replace"
|
|
||||||
old_text: string
|
|
||||||
new_text: string | string[]
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Append {
|
|
||||||
type: "append"
|
|
||||||
text: string | string[]
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Prepend {
|
|
||||||
type: "prepend"
|
|
||||||
text: string | string[]
|
|
||||||
}
|
|
||||||
|
|
||||||
export type HashlineEdit =
|
|
||||||
| SetLine
|
|
||||||
| ReplaceLines
|
|
||||||
| InsertAfter
|
|
||||||
| InsertBefore
|
|
||||||
| InsertBetween
|
|
||||||
| Replace
|
|
||||||
| Append
|
|
||||||
| Prepend
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user