textedit

package
v0.10.129 Latest Latest
Warning

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

Go to latest
Published: Sep 24, 2026 License: Apache-2.0 Imports: 6 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. 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

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

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