diff --git a/internal/llm/tools/grep.go b/internal/llm/tools/grep.go index 30d6e0b16a06c28aa33783f76fcdaa5ccb800915..cbf50360b9355c05797690678a99d1310b19556f 100644 --- a/internal/llm/tools/grep.go +++ b/internal/llm/tools/grep.go @@ -259,18 +259,16 @@ func searchWithRipgrep(ctx context.Context, pattern, path, include string) ([]gr continue } - // Parse ripgrep output format: file:line:content - parts := strings.SplitN(line, ":", 3) - if len(parts) < 3 { + // Parse ripgrep output using null separation + filePath, lineNumStr, lineText, ok := parseRipgrepLine(line) + if !ok { continue } - filePath := parts[0] - lineNum, err := strconv.Atoi(parts[1]) + lineNum, err := strconv.Atoi(lineNumStr) if err != nil { continue } - lineText := parts[2] fileInfo, err := os.Stat(filePath) if err != nil { @@ -288,6 +286,33 @@ func searchWithRipgrep(ctx context.Context, pattern, path, include string) ([]gr return matches, nil } +// parseRipgrepLine parses ripgrep output with null separation to handle Windows paths +func parseRipgrepLine(line string) (filePath, lineNum, lineText string, ok bool) { + // Split on null byte first to separate filename from rest + parts := strings.SplitN(line, "\x00", 2) + if len(parts) != 2 { + return "", "", "", false + } + + filePath = parts[0] + remainder := parts[1] + + // Now split the remainder on first colon: "linenum:content" + colonIndex := strings.Index(remainder, ":") + if colonIndex == -1 { + return "", "", "", false + } + + lineNumStr := remainder[:colonIndex] + lineText = remainder[colonIndex+1:] + + if _, err := strconv.Atoi(lineNumStr); err != nil { + return "", "", "", false + } + + return filePath, lineNumStr, lineText, true +} + func searchFilesWithRegex(pattern, rootPath, include string) ([]grepMatch, error) { matches := []grepMatch{} diff --git a/internal/llm/tools/rg.go b/internal/llm/tools/rg.go index 40ab7f2f520697659e3ef092a7ff3e96b2c3c47c..8809b57c8db30b4ac1ed6c070df5a7218c59e233 100644 --- a/internal/llm/tools/rg.go +++ b/internal/llm/tools/rg.go @@ -42,8 +42,8 @@ func getRgSearchCmd(ctx context.Context, pattern, path, include string) *exec.Cm if name == "" { return nil } - // Use -n to show line numbers and include the matched line - args := []string{"-H", "-n", pattern} + // Use -n to show line numbers, -0 for null separation to handle Windows paths + args := []string{"-H", "-n", "-0", pattern} if include != "" { args = append(args, "--glob", include) }