1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
|
package fileedit
import (
"fmt"
"sort"
"strings"
)
// Edit is one replacement: a text to find, a text to put in its place. The
// JSON names are the ones the model sees in the edit_file schema.
type Edit struct {
Old string `json:"old" jsonschema_description:"Exact text to find: copied from the file character for character, and present exactly once"`
New string `json:"new" jsonschema_description:"Replacement text; empty deletes the matched text"`
}
// span is the region an edit occupies in the ORIGINAL body.
type span struct {
start, end int
index int // rank of the edit, for error messages
}
// apply applies every edit to the body and returns the result.
//
// The rules of the `edit` CLI, kept to the letter because they are what makes
// an edit verifiable: each `old` appears once and only once; edits are resolved
// against the ORIGINAL body, never in cascade; two edits must not touch, nest
// or repeat; an empty `new` deletes. An ambiguous edit — two occurrences, two
// overlapping regions — would give a result that depends on application order,
// hence a file nobody has read. Nothing is written when one edit fails: a
// half-edited file is the worst outcome, because it looks like a success.
func apply(body string, list []Edit) (string, error) {
if len(list) == 0 {
return "", fmt.Errorf("no edit given: each edit needs an old and a new text")
}
spans := make([]span, 0, len(list))
for i, e := range list {
s, err := locate(body, e, i)
if err != nil {
return "", err
}
spans = append(spans, s)
}
// Sorted by position: the overlap check and the rebuild both walk the file
// in its order, not in the order of the request.
sort.Slice(spans, func(i, j int) bool { return spans[i].start < spans[j].start })
for i := 1; i < len(spans); i++ {
if spans[i].start < spans[i-1].end {
a, b := spans[i-1], spans[i]
return "", fmt.Errorf(
"edits #%d and #%d overlap (lines %d-%d and %d-%d): merge them into a single old/new pair",
a.index+1, b.index+1,
lineOf(body, a.start), lineOf(body, a.end),
lineOf(body, b.start), lineOf(body, b.end))
}
}
var b strings.Builder
at := 0
for _, s := range spans {
b.WriteString(body[at:s.start])
b.WriteString(list[s.index].New)
at = s.end
}
b.WriteString(body[at:])
return b.String(), nil
}
// locate finds the single occurrence of an `old` text.
//
// The messages are written FOR A MODEL: they say what is wrong and the move
// that fixes it, because a model is what will retry the call.
func locate(body string, e Edit, i int) (span, error) {
if e.Old == "" {
return span{}, fmt.Errorf(
"edit #%d has an empty old text: it would match everywhere — use write_file to replace the whole file",
i+1)
}
if e.Old == e.New {
return span{}, fmt.Errorf("edit #%d changes nothing: old and new are identical", i+1)
}
switch n := strings.Count(body, e.Old); {
case n == 0:
return span{}, fmt.Errorf(
"edit #%d: old text not found — it must match the file exactly, including indentation and line breaks:\n%s",
i+1, quote(e.Old))
case n > 1:
return span{}, fmt.Errorf(
"edit #%d: old text appears %d times — add the surrounding lines until it is unique:\n%s",
i+1, n, quote(e.Old))
}
start := strings.Index(body, e.Old)
return span{start: start, end: start + len(e.Old), index: i}, nil
}
// lineOf returns the 1-based line number of an offset in the body.
func lineOf(body string, offset int) int {
return 1 + strings.Count(body[:offset], "\n")
}
// quote shows the searched text, indented, capped to its first lines: past
// that the error would drown the output and teach nothing more.
func quote(s string) string {
const maxLines = 6
lines := strings.Split(s, "\n")
shown := lines
if len(lines) > maxLines {
shown = lines[:maxLines]
}
var b strings.Builder
for _, l := range shown {
b.WriteString(" │ " + l + "\n")
}
if len(lines) > len(shown) {
fmt.Fprintf(&b, " └ … %d more line(s)\n", len(lines)-len(shown))
}
return strings.TrimRight(b.String(), "\n")
}
|