feat(dashboard): CodeMirror editors for Rhai source + JSON
Replaces the four <textarea> usages with a CodeMirror 6 editor that
brings, just by being a real editor: syntax highlighting, line
numbers, bracket matching, multi-cursor, proper undo/redo, and
search/replace (Ctrl+F / Ctrl+H). Plus a Rhai-aware autocomplete and
a "Format JSON" button on the test-invoke panels.
Per discussion, deliberately did NOT add: LSP, go-to-definition,
Rhai formatter (none exists), or anything else IDE-shaped. The
existing CodeEditor component is wired so swapping the language
extension later is a one-line change.
Lay of the land (from the research pass):
* No CodeMirror Rhai package exists on npm.
* No Rhai formatter exists anywhere.
* The Rhai authors publish a TextMate grammar at
rhaiscript/vscode-rhai (MPL-2.0). We don't load the full
grammar (would cost ~250KB of vscode-textmate + oniguruma);
we cite it as the source-of-truth for our keyword/operator
lists in a small custom StreamLanguage.
* rhaiscript/lsp exists but is experimental + unmaintained
since 2023; skipped.
Files:
* dashboard/src/lib/editor-theme.ts — CodeMirror theme +
HighlightStyle wired to the existing slate/sky palette so the
editor blends into the cards instead of looking transplanted.
* dashboard/src/lib/rhai-mode.ts — StreamLanguage tokenizer for
Rhai with the upstream grammar's keyword/operator lists, plus
a completion source pulling ctx.* / log::* from our SDK
contract suite (the authoritative list).
* dashboard/src/lib/CodeEditor.svelte — wraps EditorView with
two-way $bindable() value, language picker ('rhai' | 'json'),
placeholder, minHeight props. Guards against the update
listener echoing parent-driven changes back as edits.
* Replaces textareas in:
routes/+page.svelte — create form source
routes/scripts/[id]/+page.svelte — Edit tab source +
Test invoke body +
headers
* Format buttons next to the body/headers editors run
JSON.stringify(JSON.parse(value), null, 2); errors surface
inline next to the button without trashing the field.
Bundle:
* +~430KB to the CodeMirror chunk in dashboard build (~150KB
gzipped on the wire). Lazy-loaded — only fetched when a route
that uses CodeEditor renders.
* `npm install` clean, 0 vulnerabilities, `npm run check`
clean, `npm run build` clean.
No backend / API / SDK / schema / wire changes. No version bumps.
This commit is contained in:
107
dashboard/src/lib/CodeEditor.svelte
Normal file
107
dashboard/src/lib/CodeEditor.svelte
Normal file
@@ -0,0 +1,107 @@
|
||||
<!--
|
||||
CodeMirror-backed text editor for the dashboard.
|
||||
|
||||
Replaces our plain <textarea> with line numbers, syntax highlighting,
|
||||
bracket matching, search/replace (Ctrl+F), and language-aware
|
||||
autocomplete. Two-way bound via `value`; the parent treats it the
|
||||
same as a textarea.
|
||||
|
||||
Languages: `rhai` (custom mode, ./rhai-mode.ts) and `json`
|
||||
(@codemirror/lang-json).
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { onMount, onDestroy } from 'svelte';
|
||||
import { basicSetup } from 'codemirror';
|
||||
import { EditorView, keymap, placeholder as cmPlaceholder } from '@codemirror/view';
|
||||
import { EditorState } from '@codemirror/state';
|
||||
import { indentWithTab } from '@codemirror/commands';
|
||||
import { json as jsonLang } from '@codemirror/lang-json';
|
||||
import { rhai as rhaiLang } from './rhai-mode';
|
||||
import { dashboardSyntaxHighlighting, dashboardTheme } from './editor-theme';
|
||||
|
||||
type Language = 'rhai' | 'json';
|
||||
|
||||
let {
|
||||
value = $bindable(''),
|
||||
language = 'rhai' as Language,
|
||||
placeholder = '',
|
||||
minHeight = '12rem'
|
||||
}: {
|
||||
value?: string;
|
||||
language?: Language;
|
||||
placeholder?: string;
|
||||
minHeight?: string;
|
||||
} = $props();
|
||||
|
||||
let host: HTMLDivElement | null = null;
|
||||
let view: EditorView | null = null;
|
||||
// Guard against the update-listener firing while we're pushing an
|
||||
// external value into the editor — without this, parent-driven
|
||||
// `value` changes would echo back through the listener and create a
|
||||
// dispatch loop in Svelte 5 reactivity.
|
||||
let pushingFromOutside = false;
|
||||
|
||||
function buildExtensions(lang: Language) {
|
||||
const extensions = [
|
||||
basicSetup,
|
||||
lang === 'json' ? jsonLang() : rhaiLang(),
|
||||
keymap.of([indentWithTab]),
|
||||
dashboardSyntaxHighlighting,
|
||||
dashboardTheme,
|
||||
EditorView.updateListener.of((update) => {
|
||||
if (update.docChanged && !pushingFromOutside) {
|
||||
value = update.state.doc.toString();
|
||||
}
|
||||
})
|
||||
];
|
||||
if (placeholder) extensions.push(cmPlaceholder(placeholder));
|
||||
return extensions;
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
if (!host) return;
|
||||
view = new EditorView({
|
||||
state: EditorState.create({
|
||||
doc: value,
|
||||
extensions: buildExtensions(language)
|
||||
}),
|
||||
parent: host
|
||||
});
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
view?.destroy();
|
||||
view = null;
|
||||
});
|
||||
|
||||
// Push parent-driven `value` updates back into the editor (e.g.
|
||||
// when the script is reloaded after Save, or "Format JSON" rewrites
|
||||
// the body). We only dispatch when the document genuinely differs
|
||||
// from the current `value`.
|
||||
$effect(() => {
|
||||
if (!view) return;
|
||||
const current = view.state.doc.toString();
|
||||
if (current !== value) {
|
||||
pushingFromOutside = true;
|
||||
view.dispatch({
|
||||
changes: { from: 0, to: current.length, insert: value }
|
||||
});
|
||||
pushingFromOutside = false;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<div bind:this={host} class="cm-host" style:min-height={minHeight}></div>
|
||||
|
||||
<style>
|
||||
.cm-host :global(.cm-editor) {
|
||||
height: 100%;
|
||||
border-radius: 0.375rem;
|
||||
border: 1px solid #334155;
|
||||
overflow: hidden;
|
||||
}
|
||||
.cm-host :global(.cm-scroller) {
|
||||
font-family:
|
||||
ui-monospace, SFMono-Regular, Menlo, Consolas, 'Liberation Mono', monospace;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user