Vectrum
Sign In
76 lines · 2.6 KB · Go
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
}