From 8c68b6f314602251b9718999f118c3a560acc483 Mon Sep 17 00:00:00 2001 From: Saidheerajgollu <158853598+Saidheerajgollu@users.noreply.github.com> Date: Thu, 23 Jul 2026 13:23:18 -0700 Subject: [PATCH] Parse ripgrep output with a drive-letter-aware regex The grep tool parsed each rg match line with split(":", 2). rg is invoked with an absolute search base, so on Windows every line comes back as C:\ws\src\a.py:12:text - the split yields file="C", line="\ws\src\a.py" (not a digit, coerced to 0), and the real line number ends up glued to the front of the text. Every grep result on Windows is garbage: unusable file names, line 0, wrong text. Match lines are now parsed with a regex whose path group tolerates an optional drive prefix; the line group requires digits, so stray output lines are skipped instead of misparsed. POSIX output and matches whose text contains colons parse exactly as before. Co-authored-by: Cursor --- coworker/tools/search.py | 17 +++++++++++------ tests/test_code_tools.py | 25 ++++++++++++++++++++++++- 2 files changed, 35 insertions(+), 7 deletions(-) diff --git a/coworker/tools/search.py b/coworker/tools/search.py index ad489e1fa2..78a416a3e3 100644 --- a/coworker/tools/search.py +++ b/coworker/tools/search.py @@ -134,17 +134,22 @@ def _rel(path: str, root: Path) -> str: return path +# One `path:line:text` match line. The path part must tolerate a Windows drive prefix: +# a naive split(":", 2) on `C:\ws\a.py:12:text` yields file="C", line="\ws\a.py" — every +# match garbled on Windows, where rg (invoked with an absolute base) echoes absolute paths. +_RG_LINE = re.compile(r"^(?P(?:[A-Za-z]:)?[^:]*):(?P\d+):(?P.*)$") + + def _parse_rg(stdout: str, root: Path, n: int) -> dict[str, Any]: matches: list[dict[str, Any]] = [] for line in stdout.splitlines(): - parts = line.split(":", 2) - if len(parts) == 3: - f, ln, txt = parts + m = _RG_LINE.match(line) + if m: matches.append( { - "file": _rel(f, root), - "line": int(ln) if ln.isdigit() else 0, - "text": txt[:300], + "file": _rel(m["file"], root), + "line": int(m["line"]), + "text": m["text"][:300], } ) if len(matches) >= n: diff --git a/tests/test_code_tools.py b/tests/test_code_tools.py index 0edc83b2f1..f3ebcfccc0 100644 --- a/tests/test_code_tools.py +++ b/tests/test_code_tools.py @@ -13,7 +13,7 @@ from coworker.tools.files import file_tools from coworker.tools.git import git_tools -from coworker.tools.search import _py_grep, search_tools +from coworker.tools.search import _parse_rg, _py_grep, search_tools from coworker.web.fetch import _html_to_text, make_web_fetch_tool @@ -66,6 +66,29 @@ def test_grep_rejects_path_escape(tmp_path): assert "escapes" in grep(pattern="x", path="../..")["error"] +def test_parse_rg_posix_paths_and_colons_in_text(tmp_path): + root = tmp_path.resolve() + out = f"{root}/src/a.py:12:url = 'http://x:8080'\n" + res = _parse_rg(out, root, 100) + assert res["count"] == 1 + m = res["matches"][0] + assert m["file"] == "src/a.py" + assert m["line"] == 12 + assert m["text"] == "url = 'http://x:8080'" + + +def test_parse_rg_windows_drive_paths(tmp_path): + """rg on Windows echoes absolute paths (`C:\\ws\\a.py:12:text`); a naive + split(':', 2) turned the drive letter into the filename and lost the line number.""" + out = "C:\\ws\\src\\a.py:12:def hello():\n" + res = _parse_rg(out, tmp_path.resolve(), 100) + assert res["count"] == 1 + m = res["matches"][0] + assert m["file"] == "C:\\ws\\src\\a.py" # not 'C' + assert m["line"] == 12 + assert m["text"] == "def hello():" + + def test_py_grep_fallback_skips_ignored_dirs(tmp_path): _seed(tmp_path) res = _py_grep(tmp_path.resolve(), tmp_path.resolve(), "hello", None, 100)