textedit

package
v0.11.40 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 18, 2026 License: Apache-2.0 Imports: 8 Imported by: 0

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. One 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.

An agent also misses when it copies text in the escaped form a JSON tool result shows it: a backslash and "t" where the file has a tab, or \u0026 where it has "&". UnescapeJSON decodes such text when it is a whole JSON string body, and UnescapeJSONSequences decodes the escape sequences of text that mixes escaped and raw characters.

Index

Examples

Constants

View Source
const UnescapedNote = `` /* 247-byte string literal not displayed */

UnescapedNote is the note for an edit that applied only after old_string and new_string were decoded with UnescapeJSON.

Variables

This section is empty.

Functions

func EscapedNotFound added in v0.11.40

func EscapedNotFound(offsets []int64) error

EscapedNotFound is the error for an old_string that matches nothing as written but matches once UnescapeJSONSequences decodes it. offsets holds the byte offsets of its first one or two decoded matches; two mean the decoded text also needs more context to be unique.

func LineEnd

func LineEnd(r io.ReaderAt, end, size int64, bound int) (int64, error)

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

func LineStart(r io.ReaderAt, offset int64, bound int) (int64, error)

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

func ShiftIndentation(s string, shift Shift) string

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

func UnescapeJSON added in v0.11.40

func UnescapeJSON(s string) (string, bool)

UnescapeJSON decodes s as the body of a JSON string literal. ok is false when s between double quotes is not valid JSON: s holds a raw control character such as a newline or tab, an unescaped double quote, or a backslash that starts no JSON escape sequence.

func UnescapeJSONSequences added in v0.11.40

func UnescapeJSONSequences(s string) string

UnescapeJSONSequences decodes every JSON escape sequence in s and keeps the rest of s as it is, so unlike UnescapeJSON it also decodes text that mixes escaped and raw characters, such as a raw newline beside an escaped quote.

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

func MatchIgnoringIndentation(r io.Reader, old string) (Match, error)

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

type Shift struct {
	Add        bool
	Whitespace string
}

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

func (s Shift) String() 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"

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL