on main help sign in
fix(grep): resolve Windows path parsing with null separation (#1095)
1f46a69Rory de Zoete
internal/llm/tools/grep.go+31−6
@@ -259,18 +259,16 @@ func searchWithRipgrep(ctx context.Context, pattern, path, include string) ([]gr
259 continue
259 continue
260 }
260 }
261
261
262− // Parse ripgrep output format: file:line:content
262+ // Parse ripgrep output using null separation
263− parts := strings.SplitN(line, ":", 3)
263+ filePath, lineNumStr, lineText, ok := parseRipgrepLine(line)
264− if len(parts) < 3 {
264+ if !ok {
265 continue
265 continue
266 }
266 }
267
267
268− filePath := parts[0]
269− lineNum, err := strconv.Atoi(parts[1])
268+ lineNum, err := strconv.Atoi(lineNumStr)
270 if err != nil {
269 if err != nil {
271 continue
270 continue
272 }
271 }
273− lineText := parts[2]
274
272
275 fileInfo, err := os.Stat(filePath)
273 fileInfo, err := os.Stat(filePath)
276 if err != nil {
274 if err != nil {
@@ -288,6 +286,33 @@ func searchWithRipgrep(ctx context.Context, pattern, path, include string) ([]gr
288 return matches, nil
286 return matches, nil
289}
287}
290
288
289+// parseRipgrepLine parses ripgrep output with null separation to handle Windows paths
290+func parseRipgrepLine(line string) (filePath, lineNum, lineText string, ok bool) {
291+ // Split on null byte first to separate filename from rest
292+ parts := strings.SplitN(line, "\x00", 2)
293+ if len(parts) != 2 {
294+ return "", "", "", false
295+ }
296+
297+ filePath = parts[0]
298+ remainder := parts[1]
299+
300+ // Now split the remainder on first colon: "linenum:content"
301+ colonIndex := strings.Index(remainder, ":")
302+ if colonIndex == -1 {
303+ return "", "", "", false
304+ }
305+
306+ lineNumStr := remainder[:colonIndex]
307+ lineText = remainder[colonIndex+1:]
308+
309+ if _, err := strconv.Atoi(lineNumStr); err != nil {
310+ return "", "", "", false
311+ }
312+
313+ return filePath, lineNumStr, lineText, true
314+}
315+
291func searchFilesWithRegex(pattern, rootPath, include string) ([]grepMatch, error) {
316func searchFilesWithRegex(pattern, rootPath, include string) ([]grepMatch, error) {
292 matches := []grepMatch{}
317 matches := []grepMatch{}
293
318
internal/llm/tools/rg.go+2−2
@@ -42,8 +42,8 @@ func getRgSearchCmd(ctx context.Context, pattern, path, include string) *exec.Cm
42 if name == "" {
42 if name == "" {
43 return nil
43 return nil
44 }
44 }
45− // Use -n to show line numbers and include the matched line
45+ // Use -n to show line numbers, -0 for null separation to handle Windows paths
46− args := []string{"-H", "-n", pattern}
46+ args := []string{"-H", "-n", "-0", pattern}
47 if include != "" {
47 if include != "" {
48 args = append(args, "--glob", include)
48 args = append(args, "--glob", include)
49 }
49 }