currentText was $state, and the validated-state sync $effect both reads and writes it. Every keystroke (which sets currentText in the CodeMirror updateListener) therefore re-ran the effect while re-validation was still in flight, dispatching a full-document replacement with the stale validatedState text — visibly reverting the keystroke and resetting the cursor until validation caught up. The effect only needs to react to validatedState publishes, so make currentText a plain variable, matching DesktopEditor. No unit test: the repo has no component-test harness, and CodeMirror cannot mount under jsdom without one. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
101 lines
3.1 KiB
Svelte
101 lines
3.1 KiB
Svelte
<script lang="ts">
|
|
import type { EditorProps } from '$/types';
|
|
import { validatedState } from '$/util/state.svelte';
|
|
import { json, jsonLanguage } from '@codemirror/lang-json';
|
|
import { markdown } from '@codemirror/lang-markdown';
|
|
import { yamlFrontmatter } from '@codemirror/lang-yaml';
|
|
import { language } from '@codemirror/language';
|
|
import { Compartment, EditorState } from '@codemirror/state';
|
|
import { EditorView } from '@codemirror/view';
|
|
import { vsCodeDark } from '@fsegurai/codemirror-theme-vscode-dark';
|
|
import { vsCodeLight } from '@fsegurai/codemirror-theme-vscode-light';
|
|
import { basicSetup } from 'codemirror';
|
|
import { mode } from 'mode-watcher';
|
|
import { onMount } from 'svelte';
|
|
|
|
let editorView: EditorView | undefined;
|
|
let editorContainer: HTMLDivElement;
|
|
// Deliberately not $state: the sync effect below both reads and writes it,
|
|
// so a reactive currentText would make every keystroke re-run the effect
|
|
// against the not-yet-revalidated state and revert the user's input.
|
|
let currentText = '';
|
|
const themeCompartment = new Compartment();
|
|
const languageCompartment = new Compartment();
|
|
|
|
const { onUpdate }: EditorProps = $props();
|
|
|
|
$effect(() => {
|
|
editorView?.dispatch({
|
|
effects: themeCompartment.reconfigure(mode.current === 'dark' ? vsCodeDark : vsCodeLight)
|
|
});
|
|
});
|
|
|
|
onMount(() => {
|
|
editorView = new EditorView({
|
|
state: EditorState.create({
|
|
doc: currentText,
|
|
extensions: [
|
|
basicSetup,
|
|
languageCompartment.of([]),
|
|
themeCompartment.of([]),
|
|
EditorView.updateListener.of((update) => {
|
|
if (update.docChanged) {
|
|
const newText = update.state.doc.toString();
|
|
if (currentText === newText) {
|
|
return;
|
|
}
|
|
currentText = newText;
|
|
onUpdate(newText);
|
|
}
|
|
}),
|
|
EditorView.theme({
|
|
'&.cm-focused': {
|
|
outline: 'none'
|
|
},
|
|
'&.cm-editor': {
|
|
height: '100%'
|
|
},
|
|
'&.cm-scroller': {
|
|
overflow: 'auto'
|
|
}
|
|
})
|
|
]
|
|
}),
|
|
parent: editorContainer
|
|
});
|
|
|
|
return () => {
|
|
editorView?.destroy();
|
|
};
|
|
});
|
|
|
|
$effect(() => {
|
|
const { editorMode, code, mermaid } = validatedState.current;
|
|
const text = editorMode === 'code' ? code : mermaid;
|
|
if (currentText === text || !editorView) {
|
|
return;
|
|
}
|
|
currentText = text;
|
|
editorView.dispatch({
|
|
changes: {
|
|
from: 0,
|
|
to: editorView.state.doc.length,
|
|
insert: text
|
|
}
|
|
});
|
|
const stateLanguage = editorView.state.facet(language);
|
|
const isStateJson = stateLanguage === jsonLanguage;
|
|
const isCodeJson = editorMode === 'config';
|
|
if (stateLanguage && isStateJson === isCodeJson) {
|
|
return;
|
|
}
|
|
editorView.dispatch({
|
|
effects: languageCompartment.reconfigure(
|
|
isCodeJson ? json() : yamlFrontmatter({ content: markdown() })
|
|
)
|
|
});
|
|
});
|
|
</script>
|
|
|
|
<div bind:this={editorContainer} class="size-full"></div>
|