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
fileedit.go · 166 lines · 5.2 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
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
package fileedit

import (
	"fmt"
	"strings"
)

// Result is what a write or a replace reports — the fields the `edit` CLI
// prints and the Pi tool returns: a headline, the readable diff, the unified
// patch, the first changed line. Headline + Diff is what goes back to the
// model; the caller decides what the screen shows.
type Result struct {
	Path             string
	Created          bool
	Changed          bool
	DryRun           bool
	Edits            int
	Added, Deleted   int
	FirstChangedLine int
	Headline         string
	Diff             string
	Patch            string
}

// Read returns the file, or a slice of it (1-based, inclusive; 0 = from the
// start / to the end). It is the mandatory companion of Replace: an exact
// replacement assumes the exact text was READ, not remembered.
func Read(path string, start, end int, numbered bool) (string, error) {
	f, err := load(path)
	if err != nil {
		return "", fmt.Errorf("cannot read %s: %v", path, err)
	}
	if f.isNew {
		return "", fmt.Errorf("%s does not exist. Create it with write_file", path)
	}

	lines, finalNewline := splitLines(f.body)
	from, to, err := bounds(start, end, len(lines))
	if err != nil {
		return "", fmt.Errorf("%s: %v", path, err)
	}
	slice := lines[from-1 : to]

	if numbered {
		var b strings.Builder
		for i, l := range slice {
			fmt.Fprintf(&b, "%6d  %s\n", from+i, l)
		}
		return b.String(), nil
	}
	body := strings.Join(slice, "\n")
	// The final newline is only put back if it existed AND the end of the file
	// is shown: otherwise a slice would invent a line the file does not have.
	if finalNewline || to < len(lines) {
		body += "\n"
	}
	return body, nil
}

// bounds turns start/end into a valid slice, or says why it is not one.
func bounds(start, end, total int) (int, int, error) {
	if total == 0 {
		return 1, 0, nil // empty file: nothing to show, not an error
	}
	if start == 0 {
		start = 1
	}
	if end == 0 {
		end = total
	}
	if start < 1 || end < 1 {
		return 0, 0, fmt.Errorf("start and end are 1-based line numbers")
	}
	if start > total {
		return 0, 0, fmt.Errorf("start %d is past the end of the file (%d lines)", start, total)
	}
	if end > total {
		end = total
	}
	if start > end {
		return 0, 0, fmt.Errorf("start %d comes after end %d", start, end)
	}
	return start, end, nil
}

// Write writes the WHOLE file. The tool for a new file or a deliberate
// rewrite; to change three lines of an existing file, Replace is safer — it
// fails when the targeted text is not what one thought, where Write overwrites
// without checking.
func Write(path, content string) (Result, error) {
	f, err := load(path)
	if err != nil {
		return Result{}, fmt.Errorf("cannot read %s: %v", path, err)
	}
	// A content without a final newline would give a file without one: on a
	// text file that is almost always an accident, and every diff tool then
	// flags it.
	if content != "" && !strings.HasSuffix(content, "\n") {
		content += "\n"
	}
	return finish(Result{Path: path, Created: f.isNew}, f, content)
}

// Replace applies exact replacements to an existing file — the operation that
// matters: it changes a file without rewriting it, and refuses anything
// ambiguous. With dryRun the diff is computed and nothing is written.
func Replace(path string, edits []Edit, dryRun bool) (Result, error) {
	f, err := load(path)
	if err != nil {
		return Result{}, fmt.Errorf("cannot read %s: %v", path, err)
	}
	if f.isNew {
		return Result{}, fmt.Errorf("%s does not exist — nothing to replace. Create it with write_file", path)
	}
	body, err := apply(f.body, edits)
	if err != nil {
		// The error carries the file name: it is read out of context, in a
		// tool result among others.
		return Result{}, fmt.Errorf("%s: %v", path, err)
	}
	return finish(Result{Path: path, Edits: len(edits), DryRun: dryRun}, f, body)
}

// finish compares, writes if needed, and reports. Write and Replace both end
// here: that is what gives them exactly the same output, hence one format for
// the model to learn.
func finish(r Result, f *file, body string) (Result, error) {
	c := compare(f.body, body)
	r.Changed = c.changed()
	r.Added, r.Deleted = c.stat()
	r.FirstChangedLine = c.firstChanged()
	r.Diff = c.render()
	r.Patch = c.unified(r.Path)

	// Nothing to write: SAY so rather than touch the file. An identical
	// rewrite would bump the modification time and wake every watcher, for
	// nothing.
	if r.Changed && !r.DryRun {
		if err := f.write(body); err != nil {
			return Result{}, fmt.Errorf("cannot write %s: %v", r.Path, err)
		}
	}
	r.Headline = headline(r)
	return r, nil
}

// headline sums the operation up in one line — the one the model re-reads in
// its tool result, and the one the screen shows under the display line.
func headline(r Result) string {
	switch {
	case !r.Changed:
		return fmt.Sprintf("%s: unchanged", r.Path)
	case r.Created && r.DryRun:
		return fmt.Sprintf("%s: would be created, %d line(s)", r.Path, r.Added)
	case r.Created:
		return fmt.Sprintf("%s: created, %d line(s)", r.Path, r.Added)
	}
	what := fmt.Sprintf("+%d -%d, first change at line %d", r.Added, r.Deleted, r.FirstChangedLine)
	if r.Edits > 0 {
		what = fmt.Sprintf("%d edit(s) applied, %s", r.Edits, what)
	}
	if r.DryRun {
		return fmt.Sprintf("%s: dry run — nothing written (%s)", r.Path, what)
	}
	return fmt.Sprintf("%s: %s", r.Path, what)
}