feat(hashline): port hashline edit tool from oh-my-pi
This PR ports the hashline edit tool from oh-my-pi to oh-my-opencode as an experimental feature. ## Features - New experimental.hashline_edit config flag - hashline_edit tool with 4 operations: set_line, replace_lines, insert_after, replace - Hash-based line anchors for safe concurrent editing - Edit tool disabler for non-OpenAI providers - Read output enhancer with LINE:HASH prefixes - Provider state tracking module ## Technical Details - xxHash32-based 2-char hex hashes - Bottom-up edit application to prevent index shifting - OpenAI provider exemption (uses native apply_patch) - 90 tests covering all operations and edge cases - All files under 200 LOC limit ## Files Added/Modified - src/tools/hashline-edit/ (7 files, ~400 LOC) - src/hooks/hashline-edit-disabler/ (4 files, ~200 LOC) - src/hooks/hashline-read-enhancer/ (3 files, ~400 LOC) - src/features/hashline-provider-state.ts (13 LOC) - src/config/schema/experimental.ts (hashline_edit flag) - src/config/schema/hooks.ts (2 new hook names) - src/plugin/tool-registry.ts (conditional registration) - src/plugin/chat-params.ts (provider state tracking) - src/tools/index.ts (export) - src/hooks/index.ts (exports)
This commit is contained in:
@@ -0,0 +1,123 @@
|
||||
import { parseLineRef, validateLineRef } from "./validation"
|
||||
import type { HashlineEdit } from "./types"
|
||||
|
||||
function unescapeNewlines(text: string): string {
|
||||
return text.replace(/\\n/g, "\n")
|
||||
}
|
||||
|
||||
export function applySetLine(lines: string[], anchor: string, newText: string): string[] {
|
||||
validateLineRef(lines, anchor)
|
||||
const { line } = parseLineRef(anchor)
|
||||
const result = [...lines]
|
||||
result[line - 1] = unescapeNewlines(newText)
|
||||
return result
|
||||
}
|
||||
|
||||
export function applyReplaceLines(
|
||||
lines: string[],
|
||||
startAnchor: string,
|
||||
endAnchor: string,
|
||||
newText: string
|
||||
): string[] {
|
||||
validateLineRef(lines, startAnchor)
|
||||
validateLineRef(lines, endAnchor)
|
||||
|
||||
const { line: startLine } = parseLineRef(startAnchor)
|
||||
const { line: endLine } = parseLineRef(endAnchor)
|
||||
|
||||
if (startLine > endLine) {
|
||||
throw new Error(
|
||||
`Invalid range: start line ${startLine} cannot be greater than end line ${endLine}`
|
||||
)
|
||||
}
|
||||
|
||||
const result = [...lines]
|
||||
const newLines = unescapeNewlines(newText).split("\n")
|
||||
result.splice(startLine - 1, endLine - startLine + 1, ...newLines)
|
||||
return result
|
||||
}
|
||||
|
||||
export function applyInsertAfter(lines: string[], anchor: string, text: string): string[] {
|
||||
validateLineRef(lines, anchor)
|
||||
const { line } = parseLineRef(anchor)
|
||||
const result = [...lines]
|
||||
const newLines = unescapeNewlines(text).split("\n")
|
||||
result.splice(line, 0, ...newLines)
|
||||
return result
|
||||
}
|
||||
|
||||
export function applyReplace(content: string, oldText: string, newText: string): string {
|
||||
if (!content.includes(oldText)) {
|
||||
throw new Error(`Text not found: "${oldText}"`)
|
||||
}
|
||||
return content.replaceAll(oldText, unescapeNewlines(newText))
|
||||
}
|
||||
|
||||
function getEditLineNumber(edit: HashlineEdit): number {
|
||||
switch (edit.type) {
|
||||
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 "replace":
|
||||
return Number.POSITIVE_INFINITY
|
||||
default:
|
||||
return Number.POSITIVE_INFINITY
|
||||
}
|
||||
}
|
||||
|
||||
export function applyHashlineEdits(content: string, edits: HashlineEdit[]): string {
|
||||
if (edits.length === 0) {
|
||||
return content
|
||||
}
|
||||
|
||||
const sortedEdits = [...edits].sort((a, b) => getEditLineNumber(b) - getEditLineNumber(a))
|
||||
|
||||
let result = content
|
||||
let lines = result.split("\n")
|
||||
|
||||
for (const edit of sortedEdits) {
|
||||
switch (edit.type) {
|
||||
case "set_line": {
|
||||
validateLineRef(lines, edit.line)
|
||||
const { line } = parseLineRef(edit.line)
|
||||
lines[line - 1] = unescapeNewlines(edit.text)
|
||||
break
|
||||
}
|
||||
case "replace_lines": {
|
||||
validateLineRef(lines, edit.start_line)
|
||||
validateLineRef(lines, edit.end_line)
|
||||
const { line: startLine } = parseLineRef(edit.start_line)
|
||||
const { line: endLine } = parseLineRef(edit.end_line)
|
||||
if (startLine > endLine) {
|
||||
throw new Error(
|
||||
`Invalid range: start line ${startLine} cannot be greater than end line ${endLine}`
|
||||
)
|
||||
}
|
||||
const newLines = unescapeNewlines(edit.text).split("\n")
|
||||
lines.splice(startLine - 1, endLine - startLine + 1, ...newLines)
|
||||
break
|
||||
}
|
||||
case "insert_after": {
|
||||
validateLineRef(lines, edit.line)
|
||||
const { line } = parseLineRef(edit.line)
|
||||
const newLines = unescapeNewlines(edit.text).split("\n")
|
||||
lines.splice(line, 0, ...newLines)
|
||||
break
|
||||
}
|
||||
case "replace": {
|
||||
result = lines.join("\n")
|
||||
if (!result.includes(edit.old_text)) {
|
||||
throw new Error(`Text not found: "${edit.old_text}"`)
|
||||
}
|
||||
result = result.replaceAll(edit.old_text, unescapeNewlines(edit.new_text))
|
||||
lines = result.split("\n")
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return lines.join("\n")
|
||||
}
|
||||
Reference in New Issue
Block a user