rg.go

 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}