mirror of
https://github.com/NCBM/plyngent.git
synced 2026-11-07 07:17:54 +08:00
core/tools: edit_lineno rejections name the read_file call
This commit is contained in:
@@ -11,6 +11,9 @@ if TYPE_CHECKING:
|
||||
# How many unread line numbers to preview in the error message.
|
||||
_UNREAD_PREVIEW = 8
|
||||
|
||||
# Lines of context a suggested read covers on each side of the edit range.
|
||||
_READ_HINT_MARGIN = 3
|
||||
|
||||
|
||||
def _detect_newline(lines: list[str]) -> str:
|
||||
for sample in lines:
|
||||
@@ -68,6 +71,19 @@ def _unread_lines_in_range(
|
||||
return sorted(set(range(start_line, end_line + 1)) - read_lines)
|
||||
|
||||
|
||||
def _read_hint(path: str, start_line: int, end_line: int, n: int) -> str:
|
||||
"""A ready-to-copy ``read_file`` call covering the edit range plus a margin.
|
||||
|
||||
``read_file`` counts ``offset`` from 0 while ``edit_lineno`` counts lines
|
||||
from 1, so the call is spelled out instead of asking the model to convert.
|
||||
The window is clipped to the file (``_validate_range`` already accepted the
|
||||
edit) and the margin keeps neighbouring lines editable too.
|
||||
"""
|
||||
first = max(1, start_line - _READ_HINT_MARGIN)
|
||||
last = max(first, min(n, end_line + _READ_HINT_MARGIN))
|
||||
return f"read_file(path={path!r}, offset={first - 1}, limit={last - first + 1}, with_lineno=true)"
|
||||
|
||||
|
||||
def _append_after(target: Path, path: str, text: str, lines: list[str], new_content: str) -> str:
|
||||
n = len(lines)
|
||||
newline = _detect_newline(lines[-1:] if lines else [])
|
||||
@@ -135,8 +151,9 @@ async def edit_lineno(path: str, start_line: int, end_line: int, new_content: st
|
||||
if read_lines:
|
||||
window = f" (this turn read lines {min(read_lines)}-{max(read_lines)})"
|
||||
return (
|
||||
f"error: lines {head} not read{window}; call read_file(path, with_lineno=true) "
|
||||
"first — read with a small margin around the lines you plan to edit"
|
||||
f"error: lines {head} not read{window}; re-read with "
|
||||
f"{_read_hint(path, start_line, end_line, len(lines))} "
|
||||
"— offset is 0-based; the window keeps a margin around the edit"
|
||||
)
|
||||
|
||||
recorded_mtime = lineno_read_mtime(str(target))
|
||||
@@ -148,7 +165,8 @@ async def edit_lineno(path: str, start_line: int, end_line: int, new_content: st
|
||||
if current_mtime is not None and current_mtime != recorded_mtime:
|
||||
return (
|
||||
f"error: {path} changed since it was read (line numbers may be stale); "
|
||||
"re-read with read_file(path, with_lineno=true)"
|
||||
f"re-read with {_read_hint(path, start_line, end_line, len(lines))} "
|
||||
"— offset is 0-based"
|
||||
)
|
||||
|
||||
if start_line == len(lines) + 1:
|
||||
|
||||
@@ -49,6 +49,24 @@ def test_edit_lineno_error_shows_read_window(workspace: object) -> None:
|
||||
assert "not read" in out
|
||||
assert "read lines 2-3" in out
|
||||
assert "margin" in out
|
||||
# Ready to copy: 1-based line 1 is offset 0, and n=4 clamps the limit.
|
||||
assert "read_file(path='j.txt', offset=0, limit=4, with_lineno=true)" in out
|
||||
|
||||
|
||||
def test_edit_lineno_error_suggests_the_read_file_call(workspace: object) -> None:
|
||||
"""Both rejections hand back a ready-to-copy call with a 0-based ``offset``."""
|
||||
del workspace
|
||||
_ = call_sync(write_file, "s.txt", "".join(f"line {i}\n" for i in range(1, 21)))
|
||||
_ = reset_lineno_tracker()
|
||||
_ = call_sync(read_file, "s.txt", offset=0, limit=10, with_lineno=True) # lines 1-10
|
||||
# Line 15 was never shown; the hint reads it back with a margin (line 12 → offset 11).
|
||||
out = call_sync(edit_lineno, "s.txt", 15, 15, "X\n")
|
||||
assert "read_file(path='s.txt', offset=11, limit=7, with_lineno=true)" in out
|
||||
# A stale read gets a fresh call for the same range after an external write.
|
||||
_ = call_sync(write_file, "s.txt", "changed\n")
|
||||
out = call_sync(edit_lineno, "s.txt", 1, 1, "X\n")
|
||||
assert "changed since it was read" in out
|
||||
assert "read_file(path='s.txt', offset=0, limit=1, with_lineno=true)" in out
|
||||
|
||||
|
||||
def test_edit_lineno_requires_all_lines_read(workspace: object) -> None:
|
||||
@@ -84,6 +102,8 @@ def test_edit_lineno_append_requires_last_line_read(workspace: object) -> None:
|
||||
_ = call_sync(read_file, "ap.txt", offset=0, limit=2, with_lineno=True)
|
||||
out = call_sync(edit_lineno, "ap.txt", 4, 4, "d\n")
|
||||
assert "not read" in out and "3" in out
|
||||
# Appending needs the last line, so the suggested window ends at n.
|
||||
assert "read_file(path='ap.txt', offset=0, limit=3, with_lineno=true)" in out
|
||||
# Read the last line, then append succeeds.
|
||||
_ = call_sync(read_file, "ap.txt", offset=2, limit=1, with_lineno=True)
|
||||
out = call_sync(edit_lineno, "ap.txt", 4, 4, "d\n")
|
||||
|
||||
Reference in New Issue
Block a user