Files
oldboy/src/lib/components/editor/Editor.svelte

220 lines
6.0 KiB
Svelte

<script lang="ts">
import { onMount, onDestroy } from 'svelte';
import { EditorView } from 'codemirror';
import { Compartment } from '@codemirror/state';
import { lineNumbers, highlightActiveLineGutter, highlightSpecialChars, drawSelection, dropCursor, rectangularSelection, crosshairCursor, highlightActiveLine, keymap } from '@codemirror/view';
import { defaultKeymap, history, historyKeymap } from '@codemirror/commands';
import { searchKeymap, highlightSelectionMatches } from '@codemirror/search';
import { autocompletion, completionKeymap, closeBrackets, closeBracketsKeymap } from '@codemirror/autocomplete';
import { foldGutter, indentOnInput, syntaxHighlighting, defaultHighlightStyle, bracketMatching, foldKeymap } from '@codemirror/language';
import { lintKeymap } from '@codemirror/lint';
import { csoundMode } from '@hlolli/codemirror-lang-csound';
import { oneDark } from '@codemirror/theme-one-dark';
import { vim } from '@replit/codemirror-vim';
import type { EditorSettingsStore } from '../../stores/editorSettings';
import {
flashField,
flash,
getSelection,
getBlock,
getDocument
} from '../../editor/block-eval';
interface Props {
value: string;
onChange?: (value: string) => void;
onExecute?: (code: string, source: 'selection' | 'block' | 'document') => void;
editorSettings: EditorSettingsStore;
mode: 'composition' | 'livecoding';
}
let {
value = '',
onChange,
onExecute,
editorSettings,
mode = 'composition'
}: Props = $props();
let editorContainer: HTMLDivElement;
let editorView: EditorView | null = null;
const lineNumbersCompartment = new Compartment();
const lineWrappingCompartment = new Compartment();
const vimCompartment = new Compartment();
function handleExecute() {
if (!editorView) return;
if (mode === 'composition') {
const doc = getDocument(editorView.state);
flash(editorView, doc.from, doc.to);
onExecute?.(doc.text, 'document');
return;
}
const selection = getSelection(editorView.state);
if (selection.text) {
flash(editorView, selection.from, selection.to);
onExecute?.(selection.text, 'selection');
return;
}
const block = getBlock(editorView.state);
if (block.text) {
flash(editorView, block.from, block.to);
onExecute?.(block.text, 'block');
return;
}
const doc = getDocument(editorView.state);
flash(editorView, doc.from, doc.to);
onExecute?.(doc.text, 'document');
}
const evaluateKeymap = keymap.of([
{
key: 'Mod-e',
run: () => {
handleExecute();
return true;
}
}
]);
onMount(() => {
const baseExtensions = [
highlightActiveLineGutter(),
highlightSpecialChars(),
history(),
foldGutter(),
drawSelection(),
dropCursor(),
indentOnInput(),
syntaxHighlighting(defaultHighlightStyle, { fallback: true }),
bracketMatching(),
closeBrackets(),
autocompletion(),
rectangularSelection(),
crosshairCursor(),
highlightSelectionMatches(),
keymap.of([
...closeBracketsKeymap,
...defaultKeymap,
...searchKeymap,
...historyKeymap,
...foldKeymap,
...completionKeymap,
...lintKeymap
])
];
const initSettings = $editorSettings;
editorView = new EditorView({
doc: value,
extensions: [
...baseExtensions,
csoundMode({ fileType: 'csd' }),
oneDark,
evaluateKeymap,
flashField(),
lineNumbersCompartment.of(initSettings.showLineNumbers ? lineNumbers() : []),
lineWrappingCompartment.of(initSettings.enableLineWrapping ? EditorView.lineWrapping : []),
vimCompartment.of(initSettings.vimMode ? vim() : []),
EditorView.updateListener.of((update) => {
if (update.docChanged && onChange) {
onChange(update.state.doc.toString());
}
}),
EditorView.theme({
'&': {
fontSize: `${initSettings.fontSize}px`,
fontFamily: initSettings.fontFamily
}
})
],
parent: editorContainer
});
});
$effect(() => {
const settings = $editorSettings;
if (!editorView) return;
editorView.dispatch({
effects: [
lineNumbersCompartment.reconfigure(settings.showLineNumbers ? lineNumbers() : []),
lineWrappingCompartment.reconfigure(settings.enableLineWrapping ? EditorView.lineWrapping : []),
vimCompartment.reconfigure(settings.vimMode ? vim() : [])
]
});
editorView.dom.style.fontSize = `${settings.fontSize}px`;
editorView.dom.style.fontFamily = settings.fontFamily;
});
onDestroy(() => {
if (editorView) {
editorView.destroy();
}
});
$effect(() => {
if (editorView && value !== editorView.state.doc.toString()) {
editorView.dispatch({
changes: { from: 0, to: editorView.state.doc.length, insert: value }
});
}
});
export function getSelectedText(): string | null {
if (!editorView) return null;
const { text } = getSelection(editorView.state);
return text || null;
}
export function getCurrentBlock(): string | null {
if (!editorView) return null;
const { text } = getBlock(editorView.state);
return text || null;
}
export function getFullDocument(): string {
if (!editorView) return '';
const { text } = getDocument(editorView.state);
return text;
}
export function evaluateWithFlash(text: string, from: number | null, to: number | null) {
if (editorView && from !== null && to !== null) {
flash(editorView, from, to);
}
}
</script>
<div class="editor-wrapper">
<div bind:this={editorContainer} class="editor-container"></div>
</div>
<style>
.editor-wrapper {
width: 100%;
height: 100%;
overflow: hidden;
}
.editor-container {
width: 100%;
height: 100%;
}
:global(.cm-editor) {
height: 100%;
}
:global(.cm-scroller) {
overflow: auto;
}
</style>