Fix editor search/replace parsing, dirty flag, and cursor tracking

This commit is contained in:
Jared Miller 2026-02-07 23:06:47 -05:00
parent 0574457404
commit d3df09f4de
Signed by: shmup
GPG key ID: 22B5C6D66A38B06C
3 changed files with 33 additions and 20 deletions

View file

@ -134,8 +134,10 @@ class Editor:
# Insert at end if line_num is beyond buffer
if line_num > len(self.buffer):
self.buffer.append(text)
self.cursor = len(self.buffer) - 1
else:
self.buffer.insert(line_num - 1, text)
self.cursor = line_num - 1
self._mark_dirty()
return f"Inserted at line {line_num}"
@ -150,6 +152,7 @@ class Editor:
if end is None:
# Delete single line
self.buffer.pop(start - 1)
self.cursor = min(start - 1, len(self.buffer) - 1)
self._mark_dirty()
return f"Deleted line {start}"
else:
@ -159,6 +162,7 @@ class Editor:
# Delete from start to end (inclusive)
del self.buffer[start - 1 : end]
self.cursor = min(start - 1, len(self.buffer) - 1)
self._mark_dirty()
return f"Deleted lines {start}-{end}"
@ -202,8 +206,11 @@ class Editor:
return "Nothing to undo"
self.buffer = self.undo_stack.pop()
# Undo might restore clean state, but we'll keep dirty flag
# (conservative approach - user can save to clear it)
# Conservative approach: if we undid something, the buffer is now different
# from what was saved (even if we undo past a save point)
self.dirty = True
# Reset cursor to end of buffer after undo
self.cursor = len(self.buffer) - 1 if self.buffer else 0
return "Undone last change"
async def _save(self) -> str:
@ -254,6 +261,7 @@ Any line not starting with : is appended to the buffer."""
# Append to buffer
self._save_undo_state()
self.buffer.append(line)
self.cursor = len(self.buffer) - 1
self._mark_dirty()
return EditorResponse(output="", done=False)
@ -338,14 +346,6 @@ Any line not starting with : is appended to the buffer."""
# Search and replace
if command == "s":
parts = args.split(None, 1)
if len(parts) < 2:
return EditorResponse(
output="Error: :s requires old and new text", done=False
)
old_new = parts[1].split(None, 1) if len(parts) > 1 else []
if len(old_new) < 2:
# Try parsing the whole args as "old new"
split_args = args.split(None, 1)
if len(split_args) < 2:
return EditorResponse(
@ -353,9 +353,6 @@ Any line not starting with : is appended to the buffer."""
)
old = split_args[0]
new = split_args[1]
else:
old = parts[0]
new = old_new[1] if len(old_new) > 1 else old_new[0]
output = self._search_replace(old, new, replace_all=False)
return EditorResponse(output=output, done=False)

View file

@ -418,3 +418,19 @@ async def test_empty_buffer_with_python_content_type():
response = await editor.handle_input(":")
# Should show empty buffer message
assert "empty" in response.output.lower()
@pytest.mark.asyncio
async def test_undo_past_save_marks_dirty():
"""Undoing after save should mark buffer dirty again."""
saved = []
async def cb(content: str):
saved.append(content)
editor = Editor(initial_content="line 1", save_callback=cb)
await editor.handle_input("line 2") # dirty
await editor.handle_input(":w") # clean
assert not editor.dirty
await editor.handle_input(":u") # undo the append - should be dirty
assert editor.dirty

View file

@ -161,7 +161,7 @@ async def test_editor_prompt_uses_cursor(player):
await player.editor.handle_input("line 1")
await player.editor.handle_input("line 2")
# Cursor starts at 0. The prompt shows "cursor + 1" for 1-indexed display
# After appending, cursor should be at the last line (1 in 0-indexed)
# This test verifies the cursor field exists and can be used for prompts
assert player.editor.cursor == 0
# Shell loop prompt: f" {player.editor.cursor + 1}> " = " 1> "
assert player.editor.cursor == 1
# Shell loop prompt: f" {player.editor.cursor + 1}> " = " 2> "