Files
MyFastHtml/src/myfasthtml/assets/core/dsleditor.js

269 lines
7.7 KiB
JavaScript

/**
* Initialize DslEditor with CodeMirror 5
*
* Features:
* - DSL-based autocompletion
* - Line numbers
* - Readonly support
* - Placeholder support
* - Textarea synchronization
* - Debounced HTMX server update via updateCommandId
*
* Required CodeMirror addons:
* - addon/hint/show-hint.js
* - addon/hint/show-hint.css
* - addon/display/placeholder.js
*
* Requires:
* - htmx loaded globally
*
* @param {Object} config
*/
function initDslEditor(config) {
const {
elementId,
textareaId,
lineNumbers,
autocompletion,
linting,
placeholder,
readonly,
updateCommandId,
dslId,
dsl
} = config;
const wrapper = document.getElementById(elementId);
const textarea = document.getElementById(textareaId);
const editorContainer = document.getElementById(`cm_${elementId}`);
if (!wrapper || !textarea || !editorContainer) {
console.error(`DslEditor: Missing elements for ${elementId}`);
return;
}
if (typeof CodeMirror === "undefined") {
console.error("DslEditor: CodeMirror 5 not loaded");
return;
}
/* --------------------------------------------------
* DSL autocompletion hint (async via server)
* -------------------------------------------------- */
// Characters that trigger auto-completion
const AUTO_TRIGGER_CHARS = [".", "(", '"', " "];
function dslHint(cm, callback) {
const cursor = cm.getCursor();
const text = cm.getValue();
// 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 linting (async via server)
* -------------------------------------------------- */
function dslLint(text, updateOutput, options, cm) {
const cursor = cm.getCursor();
const params = new URLSearchParams({
e_id: dslId,
text: text,
line: cursor.line,
ch: cursor.ch
});
fetch(`/myfasthtml/validations?${params}`)
.then(response => response.json())
.then(data => {
if (!data || !data.errors || data.errors.length === 0) {
updateOutput([]);
return;
}
// 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"
}));
updateOutput(annotations);
})
.catch(err => {
console.error("DslEditor: Linting error", err);
updateOutput([]);
});
}
// Mark lint function as async for CodeMirror
dslLint.async = true;
/* --------------------------------------------------
* Register Simple Mode if available and config provided
* -------------------------------------------------- */
let modeName = null;
if (typeof CodeMirror.defineSimpleMode !== "undefined" && dsl && dsl.simpleModeConfig) {
// Generate unique mode name from DSL name
modeName = `dsl-${dsl.name.toLowerCase().replace(/\s+/g, '-')}`;
// Register the mode if not already registered
if (!CodeMirror.modes[modeName]) {
try {
CodeMirror.defineSimpleMode(modeName, dsl.simpleModeConfig);
} catch (err) {
console.error(`Failed to register Simple Mode for ${dsl.name}:`, err);
modeName = null;
}
}
}
/* --------------------------------------------------
* Create CodeMirror editor
* -------------------------------------------------- */
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 || "",
mode: modeName || undefined, // Use Simple Mode if available
theme: "daisy", // Use DaisyUI theme for automatic theme switching
lineNumbers: !!lineNumbers,
readOnly: !!readonly,
placeholder: placeholder || "",
extraKeys: enableCompletion ? {
"Ctrl-Space": "autocomplete"
} : {},
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
* -------------------------------------------------- */
let debounceTimer = null;
const DEBOUNCE_DELAY = 300;
editor.on("change", function (cm) {
const value = cm.getValue();
textarea.value = value;
if (!updateCommandId) return;
clearTimeout(debounceTimer);
debounceTimer = setTimeout(() => {
wrapper.dispatchEvent(
new CustomEvent("dsl-editor-update", {
detail: {
commandId: updateCommandId,
value: value
}
})
);
}, DEBOUNCE_DELAY);
});
/* --------------------------------------------------
* HTMX listener (LOCAL to wrapper)
* -------------------------------------------------- */
if (updateCommandId && typeof htmx !== "undefined") {
wrapper.addEventListener("dsl-editor-update", function (e) {
htmx.ajax("POST", "/myfasthtml/commands", {
target: wrapper,
swap: "none",
values: {
c_id: e.detail.commandId,
content: e.detail.value
}
});
});
}
/* --------------------------------------------------
* Public API
* -------------------------------------------------- */
wrapper._dslEditor = {
editor: editor,
getContent: () => editor.getValue(),
setContent: (content) => editor.setValue(content)
};
//console.debug(`DslEditor initialized: ${elementId}, DSL=${dsl?.name || "unknown"}, dsl_id=${dslId}, completion=${enableCompletion ? "enabled" : "disabled"}, linting=${enableLinting ? "enabled" : "disabled"}`);
}