Source code for simvx.editor.panels.code_tab

"""Code editor tab integration for the editor's center tab area.

Wraps CodeEditorPanel from simvx.core.ui with optional LSP integration
and file lifecycle management. Multiple files are managed as sub-tabs
within the CodeEditorPanel.
"""

import logging
import shutil
from pathlib import Path
from typing import TYPE_CHECKING

from simvx.core import Signal
from simvx.core.ui.code_editor_panel import CodeEditorPanel
from simvx.core.ui.core import Control

if TYPE_CHECKING:
    from simvx.editor.state import State

log = logging.getLogger(__name__)

[docs] class CodeEditorTab(Control): """Wraps CodeEditorPanel for the editor's center tab area. Manages multiple open files as sub-tabs, LSP integration, and file save/load lifecycle. Usage:: tab = CodeEditorTab(editor_state=state) tab.open_file("/path/to/script.py") tab.save_current() """ def __init__(self, editor_state: State | None = None, **kwargs): super().__init__(**kwargs) if not self.name or self.name == self.__class__.__name__: self.name = "CodeTab" self._editor_state = editor_state self._lsp_client = None self._lsp_started = False # Signals self.file_opened = Signal() self.file_saved = Signal() self.file_closed = Signal() # Create the code editor panel self._panel = CodeEditorPanel(name="CodePanel") self.add_child(self._panel) # Wire panel signals to our signals and editor state self._panel.file_opened.connect(self._on_file_opened) self._panel.file_saved.connect(self._on_file_saved) self._panel.file_closed.connect(self._on_file_closed) self._panel.active_file_changed.connect(self._on_active_file_changed) # Wire to editor state file signals if available if self._editor_state: self._panel.file_opened.connect(self._editor_state.file_opened.emit) self._panel.file_saved.connect(self._editor_state.file_saved.emit) self._panel.file_closed.connect(self._editor_state.file_closed.emit) self._panel.active_file_changed.connect(self._editor_state.active_file_changed.emit) # ------------------------------------------------------------------ # Public API # ------------------------------------------------------------------
[docs] @property def panel(self) -> CodeEditorPanel: """The underlying CodeEditorPanel instance.""" return self._panel
[docs] def open_file(self, path: str): """Open a Python file in the code editor. If the file is already open, switches to its tab. """ self._panel.open_file(path)
[docs] def save_current(self): """Save the current file. Writes the buffer to disk verbatim. Scene-shape edits made via the scene panel are reconciled with the source by the editor save path (``simvx.editor.scene_diff.apply_runtime_diff``); the code tab itself deliberately does not regenerate the file -- hand edits in this view must round-trip byte-identically. """ path = self._panel.current_path if not path: return self._panel.save_current()
[docs] def save_all(self): """Save all modified files.""" self._panel.save_all()
[docs] def close_current(self): """Close the current file tab.""" self._panel.close_current()
[docs] @property def open_paths(self) -> list[str]: """Paths of all open files.""" return self._panel.tab_paths
[docs] @property def current_path(self) -> str | None: """Path of the currently active file.""" return self._panel.current_path
# ------------------------------------------------------------------ # LSP integration # ------------------------------------------------------------------
[docs] def start_lsp(self): """Start the LSP client for Python completions. Tries pylsp first, then pyright. No-op if neither is available or if the LSP client is already running. """ if self._lsp_started: return self._lsp_started = True command = self._find_lsp_server() if not command: log.info("No LSP server found (pylsp/pyright) -- code completions disabled") return try: from simvx.ide.lsp.client import LSPClient from simvx.ide.state import State as IDEState # Create a minimal IDE state for the LSP client ide_state = IDEState( file_opened=self.file_opened, file_closed=self.file_closed, file_saved=self.file_saved, ) if self._editor_state and self._editor_state.project_path: ide_state.project_root = str(self._editor_state.project_path) self._lsp_client = LSPClient(ide_state, command=command) self._lsp_client.start() self._panel.set_lsp_client(self._lsp_client) log.info("LSP client started with %s", command) except ImportError: log.debug("simvx.ide not available -- LSP disabled") except Exception: log.exception("Failed to start LSP client")
[docs] def stop_lsp(self): """Stop the LSP client if running.""" if self._lsp_client: self._lsp_client.stop() self._lsp_client = None self._lsp_started = False self._panel.set_lsp_client(None)
[docs] def poll_lsp(self, dt: float): """Poll the LSP client for messages. Call once per frame.""" if self._lsp_client: self._lsp_client.poll()
# ------------------------------------------------------------------ # Layout # ------------------------------------------------------------------ def _update_layout(self): """Size the code panel to fill this control.""" if self._panel and self.size is not None: self._panel.size = self.size self._panel.position = type(self._panel.position)(0, 0)
[docs] def process(self, dt: float): self._update_layout() self.poll_lsp(dt)
# ------------------------------------------------------------------ # Internal # ------------------------------------------------------------------ def _on_file_opened(self, path: str): self.file_opened.emit(path) def _on_file_saved(self, path: str): self.file_saved.emit(path) def _on_file_closed(self, path: str): self.file_closed.emit(path) def _on_active_file_changed(self, path: str): pass # Could update status bar or breadcrumb in future @staticmethod def _find_lsp_server() -> str | None: """Find an available LSP server binary.""" for cmd in ("pylsp", "pyright-langserver"): if shutil.which(cmd): return cmd return None