Private
Public Access
0
0
This commit is contained in:
2026-06-03 00:47:40 -04:00
parent 79a12d2c3e
commit 7a34edf605
6 changed files with 153 additions and 60 deletions
+68 -45
View File
@@ -7,6 +7,43 @@ import re
from pathlib import Path
from typing import Optional, Dict, Callable
def _get_language_id(name: str):
"""Get a language identifier for ImGuiColorTextEdit.
Compatible with both imgui-bundle 1.92.5 (LanguageDefinitionId enum)
and 1.92.801+ (Language factory functions returning a Language object).
Returns None for "no language" or unknown names.
"""
if not name or name == "none":
return None
# Prefer the newer API (1.92.801+) which uses factory functions.
if hasattr(ed.TextEditor, "Language"):
lang_class = ed.TextEditor.Language
if hasattr(lang_class, name):
factory = getattr(lang_class, name)
if callable(factory):
return factory()
# Fall back to the older API (1.92.5) which exposes an enum.
if hasattr(ed.TextEditor, "LanguageDefinitionId"):
lang_id_class = ed.TextEditor.LanguageDefinitionId
if hasattr(lang_id_class, name):
return getattr(lang_id_class, name)
return None
def _set_editor_language(editor, lang_obj) -> None:
"""Set the editor's language via whichever API is available.
1.92.801+: editor.set_language(obj). 1.92.5: editor.set_language_definition(obj).
No-op when lang_obj is None (used to skip the call for unknown languages).
"""
if lang_obj is None:
return
if hasattr(editor, "set_language"):
editor.set_language(lang_obj)
elif hasattr(editor, "set_language_definition"):
editor.set_language_definition(lang_obj)
class MarkdownRenderer:
"""
@@ -22,29 +59,32 @@ class MarkdownRenderer:
# Base path for fonts (Inter family)
self.options.font_options.font_base_path = "fonts/Inter"
self.options.font_options.regular_size = 16.0
# Configure callbacks
self.options.callbacks.on_open_link = self._on_open_link
# Cache for TextEditor instances to maintain state
self._editor_cache: Dict[tuple[str, int], ed.TextEditor] = {}
# Parallel cache tracking the current language tag per editor (avoids per-frame
# set_language calls and is robust against imgui-bundle naming differences).
self._editor_lang_cache: Dict[tuple[str, int], Optional[str]] = {}
self._max_cache_size = 100
# Optional callback for custom local link handling (e.g., opening in IDE)
self.on_local_link: Optional[Callable[[str], None]] = None
# Language mapping for ImGuiColorTextEdit
self._lang_map = {
"python": ed.TextEditor.LanguageDefinitionId.python,
"py": ed.TextEditor.LanguageDefinitionId.python,
"json": ed.TextEditor.LanguageDefinitionId.json,
"cpp": ed.TextEditor.LanguageDefinitionId.cpp,
"c++": ed.TextEditor.LanguageDefinitionId.cpp,
"c": ed.TextEditor.LanguageDefinitionId.c,
"lua": ed.TextEditor.LanguageDefinitionId.lua,
"sql": ed.TextEditor.LanguageDefinitionId.sql,
"cs": ed.TextEditor.LanguageDefinitionId.cs,
"c#": ed.TextEditor.LanguageDefinitionId.cs,
"python": _get_language_id("python"),
"py": _get_language_id("python"),
"json": _get_language_id("json"),
"cpp": _get_language_id("cpp"),
"c++": _get_language_id("cpp"),
"c": _get_language_id("c"),
"lua": _get_language_id("lua"),
"sql": _get_language_id("sql"),
"cs": _get_language_id("cs"),
"c#": _get_language_id("cs"),
}
def _on_open_link(self, url: str) -> None:
@@ -95,25 +135,11 @@ class MarkdownRenderer:
# Wrap in fake markdown markers for the internal renderer
self._render_code_block(f"```{lang}\n{code}```", context_id, block_idx)
def _render_code_block(self, block: str, context_id: str, block_idx: int) -> None:
"""Render a code block using TextEditor for syntax highlighting."""
lines = block.strip('`').split('\n')
lang_tag = lines[0].strip().lower() if lines else ""
# Heuristic to separate lang tag from code
if lang_tag and lang_tag not in self._lang_map and not self._is_likely_lang_tag(lang_tag):
lang_tag = ""
code = '\n'.join(lines)
else:
code = '\n'.join(lines[1:]) if len(lines) > 1 else ""
if not lang_tag:
lang_tag = self.detect_language(code)
# Cache management
if len(self._editor_cache) > self._max_cache_size:
# Simple LRU-ish: just clear it all if it gets too big
self._editor_cache.clear()
self._editor_lang_cache.clear()
cache_key = (context_id, block_idx)
if cache_key not in self._editor_cache:
@@ -121,27 +147,24 @@ class MarkdownRenderer:
editor.set_read_only_enabled(True)
editor.set_show_line_numbers_enabled(True)
self._editor_cache[cache_key] = editor
self._editor_lang_cache[cache_key] = None
editor = self._editor_cache[cache_key]
# Sync text and language
lang_id = self._lang_map.get(lang_tag, ed.TextEditor.LanguageDefinitionId.none)
current_lang = self._editor_lang_cache[cache_key]
# Sync text and language. None means "no language set" (skip the call).
lang_id = self._lang_map.get(lang_tag)
# Robust check to avoid re-setting text every frame (which resets scroll)
curr_text = editor.get_text().replace('\r\n', '\n').strip()
if curr_text != code.replace('\r\n', '\n').strip():
editor.set_text(code)
editor.set_language_definition(lang_id)
elif editor.get_language_definition_name().lower() != lang_tag:
editor.set_language_definition(lang_id)
# Dynamic height calculation
line_count = code.count('\n') + 1
line_height = imgui.get_text_line_height()
height = (line_count * line_height) + 20
height = min(max(height, 40), 500)
editor.render(f"##code_{context_id}_{block_idx}", a_size=imgui.ImVec2(0, height))
if current_lang != lang_tag:
_set_editor_language(editor, lang_id)
self._editor_lang_cache[cache_key] = lang_tag
elif current_lang != lang_tag:
_set_editor_language(editor, lang_id)
self._editor_lang_cache[cache_key] = lang_tag
def _is_likely_lang_tag(self, tag: str) -> bool:
return bool(re.match(r'^[a-zA-Z0-9+#-]+$', tag)) and len(tag) < 15