269 lines
7.7 KiB
JavaScript
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"}`);
|
|
} |