mirror of
https://github.com/tiennm99/serena.git
synced 2026-10-13 08:15:59 +00:00
fix(editing): delete last line of a file without a trailing newline
delete_lines/replace_lines address the position one line past the last line (line == number of lines, col 0). get_index_from_line_col cannot resolve that position when the file has no trailing newline (no closing newline to count), so delete_text_between_positions raised InvalidTextLocationError and the delete crashed on a very common file shape. Clamp that past-EOF end position to end-of-file in delete_text_between_positions, mirroring the guard insert_text_at_position already has. A file with no trailing newline now deletes its last line identically to one with a trailing newline. Adds TextUtils unit tests and a CHANGELOG entry.
This commit is contained in:
1 parent
755de59f16
commit
cb38f6f82a
3 files changed
+48
-2
No files matched your search
@@ -38,6 +38,10 @@ Status of the `main` branch. Changes prior to the next official version change w
|
||||
- Fix: Use LSP-compliant line splitting ("\n", "\r\n" and "\r" can define line breaks); previously only "\n" considered
|
||||
- Fix: Apply consistent line splitting across tools, uniformly applying the LSP splitting semantics;
|
||||
affects `search_text` used by `search_for_pattern` tool (reported in #1684)
|
||||
- Fix in `TextUtils` (used by editors): Deleting up to the end of the file, referencing the line one past
|
||||
the end of the file (particularly for files with no newline at end of file) raised `InvalidTextLocationError`
|
||||
instead of accepting the deletion (change in `TextUtils.delete_text_between_positions`,
|
||||
which now accepts the end position similar to `insert_text_at_position`).
|
||||
- Fix: glob pattern expansion in `expand_braces` did not terminate with empty or unbalanced braces #1690
|
||||
|
||||
* CLI:
|
||||
|
||||
@@ -237,7 +237,21 @@ class TextUtils:
|
||||
:return: a tuple containing the modified text and the deleted text
|
||||
"""
|
||||
del_start_idx = TextUtils.get_index_from_line_col(text, start_line, start_col)
|
||||
del_end_idx = TextUtils.get_index_from_line_col(text, end_line, end_col)
|
||||
try:
|
||||
del_end_idx = TextUtils.get_index_from_line_col(text, end_line, end_col)
|
||||
except InvalidTextLocationError:
|
||||
# Deleting through the final line addresses the position one line past the
|
||||
# last line (line == number of lines, col 0). When the file has no trailing
|
||||
# newline there is no closing newline to count, so get_index_from_line_col
|
||||
# cannot resolve it; that position means "end of file". Clamp to len(text).
|
||||
# (insert_text_at_position handles the same past-EOF position.)
|
||||
text_stepper = TextStepper(text)
|
||||
text_stepper.process_all()
|
||||
num_lines_in_text = text_stepper.line + 1
|
||||
if end_line == num_lines_in_text and end_col == 0:
|
||||
del_end_idx = len(text)
|
||||
else:
|
||||
raise
|
||||
|
||||
deleted_text = text[del_start_idx:del_end_idx]
|
||||
new_text = text[:del_start_idx] + text[del_end_idx:]
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
from solidlsp.ls_utils import TextUtils
|
||||
import pytest
|
||||
|
||||
from solidlsp.ls_utils import InvalidTextLocationError, TextUtils
|
||||
|
||||
|
||||
class TestTextUtils:
|
||||
@@ -48,3 +50,29 @@ class TestTextUtils:
|
||||
new_text, l, c = TextUtils.insert_text_at_position(self.TEXT, 4, 0, insertion)
|
||||
assert (l, c) == (4, len(insertion))
|
||||
assert new_text == self.TEXT + "\n" + insertion
|
||||
|
||||
def test_delete_text_deletes_last_line_without_trailing_newline(self) -> None:
|
||||
"""Deleting the final line must work whether or not the file ends in a newline.
|
||||
|
||||
delete_lines(k, N-1) addresses the position one line past the last line
|
||||
(line N, col 0). With no trailing newline there is no closing newline to
|
||||
count, so get_index_from_line_col cannot resolve it; the delete must still
|
||||
remove the last line instead of raising InvalidTextLocationError.
|
||||
"""
|
||||
# File with 3 lines, no trailing newline: read_file (splitlines) shows 0='a',1='b',2='c'.
|
||||
text = "a\nb\nc"
|
||||
new_text, deleted = TextUtils.delete_text_between_positions(text, 2, 0, 3, 0)
|
||||
assert new_text == "a\nb\n"
|
||||
assert deleted == "c"
|
||||
|
||||
def test_delete_text_last_line_matches_trailing_newline_variant(self) -> None:
|
||||
"""Deleting the last line yields the same result with or without a trailing newline."""
|
||||
without_nl, _ = TextUtils.delete_text_between_positions("a\nb\nc", 2, 0, 3, 0)
|
||||
with_nl, _ = TextUtils.delete_text_between_positions("a\nb\nc\n", 2, 0, 3, 0)
|
||||
assert without_nl == with_nl == "a\nb\n"
|
||||
|
||||
def test_delete_text_still_raises_for_out_of_range_end(self) -> None:
|
||||
"""A genuinely out-of-range end position (beyond one-past-EOF) still raises."""
|
||||
with pytest.raises(InvalidTextLocationError):
|
||||
# end_line = 5 is well past the one-line-past-EOF position (3) for a 3-line file.
|
||||
TextUtils.delete_text_between_positions("a\nb\nc", 0, 0, 5, 0)
|
||||
Reference in new issue
Block a user