fix: make text_editor line numbering 1-based to match grep/sed/editors
linuztx committed
Feb 27, 2026 at 10:09 UTC
9419c0be35d6cb436c17bdb046e9f87bdba76d27
2 files changed
+19
-14
plugins/text_editor/helpers/file_ops.py
+18
-13
@@ -59,7 +59,7 @@ class ReadResult:
59
60
def read_file(
61
path: str,
62
- line_from: int = 0,
62
+ line_from: int = 1,
63
line_to: int | None = None,
64
max_line_tokens: int = 500,
65
default_line_count: int = 100,
@@ -68,8 +68,9 @@ def read_file(
68
"""
69
Read a text file and return numbered lines with token budgeting.
70
71
- line_from and line_to are both inclusive. None line_to defaults to
72
- line_from + default_line_count - 1.
71
+ Line numbers are 1-based (matching grep, sed, editors).
72
+ line_from and line_to are both inclusive.
73
+ None line_to defaults to line_from + default_line_count - 1.
74
"""
75
path = os.path.expanduser(path)
76
@@ -86,13 +87,15 @@ def read_file(
87
return ReadResult(error=str(exc))
88
89
total_lines = len(all_lines)
89
- line_from = max(line_from, 0)
90
+ line_from = max(line_from, 1)
91
if line_to is None:
92
line_to = line_from + default_line_count - 1
92
- line_to = min(line_to, total_lines - 1)
93
+ line_to = min(line_to, total_lines)
94
94
- # Slice is exclusive on the right, so +1
95
- selected = all_lines[line_from:line_to + 1]
95
+ # Convert 1-based inclusive range to 0-based slice
96
+ idx_from = line_from - 1
97
+ idx_to = line_to # slice is exclusive, line_to is inclusive 1-based
98
+ selected = all_lines[idx_from:idx_to]
99
100
warn_parts: list[str] = []
101
cropped_lines: list[int] = []
@@ -101,7 +104,7 @@ def read_file(
104
trimmed_by_total = False
105
106
for i, raw_line in enumerate(selected):
104
- line_no = line_from + i
107
+ line_no = line_from + i # 1-based
108
stripped = raw_line.rstrip("\n").rstrip("\r")
109
line_tok = tokens.count_tokens(stripped)
110
@@ -185,6 +188,7 @@ def validate_edits(edits: list | None) -> tuple[list[dict], str]:
188
"""
189
Normalise and validate an edits array.
190
191
+ Line numbers are 1-based (matching grep, sed, editors).
192
Semantics (to is inclusive):
193
{from:2, to:2, content:"x\\n"} - replace line 2
194
{from:1, to:3, content:"x\\n"} - replace lines 1-3
@@ -200,9 +204,9 @@ def validate_edits(edits: list | None) -> tuple[list[dict], str]:
204
for e in edits:
205
if not isinstance(e, dict):
206
return [], f"invalid edit entry: {e}"
203
- frm = int(e.get("from", -1))
204
- if frm < 0:
205
- return [], f"edit missing from: {e}"
207
+ frm = int(e.get("from", 0))
208
+ if frm < 1:
209
+ return [], f"edit missing or invalid from (must be >= 1): {e}"
210
# to == -1 or absent means pure insert (no lines removed)
211
to = int(e.get("to", -1))
212
is_insert = to < 0 or to < frm
@@ -237,7 +241,8 @@ def apply_patch(path: str, edits: list[dict]) -> int:
241
"""
242
Apply sorted, validated edits by streaming to a temp file.
243
240
- Edits use inclusive 'to'. Inserts have 'insert': True.
244
+ Line numbers are 1-based. Edits use inclusive 'to'.
245
+ Inserts have 'insert': True.
246
Returns total line count after patching.
247
"""
248
dir_name = os.path.dirname(path) or "."
@@ -248,7 +253,7 @@ def apply_patch(path: str, edits: list[dict]) -> int:
253
os.fdopen(fd, "w", encoding="utf-8") as dst,
254
):
255
edit_idx = 0
251
- line_no = 0
256
+ line_no = 1 # 1-based
257
total_written = 0
258
259
for raw_line in src:
plugins/text_editor/tools/text_editor.py
+1
-1
@@ -33,7 +33,7 @@ class TextEditor(Tool):
33
return self._error("read", path, "path is required")
34
35
cfg = get_config(self.agent)
36
- line_from = int(kwargs.get("line_from", 0))
36
+ line_from = int(kwargs.get("line_from", 1))
37
raw_to = kwargs.get("line_to")
38
line_to = int(raw_to) if raw_to is not None else None
39