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)