2025-12-30 03:11:04 +08:00
|
|
|
import { catppuccinLatte, catppuccinMocha } from "@catppuccin/codemirror";
|
|
|
|
|
import { closeBrackets, closeBracketsKeymap } from "@codemirror/autocomplete";
|
|
|
|
|
import {
|
|
|
|
|
defaultKeymap,
|
|
|
|
|
history,
|
|
|
|
|
historyKeymap,
|
|
|
|
|
indentWithTab,
|
|
|
|
|
} from "@codemirror/commands";
|
|
|
|
|
import {
|
|
|
|
|
bracketMatching,
|
|
|
|
|
foldable,
|
|
|
|
|
foldEffect,
|
|
|
|
|
foldGutter as foldGutterExt,
|
|
|
|
|
foldKeymap,
|
|
|
|
|
} from "@codemirror/language";
|
|
|
|
|
import {
|
|
|
|
|
Compartment,
|
|
|
|
|
EditorState,
|
|
|
|
|
StateEffect,
|
|
|
|
|
StateField,
|
|
|
|
|
} from "@codemirror/state";
|
|
|
|
|
import {
|
|
|
|
|
Decoration,
|
|
|
|
|
EditorView,
|
|
|
|
|
keymap,
|
|
|
|
|
lineNumbers as lineNumbersExt,
|
|
|
|
|
} from "@codemirror/view";
|
2025-12-10 23:48:10 +08:00
|
|
|
import { onMounted, onUnmounted, useTemplateRef, watch } from "vue";
|
2025-12-30 03:11:04 +08:00
|
|
|
import { useTheme } from "./composables/use-theme.js";
|
2025-12-30 17:11:09 +08:00
|
|
|
import { getLanguageExtension } from "./panel/language.js";
|
2018-04-11 18:22:03 -03:00
|
|
|
|
2025-12-10 23:48:10 +08:00
|
|
|
function setup(props, { emit }) {
|
2025-12-30 03:11:04 +08:00
|
|
|
const editorRef = useTemplateRef("editorRef");
|
|
|
|
|
|
2025-12-10 23:48:10 +08:00
|
|
|
let _codeMirror = null;
|
|
|
|
|
let _cached = "";
|
2025-12-30 03:11:04 +08:00
|
|
|
const _overlay = StateEffect.define();
|
|
|
|
|
|
|
|
|
|
const overlayField = StateField.define({
|
|
|
|
|
create() {
|
|
|
|
|
return Decoration.none;
|
|
|
|
|
},
|
|
|
|
|
update(decorations, tr) {
|
|
|
|
|
for (const effect of tr.effects) {
|
|
|
|
|
if (effect.is(_overlay)) {
|
|
|
|
|
let { start, end } = effect.value;
|
|
|
|
|
if (start !== undefined && end !== undefined) {
|
|
|
|
|
if (end < start) {
|
|
|
|
|
[start, end] = [end, start];
|
|
|
|
|
}
|
|
|
|
|
const { length } = _codeMirror.state.doc;
|
|
|
|
|
start = Math.min(Math.max(start, 0), length);
|
|
|
|
|
end = Math.min(Math.max(end, 0), length);
|
|
|
|
|
if (start !== end) {
|
|
|
|
|
return Decoration.set([overlayMark.range(start, end)]);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return Decoration.none;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return decorations.map(tr.changes);
|
|
|
|
|
},
|
|
|
|
|
provide: (f) => EditorView.decorations.from(f),
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const overlayMark = Decoration.mark({
|
|
|
|
|
class: "cm-overlay-highlight",
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const languageExt = new Compartment();
|
|
|
|
|
const rulerTheme = new Compartment();
|
|
|
|
|
const foldGutterCompartment = new Compartment();
|
|
|
|
|
|
|
|
|
|
const { theme } = useTheme();
|
|
|
|
|
const editorTheme = new Compartment();
|
|
|
|
|
|
2025-12-10 23:48:10 +08:00
|
|
|
const onChange = (value) => {
|
|
|
|
|
emit("change", value);
|
|
|
|
|
};
|
2018-04-19 14:46:29 -03:00
|
|
|
|
2025-12-30 03:11:04 +08:00
|
|
|
const createRulerTheme = (printWidth, color) => {
|
|
|
|
|
if (!printWidth) {
|
|
|
|
|
return EditorView.theme({});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return EditorView.theme({
|
|
|
|
|
".cm-content::before": {
|
|
|
|
|
content: '""',
|
|
|
|
|
position: "absolute",
|
|
|
|
|
top: "0",
|
|
|
|
|
bottom: "0",
|
|
|
|
|
left: `${printWidth}ch`,
|
|
|
|
|
width: "1px",
|
|
|
|
|
backgroundColor: color || "var(--color-gray-300)",
|
|
|
|
|
pointerEvents: "none",
|
|
|
|
|
zIndex: "1",
|
|
|
|
|
marginLeft: "6px",
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const componentDidMount = async () => {
|
2025-12-10 23:48:10 +08:00
|
|
|
const options = { ...props };
|
|
|
|
|
for (const property of [
|
|
|
|
|
"ruler",
|
|
|
|
|
"rulerColor",
|
|
|
|
|
"value",
|
|
|
|
|
"selection",
|
|
|
|
|
"onChange",
|
|
|
|
|
]) {
|
|
|
|
|
delete options[property];
|
|
|
|
|
}
|
2020-04-13 14:59:49 +03:00
|
|
|
|
2025-12-10 23:48:10 +08:00
|
|
|
for (const [key, value] of Object.entries(options)) {
|
|
|
|
|
if (value === undefined) {
|
|
|
|
|
delete options[key];
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-30 14:14:17 +08:00
|
|
|
const lang = await getLanguageExtension(props.mode);
|
|
|
|
|
|
2025-12-30 03:11:04 +08:00
|
|
|
const state = EditorState.create({
|
|
|
|
|
doc: props.value || "",
|
|
|
|
|
extensions: [
|
|
|
|
|
props.lineNumbers ? lineNumbersExt() : undefined,
|
|
|
|
|
foldGutterCompartment.of(
|
|
|
|
|
props.foldGutter
|
|
|
|
|
? foldGutterExt({
|
|
|
|
|
openText: "▾",
|
|
|
|
|
closedText: "▸",
|
|
|
|
|
})
|
|
|
|
|
: [],
|
|
|
|
|
),
|
|
|
|
|
props.autoCloseBrackets ? closeBrackets() : undefined,
|
|
|
|
|
props.matchBrackets ? bracketMatching() : undefined,
|
2025-12-10 23:48:10 +08:00
|
|
|
|
2025-12-30 14:14:17 +08:00
|
|
|
languageExt.of(lang),
|
2025-12-30 03:11:04 +08:00
|
|
|
editorTheme.of(
|
|
|
|
|
theme.value === "dark" ? catppuccinMocha : catppuccinLatte,
|
|
|
|
|
),
|
|
|
|
|
overlayField,
|
|
|
|
|
history(),
|
|
|
|
|
keymap.of([
|
|
|
|
|
...defaultKeymap,
|
|
|
|
|
...closeBracketsKeymap,
|
|
|
|
|
...historyKeymap,
|
|
|
|
|
...foldKeymap,
|
|
|
|
|
indentWithTab,
|
|
|
|
|
{ key: "Ctrl-l", run: () => false },
|
|
|
|
|
...(props.extraKeys
|
|
|
|
|
? Object.entries(props.extraKeys).map(([key, cmd]) => ({
|
|
|
|
|
key,
|
|
|
|
|
run: cmd,
|
|
|
|
|
}))
|
|
|
|
|
: []),
|
|
|
|
|
]),
|
2025-12-10 23:48:10 +08:00
|
|
|
|
2025-12-30 03:11:04 +08:00
|
|
|
EditorState.tabSize.of(props.tabSize ?? 4),
|
|
|
|
|
EditorState.readOnly.of(props.readOnly ?? false),
|
2018-04-17 14:50:53 -03:00
|
|
|
|
2025-12-30 03:11:04 +08:00
|
|
|
rulerTheme.of(createRulerTheme(props.ruler, props.rulerColor)),
|
|
|
|
|
|
|
|
|
|
EditorView.updateListener.of((update) => {
|
|
|
|
|
if (update.docChanged) {
|
|
|
|
|
const value = update.state.doc.toString();
|
|
|
|
|
if (value !== _cached) {
|
|
|
|
|
handleChange(value);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (update.selectionSet) {
|
|
|
|
|
const selection = update.state.selection.main;
|
|
|
|
|
handleSelectionChange(selection);
|
|
|
|
|
}
|
|
|
|
|
}),
|
|
|
|
|
].filter(Boolean),
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
_codeMirror = new EditorView({
|
|
|
|
|
state,
|
|
|
|
|
parent: editorRef.value,
|
|
|
|
|
});
|
2019-05-22 11:17:46 -05:00
|
|
|
|
2025-12-10 23:48:10 +08:00
|
|
|
updateSelection();
|
|
|
|
|
updateOverlay();
|
|
|
|
|
};
|
2018-04-11 18:22:03 -03:00
|
|
|
|
2025-12-10 23:48:10 +08:00
|
|
|
const componentWillUnmount = () => {
|
2025-12-30 03:11:04 +08:00
|
|
|
_codeMirror?.destroy();
|
2025-12-10 23:48:10 +08:00
|
|
|
};
|
2018-04-11 18:22:03 -03:00
|
|
|
|
2025-12-10 23:48:10 +08:00
|
|
|
const updateValue = (value) => {
|
2025-12-30 03:11:04 +08:00
|
|
|
if (_cached === value) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
2025-12-10 23:48:10 +08:00
|
|
|
_cached = value;
|
2025-12-30 03:11:04 +08:00
|
|
|
_codeMirror.dispatch({
|
|
|
|
|
changes: {
|
|
|
|
|
from: 0,
|
|
|
|
|
to: _codeMirror.state.doc.length,
|
|
|
|
|
insert: value,
|
|
|
|
|
},
|
|
|
|
|
});
|
2020-05-06 03:35:12 +03:00
|
|
|
|
2025-12-30 03:11:04 +08:00
|
|
|
if (!(props.autoFold instanceof RegExp)) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const lines = value.split("\n");
|
|
|
|
|
const effects = [];
|
|
|
|
|
|
|
|
|
|
for (let i = lines.length - 1; i >= 0; i--) {
|
|
|
|
|
if (props.autoFold.test(lines[i])) {
|
|
|
|
|
const lineNumber = i + 1;
|
|
|
|
|
const range = foldable(
|
|
|
|
|
_codeMirror.state,
|
|
|
|
|
_codeMirror.state.doc.line(lineNumber).from,
|
|
|
|
|
_codeMirror.state.doc.line(lineNumber).to,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
if (range) {
|
|
|
|
|
effects.push(foldEffect.of(range));
|
2020-05-06 03:35:12 +03:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-12-30 03:11:04 +08:00
|
|
|
|
|
|
|
|
if (effects.length > 0) {
|
|
|
|
|
_codeMirror.dispatch({ effects });
|
|
|
|
|
}
|
2025-12-10 23:48:10 +08:00
|
|
|
};
|
2018-04-17 14:50:53 -03:00
|
|
|
|
2025-12-10 23:48:10 +08:00
|
|
|
const updateSelection = () => {
|
2025-12-30 03:11:04 +08:00
|
|
|
if (!props.selection || !_codeMirror) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const { doc } = _codeMirror.state;
|
|
|
|
|
const anchor = Math.max(props.selection.anchor ?? 0, 0);
|
|
|
|
|
const head = Math.min(props.selection.head ?? anchor, doc.length);
|
|
|
|
|
|
|
|
|
|
const currentSelection = _codeMirror.state.selection.main;
|
|
|
|
|
if (currentSelection.anchor !== anchor || currentSelection.head !== head) {
|
|
|
|
|
_codeMirror.dispatch({
|
|
|
|
|
selection: { anchor, head },
|
|
|
|
|
});
|
|
|
|
|
}
|
2025-12-10 23:48:10 +08:00
|
|
|
};
|
2022-09-01 23:20:56 +03:00
|
|
|
|
2025-12-10 23:48:10 +08:00
|
|
|
const updateOverlay = () => {
|
2025-12-30 03:11:04 +08:00
|
|
|
if (!_codeMirror) {
|
|
|
|
|
return;
|
2018-04-19 18:57:11 -03:00
|
|
|
}
|
2025-12-30 03:11:04 +08:00
|
|
|
|
|
|
|
|
_codeMirror.dispatch({
|
|
|
|
|
effects: _overlay.of({
|
|
|
|
|
start: props.overlayStart,
|
|
|
|
|
end: props.overlayEnd,
|
|
|
|
|
}),
|
|
|
|
|
});
|
2025-12-10 23:48:10 +08:00
|
|
|
};
|
2018-04-19 18:57:11 -03:00
|
|
|
|
2025-12-30 03:11:04 +08:00
|
|
|
const handleChange = (value) => {
|
|
|
|
|
_cached = value;
|
|
|
|
|
onChange(value);
|
|
|
|
|
updateOverlay();
|
2025-12-10 23:48:10 +08:00
|
|
|
};
|
2018-07-01 16:17:25 +08:00
|
|
|
|
2025-12-30 03:11:04 +08:00
|
|
|
const handleSelectionChange = (selection) => {
|
|
|
|
|
if (!props.onSelectionChange || !selection) {
|
|
|
|
|
return;
|
2018-04-11 18:22:03 -03:00
|
|
|
}
|
|
|
|
|
|
2025-12-30 03:11:04 +08:00
|
|
|
props.onSelectionChange({
|
|
|
|
|
anchor: selection.anchor,
|
|
|
|
|
head: selection.head,
|
|
|
|
|
});
|
2025-12-10 23:48:10 +08:00
|
|
|
};
|
2022-07-23 22:46:16 +03:00
|
|
|
|
2025-12-10 23:48:10 +08:00
|
|
|
const render = () => (
|
|
|
|
|
<div class="editor input">
|
2025-12-30 03:11:04 +08:00
|
|
|
<div ref="editorRef" style={{ position: "absolute", inset: 0 }} />
|
2025-12-10 23:48:10 +08:00
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
onMounted(componentDidMount);
|
|
|
|
|
onUnmounted(componentWillUnmount);
|
2025-12-30 03:11:04 +08:00
|
|
|
|
|
|
|
|
watch(
|
|
|
|
|
() => props.value,
|
|
|
|
|
(value) => {
|
|
|
|
|
if (!_codeMirror) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (value !== _cached) {
|
|
|
|
|
updateValue(value);
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
watch(
|
|
|
|
|
() => props.mode,
|
|
|
|
|
async (mode) => {
|
|
|
|
|
if (!_codeMirror) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
_codeMirror.dispatch({
|
|
|
|
|
effects: languageExt.reconfigure(await getLanguageExtension(mode)),
|
|
|
|
|
});
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
watch(
|
|
|
|
|
() => props.ruler,
|
|
|
|
|
(ruler) => {
|
|
|
|
|
if (!_codeMirror) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
_codeMirror.dispatch({
|
|
|
|
|
effects: rulerTheme.reconfigure(
|
|
|
|
|
createRulerTheme(ruler, props.rulerColor),
|
|
|
|
|
),
|
|
|
|
|
});
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
watch(
|
|
|
|
|
() => props.rulerColor,
|
|
|
|
|
(rulerColor) => {
|
|
|
|
|
if (!_codeMirror) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
_codeMirror.dispatch({
|
|
|
|
|
effects: rulerTheme.reconfigure(
|
|
|
|
|
createRulerTheme(props.ruler, rulerColor),
|
|
|
|
|
),
|
|
|
|
|
});
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
watch(
|
|
|
|
|
() => props.selection,
|
|
|
|
|
() => {
|
|
|
|
|
if (!_codeMirror) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
updateSelection();
|
|
|
|
|
},
|
|
|
|
|
{ deep: true },
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
watch(
|
|
|
|
|
() => [props.overlayStart, props.overlayEnd],
|
|
|
|
|
() => {
|
|
|
|
|
if (!_codeMirror) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
updateOverlay();
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
watch(
|
|
|
|
|
() => props.foldGutter,
|
|
|
|
|
(foldGutter) => {
|
|
|
|
|
if (!_codeMirror) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
_codeMirror.dispatch({
|
|
|
|
|
effects: foldGutterCompartment.reconfigure(
|
|
|
|
|
foldGutter ? foldGutterExt() : [],
|
|
|
|
|
),
|
|
|
|
|
});
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
watch(theme, (newTheme) => {
|
|
|
|
|
_codeMirror.dispatch({
|
|
|
|
|
effects: editorTheme.reconfigure(
|
|
|
|
|
newTheme === "dark" ? catppuccinMocha : catppuccinLatte,
|
|
|
|
|
),
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
2025-12-10 23:48:10 +08:00
|
|
|
return render;
|
2018-04-11 18:22:03 -03:00
|
|
|
}
|
|
|
|
|
|
2025-12-10 23:48:10 +08:00
|
|
|
const CodeMirrorPanel = {
|
|
|
|
|
name: "CodeMirrorPanel",
|
|
|
|
|
props: {
|
|
|
|
|
value: { type: String, required: true },
|
|
|
|
|
selection: { type: Object, default: undefined },
|
|
|
|
|
onSelectionChange: { type: Function, default: undefined },
|
|
|
|
|
ruler: { type: Number, default: undefined },
|
|
|
|
|
rulerColor: { type: String, default: undefined },
|
|
|
|
|
overlayStart: { type: Number, default: undefined },
|
|
|
|
|
overlayEnd: { type: Number, default: undefined },
|
|
|
|
|
mode: { type: String, required: true },
|
|
|
|
|
foldGutter: { type: Boolean, default: undefined },
|
|
|
|
|
autoFold: { type: RegExp, default: undefined },
|
|
|
|
|
codeSample: { type: String, default: undefined },
|
|
|
|
|
lineNumbers: { type: Boolean, default: undefined },
|
|
|
|
|
keyMap: { type: String, default: undefined },
|
|
|
|
|
autoCloseBrackets: { type: Boolean, default: undefined },
|
|
|
|
|
matchBrackets: { type: Boolean, default: undefined },
|
|
|
|
|
showCursorWhenSelecting: { type: Boolean, default: undefined },
|
|
|
|
|
tabSize: { type: Number, default: undefined },
|
|
|
|
|
readOnly: { type: Boolean, default: undefined },
|
2025-12-30 03:11:04 +08:00
|
|
|
extraKeys: { type: Object, default: undefined },
|
2025-12-10 23:48:10 +08:00
|
|
|
},
|
|
|
|
|
emits: ["change"],
|
|
|
|
|
setup,
|
|
|
|
|
};
|
|
|
|
|
|
2018-04-19 14:46:29 -03:00
|
|
|
export function InputPanel(props) {
|
2018-04-11 18:22:03 -03:00
|
|
|
return (
|
|
|
|
|
<CodeMirrorPanel
|
2018-04-19 14:46:29 -03:00
|
|
|
lineNumbers={true}
|
|
|
|
|
keyMap="sublime"
|
|
|
|
|
autoCloseBrackets={true}
|
|
|
|
|
matchBrackets={true}
|
|
|
|
|
showCursorWhenSelecting={true}
|
2018-07-01 16:17:25 +08:00
|
|
|
tabSize={4}
|
2025-11-01 17:48:30 -07:00
|
|
|
rulerColor="color-mix(in oklab, currentColor 10%, transparent)"
|
2018-04-19 14:46:29 -03:00
|
|
|
{...props}
|
2018-04-11 18:22:03 -03:00
|
|
|
/>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2018-04-19 14:46:29 -03:00
|
|
|
export function OutputPanel(props) {
|
2018-04-11 18:22:03 -03:00
|
|
|
return (
|
|
|
|
|
<CodeMirrorPanel
|
2018-04-19 14:46:29 -03:00
|
|
|
readOnly={true}
|
|
|
|
|
lineNumbers={true}
|
2025-11-01 17:48:30 -07:00
|
|
|
rulerColor="currentColor"
|
2018-04-19 14:46:29 -03:00
|
|
|
{...props}
|
2018-04-11 18:22:03 -03:00
|
|
|
/>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-30 17:11:09 +08:00
|
|
|
export function DebugPanel({ mode, value, autoFold }) {
|
2018-04-11 18:22:03 -03:00
|
|
|
return (
|
|
|
|
|
<CodeMirrorPanel
|
2018-04-19 14:46:29 -03:00
|
|
|
readOnly={true}
|
|
|
|
|
lineNumbers={false}
|
2020-04-13 14:59:49 +03:00
|
|
|
foldGutter={true}
|
2020-05-06 03:35:12 +03:00
|
|
|
autoFold={autoFold}
|
2025-12-30 17:11:09 +08:00
|
|
|
mode={mode}
|
2018-04-11 18:22:03 -03:00
|
|
|
value={value}
|
|
|
|
|
/>
|
|
|
|
|
);
|
|
|
|
|
}
|