|
@@ -1,11 +1,13 @@
|
|
|
package tools
|
|
package tools
|
|
|
|
|
|
|
|
import (
|
|
import (
|
|
|
|
|
+ "bytes"
|
|
|
"context"
|
|
"context"
|
|
|
"encoding/json"
|
|
"encoding/json"
|
|
|
"fmt"
|
|
"fmt"
|
|
|
"io/fs"
|
|
"io/fs"
|
|
|
"os"
|
|
"os"
|
|
|
|
|
+ "os/exec"
|
|
|
"path/filepath"
|
|
"path/filepath"
|
|
|
"sort"
|
|
"sort"
|
|
|
"strings"
|
|
"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) {
|
|
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.HasPrefix(pattern, "/") && !strings.HasPrefix(pattern, searchPath) {
|
|
|
if !strings.HasSuffix(searchPath, "/") {
|
|
if !strings.HasSuffix(searchPath, "/") {
|
|
|
searchPath += "/"
|
|
searchPath += "/"
|