diff --git a/internal/llm/tools/glob.go b/internal/llm/tools/glob.go index 737b3ea27fd78f1ce821c06d2b9f5d9b40fcf5ea..dd224267ce9508b6f6f272ad77b775c8e4fe7113 100644 --- a/internal/llm/tools/glob.go +++ b/internal/llm/tools/glob.go @@ -1,11 +1,13 @@ package tools import ( + "bytes" "context" "encoding/json" "fmt" "io/fs" "os" + "os/exec" "path/filepath" "sort" "strings" @@ -132,6 +134,73 @@ func (g *globTool) Run(ctx context.Context, call ToolCall) (ToolResponse, error) } func globFiles(pattern, searchPath string, limit int) ([]string, bool, error) { + matches, err := globWithRipgrep(pattern, searchPath, limit) + if err == nil { + return matches, len(matches) >= limit, nil + } + + return globWithDoublestar(pattern, searchPath, limit) +} + +func globWithRipgrep( + pattern, searchRoot string, + limit int, +) ([]string, error) { + + if searchRoot == "" { + searchRoot = "." + } + + rgBin, err := exec.LookPath("rg") + if err != nil { + return nil, fmt.Errorf("ripgrep not found in $PATH: %w", err) + } + + if !filepath.IsAbs(pattern) && !strings.HasPrefix(pattern, "/") { + pattern = "/" + pattern + } + + args := []string{ + "--files", + "--null", + "--glob", pattern, + "-L", + } + + cmd := exec.Command(rgBin, args...) + cmd.Dir = searchRoot + + out, err := cmd.CombinedOutput() + if err != nil { + if ee, ok := err.(*exec.ExitError); ok && ee.ExitCode() == 1 { + return nil, nil + } + return nil, fmt.Errorf("ripgrep: %w\n%s", err, out) + } + + var matches []string + for _, p := range bytes.Split(out, []byte{0}) { + if len(p) == 0 { + continue + } + abs := filepath.Join(searchRoot, string(p)) + if skipHidden(abs) { + continue + } + matches = append(matches, abs) + } + + sort.SliceStable(matches, func(i, j int) bool { + return len(matches[i]) < len(matches[j]) + }) + + if len(matches) > limit { + matches = matches[:limit] + } + return matches, nil +} + +func globWithDoublestar(pattern, searchPath string, limit int) ([]string, bool, error) { if !strings.HasPrefix(pattern, "/") && !strings.HasPrefix(pattern, searchPath) { if !strings.HasSuffix(searchPath, "/") { searchPath += "/"