I can validate formatting in editor

This commit is contained in:
2026-02-01 21:49:46 +01:00
parent d7ec99c3d9
commit 0620cb678b
23 changed files with 794 additions and 501 deletions

View File

@@ -2,10 +2,13 @@
```
cd src/myfasthtml/assets
# codemirror version 5 . Attenntion the version number is the url is misleading !
# Url to get codemirror resources : https://cdnjs.com/libraries/codemirror
wget https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.20/codemirror.min.js
wget https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.20/codemirror.min.css
wget https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.20/addon/hint/show-hint.min.js
wget https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.20/addon/hint/show-hint.min.css
wget https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.20/addon/display/placeholder.min.js
wget https://cdnjs.cloudflare.com/ajax/libs/codemirror/6.65.7/addon/lint/lint.min.css
wget https://cdnjs.cloudflare.com/ajax/libs/codemirror/6.65.7/addon/lint/lint.min.js
```

1
src/myfasthtml/assets/lint.min.css vendored Normal file
View File

@@ -0,0 +1 @@
.CodeMirror-lint-markers{width:16px}.CodeMirror-lint-tooltip{background-color:#ffd;border:1px solid #000;border-radius:4px 4px 4px 4px;color:#000;font-family:monospace;font-size:10pt;overflow:hidden;padding:2px 5px;position:fixed;white-space:pre;white-space:pre-wrap;z-index:100;max-width:600px;opacity:0;transition:opacity .4s;-moz-transition:opacity .4s;-webkit-transition:opacity .4s;-o-transition:opacity .4s;-ms-transition:opacity .4s}.CodeMirror-lint-mark{background-position:left bottom;background-repeat:repeat-x}.CodeMirror-lint-mark-warning{background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAQAAAADCAYAAAC09K7GAAAAAXNSR0IArs4c6QAAAAZiS0dEAP8A/wD/oL2nkwAAAAlwSFlzAAALEwAACxMBAJqcGAAAAAd0SU1FB9sJFhQXEbhTg7YAAAAZdEVYdENvbW1lbnQAQ3JlYXRlZCB3aXRoIEdJTVBXgQ4XAAAAMklEQVQI12NkgIIvJ3QXMjAwdDN+OaEbysDA4MPAwNDNwMCwiOHLCd1zX07o6kBVGQEAKBANtobskNMAAAAASUVORK5CYII=)}.CodeMirror-lint-mark-error{background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAQAAAADCAYAAAC09K7GAAAAAXNSR0IArs4c6QAAAAZiS0dEAP8A/wD/oL2nkwAAAAlwSFlzAAALEwAACxMBAJqcGAAAAAd0SU1FB9sJDw4cOCW1/KIAAAAZdEVYdENvbW1lbnQAQ3JlYXRlZCB3aXRoIEdJTVBXgQ4XAAAAHElEQVQI12NggIL/DAz/GdA5/xkY/qPKMDAwAADLZwf5rvm+LQAAAABJRU5ErkJggg==)}.CodeMirror-lint-marker{background-position:center center;background-repeat:no-repeat;cursor:pointer;display:inline-block;height:16px;width:16px;vertical-align:middle;position:relative}.CodeMirror-lint-message{padding-left:18px;background-position:top left;background-repeat:no-repeat}.CodeMirror-lint-marker-warning,.CodeMirror-lint-message-warning{background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAAANlBMVEX/uwDvrwD/uwD/uwD/uwD/uwD/uwD/uwD/uwD6twD/uwAAAADurwD2tQD7uAD+ugAAAAD/uwDhmeTRAAAADHRSTlMJ8mN1EYcbmiixgACm7WbuAAAAVklEQVR42n3PUQqAIBBFUU1LLc3u/jdbOJoW1P08DA9Gba8+YWJ6gNJoNYIBzAA2chBth5kLmG9YUoG0NHAUwFXwO9LuBQL1giCQb8gC9Oro2vp5rncCIY8L8uEx5ZkAAAAASUVORK5CYII=)}.CodeMirror-lint-marker-error,.CodeMirror-lint-message-error{background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAAAHlBMVEW7AAC7AACxAAC7AAC7AAAAAAC4AAC5AAD///+7AAAUdclpAAAABnRSTlMXnORSiwCK0ZKSAAAATUlEQVR42mWPOQ7AQAgDuQLx/z8csYRmPRIFIwRGnosRrpamvkKi0FTIiMASR3hhKW+hAN6/tIWhu9PDWiTGNEkTtIOucA5Oyr9ckPgAWm0GPBog6v4AAAAASUVORK5CYII=)}.CodeMirror-lint-marker-multiple{background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAcAAAAHCAMAAADzjKfhAAAACVBMVEUAAAAAAAC/v7914kyHAAAAAXRSTlMAQObYZgAAACNJREFUeNo1ioEJAAAIwmz/H90iFFSGJgFMe3gaLZ0od+9/AQZ0ADosbYraAAAAAElFTkSuQmCC);background-repeat:no-repeat;background-position:right bottom;width:100%;height:100%}.CodeMirror-lint-line-error{background-color:rgba(183,76,81,.08)}.CodeMirror-lint-line-warning{background-color:rgba(255,211,0,.1)}

1
src/myfasthtml/assets/lint.min.js vendored Normal file
View File

@@ -0,0 +1 @@
!function(t){"object"==typeof exports&&"object"==typeof module?t(require("../../lib/codemirror")):"function"==typeof define&&define.amd?define(["../../lib/codemirror"],t):t(CodeMirror)}(function(h){"use strict";var g="CodeMirror-lint-markers",v="CodeMirror-lint-line-";function u(t){t.parentNode&&t.parentNode.removeChild(t)}function C(t,e,n,o){t=t,e=e,n=n,(i=document.createElement("div")).className="CodeMirror-lint-tooltip cm-s-"+t.options.theme,i.appendChild(n.cloneNode(!0)),(t.state.lint.options.selfContain?t.getWrapperElement():document.body).appendChild(i),h.on(document,"mousemove",a),a(e),null!=i.style.opacity&&(i.style.opacity=1);var i,r=i;function a(t){if(!i.parentNode)return h.off(document,"mousemove",a);i.style.top=Math.max(0,t.clientY-i.offsetHeight-5)+"px",i.style.left=t.clientX+5+"px"}function s(){var t;h.off(o,"mouseout",s),r&&((t=r).parentNode&&(null==t.style.opacity&&u(t),t.style.opacity=0,setTimeout(function(){u(t)},600)),r=null)}var l=setInterval(function(){if(r)for(var t=o;;t=t.parentNode){if((t=t&&11==t.nodeType?t.host:t)==document.body)return;if(!t){s();break}}if(!r)return clearInterval(l)},400);h.on(o,"mouseout",s)}function a(l,t,e){for(var n in this.marked=[],(t=t instanceof Function?{getAnnotations:t}:t)&&!0!==t||(t={}),this.options={},this.linterOptions=t.options||{},o)this.options[n]=o[n];for(var n in t)o.hasOwnProperty(n)?null!=t[n]&&(this.options[n]=t[n]):t.options||(this.linterOptions[n]=t[n]);this.timeout=null,this.hasGutter=e,this.onMouseOver=function(t){var e=l,n=t.target||t.srcElement;if(/\bCodeMirror-lint-mark-/.test(n.className)){for(var n=n.getBoundingClientRect(),o=(n.left+n.right)/2,n=(n.top+n.bottom)/2,i=e.findMarksAt(e.coordsChar({left:o,top:n},"client")),r=[],a=0;a<i.length;++a){var s=i[a].__annotation;s&&r.push(s)}r.length&&!function(t,e,n){for(var o=n.target||n.srcElement,i=document.createDocumentFragment(),r=0;r<e.length;r++){var a=e[r];i.appendChild(M(a))}C(t,n,i,o)}(e,r,t)}},this.waitingFor=0}var o={highlightLines:!1,tooltips:!0,delay:500,lintOnChange:!0,getAnnotations:null,async:!1,selfContain:null,formatAnnotation:null,onUpdateLinting:null};function y(t){var n,e=t.state.lint;e.hasGutter&&t.clearGutter(g),e.options.highlightLines&&(n=t).eachLine(function(t){var e=t.wrapClass&&/\bCodeMirror-lint-line-\w+\b/.exec(t.wrapClass);e&&n.removeLineClass(t,"wrap",e[0])});for(var o=0;o<e.marked.length;++o)e.marked[o].clear();e.marked.length=0}function M(t){var e=(e=t.severity)||"error",n=document.createElement("div");return n.className="CodeMirror-lint-message CodeMirror-lint-message-"+e,void 0!==t.messageHTML?n.innerHTML=t.messageHTML:n.appendChild(document.createTextNode(t.message)),n}function s(e){var t,n,o,i,r,a,s=e.state.lint;function l(){a=-1,o.off("change",l)}!s||(t=(i=s.options).getAnnotations||e.getHelper(h.Pos(0,0),"lint"))&&(i.async||t.async?(i=t,r=(o=e).state.lint,a=++r.waitingFor,o.on("change",l),i(o.getValue(),function(t,e){o.off("change",l),r.waitingFor==a&&(e&&t instanceof h&&(t=e),o.operation(function(){c(o,t)}))},r.linterOptions,o)):(n=t(e.getValue(),s.linterOptions,e))&&(n.then?n.then(function(t){e.operation(function(){c(e,t)})}):e.operation(function(){c(e,n)})))}function c(t,e){var n=t.state.lint;if(n){for(var o,i,r=n.options,a=(y(t),function(t){for(var e=[],n=0;n<t.length;++n){var o=t[n],i=o.from.line;(e[i]||(e[i]=[])).push(o)}return e}(e)),s=0;s<a.length;++s)if(u=a[s]){for(var l=[],u=u.filter(function(t){return!(-1<l.indexOf(t.message))&&l.push(t.message)}),c=null,f=n.hasGutter&&document.createDocumentFragment(),m=0;m<u.length;++m){var p=u[m],d=p.severity;i=d=d||"error",c="error"==(o=c)?o:i,r.formatAnnotation&&(p=r.formatAnnotation(p)),n.hasGutter&&f.appendChild(M(p)),p.to&&n.marked.push(t.markText(p.from,p.to,{className:"CodeMirror-lint-mark CodeMirror-lint-mark-"+d,__annotation:p}))}n.hasGutter&&t.setGutterMarker(s,g,function(e,n,t,o,i){var r=document.createElement("div"),a=r;return r.className="CodeMirror-lint-marker CodeMirror-lint-marker-"+t,o&&((a=r.appendChild(document.createElement("div"))).className="CodeMirror-lint-marker CodeMirror-lint-marker-multiple"),0!=i&&h.on(a,"mouseover",function(t){C(e,t,n,a)}),r}(t,f,c,1<a[s].length,r.tooltips)),r.highlightLines&&t.addLineClass(s,"wrap",v+c)}r.onUpdateLinting&&r.onUpdateLinting(e,a,t)}}function l(t){var e=t.state.lint;e&&(clearTimeout(e.timeout),e.timeout=setTimeout(function(){s(t)},e.options.delay))}h.defineOption("lint",!1,function(t,e,n){if(n&&n!=h.Init&&(y(t),!1!==t.state.lint.options.lintOnChange&&t.off("change",l),h.off(t.getWrapperElement(),"mouseover",t.state.lint.onMouseOver),clearTimeout(t.state.lint.timeout),delete t.state.lint),e){for(var o=t.getOption("gutters"),i=!1,r=0;r<o.length;++r)o[r]==g&&(i=!0);n=t.state.lint=new a(t,e,i);n.options.lintOnChange&&t.on("change",l),0!=n.options.tooltips&&"gutter"!=n.options.tooltips&&h.on(t.getWrapperElement(),"mouseover",n.onMouseOver),s(t)}}),h.defineExtension("performLint",function(){s(this)})});

View File

@@ -2141,9 +2141,11 @@ function initDslEditor(config) {
textareaId,
lineNumbers,
autocompletion,
linting,
placeholder,
readonly,
updateCommandId,
dslId,
dsl
} = config;
@@ -2162,68 +2164,146 @@ function initDslEditor(config) {
}
/* --------------------------------------------------
* Build completion list from DSL config
* DSL autocompletion hint (async via server)
* -------------------------------------------------- */
const completionItems = [];
// Characters that trigger auto-completion
const AUTO_TRIGGER_CHARS = [".", "(", '"', " "];
if (dsl && dsl.completions) {
const pushAll = (items) => {
if (!Array.isArray(items)) return;
items.forEach(item => completionItems.push(item));
};
function dslHint(cm, callback) {
const cursor = cm.getCursor();
const text = cm.getValue();
pushAll(dsl.completions.keywords);
pushAll(dsl.completions.operators);
pushAll(dsl.completions.functions);
pushAll(dsl.completions.types);
pushAll(dsl.completions.literals);
// Build URL with query params
const params = new URLSearchParams({
e_id: dslId,
text: text,
line: cursor.line,
ch: cursor.ch
});
fetch(`/myfasthtml/completions?${params}`)
.then(response => response.json())
.then(data => {
if (!data || !data.suggestions || data.suggestions.length === 0) {
callback(null);
return;
}
callback({
list: data.suggestions.map(s => ({
text: s.label,
displayText: s.detail ? `${s.label} - ${s.detail}` : s.label
})),
from: CodeMirror.Pos(data.from.line, data.from.ch),
to: CodeMirror.Pos(data.to.line, data.to.ch)
});
})
.catch(err => {
console.error("DslEditor: Completion error", err);
callback(null);
});
}
// Mark hint function as async for CodeMirror
dslHint.async = true;
/* --------------------------------------------------
* DSL autocompletion hint
* DSL linting (async via server)
* -------------------------------------------------- */
function dslHint(cm) {
function dslLint(text, updateOutput, options, cm) {
const cursor = cm.getCursor();
const line = cm.getLine(cursor.line);
const ch = cursor.ch;
let start = ch;
while (start > 0 && /\w/.test(line.charAt(start - 1))) {
start--;
}
const params = new URLSearchParams({
e_id: dslId,
text: text,
line: cursor.line,
ch: cursor.ch
});
const word = line.slice(start, ch);
fetch(`/myfasthtml/validations?${params}`)
.then(response => response.json())
.then(data => {
if (!data || !data.errors || data.errors.length === 0) {
updateOutput([]);
return;
}
const matches = completionItems.filter(item =>
item.startsWith(word)
);
// Convert server errors to CodeMirror lint format
// Server returns 1-based positions, CodeMirror expects 0-based
const annotations = data.errors.map(err => ({
from: CodeMirror.Pos(err.line - 1, Math.max(0, err.column - 1)),
to: CodeMirror.Pos(err.line - 1, err.column),
message: err.message,
severity: err.severity || "error"
}));
return {
list: matches,
from: CodeMirror.Pos(cursor.line, start),
to: CodeMirror.Pos(cursor.line, ch)
};
updateOutput(annotations);
})
.catch(err => {
console.error("DslEditor: Linting error", err);
updateOutput([]);
});
}
// Mark lint function as async for CodeMirror
dslLint.async = true;
/* --------------------------------------------------
* Create CodeMirror editor
* -------------------------------------------------- */
const editor = CodeMirror(editorContainer, {
const enableCompletion = autocompletion && dslId;
// Only enable linting if the lint addon is loaded
const lintAddonLoaded = typeof CodeMirror.lint !== "undefined" ||
(CodeMirror.defaults && "lint" in CodeMirror.defaults);
const enableLinting = linting && dslId && lintAddonLoaded;
const editorOptions = {
value: textarea.value || "",
lineNumbers: !!lineNumbers,
readOnly: !!readonly,
placeholder: placeholder || "",
extraKeys: autocompletion ? {
extraKeys: enableCompletion ? {
"Ctrl-Space": "autocomplete"
} : {},
hintOptions: autocompletion ? {
hintOptions: enableCompletion ? {
hint: dslHint,
completeSingle: false
} : undefined
});
};
// Add linting options if enabled and addon is available
if (enableLinting) {
// Include linenumbers gutter if lineNumbers is enabled
editorOptions.gutters = lineNumbers
? ["CodeMirror-linenumbers", "CodeMirror-lint-markers"]
: ["CodeMirror-lint-markers"];
editorOptions.lint = {
getAnnotations: dslLint,
async: true
};
}
const editor = CodeMirror(editorContainer, editorOptions);
/* --------------------------------------------------
* Auto-trigger completion on specific characters
* -------------------------------------------------- */
if (enableCompletion) {
editor.on("inputRead", function (cm, change) {
if (change.origin !== "+input") return;
const lastChar = change.text[change.text.length - 1];
const lastCharOfInput = lastChar.slice(-1);
if (AUTO_TRIGGER_CHARS.includes(lastCharOfInput)) {
cm.showHint({completeSingle: false});
}
});
}
/* --------------------------------------------------
* Debounced update + HTMX transport
@@ -2278,11 +2358,10 @@ function initDslEditor(config) {
setContent: (content) => editor.setValue(content)
};
console.debug(`DslEditor initialized (CM5 + HTMX): ${elementId} with ${dsl?.name || "DSL"}`);
console.debug(`DslEditor initialized: ${elementId}, DSL=${dsl?.name || "unknown"}, dsl_id=${dslId}, completion=${enableCompletion ? "enabled" : "disabled"}, linting=${enableLinting ? "enabled" : "disabled"}`);
}
function updateDatagridSelection(datagridId) {
const selectionManager = document.getElementById(`tsm_${datagridId}`);
if (!selectionManager) return;

View File

@@ -13,8 +13,9 @@ from pandas import DataFrame
from myfasthtml.controls.BaseCommands import BaseCommands
from myfasthtml.controls.CycleStateControl import CycleStateControl
from myfasthtml.controls.DataGridColumnsManager import DataGridColumnsManager
from myfasthtml.controls.DataGridFormattingEditor import DataGridFormattingEditor
from myfasthtml.controls.DataGridQuery import DataGridQuery, DG_QUERY_FILTER
from myfasthtml.controls.DslEditor import DslEditor
from myfasthtml.controls.DslEditor import DslEditorConf
from myfasthtml.controls.Mouse import Mouse
from myfasthtml.controls.Panel import Panel, PanelConf
from myfasthtml.controls.datagrid_objects import DataGridColumnState, DataGridRowState, \
@@ -180,7 +181,7 @@ 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(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"))
@@ -206,7 +207,12 @@ 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())
editor_conf = DslEditorConf()
self._formatting_editor = DataGridFormattingEditor(self,
conf=editor_conf,
dsl=FormattingDSL(),
save_state=self._settings.save_state,
_id="#formatting_editor")
# other definitions
self._mouse_support = {

View File

@@ -0,0 +1,7 @@
from myfasthtml.controls.DslEditor import DslEditor
class DataGridFormattingEditor(DslEditor):
def on_dsl_change(self, dsl):
pass

View File

@@ -14,6 +14,11 @@ 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.dsls import DslsManager
from myfasthtml.core.formatting.dsl.completion.engine import FormattingCompletionEngine
from myfasthtml.core.formatting.dsl.completion.provider import DatagridMetadataProvider
from myfasthtml.core.formatting.dsl.definition import FormattingDSL
from myfasthtml.core.formatting.dsl.parser import DSLParser
from myfasthtml.core.formatting.presets import DEFAULT_STYLE_PRESETS, DEFAULT_FORMATTER_PRESETS
from myfasthtml.core.instances import InstancesManager, SingleInstance
from myfasthtml.icons.fluent_p1 import table_add20_regular
@@ -72,7 +77,7 @@ class Commands(BaseCommands):
key="SelectNode")
class DataGridsManager(SingleInstance):
class DataGridsManager(SingleInstance, DatagridMetadataProvider):
def __init__(self, parent, _id=None):
if not getattr(self, "_is_new_instance", False):
@@ -89,6 +94,11 @@ class DataGridsManager(SingleInstance):
# Global presets shared across all DataGrids
self.style_presets: dict = DEFAULT_STYLE_PRESETS.copy()
self.formatter_presets: dict = DEFAULT_FORMATTER_PRESETS.copy()
# register the auto-completion for the formatter DSL
DslsManager.register(FormattingDSL().get_id(),
FormattingCompletionEngine(self),
DSLParser())
def upload_from_source(self):
file_upload = FileUpload(self)
@@ -104,6 +114,7 @@ class DataGridsManager(SingleInstance):
dg_conf = DatagridConf(namespace=namespace, name=name)
dg = DataGrid(self._tabs_manager, conf=dg_conf, save_state=True) # first time the Datagrid is created
dg.init_from_dataframe(df)
self._registry.put(namespace, name, dg.get_id())
document = DocumentDefinition(
document_id=str(uuid.uuid4()),
namespace=namespace,
@@ -154,6 +165,26 @@ class DataGridsManager(SingleInstance):
self._tree.clear()
return self._tree
# === DatagridMetadataProvider ===
def list_tables(self):
return self._registry.get_all_tables()
def list_columns(self, table_name):
return self._registry.get_columns(table_name)
def list_column_values(self, table_name, column_name):
return self._registry.get_column_values(table_name, column_name)
def get_row_count(self, table_name):
return self._registry.get_row_count(table_name)
def list_style_presets(self) -> list[str]:
return list(self.style_presets.keys())
def list_format_presets(self) -> list[str]:
return list(self.formatter_presets.keys())
# === Presets Management ===
def get_style_presets(self) -> dict:
@@ -194,6 +225,8 @@ class DataGridsManager(SingleInstance):
if name in self.formatter_presets:
del self.formatter_presets[name]
# === UI ===
def mk_main_icons(self):
return Div(
mk.icon(folder_open20_regular, tooltip="Upload from source", command=self.commands.upload_from_source()),

View File

@@ -16,6 +16,7 @@ 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
@@ -25,19 +26,22 @@ 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:
class DslEditorState(DbObject):
"""Non-persisted state for DslEditor."""
def __init__(self):
self.content: str = ""
self.auto_save: bool = True
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):
@@ -87,13 +91,14 @@ class DslEditor(MultipleInstance):
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._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}")
@@ -114,9 +119,9 @@ class DslEditor(MultipleInstance):
self.on_content_changed()
logger.debug(f"Content updated: {len(content)} chars")
def toggle_auto_save(self) -> None:
def toggle_auto_save(self):
self._state.auto_save = not self._state.auto_save
self._mk_auto_save()
return self._mk_auto_save()
def on_content_changed(self) -> None:
pass
@@ -128,9 +133,11 @@ class DslEditor(MultipleInstance):
"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,

View File

@@ -159,31 +159,31 @@ class Panel(MultipleInstance):
enabled = self.conf.left if side == "left" else self.conf.right
if not enabled:
return None
visible = self._state.left_visible if side == "left" else self._state.right_visible
content = self._right if side == "right" else self._left
show_title = self.conf.show_left_title if side == "left" else self.conf.show_right_title
title = self.conf.left_title if side == "left" else self.conf.right_title
resizer = Div(
cls=f"mf-resizer mf-resizer-{side}",
data_command_id=self.commands.update_side_width(side).id,
data_side=side
)
hide_icon = mk.icon(
subtract20_regular,
size=20,
command=self.commands.set_side_visible(side, False),
cls="mf-panel-hide-icon"
)
panel_cls = f"mf-panel-{side}"
if not visible:
panel_cls += " mf-hidden"
if show_title:
panel_cls += " mf-panel-with-title"
# Left panel: content then resizer (resizer on the right)
# Right panel: resizer then content (resizer on the left)
if show_title:
@@ -202,6 +202,7 @@ class Panel(MultipleInstance):
body,
resizer,
cls=panel_cls,
style=f"width: {self._state.left_width}px;",
id=self._ids.panel(side)
)
else:
@@ -209,6 +210,7 @@ class Panel(MultipleInstance):
resizer,
body,
cls=panel_cls,
style=f"width: {self._state.right_width}px;",
id=self._ids.panel(side)
)
else:
@@ -218,6 +220,7 @@ class Panel(MultipleInstance):
Div(content, id=self._ids.content(side)),
resizer,
cls=panel_cls,
style=f"width: {self._state.left_width}px;",
id=self._ids.panel(side)
)
else:
@@ -226,6 +229,7 @@ class Panel(MultipleInstance):
hide_icon,
Div(content, id=self._ids.content(side)),
cls=panel_cls,
style=f"width: {self._state.left_width}px;",
id=self._ids.panel(side)
)
@@ -250,14 +254,14 @@ class Panel(MultipleInstance):
enabled = self.conf.left if side == "left" else self.conf.right
if not enabled:
return None
show_display = self.conf.show_display_left if side == "left" else self.conf.show_display_right
if not show_display:
return None
is_visible = self._state.left_visible if side == "left" else self._state.right_visible
icon_cls = "hidden" if is_visible else f"mf-panel-show-icon mf-panel-show-icon-{side}"
return mk.icon(
more_horizontal20_regular,
command=self.commands.set_side_visible(side, True),

View File

@@ -1,7 +1,7 @@
from myfasthtml.core.dbmanager import DbManager
from myfasthtml.core.instances import SingleInstance
DATAGRIDS_REGISTRY_ENTRY_KEY = "DataGridsRegistryEntry"
DATAGRIDS_REGISTRY_ENTRY_KEY = "data_grids_registry"
class DataGridsRegistry(SingleInstance):

View File

@@ -1,14 +0,0 @@
class CompletionsManager:
completions = {}
@staticmethod
def register(engine):
CompletionsManager.completions[engine.get_id()] = engine
@staticmethod
def get_completions(engine_id):
return CompletionsManager.completions[engine_id]
@staticmethod
def reset():
CompletionsManager.completions = {}

View File

@@ -15,6 +15,7 @@ class Routes:
Commands = "/commands"
Bindings = "/bindings"
Completions = "/completions"
Validations = "/validations"
class ColumnType(Enum):

View File

@@ -43,7 +43,9 @@ class DbObject:
def __init__(self, owner: BaseInstance, name=None, db_manager=None, save_state=True):
self._owner = owner
self._name = name or owner.get_full_id()
self._name = name or owner.get_id()
if self._name.startswith(("#", "-")) and owner.get_parent() is not None:
self._name = owner.get_parent().get_id() + self._name
self._db_manager = db_manager or DbManager(self._owner)
self._save_state = save_state

View File

@@ -13,6 +13,7 @@ from myfasthtml.core.dsl.lark_to_lezer import (
lark_to_lezer_grammar,
extract_completions_from_grammar,
)
from myfasthtml.core.utils import make_safe_id
class DSLDefinition(ABC):
@@ -82,3 +83,6 @@ class DSLDefinition(ABC):
"lezerGrammar": self.lezer_grammar,
"completions": self.completions,
}
def get_id(self):
return make_safe_id(self.name)

View File

@@ -0,0 +1,31 @@
from dataclasses import dataclass
from myfasthtml.core.dsl.base_completion import BaseCompletionEngine
from myfasthtml.core.formatting.dsl.parser import DSLParser
@dataclass
class DslDefinition:
completion: BaseCompletionEngine
validation: DSLParser # To do, this parser is not generic (specific to the Formatting DSL)
class DslsManager:
dsls: dict[str, DslDefinition] = {}
@staticmethod
def register(dsl_id: str, completion: BaseCompletionEngine, validation: DSLParser):
# then engine_id is actually the DSL id
DslsManager.dsls[dsl_id] = DslDefinition(completion, validation)
@staticmethod
def get_completion_engine(engine_id) -> BaseCompletionEngine:
return DslsManager.dsls[engine_id].completion
@staticmethod
def get_validation_parser(engine_id) -> DSLParser:
return DslsManager.dsls[engine_id].validation
@staticmethod
def reset():
DslsManager.dsls = {}

View File

@@ -22,7 +22,7 @@ class DatagridMetadataProvider(Protocol):
DataGrid names follow the pattern namespace.name (multi-level namespaces).
"""
def get_tables(self) -> list[str]:
def list_tables(self) -> list[str]:
"""
Return the list of available DataGrid names.
@@ -31,7 +31,7 @@ class DatagridMetadataProvider(Protocol):
"""
...
def get_columns(self, table_name: str) -> list[str]:
def list_columns(self, table_name: str) -> list[str]:
"""
Return the column names for a specific DataGrid.
@@ -43,7 +43,7 @@ class DatagridMetadataProvider(Protocol):
"""
...
def get_column_values(self, table_name, column_name: str) -> list[Any]:
def list_column_values(self, table_name, column_name: str) -> list[Any]:
"""
Return the distinct values for a column in the current DataGrid.
@@ -71,7 +71,7 @@ class DatagridMetadataProvider(Protocol):
"""
...
def get_style_presets(self) -> list[str]:
def list_style_presets(self) -> list[str]:
"""
Return the list of available style preset names.
@@ -82,7 +82,7 @@ class DatagridMetadataProvider(Protocol):
"""
...
def get_format_presets(self) -> list[str]:
def list_format_presets(self) -> list[str]:
"""
Return the list of available format preset names.

View File

@@ -160,9 +160,9 @@ def _get_column_suggestions(provider: DatagridMetadataProvider) -> list[Suggesti
"""Get column name suggestions from provider."""
try:
# Try to get columns from the first available table
tables = provider.get_tables()
tables = provider.list_tables()
if tables:
columns = provider.get_columns(tables[0])
columns = provider.list_columns(tables[0])
return [Suggestion(col, "Column", "column") for col in columns]
except Exception:
pass
@@ -174,9 +174,9 @@ def _get_column_suggestions_with_closing_quote(
) -> list[Suggestion]:
"""Get column name suggestions with closing quote."""
try:
tables = provider.get_tables()
tables = provider.list_tables()
if tables:
columns = provider.get_columns(tables[0])
columns = provider.list_columns(tables[0])
return [Suggestion(f'{col}"', "Column", "column") for col in columns]
except Exception:
pass
@@ -189,7 +189,7 @@ def _get_style_preset_suggestions(provider: DatagridMetadataProvider) -> list[Su
# Add provider presets if available
try:
custom_presets = provider.get_style_presets()
custom_presets = provider.list_style_presets()
for preset in custom_presets:
# Check if it's already in default presets
if not any(s.label == preset for s in presets.STYLE_PRESETS):
@@ -212,7 +212,7 @@ def _get_style_preset_suggestions_quoted(
# Add provider presets if available
try:
custom_presets = provider.get_style_presets()
custom_presets = provider.list_style_presets()
for preset in custom_presets:
if not any(s.label == preset for s in presets.STYLE_PRESETS):
suggestions.append(Suggestion(f'"{preset}"', "Custom preset", "preset"))
@@ -232,7 +232,7 @@ def _get_format_preset_suggestions(provider: DatagridMetadataProvider) -> list[S
# Add provider presets if available
try:
custom_presets = provider.get_format_presets()
custom_presets = provider.list_format_presets()
for preset in custom_presets:
if not any(s.label == preset for s in presets.FORMAT_PRESETS):
suggestions.append(Suggestion(preset, "Custom preset", "preset"))
@@ -251,7 +251,7 @@ def _get_row_index_suggestions(provider: DatagridMetadataProvider) -> list[Sugge
suggestions = []
try:
tables = provider.get_tables()
tables = provider.list_tables()
if tables:
row_count = provider.get_row_count(tables[0])
if row_count > 0:
@@ -285,7 +285,7 @@ def _get_column_value_suggestions(
return []
try:
values = provider.get_column_values(scope.column_name)
values = provider.list_column_values(scope.column_name)
suggestions = []
for value in values:
if value is None:

View File

@@ -116,7 +116,7 @@ class BaseInstance:
_id = f"{prefix}-{str(uuid.uuid4())}"
return _id
if _id.startswith("-") and parent is not None:
if _id.startswith(("-", "#")) and parent is not None:
return f"{parent.get_id()}{_id}"
return _id

View File

@@ -10,6 +10,9 @@ from rich.table import Table
from starlette.routing import Mount
from myfasthtml.core.constants import Routes, ROUTE_ROOT
from myfasthtml.core.dsl.types import Position
from myfasthtml.core.dsls import DslsManager
from myfasthtml.core.formatting.dsl import DSLSyntaxError
from myfasthtml.test.MyFT import MyFT
utils_app, utils_rt = fast_app()
@@ -383,12 +386,41 @@ def post(session, b_id: str, values: dict):
@utils_rt(Routes.Completions)
def get(session, c_id, text: str, line: int, ch: int):
def get(session, e_id: str, text: str, line: int, ch: int):
"""
Default routes for Domaine Specific Languages completion
:param session:
:param c_id:
:param values:
:param e_id: engine_id
:param text:
:param line:
:param ch:
:return:
"""
logger.debug(f"Entering {Routes.Bindings} with {session=}, {c_id=}, {values=}")
logger.debug(f"Entering {Routes.Completions} with {session=}, {e_id=}, {text=}, {line=}, {ch}")
completion = DslsManager.get_completion_engine(e_id)
result = completion.get_completions(text, Position(line, ch))
return result.to_dict()
@utils_rt(Routes.Validations)
def get(session, e_id: str, text: str, line: int, ch: int):
"""
Default routes for Domaine Specific Languages syntax validation
:param session:
:param e_id:
:param text:
:param line:
:param ch:
:return:
"""
logger.debug(f"Entering {Routes.Validations} with {session=}, {e_id=}, {text=}, {line=}, {ch}")
validation = DslsManager.get_validation_parser(e_id)
try:
validation.parse(text)
return {"errors": []}
except DSLSyntaxError as e:
return {"errors": [{
"line": e.line or 1,
"column": e.column or 1,
"message": e.message
}]}

View File

@@ -87,6 +87,9 @@ def create_app(daisyui: Optional[bool] = True,
Script(src="/myfasthtml/show-hint.min.js"),
Link(href="/myfasthtml/show-hint.min.css", rel="stylesheet", type="text/css"),
Script(src="/myfasthtml/lint.min.js"),
Link(href="/myfasthtml/lint.min.css", rel="stylesheet", type="text/css"),
]
beforeware = create_auth_beforeware() if protect_routes else None