diff --git a/src/plyngent/tools/file/read.py b/src/plyngent/tools/file/read.py index d7b16c5..bb1b78d 100644 --- a/src/plyngent/tools/file/read.py +++ b/src/plyngent/tools/file/read.py @@ -3,12 +3,33 @@ from __future__ import annotations from plyngent.agent import tool from plyngent.tools.workspace import resolve_path +_LINENO_WIDTH = 6 + + +def _format_with_lineno(lines: list[str], *, start_lineno: int) -> str: + """Prefix each line with a 1-based absolute line number (``edit_lineno`` style).""" + out: list[str] = [] + for index, line in enumerate(lines): + lineno = start_lineno + index + # Strip keepends for the body; re-add a single newline after the prefix. + body = line.rstrip("\r\n") + out.append(f"{lineno:>{_LINENO_WIDTH}}|{body}\n") + return "".join(out) + @tool -def read_file(path: str, *, offset: int = 0, limit: int | None = None) -> str: +def read_file( + path: str, + *, + offset: int = 0, + limit: int | None = None, + with_lineno: bool = False, +) -> str: """Read a text file under the workspace. ``offset`` is 0-based line start; ``limit`` is max lines (None = rest of file). + When ``with_lineno`` is true, each line is prefixed with its 1-based file line + number (`` N|…``), matching ``edit_lineno`` numbering. """ target = resolve_path(path) if not target.is_file(): @@ -21,4 +42,7 @@ def read_file(path: str, *, offset: int = 0, limit: int | None = None) -> str: end = len(lines) if limit is None else min(len(lines), start + limit) if start >= len(lines): return "" - return "".join(lines[start:end]) + slice_lines = lines[start:end] + if with_lineno: + return _format_with_lineno(slice_lines, start_lineno=start + 1) + return "".join(slice_lines) diff --git a/tests/test_tools/test_file.py b/tests/test_tools/test_file.py index bd2d68d..17cc75c 100644 --- a/tests/test_tools/test_file.py +++ b/tests/test_tools/test_file.py @@ -35,6 +35,18 @@ def test_read_offset_limit(workspace: object) -> None: assert call_sync(read_file, "lines.txt", offset=1, limit=2) == "b\nc\n" +def test_read_with_lineno(workspace: object) -> None: + del workspace + _ = call_sync(write_file, "num.txt", "a\nb\nc\n") + out = call_sync(read_file, "num.txt", with_lineno=True) + assert " 1|a\n" in out + assert " 2|b\n" in out + assert " 3|c\n" in out + # offset is 0-based; line numbers stay absolute 1-based file lines + mid = call_sync(read_file, "num.txt", offset=1, limit=1, with_lineno=True) + assert mid == " 2|b\n" + + def test_listdir_missing(workspace: object) -> None: del workspace assert "error" in call_sync(listdir, "nope")