diff --git a/navi/tools/filesystem.py b/navi/tools/filesystem.py index e77fe2a..87be385 100644 --- a/navi/tools/filesystem.py +++ b/navi/tools/filesystem.py @@ -279,7 +279,14 @@ max_num = 0 old_line = 0 new_line = 0 - for line in diff_lines: + for raw_line in diff_lines: + # difflib.unified_diff keeps a trailing "\n" on content lines when the + # input was fed with explicit newlines (see _unified_diff, which adds + # "\n" so difflib does not emit a "\ No newline at end of file" marker). + # Strip it here so the later "\n".join does not produce a blank line + # between every diff line. No-op for the _diff action, whose lines come + # from splitlines() without trailing newlines. + line = raw_line.rstrip("\n") if line.startswith("@@"): m = _DIFF_HUNK_RE.match(line) if m: @@ -419,7 +426,7 @@ }, "numbered": { "type": "boolean", - "description": "Include 1-based line numbers in read output (default false).", + "description": "Include 1-based line numbers in read output (default true). Set false to read raw file content without the number column.", }, "destination": { "type": "string", @@ -586,14 +593,20 @@ f"[{path} | lines {start + 1}–{actual_end} of {total_lines}" f" | {_fmt_size(file_size)}]\n" ) - return ToolResult(success=True, output=header + "".join(selected)) + numbered = params.get("numbered", True) + body = ( + _number_lines([l.rstrip("\r\n") for l in selected], start=start + 1) + if numbered + else "".join(selected) + ) + return ToolResult(success=True, output=header + body) warn = ( f"⚠ Large file ({_fmt_size(file_size)}) — consider offset/limit next time.\n" if file_size > _READ_WARN_BYTES else "" ) - numbered = params.get("numbered", False) + numbered = params.get("numbered", True) header = f"[{path} | {total_lines} lines | {_fmt_size(file_size)}]\n" if numbered: return ToolResult( diff --git a/tests/unit/tools/test_filesystem.py b/tests/unit/tools/test_filesystem.py index 7465ffa..c81ef65 100644 --- a/tests/unit/tools/test_filesystem.py +++ b/tests/unit/tools/test_filesystem.py @@ -303,3 +303,74 @@ assert not result.success assert "directories" in result.output.lower() + + +class TestNumberedDiff: + """``_number_diff`` / ``_unified_diff`` produce a single-newline-separated + diff — no blank line between content lines. The ``_unified_diff`` helper + feeds difflib with ``line + "\\n"`` so difflib does not emit a + "\\ No newline at end of file" marker; difflib then keeps that trailing + "\\n" on content lines, so ``_number_diff`` must strip it or the final + ``"\\n".join`` yields a blank line between every diff line.""" + + def test_unified_diff_has_no_blank_lines_between_content_lines(self, tmp_path): + from pathlib import Path + + from navi.tools.filesystem import _unified_diff + + out = _unified_diff( + ["import os", "OLD = 1", "", "def f():"], + ["import os", "NEW = 2", "", "def f():"], + Path("f.py"), + ) + # No double newline (blank line) anywhere in the output. + assert "\n\n" not in out + # Content lines are present and single-separated. + assert " 1│ import os" in out + assert "- 2│ OLD = 1" in out + assert "+ 2│ NEW = 2" in out + # An empty source line still renders its content slot as a space. + assert " 3│ " in out or " 3│ " in out + + def test_number_diff_strips_trailing_newline_from_content(self): + from navi.tools.filesystem import _number_diff + + # Lines as difflib emits them when input carried a trailing "\n": + # each content line ends with "\n". Without stripping, "\n".join would + # double the newline. + diff_lines = [ + "--- a/f.py\n", + "+++ b/f.py\n", + "@@ -1,3 +1,3 @@", + " import os\n", + "-OLD = 1\n", + "+NEW = 2\n", + " def f():\n", + ] + out = "\n".join(_number_diff(diff_lines)) + assert "\n\n" not in out + assert out.endswith("def f():") + assert " 1│ import os" in out + assert "- 2│ OLD = 1" in out + assert "+ 2│ NEW = 2" in out + + def test_number_diff_noop_for_lines_without_trailing_newline(self): + """The _diff action feeds splitlines() output (no trailing newline); + stripping must be a no-op there so its diff is unchanged.""" + from navi.tools.filesystem import _number_diff + + diff_lines = [ + "--- a/f.py", + "+++ b/f.py", + "@@ -1,3 +1,3 @@", + " import os", + "-OLD = 1", + "+NEW = 2", + " def f():", + ] + out = "\n".join(_number_diff(diff_lines)) + assert "\n\n" not in out + assert out.endswith("def f():") + assert " 1│ import os" in out + assert "- 2│ OLD = 1" in out + assert "+ 2│ NEW = 2" in out