MCPcopy Create free account
hub / github.com/codehamr/codehamr / EditFile

Function EditFile

internal/tools/edit.go:15–62  ·  view source on GitHub ↗

EditFile replaces old_string with new_string in path. old_string must match EXACTLY ONCE; otherwise the file is untouched and an error string is returned so the model sees the failure and reacts, same convention as bash/WriteFile. Empty old_string is rejected (no anchor, every position matches); ol

(path, oldString, newString string)

Source from the content-addressed store, hash-verified

13// Empty old_string is rejected (no anchor, every position matches);
14// old_string == new_string is rejected as a no-op turn-waster.
15func EditFile(path, oldString, newString string) string {
16 if path == "" {
17 return "(empty path)"
18 }
19 if oldString == "" {
20 return "(empty old_string)"
21 }
22 if oldString == newString {
23 return "(no change: old_string equals new_string)"
24 }
25 // Same guard as ReadFile: open(2) on a FIFO blocks forever and Ctrl+C
26 // can't unblock it, leaking the tool goroutine. Stat never blocks. The same
27 // Stat also size-gates the whole-file read below: os.ReadFile on a multi-GB
28 // log would OOM-kill the TUI, the hazard bash's capture cap already stops.
29 if info, err := os.Stat(path); err == nil {
30 if !info.Mode().IsRegular() && !info.IsDir() {
31 return fmt.Sprintf("(read error: %s is not a regular file)", path)
32 }
33 if info.Mode().IsRegular() && info.Size() > maxFileBytes {
34 return fmt.Sprintf("(too large: %s is %d bytes, over edit_file's %dMB cap - edit a file this size with bash instead: sed -i or a python one-liner)", path, info.Size(), maxFileBytes>>20)
35 }
36 }
37 raw, err := os.ReadFile(path)
38 if err != nil {
39 return fmt.Sprintf("(read error: %v)", err)
40 }
41 content := string(raw)
42 n := strings.Count(content, oldString)
43 if n == 0 {
44 // A near-miss that differs only in whitespace (wrong indentation, tabs vs
45 // spaces) is the most common edit_file failure for an LLM; each one costs
46 // a re-read round plus a failure-streak entry. When the near-miss is a
47 // run of WHOLE lines matching exactly once, apply it: the uniqueness gate
48 // preserves the exactly-once guarantee, and the spliced bytes are the
49 // model's own new_string, exactly what an exact match would have written.
50 // Anything looser (mid-line fragments, 0 or 2+ fuzzy matches) still fails
51 // with a message that names the recovery.
52 if out, ok := fuzzyWhitespaceEdit(path, content, oldString, newString); ok {
53 return out
54 }
55 if differsOnlyInWhitespace(content, oldString) {
56 return fmt.Sprintf("(not found: no exact match in %s - a block there differs only in whitespace (indentation/tabs/newlines); copy the exact bytes, including indentation)", path)
57 }
58 return fmt.Sprintf("(not found: old_string does not appear in %s - read the exact bytes back with read_file before retrying, don't retype them from memory)", path)
59 }
60 if n > 1 {
61 return fmt.Sprintf("(ambiguous: old_string appears %d times (lines %s) - provide more context to make it unique)", n, matchLines(content, oldString))
62 }
63 // strings.Count only counts non-overlapping occurrences, so a self-
64 // overlapping old_string ("==" in "a === b") passes n == 1 yet matches at
65 // two positions with different results. Catch the overlapping second match

Callers 13

TestEditFileHappyFunction · 0.85
TestEditFileEmptyPathFunction · 0.85
TestEditFileMissingFileFunction · 0.85
TestEditFileOldNotFoundFunction · 0.85
TestEditFileOldNotUniqueFunction · 0.85
TestEditFileNoOpFunction · 0.85
TestEditFileDeleteFunction · 0.85

Calls 1

differsOnlyInWhitespaceFunction · 0.85

Tested by 12

TestEditFileHappyFunction · 0.68
TestEditFileEmptyPathFunction · 0.68
TestEditFileMissingFileFunction · 0.68
TestEditFileOldNotFoundFunction · 0.68
TestEditFileOldNotUniqueFunction · 0.68
TestEditFileNoOpFunction · 0.68
TestEditFileDeleteFunction · 0.68