function codeMirrorLoaderScript(base) {
const indexUrl = JSON.stringify(`${base}/index.js`);
return `
(async () => {
try {
const cm = await import(${indexUrl});
window.__dxcm = { ...cm };
} catch (error) {
window.__dxcmError = String(error);
console.error("dioxus_codemirror: failed to load vendored CodeMirror", error);
}
})();
`;
}
async function codeMirrorLoad(base) {
if (!window.__dxcmInjected) {
window.__dxcmInjected = true;
const script = document.createElement("script");
script.type = "module";
script.textContent = codeMirrorLoaderScript(base);
document.head.appendChild(script);
}
for (let attempt = 0; attempt < 1200; attempt += 1) {
if (window.__dxcm) {
return window.__dxcm;
}
if (window.__dxcmError) {
throw new Error(`dioxus_codemirror: ${window.__dxcmError}`);
}
await new Promise((resolve) => requestAnimationFrame(resolve));
}
throw new Error("dioxus_codemirror: timed out loading CodeMirror");
}
async function elementWait(id) {
for (let attempt = 0; attempt < 1200; attempt += 1) {
const element = document.getElementById(id);
if (element) {
return element;
}
await new Promise((resolve) => requestAnimationFrame(resolve));
}
throw new Error(`dioxus_codemirror: mount element #${id} not found`);
}
const THEME_PALETTE = [
{ name: "bg", light: "#ffffff", dark: "#0d1117" },
{ name: "fg", light: "#1f2328", dark: "#e6edf3" },
{ name: "caret", light: "#1f2328", dark: "#e6edf3" },
{ name: "selection", light: "#c7d2e0", dark: "#3a4250" },
{ name: "selection-focused", light: "#9ec2ff", dark: "#3a619c" },
{ name: "selection-match", light: "#3dd3ff55", dark: "#2299d255" },
{ name: "selection-match-main", light: "#3dd3ff99", dark: "#2299d299" },
{ name: "gutter-bg", light: "#f6f8fa", dark: "#0d1117" },
{ name: "gutter-fg", light: "#8c959f", dark: "#6e7681" },
{ name: "highlight-space", light: "#d0d3d6", dark: "#363b42" },
{ name: "active-line", light: "#f0f3f6", dark: "#161b22" },
{ name: "active-line-gutter-bg", light: "#eaeef2", dark: "#161b22" },
{ name: "border", light: "#d0d7de", dark: "#30363d" },
{ name: "tooltip-bg", light: "#ffffff", dark: "#161b22" },
{ name: "tooltip-fg", light: "#1f2328", dark: "#e6edf3" },
{ name: "tooltip-selected-bg", light: "#0969da", dark: "#094771" },
{ name: "tooltip-selected-fg", light: "#ffffff", dark: "#ffffff" },
{ name: "tooltip-info-bg", light: "#f6f8fa", dark: "#0d1117" },
{ name: "syntax-keyword", light: "#cf222e", dark: "#ff7b72" },
{ name: "syntax-string", light: "#0a3069", dark: "#a5d6ff" },
{ name: "syntax-comment", light: "#6e7781", dark: "#8b949e" },
{ name: "syntax-number", light: "#0550ae", dark: "#79c0ff" },
{ name: "syntax-function", light: "#8250df", dark: "#d2a8ff" },
{ name: "syntax-type", light: "#953800", dark: "#ffa657" },
{ name: "syntax-constant", light: "#0550ae", dark: "#79c0ff" },
{ name: "syntax-operator", light: "#0550ae", dark: "#79c0ff" },
{ name: "syntax-property", light: "#116329", dark: "#7ee787" },
{ name: "syntax-heading", light: "#0550ae", dark: "#79c0ff" },
{ name: "syntax-link", light: "#0a3069", dark: "#a5d6ff" },
{ name: "syntax-invalid", light: "#cf222e", dark: "#ffa198" },
];
const themePaletteSource = THEME_PALETTE.map(
({ name, light, dark }) => ` --dxcm-light-${name}: ${light};\n --dxcm-dark-${name}: ${dark};`,
).join("\n");
function themeActivate(scheme) {
return THEME_PALETTE.map(({ name }) => ` --dxcm-${name}: var(--dxcm-${scheme}-${name});`).join(
"\n",
);
}
function themeStylesInject() {
if (window.__dxcmStyleInjected) {
return;
}
window.__dxcmStyleInjected = true;
const style = document.createElement("style");
style.id = "dioxus-codemirror-theme";
style.textContent = `
/* Both palette sources, plus the default (light) active aliases. */
.dioxus-codemirror {
${themePaletteSource}
${themeActivate("light")}
}
/* \`theme: Auto\` on a dark OS: re-alias the active variables to the dark
sources. The \`:not\` leaves editors pinned to \`theme: Light\` untouched. */
@media (prefers-color-scheme: dark) {
.dioxus-codemirror:not([data-theme="light"]) {
${themeActivate("dark")}
}
}
/* \`theme: Dark\`: dark regardless of the OS. */
.dioxus-codemirror[data-theme="dark"] {
${themeActivate("dark")}
}
.dioxus-codemirror .cm-editor {
background: var(--dxcm-bg);
color: var(--dxcm-fg);
}
.dioxus-codemirror .cm-editor.cm-focused {
outline: 1px solid var(--dxcm-border);
}
.dioxus-codemirror .cm-content {
caret-color: var(--dxcm-caret);
}
.dioxus-codemirror .cm-cursor,
.dioxus-codemirror .cm-dropCursor {
border-left-color: var(--dxcm-caret);
}
/* Selection background, drawn by \`drawSelection\` as \`.cm-selectionBackground\`
layers. CodeMirror's base theme styles these with high-specificity selectors
(\`&light.cm-focused > .cm-scroller > .cm-selectionLayer .cm-selectionBackground\`,
~5 classes) and keys light/dark off its own \`darkTheme\` facet -- which we do
not set, so it would always apply its light colors regardless of our scheme.
We therefore match its structure (and add \`.cm-editor\` to outweigh it) so our
variables win in both focus states and both schemes. */
.dioxus-codemirror .cm-selectionLayer .cm-selectionBackground,
.dioxus-codemirror .cm-content ::selection {
background: var(--dxcm-selection);
}
.dioxus-codemirror .cm-editor .cm-scroller .cm-highlightSpace {
background-image: radial-gradient(circle at 50% 55%, var(--dxcm-highlight-space) 20%, transparent 5%);
}
.dioxus-codemirror .cm-editor.cm-focused .cm-scroller .cm-selectionLayer .cm-selectionBackground {
background: var(--dxcm-selection-focused);
}
/* Occurrences of the selected text, marked by \`selectionMatchHighlighter\` and
themed so they track the color scheme. Other occurrences get the match tint;
the actively selected occurrence (\`-main\`) is left to its (more prominent)
selection background instead, so it reads as the selection rather than as a
match. */
.dioxus-codemirror .cm-selectionMatch {
background: var(--dxcm-selection-match);
}
.dioxus-codemirror .cm-selectionMatch.cm-selectionMatch-main {
background: var(--dxcm-selection-match-main);
}
.dioxus-codemirror .cm-gutters {
background: var(--dxcm-gutter-bg);
color: var(--dxcm-gutter-fg);
border-right-color: var(--dxcm-border);
}
.dioxus-codemirror .cm-activeLine {
background: var(--dxcm-active-line);
}
.dioxus-codemirror .cm-activeLineGutter {
background: var(--dxcm-active-line-gutter-bg);
color: var(--dxcm-fg);
}
/* Autocomplete and hover tooltips. CodeMirror renders these inside the editor
DOM (so they fall under \`.dioxus-codemirror\`) with only a single-class base
theme, which these scoped rules outweigh. */
.dioxus-codemirror .cm-tooltip {
background: var(--dxcm-tooltip-bg);
color: var(--dxcm-tooltip-fg);
border: 1px solid var(--dxcm-border);
border-radius: 6px;
}
.dioxus-codemirror .cm-tooltip-autocomplete ul li {
color: var(--dxcm-tooltip-fg);
}
.dioxus-codemirror .cm-tooltip-autocomplete ul li[aria-selected] {
background: var(--dxcm-tooltip-selected-bg);
color: var(--dxcm-tooltip-selected-fg);
}
.dioxus-codemirror .cm-completionInfo {
background: var(--dxcm-tooltip-info-bg);
color: var(--dxcm-tooltip-fg);
border: 1px solid var(--dxcm-border);
}
`;
document.head.appendChild(style);
}
function themeHighlightStyle() {
return HighlightStyle.define([
{ tag: tags.keyword, color: "var(--dxcm-syntax-keyword)" },
{
tag: [tags.name, tags.deleted, tags.character, tags.macroName],
color: "var(--dxcm-fg)",
},
{
tag: [tags.propertyName, tags.attributeName],
color: "var(--dxcm-syntax-property)",
},
{
tag: [tags.function(tags.variableName), tags.labelName],
color: "var(--dxcm-syntax-function)",
},
{
tag: [tags.color, tags.constant(tags.name), tags.standard(tags.name)],
color: "var(--dxcm-syntax-constant)",
},
{
tag: [
tags.typeName,
tags.className,
tags.namespace,
tags.changed,
tags.annotation,
tags.modifier,
tags.self,
],
color: "var(--dxcm-syntax-type)",
},
{
tag: [tags.number, tags.integer, tags.float, tags.atom, tags.bool],
color: "var(--dxcm-syntax-number)",
},
{
tag: [
tags.operator,
tags.operatorKeyword,
tags.escape,
tags.regexp,
tags.special(tags.string),
],
color: "var(--dxcm-syntax-operator)",
},
{
tag: [tags.meta, tags.comment],
color: "var(--dxcm-syntax-comment)",
fontStyle: "italic",
},
{
tag: [tags.string, tags.inserted, tags.processingInstruction],
color: "var(--dxcm-syntax-string)",
},
{
tag: [tags.url, tags.link],
color: "var(--dxcm-syntax-link)",
textDecoration: "underline",
},
{ tag: tags.heading, color: "var(--dxcm-syntax-heading)", fontWeight: "bold" },
{ tag: tags.strong, fontWeight: "bold" },
{ tag: tags.emphasis, fontStyle: "italic" },
{ tag: tags.strikethrough, textDecoration: "line-through" },
{ tag: tags.invalid, color: "var(--dxcm-syntax-invalid)" },
]);
}
function selectNextMatch(view) {
const { state } = view;
const { selection } = state;
const main = selection.main;
if (main.empty) {
const word = state.wordAt(main.head);
if (!word) {
return false;
}
view.dispatch({
selection: EditorSelection.create(
selection.ranges.map((range, index) =>
index === selection.mainIndex ? EditorSelection.range(word.from, word.to) : range,
),
selection.mainIndex,
),
});
return true;
}
const query = state.sliceDoc(main.from, main.to);
const sameText = selection.ranges.every(
(range) => state.sliceDoc(range.from, range.to) === query,
);
if (!query || !sameText) {
return false;
}
const next = selectNextMatchFind(state, query, selection.ranges);
if (!next) {
return false;
}
view.dispatch({
selection: selection.addRange(EditorSelection.range(next.from, next.to)),
scrollIntoView: true,
});
return true;
}
function selectNextMatchFind(state, query, ranges) {
const taken = new Set(ranges.map((range) => `${range.from}:${range.to}`));
const after = ranges[ranges.length - 1].to;
const scan = (from, to) => {
const cursor = new SearchCursor(state.doc, query, from, to);
while (!cursor.next().done) {
if (!taken.has(`${cursor.value.from}:${cursor.value.to}`)) {
return cursor.value;
}
}
return null;
};
return scan(after, state.doc.length) ?? scan(0, after);
}
function selectAllMatches(view) {
const { state } = view;
const main = state.selection.main;
let term;
if (main.empty) {
const word = state.wordAt(main.head);
term = word && state.sliceDoc(word.from, word.to);
} else {
term = state.sliceDoc(main.from, main.to);
}
if (!term) {
return false;
}
const ranges = [];
const cursor = new SearchCursor(state.doc, term);
while (!cursor.next().done) {
ranges.push(EditorSelection.range(cursor.value.from, cursor.value.to));
}
if (ranges.length === 0) {
return false;
}
const mainIndex = ranges.findIndex((range) => range.from >= main.from);
view.dispatch({
selection: EditorSelection.create(ranges, mainIndex < 0 ? 0 : mainIndex),
scrollIntoView: true,
});
return true;
}
function selectionMatchHighlighter() {
const matchDeco = Decoration.mark({ class: "cm-selectionMatch" });
const mainDeco = Decoration.mark({
class: "cm-selectionMatch cm-selectionMatch-main",
});
const compute = (view) => {
const { state } = view;
const main = state.selection.main;
if (main.empty) {
return Decoration.none;
}
const term = state.sliceDoc(main.from, main.to);
const selected = new Set();
for (const range of state.selection.ranges) {
if (!range.empty) {
selected.add(`${range.from}:${range.to}`);
}
}
const ranges = [];
for (const visible of view.visibleRanges) {
const cursor = new SearchCursor(state.doc, term, visible.from, visible.to);
while (!cursor.next().done) {
const { from, to } = cursor.value;
const deco = selected.has(`${from}:${to}`) ? mainDeco : matchDeco;
ranges.push(deco.range(from, to));
}
}
return ranges.length ? Decoration.set(ranges, true) : Decoration.none;
};
return ViewPlugin.fromClass(
class {
constructor(view) {
this.decorations = compute(view);
}
update(update) {
if (update.selectionSet || update.docChanged || update.viewportChanged) {
this.decorations = compute(update.view);
}
}
},
{ decorations: (plugin) => plugin.decorations },
);
}
const config = await dioxus.recv();
const {
EditorView,
minimalSetup,
EditorState,
EditorSelection,
Annotation,
lineNumbers,
highlightActiveLineGutter,
highlightActiveLine,
highlightWhitespace,
rectangularSelection,
crosshairCursor,
keymap,
Decoration,
ViewPlugin,
HighlightStyle,
syntaxHighlighting,
bracketMatching,
indentOnInput,
SearchCursor,
closeBrackets,
closeBracketsKeymap,
indentWithTab,
tags,
LSPClient,
languageServerExtensions,
languages,
} = await codeMirrorLoad(config.cm_base);
let applyingRemote = false;
const remoteAnnotation = Annotation.define();
themeStylesInject();
const extensions = [
minimalSetup,
syntaxHighlighting(themeHighlightStyle()),
EditorView.updateListener.of((update) => {
if (update.docChanged && !applyingRemote) {
dioxus.send({ type: "doc_changed", doc: update.state.doc.toString() });
}
}),
];
if (config.line_numbers) {
extensions.push(lineNumbers(), highlightActiveLineGutter());
}
if (config.allow_multiple_selections) {
extensions.push(
EditorState.allowMultipleSelections.of(true),
keymap.of([
{ key: "Mod-d", run: selectNextMatch, preventDefault: true },
{ key: "Mod-F2", run: selectAllMatches, preventDefault: true },
]),
);
}
if (config.highlight_active_line) {
extensions.push(highlightActiveLine());
}
if (config.highlight_selection_matches) {
extensions.push(selectionMatchHighlighter());
}
if (config.bracket_matching) {
extensions.push(bracketMatching());
}
if (config.close_brackets) {
extensions.push(closeBrackets(), keymap.of(closeBracketsKeymap));
}
if (config.rectangular_selection) {
extensions.push(rectangularSelection(), crosshairCursor());
}
if (config.indent_on_input) {
extensions.push(indentOnInput());
}
if (config.highlight_whitespace) {
extensions.push(highlightWhitespace());
}
if (config.line_wrapping) {
extensions.push(EditorView.lineWrapping);
}
if (config.indent_with_tab) {
extensions.push(keymap.of([indentWithTab]));
}
if (config.read_only) {
extensions.push(EditorState.readOnly.of(true));
}
if (typeof config.tab_size === "number") {
extensions.push(EditorState.tabSize.of(config.tab_size));
}
if (config.language) {
const languageFactory = languages?.[config.language];
if (languageFactory) {
extensions.push(languageFactory());
} else {
console.warn(
`dioxus_codemirror: language "${config.language}" is not bundled; ` +
`enable its Cargo feature (lang-${config.language}) on dioxus_codemirror`,
);
}
}
let lspHandlers = [];
if (config.lsp_uri) {
try {
const transport = {
send(message) {
dioxus.send({ type: "lsp_message_recv", json: message });
},
subscribe(handler) {
lspHandlers.push(handler);
},
unsubscribe(handler) {
lspHandlers = lspHandlers.filter((h) => h !== handler);
},
};
const client = new LSPClient({
rootUri: config.lsp_uri.replace(/\/[^/]*$/, "") || config.lsp_uri,
timeout: 30000,
extensions: languageServerExtensions(),
}).connect(transport);
extensions.push(client.plugin(config.lsp_uri));
} catch (error) {
console.warn("dioxus_codemirror: LSP client setup failed", error);
}
}
const parent = await elementWait(config.mount_id);
let view;
try {
view = new EditorView({
state: EditorState.create({ doc: config.doc ?? "", extensions }),
parent,
});
} catch (error) {
console.error("dioxus_codemirror: editor creation failed for", config.mount_id, error);
throw error;
}
dioxus.send({ type: "ready" });
while (true) {
let cmd;
try {
cmd = await dioxus.recv();
} catch (error) {
break;
}
switch (cmd.type) {
case "doc_set": {
const current = view.state.doc.toString();
if (current === cmd.doc) {
break;
}
applyingRemote = true;
view.dispatch({
changes: { from: 0, to: current.length, insert: cmd.doc },
annotations: remoteAnnotation.of(true),
});
applyingRemote = false;
break;
}
case "lsp_message_send": {
for (const handler of lspHandlers) {
handler(cmd.json);
}
break;
}
case "destroy": {
view.destroy();
return;
}
default:
console.warn("dioxus_codemirror: unknown command", cmd);
}
}