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:
@@ -13,6 +13,18 @@
|
||||
} from '$lib/api';
|
||||
import { logLevelColor, statusColor } from '$lib/styles';
|
||||
import { guessHostKind, guessPathKind, pathKindMismatchWarning } from '$lib/route-utils';
|
||||
import CodeEditor from '$lib/CodeEditor.svelte';
|
||||
|
||||
/// Pretty-print a JSON string in place, leaving it untouched if the
|
||||
/// input doesn't parse. The error state is shown next to the button
|
||||
/// so users see why it didn't reformat.
|
||||
function formatJson(s: string): { ok: true; text: string } | { ok: false; error: string } {
|
||||
try {
|
||||
return { ok: true, text: JSON.stringify(JSON.parse(s), null, 2) };
|
||||
} catch (e) {
|
||||
return { ok: false, error: e instanceof Error ? e.message : String(e) };
|
||||
}
|
||||
}
|
||||
|
||||
// Route is `/scripts/[id]` so `page.params.id` is always present.
|
||||
let id = $derived(page.params.id ?? '');
|
||||
@@ -72,7 +84,28 @@
|
||||
|
||||
let testBody = $state('{}');
|
||||
let testHeaders = $state('{}');
|
||||
let testBodyFormatError = $state<string | null>(null);
|
||||
let testHeadersFormatError = $state<string | null>(null);
|
||||
let testInProgress = $state(false);
|
||||
|
||||
function formatTestBody() {
|
||||
const r = formatJson(testBody);
|
||||
if (r.ok) {
|
||||
testBody = r.text;
|
||||
testBodyFormatError = null;
|
||||
} else {
|
||||
testBodyFormatError = r.error;
|
||||
}
|
||||
}
|
||||
function formatTestHeaders() {
|
||||
const r = formatJson(testHeaders);
|
||||
if (r.ok) {
|
||||
testHeaders = r.text;
|
||||
testHeadersFormatError = null;
|
||||
} else {
|
||||
testHeadersFormatError = r.error;
|
||||
}
|
||||
}
|
||||
let testResult = $state<{
|
||||
status: number;
|
||||
headers: Record<string, string>;
|
||||
@@ -352,7 +385,7 @@
|
||||
<div class="grid">
|
||||
<section class="card">
|
||||
<h2>Source</h2>
|
||||
<textarea bind:value={editableSource} rows="14" spellcheck="false"></textarea>
|
||||
<CodeEditor bind:value={editableSource} language="rhai" minHeight="22rem" />
|
||||
{#if saveSourceError}
|
||||
<div class="error inline">{saveSourceError}</div>
|
||||
{/if}
|
||||
@@ -369,14 +402,30 @@
|
||||
|
||||
<section class="card">
|
||||
<h2>Test invoke</h2>
|
||||
<label>
|
||||
<span>Request body (JSON)</span>
|
||||
<textarea bind:value={testBody} rows="5" spellcheck="false"></textarea>
|
||||
</label>
|
||||
<label>
|
||||
<span>Headers (JSON object)</span>
|
||||
<textarea bind:value={testHeaders} rows="3" spellcheck="false"></textarea>
|
||||
</label>
|
||||
<div class="json-block">
|
||||
<header class="json-header">
|
||||
<span>Request body (JSON)</span>
|
||||
<button type="button" class="ghost small" onclick={formatTestBody}>
|
||||
Format
|
||||
</button>
|
||||
</header>
|
||||
<CodeEditor bind:value={testBody} language="json" minHeight="9rem" />
|
||||
{#if testBodyFormatError}
|
||||
<div class="error inline">{testBodyFormatError}</div>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="json-block">
|
||||
<header class="json-header">
|
||||
<span>Headers (JSON object)</span>
|
||||
<button type="button" class="ghost small" onclick={formatTestHeaders}>
|
||||
Format
|
||||
</button>
|
||||
</header>
|
||||
<CodeEditor bind:value={testHeaders} language="json" minHeight="6rem" />
|
||||
{#if testHeadersFormatError}
|
||||
<div class="error inline">{testHeadersFormatError}</div>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="actions">
|
||||
<button type="button" onclick={invoke} disabled={testInProgress}>
|
||||
{testInProgress ? 'Running…' : 'Send'}
|
||||
@@ -727,6 +776,24 @@
|
||||
color: #94a3b8;
|
||||
border: 1px solid #334155;
|
||||
}
|
||||
button.small {
|
||||
padding: 0.2rem 0.6rem;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.json-block {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.4rem;
|
||||
}
|
||||
.json-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
font-size: 0.85rem;
|
||||
color: #cbd5e1;
|
||||
}
|
||||
button.link {
|
||||
background: transparent;
|
||||
color: #94a3b8;
|
||||
@@ -798,7 +865,6 @@
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
textarea,
|
||||
input,
|
||||
select {
|
||||
background: #0b1220;
|
||||
@@ -811,12 +877,6 @@
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
textarea {
|
||||
font-family:
|
||||
ui-monospace, SFMono-Regular, Menlo, Consolas, 'Liberation Mono', monospace;
|
||||
font-size: 0.85rem;
|
||||
resize: vertical;
|
||||
}
|
||||
input:disabled,
|
||||
select:disabled {
|
||||
opacity: 0.5;
|
||||
|
||||
Reference in New Issue
Block a user