1package tools
2
3import (
4 "context"
5 "log/slog"
6 "os/exec"
7 "path/filepath"
8 "strings"
9 "sync"
10
11 "github.com/charmbracelet/crush/internal/log"
12)
13
14var getRg = sync.OnceValue(func() string {
15 path, err := exec.LookPath("rg")
16 if err != nil {
17 if log.Initialized() {
18 slog.Warn("Ripgrep (rg) not found in $PATH. Some grep features might be limited or slower.")
19 }
20 return ""
21 }
22 return path
23})
24
25func getRgCmd(ctx context.Context, globPattern string) *exec.Cmd {
26 name := getRg()
27 if name == "" {
28 return nil
29 }
30 args := []string{"--files", "-L", "--null"}
31 if globPattern != "" {
32 if !filepath.IsAbs(globPattern) && !strings.HasPrefix(globPattern, "/") {
33 globPattern = "/" + globPattern
34 }
35 args = append(args, "--glob", globPattern)
36 }
37 return exec.CommandContext(ctx, name, args...)
38}
39
40type execCmd interface {
41 AddArgs(arg ...string)
42 Output() ([]byte, error)
43}
44
45type wrappedCmd struct {
46 cmd *exec.Cmd
47}
48
49func (w wrappedCmd) AddArgs(arg ...string) {
50 w.cmd.Args = append(w.cmd.Args, arg...)
51}
52
53func (w wrappedCmd) Output() ([]byte, error) {
54 return w.cmd.Output()
55}
56
57type resolveRgSearchCmd func(ctx context.Context, pattern, path, include string) execCmd
58
59func getRgSearchCmd(ctx context.Context, pattern, path, include string) execCmd {
60 name := getRg()
61 if name == "" {
62 return nil
63 }
64 // Use -n to show line numbers, -0 for null separation to handle Windows paths
65 args := []string{"-H", "-n", "-0", pattern}
66 if include != "" {
67 args = append(args, "--glob", include)
68 }
69 args = append(args, path)
70
71 return wrappedCmd{
72 cmd: exec.CommandContext(ctx, name, args...),
73 }
74}