Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 12 additions & 4 deletions coworker/tools/search.py
Original file line number Diff line number Diff line change
Expand Up @@ -134,16 +134,24 @@ def _rel(path: str, root: Path) -> str:
return path


# ripgrep --no-heading --line-number emits "<path>:<line>:<text>". A positional
# split(":", 2) mis-parses on Windows, where the absolute path starts with a
# drive letter ("C:\...") whose colon looks like the field separator. Anchor on
# the numeric ":<line>:" instead, so both the drive-letter colon and any colons
# inside the matched text stay intact.
_RG_LINE = re.compile(r"(.+?):(\d+):(.*)")


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:
f, ln, txt = m.group(1), m.group(2), m.group(3)
matches.append(
{
"file": _rel(f, root),
"line": int(ln) if ln.isdigit() else 0,
"line": int(ln),
"text": txt[:300],
}
)
Expand Down
19 changes: 18 additions & 1 deletion tests/test_code_tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down Expand Up @@ -61,6 +61,23 @@ def test_ripgrep_uses_the_same_ignored_dirs_as_the_python_fallback(tmp_path, mon
assert commands[0].index(f"!**/{ignored}/**") > user_glob


def test_parse_rg_handles_windows_drive_letter_and_colons_in_text():
# ripgrep prints absolute paths; on Windows they begin with a drive letter
# ("C:\\..."), whose colon must not be mistaken for the path:line:text
# separator. The matched text can also contain colons. Hardcode a Windows
# line so this guards the parser on every platform, not just Windows CI.
from pathlib import Path

stdout = "C:\\Users\\me\\ws\\a.py:12:x = {'k': 'v'}\n"
out = _parse_rg(stdout, Path("C:\\Users\\me\\ws"), 100)

assert out["count"] == 1
m = out["matches"][0]
assert m["line"] == 12 # was 0 with the old split(":", 2)
assert m["text"] == "x = {'k': 'v'}" # line number not leaked, colons kept
assert m["file"] != "C" # the drive letter is not the filename


def test_grep_rejects_path_escape(tmp_path):
grep = search_tools(str(tmp_path))[0]
assert "escapes" in grep(pattern="x", path="../..")["error"]
Expand Down