Add syntax highlighting to editor buffer display
This commit is contained in:
parent
a799b6716c
commit
0574457404
3 changed files with 110 additions and 2 deletions
|
|
@ -19,6 +19,7 @@ async def cmd_edit(player: Player, args: str) -> None:
|
||||||
player.editor = Editor(
|
player.editor = Editor(
|
||||||
save_callback=save_callback,
|
save_callback=save_callback,
|
||||||
content_type="text",
|
content_type="text",
|
||||||
|
color_depth=player.color_depth,
|
||||||
)
|
)
|
||||||
player.mode_stack.append("editor")
|
player.mode_stack.append("editor")
|
||||||
await player.send("Entering editor. Type :h for help.\r\n")
|
await player.send("Entering editor. Type :h for help.\r\n")
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,8 @@
|
||||||
from collections.abc import Awaitable, Callable
|
from collections.abc import Awaitable, Callable
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
|
|
||||||
|
from mudlib.render.highlight import highlight
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class EditorResponse:
|
class EditorResponse:
|
||||||
|
|
@ -26,6 +28,7 @@ class Editor:
|
||||||
undo_stack: list[list[str]] = field(default_factory=list)
|
undo_stack: list[list[str]] = field(default_factory=list)
|
||||||
save_callback: Callable[[str], Awaitable[None]] | None = None
|
save_callback: Callable[[str], Awaitable[None]] | None = None
|
||||||
content_type: str = "text" # hint for syntax highlighting later
|
content_type: str = "text" # hint for syntax highlighting later
|
||||||
|
color_depth: str = "16" # "16", "256", or "truecolor"
|
||||||
dirty: bool = False
|
dirty: bool = False
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
|
|
@ -33,10 +36,12 @@ class Editor:
|
||||||
initial_content: str = "",
|
initial_content: str = "",
|
||||||
save_callback: Callable[[str], Awaitable[None]] | None = None,
|
save_callback: Callable[[str], Awaitable[None]] | None = None,
|
||||||
content_type: str = "text",
|
content_type: str = "text",
|
||||||
|
color_depth: str = "16",
|
||||||
):
|
):
|
||||||
"""Initialize editor with optional content."""
|
"""Initialize editor with optional content."""
|
||||||
self.save_callback = save_callback
|
self.save_callback = save_callback
|
||||||
self.content_type = content_type
|
self.content_type = content_type
|
||||||
|
self.color_depth = color_depth
|
||||||
self.cursor = 0
|
self.cursor = 0
|
||||||
self.undo_stack = []
|
self.undo_stack = []
|
||||||
self.dirty = False
|
self.dirty = False
|
||||||
|
|
@ -62,7 +67,20 @@ class Editor:
|
||||||
if not self.buffer:
|
if not self.buffer:
|
||||||
return "(empty 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)
|
max_line_num = len(self.buffer)
|
||||||
padding = len(str(max_line_num))
|
padding = len(str(max_line_num))
|
||||||
|
|
||||||
|
|
@ -81,7 +99,22 @@ class Editor:
|
||||||
start = max(0, line_num - 1 - context)
|
start = max(0, line_num - 1 - context)
|
||||||
end = min(len(self.buffer), line_num + 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
|
max_line_num = end
|
||||||
padding = len(str(max_line_num))
|
padding = len(str(max_line_num))
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -344,3 +344,77 @@ async def test_line_numbers_padded():
|
||||||
# Find lines with numbers
|
# Find lines with numbers
|
||||||
assert any(" 1 line 1" in line for line in lines)
|
assert any(" 1 line 1" in line for line in lines)
|
||||||
assert any("11 line 11" 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()
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue