135 lines
3 KiB
Go
135 lines
3 KiB
Go
package builtin
|
|
|
|
import (
|
|
"bufio"
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io/fs"
|
|
"os"
|
|
"path/filepath"
|
|
"regexp"
|
|
"strings"
|
|
|
|
"nub/internal/tool"
|
|
)
|
|
|
|
type GrepTool struct{}
|
|
|
|
func (GrepTool) Name() string { return "grep" }
|
|
func (GrepTool) Description() string {
|
|
return "Durchsucht Dateien per Regex, in-process, mit Kontext-Zeilen."
|
|
}
|
|
|
|
func (GrepTool) Schema() json.RawMessage {
|
|
return json.RawMessage(`{
|
|
"type": "object",
|
|
"properties": {
|
|
"pattern": {"type": "string"},
|
|
"path": {"type": "string", "description": "Startverzeichnis, relativ zu RepoRoot, Default '.'"},
|
|
"context": {"type": "integer", "description": "Kontext-Zeilen vor/nach jedem Treffer"}
|
|
},
|
|
"required": ["pattern"]
|
|
}`)
|
|
}
|
|
|
|
type grepInput struct {
|
|
Pattern string `json:"pattern"`
|
|
Path string `json:"path"`
|
|
Context int `json:"context"`
|
|
}
|
|
|
|
type grepMatch struct {
|
|
path string
|
|
line int
|
|
text string
|
|
}
|
|
|
|
func (GrepTool) Run(ctx context.Context, input json.RawMessage, env tool.Env) (tool.Result, error) {
|
|
var in grepInput
|
|
if err := json.Unmarshal(input, &in); err != nil {
|
|
return tool.Result{ForModel: "invalid input: " + err.Error(), IsError: true}, nil
|
|
}
|
|
re, err := regexp.Compile(in.Pattern)
|
|
if err != nil {
|
|
return tool.Result{ForModel: "invalid pattern: " + err.Error(), IsError: true}, nil
|
|
}
|
|
|
|
startRel := in.Path
|
|
if startRel == "" {
|
|
startRel = "."
|
|
}
|
|
start, err := resolvePath(env.RepoRoot, env.Cwd, startRel)
|
|
if err != nil {
|
|
return tool.Result{ForModel: err.Error(), IsError: true}, nil
|
|
}
|
|
|
|
ignore := loadGitignore(env.RepoRoot)
|
|
|
|
var matches []grepMatch
|
|
err = filepath.WalkDir(start, func(p string, d fs.DirEntry, err error) error {
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
rel, relErr := filepath.Rel(env.RepoRoot, p)
|
|
if relErr == nil && ignore.match(rel) {
|
|
if d.IsDir() {
|
|
return filepath.SkipDir
|
|
}
|
|
return nil
|
|
}
|
|
if d.IsDir() {
|
|
return nil
|
|
}
|
|
grepFile(p, re, in.Context, &matches)
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
return tool.Result{ForModel: fmt.Sprintf("grep: %v", err), IsError: true}, nil
|
|
}
|
|
|
|
if len(matches) == 0 {
|
|
return tool.Result{ForModel: fmt.Sprintf("no matches for %q", in.Pattern)}, nil
|
|
}
|
|
|
|
var b strings.Builder
|
|
for _, m := range matches {
|
|
relPath, _ := filepath.Rel(env.RepoRoot, m.path)
|
|
fmt.Fprintf(&b, "%s:%d:%s\n", relPath, m.line, m.text)
|
|
}
|
|
|
|
return tool.Result{ForModel: b.String(), ForUI: GrepResult{Count: len(matches)}}, nil
|
|
}
|
|
|
|
func grepFile(path string, re *regexp.Regexp, contextLines int, out *[]grepMatch) {
|
|
f, err := os.Open(path)
|
|
if err != nil {
|
|
return
|
|
}
|
|
defer f.Close()
|
|
|
|
var lines []string
|
|
scanner := bufio.NewScanner(f)
|
|
scanner.Buffer(make([]byte, 64*1024), 1024*1024)
|
|
for scanner.Scan() {
|
|
lines = append(lines, scanner.Text())
|
|
}
|
|
if scanner.Err() != nil {
|
|
return
|
|
}
|
|
|
|
for i, line := range lines {
|
|
if !re.MatchString(line) {
|
|
continue
|
|
}
|
|
lo := max(0, i-contextLines)
|
|
hi := min(len(lines)-1, i+contextLines)
|
|
for j := lo; j <= hi; j++ {
|
|
*out = append(*out, grepMatch{path: path, line: j + 1, text: lines[j]})
|
|
}
|
|
}
|
|
}
|
|
|
|
type GrepResult struct {
|
|
Count int
|
|
}
|