From 0574457404f83603931d9545bd560c9c6ddf4ed5 Mon Sep 17 00:00:00 2001 From: Jared Miller Date: Sat, 7 Feb 2026 23:01:55 -0500 Subject: [PATCH] Add syntax highlighting to editor buffer display --- src/mudlib/commands/edit.py | 1 + src/mudlib/editor.py | 37 ++++++++++++++++++- tests/test_editor.py | 74 +++++++++++++++++++++++++++++++++++++ 3 files changed, 110 insertions(+), 2 deletions(-) diff --git a/src/mudlib/commands/edit.py b/src/mudlib/commands/edit.py index c0b3959..69336a3 100644 --- a/src/mudlib/commands/edit.py +++ b/src/mudlib/commands/edit.py @@ -19,6 +19,7 @@ async def cmd_edit(player: Player, args: str) -> None: player.editor = Editor( save_callback=save_callback, content_type="text", + color_depth=player.color_depth, ) player.mode_stack.append("editor") await player.send("Entering editor. Type :h for help.\r\n") diff --git a/src/mudlib/editor.py b/src/mudlib/editor.py index f90d1ae..833593d 100644 --- a/src/mudlib/editor.py +++ b/src/mudlib/editor.py @@ -3,6 +3,8 @@ from collections.abc import Awaitable, Callable from dataclasses import dataclass, field +from mudlib.render.highlight import highlight + @dataclass class EditorResponse: @@ -26,6 +28,7 @@ class Editor: undo_stack: list[list[str]] = field(default_factory=list) save_callback: Callable[[str], Awaitable[None]] | None = None content_type: str = "text" # hint for syntax highlighting later + color_depth: str = "16" # "16", "256", or "truecolor" dirty: bool = False def __init__( @@ -33,10 +36,12 @@ class Editor: initial_content: str = "", save_callback: Callable[[str], Awaitable[None]] | None = None, content_type: str = "text", + color_depth: str = "16", ): """Initialize editor with optional content.""" self.save_callback = save_callback self.content_type = content_type + self.color_depth = color_depth self.cursor = 0 self.undo_stack = [] self.dirty = False @@ -62,7 +67,20 @@ class Editor: if not self.buffer: return "(empty buffer)" - # Calculate padding for line numbers + # Use syntax highlighting if content_type is not "text" + if self.content_type != "text": + content = "\n".join(self.buffer) + highlighted = highlight( + content, + language=self.content_type, + color_depth=self.color_depth, + line_numbers=True, + ) + # If highlight() returned the original (unknown language), fall through + if "\033[" in highlighted: + return highlighted + + # Fall back to plain line-number formatting max_line_num = len(self.buffer) padding = len(str(max_line_num)) @@ -81,7 +99,22 @@ class Editor: start = max(0, line_num - 1 - context) end = min(len(self.buffer), line_num + context) - # Calculate padding for line numbers + # Use syntax highlighting if content_type is not "text" + if self.content_type != "text": + # Get the context window + context_lines = self.buffer[start:end] + content = "\n".join(context_lines) + highlighted = highlight( + content, + language=self.content_type, + color_depth=self.color_depth, + line_numbers=True, + ) + # If highlight() returned highlighted content, use it + if "\033[" in highlighted: + return highlighted + + # Fall back to plain line-number formatting max_line_num = end padding = len(str(max_line_num)) diff --git a/tests/test_editor.py b/tests/test_editor.py index 17b446e..a07e6b6 100644 --- a/tests/test_editor.py +++ b/tests/test_editor.py @@ -344,3 +344,77 @@ async def test_line_numbers_padded(): # Find lines with numbers assert any(" 1 line 1" in line for line in lines) assert any("11 line 11" in line for line in lines) + + +# Syntax highlighting tests + + +@pytest.mark.asyncio +async def test_view_buffer_with_python_includes_ansi(): + """Viewing buffer with content_type=python includes ANSI codes.""" + editor = Editor( + initial_content="def foo():\n return 42", + content_type="python", + color_depth="16", + ) + response = await editor.handle_input(":") + # Should contain ANSI escape codes from syntax highlighting + assert "\033[" in response.output + # Should contain the code + assert "def" in response.output + assert "foo" in response.output + + +@pytest.mark.asyncio +async def test_view_buffer_with_text_no_ansi(): + """Viewing buffer with content_type=text does NOT include ANSI codes.""" + editor = Editor( + initial_content="plain text line 1\nplain text line 2", content_type="text" + ) + response = await editor.handle_input(":") + # Should NOT contain ANSI escape codes (plain line numbers) + assert "\033[" not in response.output + # Should contain plain line numbers + assert "1 plain text line 1" in response.output + assert "2 plain text line 2" in response.output + + +@pytest.mark.asyncio +async def test_view_specific_line_with_python_includes_ansi(): + """Viewing a specific line with python content includes ANSI codes.""" + content = "\n".join([f"def func{i}():\n return {i}" for i in range(10)]) + editor = Editor( + initial_content=content, + content_type="python", + color_depth="16", + ) + response = await editor.handle_input(":5") + # Should contain ANSI escape codes from syntax highlighting + assert "\033[" in response.output + # Should contain code from the context window + assert "def" in response.output + + +@pytest.mark.asyncio +async def test_color_depth_passed_to_highlight(): + """Color depth is passed through to highlight function.""" + editor = Editor( + initial_content="x = 1\ny = 2", + content_type="python", + color_depth="256", + ) + response = await editor.handle_input(":") + # Should contain ANSI codes (256-color formatter) + assert "\033[" in response.output + # Should contain the code + assert "x" in response.output + assert "y" in response.output + + +@pytest.mark.asyncio +async def test_empty_buffer_with_python_content_type(): + """Empty buffer with content_type=python still works.""" + editor = Editor(content_type="python", color_depth="16") + response = await editor.handle_input(":") + # Should show empty buffer message + assert "empty" in response.output.lower()