e6868e9112
- 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
161 lines
4.9 KiB
TypeScript
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)
|
|
}
|