nub/internal/tool/builtin/grep.go
Tom a97013d876 initial commit
- v 0.1.0 siehe CHANGELOG.md
2026-07-25 11:08:01 +02:00

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
}