package tools
import (
"context"
"strings"
)
// Diagnoser is what edit_file and write_file ask about a file they have just
// changed: whatever this machine's language server or linter has to say about
// it, formatted for the model, or "" for nothing. *diag.Runner implements it.
//
// It is an interface here rather than a *diag.Runner so the tools can be
// tested against a stub, and so the tools package says nothing about how a
// file gets checked — only that it may be.
type Diagnoser interface {
Report(ctx context.Context, root, path string) string
}
// Option configures the file-changing tools. There is one so far; it is an
// option rather than a third positional argument so that every existing
// NewEdit(root, state) call, in main.go and the tests, goes on compiling.
type Option func(*options)
type options struct {
diag Diagnoser
procs *Processes
}
// WithDiagnostics has the tool append diagnostics for the changed file to its
// result. A nil d, or a Diagnoser whose Report returns "", means none: the
// result is exactly what it was before this feature existed.
func WithDiagnostics(d Diagnoser) Option {
return func(o *options) { o.diag = d }
}
func applyOptions(opts []Option) options {
var o options
for _, opt := range opts {
if opt != nil {
opt(&o)
}
}
return o
}
// describe finishes a tool description. The sentence about diagnostics is
// only added when a Diagnoser is wired in: a tool that promises the model
// diagnostics and never delivers any teaches it that silence means nothing,
// which is the opposite of what silence is meant to mean.
func (o options) describe(desc string) string {
desc = strings.TrimSpace(desc)
if o.diag == nil {
return desc
}
// Its own paragraph. Run onto the end of the last sentence it inherited
// that sentence's line breaks and came out starting mid-line, which reads
// like a bug in whatever assembled the description.
return desc + "\n\n" + `If a language server or linter has something to say about the file
afterwards it is appended under "diagnostics"; no such block means the
file was clean.`
}
// withDiagnostics appends the diagnostics block, if any, to a successful tool
// result. It is only ever called on the success path: a failed edit already
// has the model's attention, and diagnostics about a file that did not change
// would be about the previous edit.
func (o options) withDiagnostics(ctx context.Context, root, path, result string) string {
if o.diag == nil {
return result
}
report := o.diag.Report(ctx, root, path)
if report == "" {
return result
}
return strings.TrimRight(result, "\n") + "\n\n" + report
}