fixes
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
# Track markdown_helper_language_api_compat_20260603 Context
|
||||
|
||||
- [Specification](./spec.md)
|
||||
- [Implementation Plan](./plan.md)
|
||||
- [Metadata](./metadata.json)
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"id": "markdown_helper_language_api_compat_20260603",
|
||||
"title": "Fix markdown_helper.py for imgui-bundle >=1.92.801",
|
||||
"phase": null,
|
||||
"created": "2026-06-03",
|
||||
"status": "in_progress",
|
||||
"spec_file": "spec.md",
|
||||
"plan_file": "plan.md",
|
||||
"depends_on": ["clean_install_test_20260603"],
|
||||
"completion_checkpoints": []
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
# Implementation Plan: Fix markdown_helper.py for imgui-bundle >=1.92.801
|
||||
|
||||
## Phase 1: Red - Confirm bug
|
||||
- [x] Task 1.1: Run `tests/test_clean_install.py` in opt-in mode (RUN_CLEAN_INSTALL_TEST=1) - confirm AttributeError on TextEditor.LanguageDefinitionId
|
||||
- [x] Task 1.2: Capture full traceback for git note
|
||||
|
||||
## Phase 2: Green - Apply version-compat shim
|
||||
- [ ] Task 2.1: Add module-level `_get_language_id(name)` helper to markdown_helper.py
|
||||
- [ ] Task 2.2: Add module-level `_set_editor_language(editor, lang_obj)` helper
|
||||
- [ ] Task 2.3: Add module-level `_get_editor_language_name(editor)` helper
|
||||
- [ ] Task 2.4: Replace `_lang_map` initialization to use `_get_language_id(...)`
|
||||
- [ ] Task 2.5: Replace lines 128, 134, 135, 136 with shim calls
|
||||
- [ ] Task 2.6: Handle the "none" fallback case (return None, skip set call)
|
||||
- [ ] Task 2.7: Syntax check (ast.parse)
|
||||
|
||||
## Phase 3: Verify
|
||||
- [ ] Task 3.1: Run `tests/test_clean_install.py` in opt-in mode - should pass (1 passed, clone+sync+launch+hook API)
|
||||
- [ ] Task 3.2: Run local markdown-related tests (if any) to ensure no regression in 1.92.5 env
|
||||
- [ ] Task 3.3: Import test for the new helpers (both APIs should work)
|
||||
|
||||
## Phase 4: Commit
|
||||
- [ ] Task 4.1: Atomic commit with descriptive message
|
||||
- [ ] Task 4.2: Git note with root cause analysis
|
||||
- [ ] Task 4.3: Update tracks.md to register this fix track
|
||||
@@ -0,0 +1,30 @@
|
||||
# Fix markdown_helper.py for imgui-bundle >=1.92.801
|
||||
|
||||
## Bug
|
||||
|
||||
`src/markdown_helper.py` uses `ed.TextEditor.LanguageDefinitionId.<lang>` enum and `editor.set_language_definition(enum)` calls. These were removed in `imgui-bundle>=1.92.801`. Replacement: `ed.TextEditor.Language.<lang>()` factory functions and `editor.set_language(obj)` method.
|
||||
|
||||
The bug surfaces only on clean installs (where `uv sync` resolves the latest `imgui-bundle`). The local dev environment has 1.92.5 pinned, masking the issue. The `clean_install_test_20260603` opt-in test caught this on first run.
|
||||
|
||||
## Affected Code
|
||||
|
||||
- `src/markdown_helper.py:37-48` — `_lang_map` initialization with enum values
|
||||
- `src/markdown_helper.py:128` — `ed.TextEditor.LanguageDefinitionId.none` fallback
|
||||
- `src/markdown_helper.py:134` — `editor.set_language_definition(lang_id)` call
|
||||
- `src/markdown_helper.py:135` — `editor.get_language_definition_name()` getter
|
||||
- `src/markdown_helper.py:136` — `editor.set_language_definition(lang_id)` re-set
|
||||
|
||||
## Fix Strategy
|
||||
|
||||
Version-compat shim: detect which API is available at runtime and dispatch to the right one. This is safer than pinning `imgui-bundle` (avoids forcing the dev env to upgrade) and safer than hard-coding the new API (would break the 1.92.5 dev env).
|
||||
|
||||
The shim:
|
||||
- Tries `TextEditor.Language.<name>()` first (1.92.801+)
|
||||
- Falls back to `TextEditor.LanguageDefinitionId.<name>` (1.92.5)
|
||||
- Returns `None` for "no language" (handled by not calling set_language)
|
||||
- Provides `_set_editor_language(editor, lang_obj)` that dispatches to the right method
|
||||
|
||||
## Files Touched
|
||||
|
||||
- `src/markdown_helper.py` — add shim helpers, replace enum references
|
||||
- (Optional) `pyproject.toml` — add `imgui-bundle>=1.92.5,<1.93` constraint to prevent future major version drift
|
||||
+15
-15
@@ -44,20 +44,20 @@ Collapsed=0
|
||||
DockId=0x00000010,0
|
||||
|
||||
[Window][Message]
|
||||
Pos=1125,28
|
||||
Size=1616,1842
|
||||
Pos=64,28
|
||||
Size=1616,1172
|
||||
Collapsed=0
|
||||
DockId=0x00000006,0
|
||||
|
||||
[Window][Response]
|
||||
Pos=0,28
|
||||
Size=1123,1842
|
||||
Size=62,1172
|
||||
Collapsed=0
|
||||
DockId=0x00000010,4
|
||||
|
||||
[Window][Tool Calls]
|
||||
Pos=1125,28
|
||||
Size=1616,1842
|
||||
Pos=64,28
|
||||
Size=1616,1172
|
||||
Collapsed=0
|
||||
DockId=0x00000006,3
|
||||
|
||||
@@ -77,7 +77,7 @@ DockId=0xAFC85805,2
|
||||
|
||||
[Window][Theme]
|
||||
Pos=0,28
|
||||
Size=1123,1842
|
||||
Size=62,1172
|
||||
Collapsed=0
|
||||
DockId=0x00000010,0
|
||||
|
||||
@@ -105,26 +105,26 @@ Collapsed=0
|
||||
DockId=0x0000000D,0
|
||||
|
||||
[Window][Discussion Hub]
|
||||
Pos=1125,28
|
||||
Size=1616,1842
|
||||
Pos=64,28
|
||||
Size=1616,1172
|
||||
Collapsed=0
|
||||
DockId=0x00000006,1
|
||||
|
||||
[Window][Operations Hub]
|
||||
Pos=0,28
|
||||
Size=1123,1842
|
||||
Size=62,1172
|
||||
Collapsed=0
|
||||
DockId=0x00000010,3
|
||||
|
||||
[Window][Files & Media]
|
||||
Pos=0,28
|
||||
Size=1123,1842
|
||||
Size=62,1172
|
||||
Collapsed=0
|
||||
DockId=0x00000010,2
|
||||
|
||||
[Window][AI Settings]
|
||||
Pos=0,28
|
||||
Size=1123,1842
|
||||
Size=62,1172
|
||||
Collapsed=0
|
||||
DockId=0x00000010,1
|
||||
|
||||
@@ -140,8 +140,8 @@ Collapsed=0
|
||||
DockId=0x00000006,2
|
||||
|
||||
[Window][Log Management]
|
||||
Pos=1125,28
|
||||
Size=1616,1842
|
||||
Pos=64,28
|
||||
Size=1616,1172
|
||||
Collapsed=0
|
||||
DockId=0x00000006,2
|
||||
|
||||
@@ -697,13 +697,13 @@ Column 1 Weight=1.0000
|
||||
DockNode ID=0x00000008 Pos=3125,170 Size=593,1157 Split=Y
|
||||
DockNode ID=0x00000009 Parent=0x00000008 SizeRef=1029,147 Selected=0x0469CA7A
|
||||
DockNode ID=0x0000000A Parent=0x00000008 SizeRef=1029,145 Selected=0xDF822E02
|
||||
DockSpace ID=0xAFC85805 Window=0x079D3A04 Pos=0,28 Size=2741,1842 Split=X
|
||||
DockSpace ID=0xAFC85805 Window=0x079D3A04 Pos=0,28 Size=1680,1172 Split=X
|
||||
DockNode ID=0x00000003 Parent=0xAFC85805 SizeRef=2357,1183 Split=X
|
||||
DockNode ID=0x0000000B Parent=0x00000003 SizeRef=404,1186 Split=X Selected=0xF4139CA2
|
||||
DockNode ID=0x00000005 Parent=0x0000000B SizeRef=1221,1681 Split=Y Selected=0x3F1379AF
|
||||
DockNode ID=0x00000010 Parent=0x00000005 SizeRef=983,1140 CentralNode=1 Selected=0x8CA2375C
|
||||
DockNode ID=0x00000011 Parent=0x00000005 SizeRef=983,184 Selected=0x432BAE4E
|
||||
DockNode ID=0x00000006 Parent=0x0000000B SizeRef=1616,1681 Selected=0x6F2B5B04
|
||||
DockNode ID=0x00000006 Parent=0x0000000B SizeRef=1616,1681 Selected=0x2C0206CE
|
||||
DockNode ID=0x0000000D Parent=0x00000003 SizeRef=435,1186 Selected=0x363E93D6
|
||||
DockNode ID=0x00000004 Parent=0xAFC85805 SizeRef=488,1183 Selected=0x3AEC3498
|
||||
|
||||
|
||||
+68
-45
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user