Files
test-repo/website/playground/panels.jsx
T
800afd1eb3 Fix playground code folding (#19461)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: fisker <lionkay@gmail.com>
2026-06-26 11:54:59 +08:00

473 lines
11 KiB
React

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,
forceParsing,
} from "@codemirror/language";
import {
Compartment,
EditorState,
StateEffect,
StateField,
} from "@codemirror/state";
import {
Decoration,
EditorView,
keymap,
lineNumbers as lineNumbersExt,
} from "@codemirror/view";
import { onMounted, onUnmounted, useTemplateRef, watch } from "vue";
import { useTheme } from "./composables/use-theme.js";
import { getLanguageExtension } from "./panel/language.js";
function setup(props, { emit }) {
const editorRef = useTemplateRef("editorRef");
let _codeMirror = null;
let _cached = "";
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();
const onChange = (value) => {
emit("change", value);
};
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 = () => {
const options = { ...props };
for (const property of [
"ruler",
"rulerColor",
"value",
"selection",
"onChange",
]) {
delete options[property];
}
for (const [key, value] of Object.entries(options)) {
if (value === undefined) {
delete options[key];
}
}
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,
languageExt.of([]),
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,
}))
: []),
]),
EditorState.tabSize.of(props.tabSize ?? 4),
EditorState.readOnly.of(props.readOnly ?? false),
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,
});
updateSelection();
updateOverlay();
updateMode();
_cached = props.value;
};
const componentWillUnmount = () => {
_codeMirror?.destroy();
};
const foldCode = () => {
if (!_codeMirror) {
return;
}
const { autoFold, value } = props;
if (!value || !(autoFold instanceof RegExp)) {
return;
}
const effects = [];
let forceParsed = false;
for (const [lineIndex, lineText] of value.split("\n").entries()) {
if (!autoFold.test(lineText)) {
continue;
}
if (!forceParsed) {
forceParsed = true;
forceParsing(
_codeMirror,
_codeMirror.state.doc.length,
Number.POSITIVE_INFINITY,
);
}
const { from, to } = _codeMirror.state.doc.line(lineIndex + 1);
const range = foldable(_codeMirror.state, from, to);
if (!range) {
continue;
}
effects.push(foldEffect.of(range));
}
if (effects.length === 0) {
return;
}
_codeMirror.dispatch({ effects });
};
const updateValue = (value) => {
if (_cached === value) {
return;
}
_cached = value;
_codeMirror?.dispatch({
changes: {
from: 0,
to: _codeMirror.state.doc.length,
insert: value,
},
});
foldCode();
};
const updateSelection = () => {
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 },
});
}
};
const updateOverlay = () => {
_codeMirror?.dispatch({
effects: _overlay.of({
start: props.overlayStart,
end: props.overlayEnd,
}),
});
};
const updateMode = async () => {
_codeMirror?.dispatch({
effects: languageExt.reconfigure(await getLanguageExtension(props.mode)),
});
foldCode();
};
const handleChange = (value) => {
_cached = value;
onChange(value);
updateOverlay();
};
const handleSelectionChange = (selection) => {
if (!props.onSelectionChange || !selection) {
return;
}
props.onSelectionChange({
anchor: selection.anchor,
head: selection.head,
});
};
const render = () => (
<div class="editor input">
<div ref="editorRef" style={{ position: "absolute", inset: 0 }} />
<div class="editor__name">{props.name}</div>
</div>
);
onMounted(componentDidMount);
onUnmounted(componentWillUnmount);
watch(
() => props.value,
(value) => {
if (!_codeMirror) {
return;
}
if (value !== _cached) {
updateValue(value);
}
},
);
watch(() => props.mode, updateMode);
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,
),
});
});
return render;
}
const CodeMirrorPanel = {
name: "CodeMirrorPanel",
props: {
name: { type: String, required: true },
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 },
extraKeys: { type: Object, default: undefined },
},
emits: ["change"],
setup,
};
export function InputPanel(props) {
return (
<CodeMirrorPanel
lineNumbers={true}
keyMap="sublime"
autoCloseBrackets={true}
matchBrackets={true}
showCursorWhenSelecting={true}
tabSize={4}
rulerColor="color-mix(in oklab, currentColor 10%, transparent)"
{...props}
/>
);
}
export function OutputPanel(props) {
return (
<CodeMirrorPanel
readOnly={true}
lineNumbers={true}
rulerColor="currentColor"
{...props}
/>
);
}
export function DebugPanel({ mode, value, autoFold, name }) {
return (
<CodeMirrorPanel
readOnly={true}
lineNumbers={false}
foldGutter={true}
autoFold={autoFold}
mode={mode}
value={value}
name={name}
/>
);
}