MCPcopy Index your code
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.
27 if info, err := os.Stat(path); err == nil && !info.Mode().IsRegular() && !info.IsDir() {
28 return fmt.Sprintf("(read error: %s is not a regular file)", path)
29 }
30 raw, err := os.ReadFile(path)
31 if err != nil {
32 return fmt.Sprintf("(read error: %v)", err)
33 }
34 content := string(raw)
35 n := strings.Count(content, oldString)
36 if n == 0 {
37 // A near-miss that differs only in whitespace (wrong indentation, tabs vs
38 // spaces) is the most common edit_file failure for an LLM. Name it so the
39 // model fixes the bytes instead of burning retries toward the failure nudge.
40 // Detection only; never auto-apply a fuzzy match, or the exact-match-once
41 // safety the caller relies on is gone.
42 if differsOnlyInWhitespace(content, oldString) {
43 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)
44 }
45 return fmt.Sprintf("(not found: old_string does not appear in %s)", path)
46 }
47 if n > 1 {
48 return fmt.Sprintf("(ambiguous: old_string appears %d times - provide more context to make it unique)", n)
49 }
50 // strings.Count only counts non-overlapping occurrences, so a self-
51 // overlapping old_string ("==" in "a === b") passes n == 1 yet matches at
52 // two positions with different results. Catch the overlapping second match
53 // so the exactly-once guarantee holds.
54 if idx := strings.Index(content, oldString); strings.Contains(content[idx+1:], oldString) {
55 return "(ambiguous: old_string overlaps itself - provide more context to make it unique)"
56 }
57 updated := strings.Replace(content, oldString, newString, 1)
58 if err := os.WriteFile(path, []byte(updated), 0o644); err != nil {
59 return fmt.Sprintf("(write error: %v)", err)
60 }
61 return fmt.Sprintf("edited %s: -%d +%d bytes", path, len(oldString), len(newString))
62}
63
64// differsOnlyInWhitespace reports whether oldString matches content at exactly
65// one spot once every whitespace run is collapsed: i.e. the sole mismatch is

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