Fix editor search/replace parsing, dirty flag, and cursor tracking
This commit is contained in:
parent
0574457404
commit
d3df09f4de
3 changed files with 33 additions and 20 deletions
|
|
@ -134,8 +134,10 @@ class Editor:
|
||||||
# Insert at end if line_num is beyond buffer
|
# Insert at end if line_num is beyond buffer
|
||||||
if line_num > len(self.buffer):
|
if line_num > len(self.buffer):
|
||||||
self.buffer.append(text)
|
self.buffer.append(text)
|
||||||
|
self.cursor = len(self.buffer) - 1
|
||||||
else:
|
else:
|
||||||
self.buffer.insert(line_num - 1, text)
|
self.buffer.insert(line_num - 1, text)
|
||||||
|
self.cursor = line_num - 1
|
||||||
|
|
||||||
self._mark_dirty()
|
self._mark_dirty()
|
||||||
return f"Inserted at line {line_num}"
|
return f"Inserted at line {line_num}"
|
||||||
|
|
@ -150,6 +152,7 @@ class Editor:
|
||||||
if end is None:
|
if end is None:
|
||||||
# Delete single line
|
# Delete single line
|
||||||
self.buffer.pop(start - 1)
|
self.buffer.pop(start - 1)
|
||||||
|
self.cursor = min(start - 1, len(self.buffer) - 1)
|
||||||
self._mark_dirty()
|
self._mark_dirty()
|
||||||
return f"Deleted line {start}"
|
return f"Deleted line {start}"
|
||||||
else:
|
else:
|
||||||
|
|
@ -159,6 +162,7 @@ class Editor:
|
||||||
|
|
||||||
# Delete from start to end (inclusive)
|
# Delete from start to end (inclusive)
|
||||||
del self.buffer[start - 1 : end]
|
del self.buffer[start - 1 : end]
|
||||||
|
self.cursor = min(start - 1, len(self.buffer) - 1)
|
||||||
self._mark_dirty()
|
self._mark_dirty()
|
||||||
return f"Deleted lines {start}-{end}"
|
return f"Deleted lines {start}-{end}"
|
||||||
|
|
||||||
|
|
@ -202,8 +206,11 @@ class Editor:
|
||||||
return "Nothing to undo"
|
return "Nothing to undo"
|
||||||
|
|
||||||
self.buffer = self.undo_stack.pop()
|
self.buffer = self.undo_stack.pop()
|
||||||
# Undo might restore clean state, but we'll keep dirty flag
|
# Conservative approach: if we undid something, the buffer is now different
|
||||||
# (conservative approach - user can save to clear it)
|
# 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"
|
return "Undone last change"
|
||||||
|
|
||||||
async def _save(self) -> str:
|
async def _save(self) -> str:
|
||||||
|
|
@ -254,6 +261,7 @@ Any line not starting with : is appended to the buffer."""
|
||||||
# Append to buffer
|
# Append to buffer
|
||||||
self._save_undo_state()
|
self._save_undo_state()
|
||||||
self.buffer.append(line)
|
self.buffer.append(line)
|
||||||
|
self.cursor = len(self.buffer) - 1
|
||||||
self._mark_dirty()
|
self._mark_dirty()
|
||||||
return EditorResponse(output="", done=False)
|
return EditorResponse(output="", done=False)
|
||||||
|
|
||||||
|
|
@ -338,24 +346,13 @@ Any line not starting with : is appended to the buffer."""
|
||||||
|
|
||||||
# Search and replace
|
# Search and replace
|
||||||
if command == "s":
|
if command == "s":
|
||||||
parts = args.split(None, 1)
|
split_args = args.split(None, 1)
|
||||||
if len(parts) < 2:
|
if len(split_args) < 2:
|
||||||
return EditorResponse(
|
return EditorResponse(
|
||||||
output="Error: :s requires old and new text", done=False
|
output="Error: :s requires old and new text", done=False
|
||||||
)
|
)
|
||||||
old_new = parts[1].split(None, 1) if len(parts) > 1 else []
|
old = split_args[0]
|
||||||
if len(old_new) < 2:
|
new = split_args[1]
|
||||||
# Try parsing the whole args as "old new"
|
|
||||||
split_args = args.split(None, 1)
|
|
||||||
if len(split_args) < 2:
|
|
||||||
return EditorResponse(
|
|
||||||
output="Error: :s requires old and new text", done=False
|
|
||||||
)
|
|
||||||
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)
|
output = self._search_replace(old, new, replace_all=False)
|
||||||
return EditorResponse(output=output, done=False)
|
return EditorResponse(output=output, done=False)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -418,3 +418,19 @@ async def test_empty_buffer_with_python_content_type():
|
||||||
response = await editor.handle_input(":")
|
response = await editor.handle_input(":")
|
||||||
# Should show empty buffer message
|
# Should show empty buffer message
|
||||||
assert "empty" in response.output.lower()
|
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
|
||||||
|
|
|
||||||
|
|
@ -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 1")
|
||||||
await player.editor.handle_input("line 2")
|
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
|
# This test verifies the cursor field exists and can be used for prompts
|
||||||
assert player.editor.cursor == 0
|
assert player.editor.cursor == 1
|
||||||
# Shell loop prompt: f" {player.editor.cursor + 1}> " = " 1> "
|
# Shell loop prompt: f" {player.editor.cursor + 1}> " = " 2> "
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue