diff --git a/src/mudlib/editor.py b/src/mudlib/editor.py index 833593d..8258cd1 100644 --- a/src/mudlib/editor.py +++ b/src/mudlib/editor.py @@ -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,24 +346,13 @@ 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: + split_args = args.split(None, 1) + if len(split_args) < 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( - 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] + old = split_args[0] + new = split_args[1] output = self._search_replace(old, new, replace_all=False) return EditorResponse(output=output, done=False) diff --git a/tests/test_editor.py b/tests/test_editor.py index a07e6b6..82084d6 100644 --- a/tests/test_editor.py +++ b/tests/test_editor.py @@ -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 diff --git a/tests/test_editor_integration.py b/tests/test_editor_integration.py index e9c3e6c..8a47fed 100644 --- a/tests/test_editor_integration.py +++ b/tests/test_editor_integration.py @@ -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> "