""" DslEditor control - A CodeMirror wrapper for DSL editing. Provides syntax highlighting, line numbers, and autocompletion for domain-specific languages defined with Lark grammars. """ import json import logging from dataclasses import dataclass from typing import Optional from fasthtml.common import Script from fasthtml.components import * from myfasthtml.controls.BaseCommands import BaseCommands from myfasthtml.controls.helpers import mk from myfasthtml.core.commands import Command from myfasthtml.core.dbmanager import DbObject from myfasthtml.core.dsl.base import DSLDefinition from myfasthtml.core.instances import MultipleInstance logger = logging.getLogger("DslEditor") @dataclass class DslEditorConf: """Configuration for DslEditor.""" name: str = None line_numbers: bool = True autocompletion: bool = True linting: bool = True placeholder: str = "" readonly: bool = False class DslEditorState(DbObject): """Non-persisted state for DslEditor.""" def __init__(self, owner, name, save_state): with self.initializing(): super().__init__(owner, name=name, save_state=save_state) self.content: str = "" self.auto_save: bool = True class Commands(BaseCommands): """Commands for DslEditor interactions.""" def update_content(self): """Command to update content from CodeMirror.""" return Command( "UpdateContent", "Update editor content", self._owner, self._owner.update_content, ).htmx(target=f"#{self._id}", swap="none") def toggle_auto_save(self): return Command("ToggleAutoSave", "Toggle auto save", self._owner, self._owner.toggle_auto_save).htmx(target=f"#as_{self._id}", trigger="click") def save_content(self): return Command("SaveContent", "Save content", self._owner, self._owner.save_content ).htmx(target=None) class DslEditor(MultipleInstance): """ CodeMirror wrapper for editing DSL code. Provides: - Syntax highlighting based on DSL grammar - Line numbers - Autocompletion from grammar keywords/operators Args: parent: Parent instance. dsl: DSL definition providing grammar and completions. conf: Editor configuration. _id: Optional custom ID. """ def __init__( self, parent, dsl: DSLDefinition, conf: Optional[DslEditorConf] = None, save_state: bool = True, _id: Optional[str] = None, ): super().__init__(parent, _id=_id) self._dsl = dsl self.conf = conf or DslEditorConf() self._state = DslEditorState(self, name=conf.name, save_state=save_state) self.commands = Commands(self) logger.debug(f"DslEditor created with id={self._id}, dsl={dsl.name}") def set_content(self, content: str): """Set the editor content programmatically.""" self._state.content = content return self def get_content(self) -> str: """Get the current editor content.""" return self._state.content def update_content(self, content: str = ""): """Handler for content update from CodeMirror.""" self._state.content = content logger.debug(f"Content updated: {len(content)} chars") if self._state.auto_save: return None, self.on_content_changed() # on_content_changed must be second to benefit from oob swap return None def save_content(self): logger.debug("save_content") return None, self.on_content_changed() # on_content_changed must be second to benefit from oob swap def toggle_auto_save(self): logger.debug("toggle_auto_save") self._state.auto_save = not self._state.auto_save logger.debug(f" auto_save={self._state.auto_save}") return self._mk_auto_save() def on_content_changed(self) -> None: pass def _get_editor_config(self) -> dict: """Build the JavaScript configuration object.""" config = { "elementId": str(self._id), "textareaId": f"ta_{self._id}", "lineNumbers": self.conf.line_numbers, "autocompletion": self.conf.autocompletion, "linting": self.conf.linting, "placeholder": self.conf.placeholder, "readonly": self.conf.readonly, "updateCommandId": str(self.commands.update_content().id), "dslId": self._dsl.get_id(), "dsl": { "name": self._dsl.name, "completions": self._dsl.completions, }, } return config def _mk_textarea(self): """Create the hidden textarea for form submission.""" return Textarea( self._state.content, id=f"ta_{self._id}", name=f"ta_{self._id}", cls="hidden", ) def _mk_editor_container(self): """Create the container where CodeMirror will be mounted.""" return Div( id=f"cm_{self._id}", cls="mf-dsl-editor", ) def _mk_init_script(self): """Create the initialization script.""" config = self._get_editor_config() config_json = json.dumps(config) return Script(f"initDslEditor({config_json});") def _mk_auto_save(self): return Div( Label( mk.mk( Input(type="checkbox", checked="on" if self._state.auto_save else None, cls="toggle toggle-xs"), command=self.commands.toggle_auto_save() ), "Auto Save", cls="text-xs", ), mk.button("Save", cls="btn btn-xs btn-primary", disabled="disabled" if self._state.auto_save else None, command=self.commands.save_content()), cls="flex justify-between items-center p-2", id=f"as_{self._id}", ), def render(self): """Render the DslEditor component.""" return Div( self._mk_auto_save(), self._mk_textarea(), self._mk_editor_container(), self._mk_init_script(), id=self._id, cls="mf-dsl-editor-wrapper", ) def __ft__(self): """FastHTML magic method for rendering.""" return self.render()