Documentation
¶
Overview ¶
Package textedit supports file-editing tools exposed to LLM agents.
An agent's edit_file call names the text to replace by exact bytes. The most common way such a call misses is a constant indentation shift: every line of old_string matches the file once leading whitespace is ignored, but the file carries one more (or one fewer) tab on each line. MatchIgnoringIndentation finds the unique region that matches under that relaxation and reports the shift so the caller can apply the same shift to new_string with ShiftIndentation.
A related source of misses is a read window that starts mid-line, which hands the agent a first line with its indentation cut off. LineStart and LineEnd align a byte window to whole lines.
Index ¶
Examples ¶
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func LineEnd ¶
LineEnd returns the offset one past the first "\n" at or after end, so a window ending there ends with a whole line, or size when end is at or past size. It reads at most bound bytes forward; if no newline lies within that span, including when the span reaches the end of the input, it returns end unchanged so inputs without newlines read exactly as they would without alignment.
Example ¶
package main
import (
"fmt"
"strings"
"chainguard.dev/driftlessaf/internal/textedit"
)
func main() {
data := "first line\nsecond line\nthird line\n"
r := strings.NewReader(data)
// A window that would end mid-way through "second line" is extended to
// include the rest of the line and its newline.
end, err := textedit.LineEnd(r, 15, int64(len(data)), 64<<10)
if err != nil {
fmt.Println(err)
return
}
fmt.Println(end)
}
Output: 23
func LineStart ¶
LineStart returns the offset of the first byte of the line containing offset: one past the previous "\n", or 0 when offset is 0. It reads at most bound bytes backward; if no newline lies within that span, including when the span reaches the start of the input, it returns offset unchanged so inputs without newlines read exactly as they would without alignment.
Example ¶
package main
import (
"fmt"
"strings"
"chainguard.dev/driftlessaf/internal/textedit"
)
func main() {
r := strings.NewReader("first line\nsecond line\nthird line\n")
// A window that would begin mid-way through "second line" is moved back
// to the start of that line.
start, err := textedit.LineStart(r, 15, 64<<10)
if err != nil {
fmt.Println(err)
return
}
fmt.Println(start)
}
Output: 11
func ShiftIndentation ¶
ShiftIndentation applies shift to every non-blank line of s: with Add it prepends Whitespace; otherwise it removes Whitespace from the start of a line that has it and leaves other lines unchanged. Blank lines are unchanged.
Example ¶
package main
import (
"fmt"
"chainguard.dev/driftlessaf/internal/textedit"
)
func main() {
replacement := "if ok {\n\treturn 2\n}\n"
shifted := textedit.ShiftIndentation(replacement, textedit.Shift{Add: true, Whitespace: "\t"})
fmt.Printf("%q\n", shifted)
restored := textedit.ShiftIndentation(shifted, textedit.Shift{Whitespace: "\t"})
fmt.Println(restored == replacement)
}
Output: "\tif ok {\n\t\treturn 2\n\t}\n" true
Types ¶
type Match ¶
type Match struct {
Start int64 // byte offset of the region's first byte
End int64 // byte offset one past the region's last byte
Line int // 1-based line number of Start
Shift Shift
}
Match is a region of a file that equals old_string once indentation is ignored.
func MatchIgnoringIndentation ¶
MatchIgnoringIndentation scans r line by line for the unique region whose lines equal old's lines after leading spaces and tabs are stripped (and a trailing "\r" is ignored on file lines). It succeeds only when exactly one region matches and the indentation difference is the same Shift on every non-blank line. It never reads the whole input into memory: it keeps a window of len(lines(old)) lines. If old ends with "\n", the region includes the trailing newline of its last line when the file has one.
On failure the error names the reason so a caller can relay it to the agent:
- no region matches: reports whether the first non-blank line of old occurs in the file, and if so at which line and byte offset, so the agent can re-read there; otherwise says the file may have changed since it was read.
- more than one region matches: reports the count and the byte offsets of the first two, and asks for more surrounding context.
- one region matches but the shift differs across lines: reports its line and byte offset and asks the agent to copy the text exactly as read.
Every error message begins with "old_string not found in file".
Example ¶
package main
import (
"fmt"
"strings"
"chainguard.dev/driftlessaf/internal/textedit"
)
func main() {
file := "func f() {\n\tif ok {\n\t\treturn 1\n\t}\n}\n"
// The agent copied the block with one tab too few on every line.
old := "if ok {\n\treturn 1\n}"
m, err := textedit.MatchIgnoringIndentation(strings.NewReader(file), old)
if err != nil {
fmt.Println(err)
return
}
fmt.Printf("line %d, bytes [%d, %d), %s\n", m.Line, m.Start, m.End, m.Shift)
fmt.Printf("%q\n", file[m.Start:m.End])
}
Output: line 2, bytes [11, 33), added 1 tab "\tif ok {\n\t\treturn 1\n\t}"
Example (Ambiguous) ¶
package main
import (
"fmt"
"strings"
"chainguard.dev/driftlessaf/internal/textedit"
)
func main() {
file := "\tx()\n\ty()\n---\n\t\tx()\n\t\ty()\n"
_, err := textedit.MatchIgnoringIndentation(strings.NewReader(file), "x()\ny()")
fmt.Println(err)
}
Output: old_string not found in file: ignoring indentation it matches at least 2 regions (byte offsets 0 and 14); include more surrounding context to make it unique
type Shift ¶
Shift describes how the file's leading whitespace relates to old_string's on every non-blank line of a match: Add means the file carries Whitespace in addition to old_string's indentation; otherwise old_string carries it in addition to the file's. A zero Shift means the lines matched exactly apart from line endings.
func (Shift) String ¶
String renders the shift for a tool result, for example "added 1 tab", "removed 2 spaces", or "" for the zero shift. Mixed whitespace renders with %q.
Example ¶
package main
import (
"fmt"
"chainguard.dev/driftlessaf/internal/textedit"
)
func main() {
fmt.Printf("%q\n", textedit.Shift{}.String())
fmt.Println(textedit.Shift{Add: true, Whitespace: "\t"})
fmt.Println(textedit.Shift{Whitespace: " "})
fmt.Println(textedit.Shift{Add: true, Whitespace: " \t"})
}
Output: "" added 1 tab removed 4 spaces added " \t"