Files
oh-my-opencode/src/tools/hashline-edit/edit-operation-primitives.ts
T
YeonGyu-Kim e6868e9112 fix(hashline-edit): align autocorrect, BOM/CRLF, and tool description with oh-my-pi
- Rewrite restoreOldWrappedLines to use oh-my-pi's span-scanning algorithm
- Add stripTrailingContinuationTokens and stripMergeOperatorChars helpers
- Fix detectLineEnding to use first-occurrence logic instead of any-match
- Fix applyAppend/applyPrepend to replace empty-line placeholder in empty files
- Enhance tool description with 7 critical rules, tag guidance, and anti-patterns
2026-02-22 14:40:18 +09:00

161 lines
4.9 KiB
TypeScript

import { autocorrectReplacementLines } from "./autocorrect-replacement-lines"
import {
restoreLeadingIndent,
stripInsertAnchorEcho,
stripInsertBeforeEcho,
stripInsertBoundaryEcho,
stripRangeBoundaryEcho,
toNewLines,
} from "./edit-text-normalization"
import { parseLineRef, validateLineRef } from "./validation"
interface EditApplyOptions {
skipValidation?: boolean
}
function shouldValidate(options?: EditApplyOptions): boolean {
return options?.skipValidation !== true
}
export function applySetLine(
lines: string[],
anchor: string,
newText: string | string[],
options?: EditApplyOptions
): string[] {
if (shouldValidate(options)) validateLineRef(lines, anchor)
const { line } = parseLineRef(anchor)
const result = [...lines]
const originalLine = lines[line - 1] ?? ""
const corrected = autocorrectReplacementLines([originalLine], toNewLines(newText))
const replacement = corrected.map((entry, idx) => {
if (idx !== 0) return entry
return restoreLeadingIndent(originalLine, entry)
})
result.splice(line - 1, 1, ...replacement)
return result
}
export function applyReplaceLines(
lines: string[],
startAnchor: string,
endAnchor: string,
newText: string | string[],
options?: EditApplyOptions
): string[] {
if (shouldValidate(options)) {
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 originalRange = lines.slice(startLine - 1, endLine)
const stripped = stripRangeBoundaryEcho(lines, startLine, endLine, toNewLines(newText))
const corrected = autocorrectReplacementLines(originalRange, stripped)
const restored = corrected.map((entry, idx) => {
if (idx !== 0) return entry
return restoreLeadingIndent(lines[startLine - 1], entry)
})
result.splice(startLine - 1, endLine - startLine + 1, ...restored)
return result
}
export function applyInsertAfter(
lines: string[],
anchor: string,
text: string | string[],
options?: EditApplyOptions
): string[] {
if (shouldValidate(options)) validateLineRef(lines, anchor)
const { line } = parseLineRef(anchor)
const result = [...lines]
const newLines = stripInsertAnchorEcho(lines[line - 1], toNewLines(text))
if (newLines.length === 0) {
throw new Error(`insert_after requires non-empty text for ${anchor}`)
}
result.splice(line, 0, ...newLines)
return result
}
export function applyInsertBefore(
lines: string[],
anchor: string,
text: string | string[],
options?: EditApplyOptions
): string[] {
if (shouldValidate(options)) validateLineRef(lines, anchor)
const { line } = parseLineRef(anchor)
const result = [...lines]
const newLines = stripInsertBeforeEcho(lines[line - 1], toNewLines(text))
if (newLines.length === 0) {
throw new Error(`insert_before requires non-empty text for ${anchor}`)
}
result.splice(line - 1, 0, ...newLines)
return result
}
export function applyInsertBetween(
lines: string[],
afterAnchor: string,
beforeAnchor: string,
text: string | string[],
options?: EditApplyOptions
): string[] {
if (shouldValidate(options)) {
validateLineRef(lines, afterAnchor)
validateLineRef(lines, beforeAnchor)
}
const { line: afterLine } = parseLineRef(afterAnchor)
const { line: beforeLine } = parseLineRef(beforeAnchor)
if (beforeLine <= afterLine) {
throw new Error(`insert_between requires after_line (${afterLine}) < before_line (${beforeLine})`)
}
const result = [...lines]
const newLines = stripInsertBoundaryEcho(lines[afterLine - 1], lines[beforeLine - 1], toNewLines(text))
if (newLines.length === 0) {
throw new Error(`insert_between requires non-empty text for ${afterAnchor}..${beforeAnchor}`)
}
result.splice(beforeLine - 1, 0, ...newLines)
return result
}
export function applyAppend(lines: string[], text: string | string[]): string[] {
const normalized = toNewLines(text)
if (normalized.length === 0) {
throw new Error("append requires non-empty text")
}
if (lines.length === 1 && lines[0] === "") {
return [...normalized]
}
return [...lines, ...normalized]
}
export function applyPrepend(lines: string[], text: string | string[]): string[] {
const normalized = toNewLines(text)
if (normalized.length === 0) {
throw new Error("prepend requires non-empty text")
}
if (lines.length === 1 && lines[0] === "") {
return [...normalized]
}
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)
}