Working on Formating DSL completion

This commit is contained in:
2026-01-31 19:09:14 +01:00
parent 778e5ac69d
commit d7ec99c3d9
77 changed files with 7563 additions and 63 deletions

View File

@@ -1,7 +1,7 @@
from myfasthtml.controls.VisNetwork import VisNetwork
from myfasthtml.core.commands import CommandsManager
from myfasthtml.core.instances import SingleInstance, InstancesManager
from myfasthtml.core.network_utils import from_parent_child_list
from myfasthtml.core.vis_network_utils import from_parent_child_list
class CommandsDebugger(SingleInstance):

View File

@@ -14,6 +14,7 @@ from myfasthtml.controls.BaseCommands import BaseCommands
from myfasthtml.controls.CycleStateControl import CycleStateControl
from myfasthtml.controls.DataGridColumnsManager import DataGridColumnsManager
from myfasthtml.controls.DataGridQuery import DataGridQuery, DG_QUERY_FILTER
from myfasthtml.controls.DslEditor import DslEditor
from myfasthtml.controls.Mouse import Mouse
from myfasthtml.controls.Panel import Panel, PanelConf
from myfasthtml.controls.datagrid_objects import DataGridColumnState, DataGridRowState, \
@@ -23,6 +24,7 @@ from myfasthtml.core.commands import Command
from myfasthtml.core.constants import ColumnType, ROW_INDEX_ID, FooterAggregation, DATAGRID_PAGE_SIZE, FILTER_INPUT_CID
from myfasthtml.core.dbmanager import DbObject
from myfasthtml.core.formatting.dataclasses import FormatRule, Style, Condition, ConstantFormatter
from myfasthtml.core.formatting.dsl.definition import FormattingDSL
from myfasthtml.core.formatting.engine import FormattingEngine
from myfasthtml.core.instances import MultipleInstance
from myfasthtml.core.optimized_ft import OptimizedDiv
@@ -54,12 +56,13 @@ def _mk_bool_cached(_value):
class DatagridConf:
namespace: Optional[str] = None
name: Optional[str] = None
id: Optional[str] = None
class DatagridState(DbObject):
def __init__(self, owner, save_state):
with self.initializing():
super().__init__(owner, name=f"{owner.get_full_id()}#state", save_state=save_state)
super().__init__(owner, name=f"{owner.get_id()}#state", save_state=save_state)
self.sidebar_visible: bool = False
self.selected_view: str = None
self.row_index: bool = True
@@ -82,7 +85,7 @@ class DatagridState(DbObject):
class DatagridSettings(DbObject):
def __init__(self, owner, save_state, name, namespace):
with self.initializing():
super().__init__(owner, name=f"{owner.get_full_id()}#settings", save_state=save_state)
super().__init__(owner, name=f"{owner.get_id()}#settings", save_state=save_state)
self.save_state = save_state is True
self.namespace: Optional[str] = namespace
self.name: Optional[str] = name
@@ -146,11 +149,18 @@ class Commands(BaseCommands):
def toggle_columns_manager(self):
return Command("ToggleColumnsManager",
"Toggle Columns Manager",
"Hide/Show Columns Manager",
self._owner,
self._owner.toggle_columns_manager
).htmx(target=None)
def toggle_formatting_editor(self):
return Command("ToggleFormattingEditor",
"Hide/Show Formatting Editor",
self._owner,
self._owner.toggle_formatting_editor
).htmx(target=None)
def on_column_changed(self):
return Command("OnColumnChanged",
"Column definition changed",
@@ -170,9 +180,10 @@ class DataGrid(MultipleInstance):
self.init_from_dataframe(self._state.ne_df, init_state=False) # state comes from DatagridState
# add Panel
self._panel = Panel(self, conf=PanelConf(right_title="Columns", show_display_right=False), _id="-panel")
self._panel = Panel(self, conf=PanelConf(show_display_right=False), _id="-panel")
self._panel.set_side_visible("right", False) # the right Panel always starts closed
self.bind_command("ToggleColumnsManager", self._panel.commands.toggle_side("right"))
self.bind_command("ToggleFormattingEditor", self._panel.commands.toggle_side("right"))
# add DataGridQuery
self._datagrid_filter = DataGridQuery(self)
@@ -195,6 +206,8 @@ class DataGrid(MultipleInstance):
self._columns_manager.bind_command("ToggleColumn", self.commands.on_column_changed())
self._columns_manager.bind_command("UpdateColumn", self.commands.on_column_changed())
self._formatting_editor = DslEditor(self, dsl=FormattingDSL())
# other definitions
self._mouse_support = {
"click": {"command": self.commands.on_click(), "hx_vals": "js:getCellId()"},
@@ -254,12 +267,14 @@ class DataGrid(MultipleInstance):
return df
def _get_element_id_from_pos(self, selection_mode, pos):
# pos => (column, row)
if pos is None or pos == (None, None):
return None
elif selection_mode == "row":
return f"trow_{self._id}-{pos[0]}"
return f"trow_{self._id}-{pos[1]}"
elif selection_mode == "column":
return f"tcol_{self._id}-{pos[1]}"
return f"tcol_{self._id}-{pos[0]}"
else:
return f"tcell_{self._id}-{pos[0]}-{pos[1]}"
@@ -388,7 +403,7 @@ class DataGrid(MultipleInstance):
FormatRule(condition=Condition(operator="isnan"), formatter=ConstantFormatter(value="-")),
]
cell_id = self._get_element_id_from_pos("cell", (row_index, col_pos))
cell_id = self._get_element_id_from_pos("cell", (col_pos, row_index))
if cell_id in self._state.cell_formats:
return self._state.cell_formats[cell_id]
@@ -471,14 +486,23 @@ class DataGrid(MultipleInstance):
def toggle_columns_manager(self):
logger.debug(f"toggle_columns_manager")
self._panel.set_title(side="right", title="Columns")
self._panel.set_right(self._columns_manager)
def toggle_formatting_editor(self):
logger.debug(f"toggle_formatting_editor")
self._panel.set_title(side="right", title="Formatting")
self._panel.set_right(self._formatting_editor)
def save_state(self):
self._state.save()
def get_state(self):
return self._state
def get_settings(self):
return self._settings
def mk_headers(self):
resize_cmd = self.commands.set_column_width()
move_cmd = self.commands.move_column()
@@ -590,7 +614,7 @@ class DataGrid(MultipleInstance):
data_col=col_def.col_id,
data_tooltip=str(value),
style=f"width:{col_def.width}px;",
id=self._get_element_id_from_pos("cell", (row_index, col_pos)),
id=self._get_element_id_from_pos("cell", (col_pos, row_index)),
cls="dt2-cell")
def mk_body_content_page(self, page_index: int):
@@ -776,7 +800,12 @@ class DataGrid(MultipleInstance):
Div(self._datagrid_filter,
Div(
self._selection_mode_selector,
mk.icon(settings16_regular, command=self.commands.toggle_columns_manager(), tooltip="Show sidebar"),
mk.icon(settings16_regular,
command=self.commands.toggle_columns_manager(),
tooltip="Show column manager"),
mk.icon(settings16_regular,
command=self.commands.toggle_formatting_editor(),
tooltip="Show formatting editor"),
cls="flex"),
cls="flex items-center justify-between mb-2"),
self._panel.set_main(self.mk_table_wrapper()),
@@ -814,5 +843,17 @@ class DataGrid(MultipleInstance):
return tuple(res)
def dispose(self):
pass
def delete(self):
"""
remove DBEngine entries
:return:
"""
# self._state.delete()
# self._settings.delete()
pass
def __ft__(self):
return self.render()

View File

@@ -137,17 +137,29 @@ class DataGridColumnsManager(MultipleInstance):
value=col_def.col_id,
readonly=True),
Div(
Div(
Label("Visible"),
Input(name="visible",
type="checkbox",
cls=f"checkbox checkbox-{size}",
checked="true" if col_def.visible else None),
),
Div(
Label("Width"),
Input(name="width",
type="number",
cls=f"input input-{size}",
value=col_def.width),
),
cls="flex",
),
Label("Title"),
Input(name="title",
cls=f"input input-{size}",
value=col_def.title),
Label("Visible"),
Input(name="visible",
type="checkbox",
cls=f"checkbox checkbox-{size}",
checked="true" if col_def.visible else None),
Label("type"),
Select(
*[Option(option.value, value=option.value, selected=option == col_def.type) for option in ColumnType],
@@ -156,12 +168,6 @@ class DataGridColumnsManager(MultipleInstance):
value=col_def.title,
),
Label("Width"),
Input(name="width",
type="number",
cls=f"input input-{size}",
value=col_def.width),
legend="Column details",
cls="fieldset border-base-300 rounded-box"
),

View File

@@ -11,10 +11,11 @@ from myfasthtml.controls.FileUpload import FileUpload
from myfasthtml.controls.TabsManager import TabsManager
from myfasthtml.controls.TreeView import TreeView, TreeNode
from myfasthtml.controls.helpers import mk
from myfasthtml.core.DataGridsRegistry import DataGridsRegistry
from myfasthtml.core.commands import Command
from myfasthtml.core.dbmanager import DbObject
from myfasthtml.core.formatting.presets import DEFAULT_STYLE_PRESETS, DEFAULT_FORMATTER_PRESETS
from myfasthtml.core.instances import MultipleInstance, InstancesManager
from myfasthtml.core.instances import InstancesManager, SingleInstance
from myfasthtml.icons.fluent_p1 import table_add20_regular
from myfasthtml.icons.fluent_p3 import folder_open20_regular
@@ -71,7 +72,7 @@ class Commands(BaseCommands):
key="SelectNode")
class DataGridsManager(MultipleInstance):
class DataGridsManager(SingleInstance):
def __init__(self, parent, _id=None):
if not getattr(self, "_is_new_instance", False):
@@ -83,6 +84,7 @@ class DataGridsManager(MultipleInstance):
self._tree = self._mk_tree()
self._tree.bind_command("SelectNode", self.commands.show_document())
self._tabs_manager = InstancesManager.get_by_type(self._session, TabsManager)
self._registry = DataGridsRegistry(parent)
# Global presets shared across all DataGrids
self.style_presets: dict = DEFAULT_STYLE_PRESETS.copy()

View File

@@ -0,0 +1,196 @@
"""
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.dsl.base import DSLDefinition
from myfasthtml.core.instances import MultipleInstance
logger = logging.getLogger("DslEditor")
@dataclass
class DslEditorConf:
"""Configuration for DslEditor."""
line_numbers: bool = True
autocompletion: bool = True
placeholder: str = ""
readonly: bool = False
class DslEditorState:
"""Non-persisted state for DslEditor."""
def __init__(self):
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 on_content_changed(self):
return Command("OnContentChanged",
"On content changed",
self._owner,
self._owner.on_content_changed
).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,
_id: Optional[str] = None,
):
super().__init__(parent, _id=_id)
self._dsl = dsl
self.conf = conf or DslEditorConf()
self._state = DslEditorState()
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 = "") -> None:
"""Handler for content update from CodeMirror."""
self._state.content = content
if self._state.auto_save:
self.on_content_changed()
logger.debug(f"Content updated: {len(content)} chars")
def toggle_auto_save(self) -> None:
self._state.auto_save = not self._state.auto_save
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,
"placeholder": self.conf.placeholder,
"readonly": self.conf.readonly,
"updateCommandId": str(self.commands.update_content().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.update_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()

View File

@@ -3,7 +3,7 @@ from myfasthtml.controls.Properties import Properties
from myfasthtml.controls.VisNetwork import VisNetwork
from myfasthtml.core.commands import Command
from myfasthtml.core.instances import SingleInstance, InstancesManager
from myfasthtml.core.network_utils import from_parent_child_list
from myfasthtml.core.vis_network_utils import from_parent_child_list
class InstancesDebugger(SingleInstance):

View File

@@ -147,6 +147,14 @@ class Panel(MultipleInstance):
self._left = left
return Div(self._left, id=self._ids.left)
def set_title(self, side, title):
if side == "left":
self.conf.left_title = title
else:
self.conf.right_title = title
return self._mk_panel(side)
def _mk_panel(self, side: Literal["left", "right"]):
enabled = self.conf.left if side == "left" else self.conf.right
if not enabled:

View File

@@ -19,7 +19,7 @@ class DataGridColumnState:
type: ColumnType = ColumnType.Text
visible: bool = True
width: int = DATAGRID_DEFAULT_COLUMN_WIDTH
format: list = field(default_factory=list) #
format: list = field(default_factory=list) #
@dataclass
@@ -30,7 +30,7 @@ class DatagridEditionState:
@dataclass
class DatagridSelectionState:
selected: tuple[int, int] | None = None
selected: tuple[int, int] | None = None # column first, then row
last_selected: tuple[int, int] | None = None
selection_mode: str = None # valid values are "row", "column" or None for "cell"
extra_selected: list[tuple[str, str | int]] = field(default_factory=list) # list(tuple(selection_mode, element_id))