I can validate formatting in editor
This commit is contained in:
@@ -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
1
src/myfasthtml/assets/lint.min.css
vendored
Normal 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
1
src/myfasthtml/assets/lint.min.js
vendored
Normal 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)})});
|
||||
@@ -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;
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
7
src/myfasthtml/controls/DataGridFormattingEditor.py
Normal file
7
src/myfasthtml/controls/DataGridFormattingEditor.py
Normal file
@@ -0,0 +1,7 @@
|
||||
from myfasthtml.controls.DslEditor import DslEditor
|
||||
|
||||
|
||||
class DataGridFormattingEditor(DslEditor):
|
||||
|
||||
def on_dsl_change(self, dsl):
|
||||
pass
|
||||
@@ -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()),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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 = {}
|
||||
@@ -15,6 +15,7 @@ class Routes:
|
||||
Commands = "/commands"
|
||||
Bindings = "/bindings"
|
||||
Completions = "/completions"
|
||||
Validations = "/validations"
|
||||
|
||||
|
||||
class ColumnType(Enum):
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
31
src/myfasthtml/core/dsls.py
Normal file
31
src/myfasthtml/core/dsls.py
Normal 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 = {}
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}]}
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user