|
|
@@ -1,5 +1,6 @@
|
|
|
// the approaches in this edit tool are sourced from
|
|
|
// https://github.com/cline/cline/blob/main/evals/diff-edits/diff-apply/diff-06-23-25.ts
|
|
|
+// https://github.com/google-gemini/gemini-cli/blob/main/packages/core/src/utils/editCorrector.ts
|
|
|
|
|
|
import { z } from "zod"
|
|
|
import * as path from "path"
|
|
|
@@ -266,6 +267,151 @@ export const IndentationFlexibleReplacer: Replacer = function* (content, find) {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+export const EscapeNormalizedReplacer: Replacer = function* (content, find) {
|
|
|
+ const unescapeString = (str: string): string => {
|
|
|
+ return str.replace(/\\+(n|t|r|'|"|`|\\|\n|\$)/g, (match, capturedChar) => {
|
|
|
+ switch (capturedChar) {
|
|
|
+ case "n":
|
|
|
+ return "\n"
|
|
|
+ case "t":
|
|
|
+ return "\t"
|
|
|
+ case "r":
|
|
|
+ return "\r"
|
|
|
+ case "'":
|
|
|
+ return "'"
|
|
|
+ case '"':
|
|
|
+ return '"'
|
|
|
+ case "`":
|
|
|
+ return "`"
|
|
|
+ case "\\":
|
|
|
+ return "\\"
|
|
|
+ case "\n":
|
|
|
+ return "\n"
|
|
|
+ case "$":
|
|
|
+ return "$"
|
|
|
+ default:
|
|
|
+ return match
|
|
|
+ }
|
|
|
+ })
|
|
|
+ }
|
|
|
+
|
|
|
+ const unescapedFind = unescapeString(find)
|
|
|
+
|
|
|
+ // Try direct match with unescaped find string
|
|
|
+ if (content.includes(unescapedFind)) {
|
|
|
+ yield unescapedFind
|
|
|
+ }
|
|
|
+
|
|
|
+ // Also try finding escaped versions in content that match unescaped find
|
|
|
+ const lines = content.split("\n")
|
|
|
+ const findLines = unescapedFind.split("\n")
|
|
|
+
|
|
|
+ for (let i = 0; i <= lines.length - findLines.length; i++) {
|
|
|
+ const block = lines.slice(i, i + findLines.length).join("\n")
|
|
|
+ const unescapedBlock = unescapeString(block)
|
|
|
+
|
|
|
+ if (unescapedBlock === unescapedFind) {
|
|
|
+ yield block
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+export const MultiOccurrenceReplacer: Replacer = function* (content, find) {
|
|
|
+ // This replacer yields all exact matches, allowing the replace function
|
|
|
+ // to handle multiple occurrences based on replaceAll parameter
|
|
|
+ let startIndex = 0
|
|
|
+
|
|
|
+ while (true) {
|
|
|
+ const index = content.indexOf(find, startIndex)
|
|
|
+ if (index === -1) break
|
|
|
+
|
|
|
+ yield find
|
|
|
+ startIndex = index + find.length
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+export const TrimmedBoundaryReplacer: Replacer = function* (content, find) {
|
|
|
+ const trimmedFind = find.trim()
|
|
|
+
|
|
|
+ if (trimmedFind === find) {
|
|
|
+ // Already trimmed, no point in trying
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ // Try to find the trimmed version
|
|
|
+ if (content.includes(trimmedFind)) {
|
|
|
+ yield trimmedFind
|
|
|
+ }
|
|
|
+
|
|
|
+ // Also try finding blocks where trimmed content matches
|
|
|
+ const lines = content.split("\n")
|
|
|
+ const findLines = find.split("\n")
|
|
|
+
|
|
|
+ for (let i = 0; i <= lines.length - findLines.length; i++) {
|
|
|
+ const block = lines.slice(i, i + findLines.length).join("\n")
|
|
|
+
|
|
|
+ if (block.trim() === trimmedFind) {
|
|
|
+ yield block
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+export const ContextAwareReplacer: Replacer = function* (content, find) {
|
|
|
+ const findLines = find.split("\n")
|
|
|
+ if (findLines.length < 3) {
|
|
|
+ // Need at least 3 lines to have meaningful context
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ const contentLines = content.split("\n")
|
|
|
+
|
|
|
+ // Extract first and last lines as context anchors
|
|
|
+ const firstLine = findLines[0].trim()
|
|
|
+ const lastLine = findLines[findLines.length - 1].trim()
|
|
|
+
|
|
|
+ // Find blocks that start and end with the context anchors
|
|
|
+ for (let i = 0; i < contentLines.length; i++) {
|
|
|
+ if (contentLines[i].trim() !== firstLine) continue
|
|
|
+
|
|
|
+ // Look for the matching last line
|
|
|
+ for (let j = i + 2; j < contentLines.length; j++) {
|
|
|
+ if (contentLines[j].trim() === lastLine) {
|
|
|
+ // Found a potential context block
|
|
|
+ const blockLines = contentLines.slice(i, j + 1)
|
|
|
+ const block = blockLines.join("\n")
|
|
|
+
|
|
|
+ // Check if the middle content has reasonable similarity
|
|
|
+ // (simple heuristic: at least 50% of non-empty lines should match when trimmed)
|
|
|
+ if (blockLines.length === findLines.length) {
|
|
|
+ let matchingLines = 0
|
|
|
+ let totalNonEmptyLines = 0
|
|
|
+
|
|
|
+ for (let k = 1; k < blockLines.length - 1; k++) {
|
|
|
+ const blockLine = blockLines[k].trim()
|
|
|
+ const findLine = findLines[k].trim()
|
|
|
+
|
|
|
+ if (blockLine.length > 0 || findLine.length > 0) {
|
|
|
+ totalNonEmptyLines++
|
|
|
+ if (blockLine === findLine) {
|
|
|
+ matchingLines++
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if (
|
|
|
+ totalNonEmptyLines === 0 ||
|
|
|
+ matchingLines / totalNonEmptyLines >= 0.5
|
|
|
+ ) {
|
|
|
+ yield block
|
|
|
+ break // Only match the first occurrence
|
|
|
+ }
|
|
|
+ }
|
|
|
+ break
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
function trimDiff(diff: string): string {
|
|
|
const lines = diff.split("\n")
|
|
|
const contentLines = lines.filter(
|
|
|
@@ -314,6 +460,10 @@ export function replace(
|
|
|
BlockAnchorReplacer,
|
|
|
WhitespaceNormalizedReplacer,
|
|
|
IndentationFlexibleReplacer,
|
|
|
+ EscapeNormalizedReplacer,
|
|
|
+ TrimmedBoundaryReplacer,
|
|
|
+ ContextAwareReplacer,
|
|
|
+ MultiOccurrenceReplacer,
|
|
|
]) {
|
|
|
for (const search of replacer(content, oldString)) {
|
|
|
const index = content.indexOf(search)
|