bots-garden/mini-mepublic Fork 0
d72271127802973540c648bfb372176cdaaa8e4f
Commits
Clone
git clone https://git.rickub.com/bots-garden/mini-me.git
git clone ssh://git@rickub.com/bots-garden/mini-me.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

💾 Saved. d722711 · on d72271127802973540c648bfb372176cdaaa8e4f · k33g · 6h ago
edits.go · 122 lines · 4.0 KBGo Blame HistoryRaw
  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")
}