Stub out story/controls and use code-mirror with vim for text-editing

This commit is contained in:
Yura Dupyn 2026-02-13 20:06:43 +01:00
parent c255e19c42
commit 6a0f95812a
11 changed files with 383 additions and 44 deletions

View file

@ -1,26 +1,20 @@
import { createSignal } from 'solid-js';
import { ExprREPL } from './REPL';
function Hello() {
const [ count, setCount ] = createSignal(0);
return (
<div>
{ count() > 5 ? <span style={{ color: "red" }}>too damn high</span> : <span>{ count() }</span> }
<div>
<button onClick={ () => setCount((x: number) => x + 1) }>+</button>
<button onClick={ () => setCount((x: number) => Math.max(x - 1, 0)) }>-</button>
</div>
</div>
);
}
import { Story } from './Story';
import { Controls } from './Controls';
export default function App() {
return (
<div>
<Hello />
<ExprREPL />
</div>
<main
class="container"
style={{
display: "grid",
"grid-template-columns": "2fr 1fr",
gap: "2rem",
"margin-top": "2rem",
}}
>
<Story />
<Controls />
</main>
);
}

67
src/ui/CodeEditor.tsx Normal file
View file

@ -0,0 +1,67 @@
import { onMount, onCleanup } from "solid-js";
import { EditorView, basicSetup } from "codemirror";
import { EditorState } from "@codemirror/state";
import { oneDark } from "@codemirror/theme-one-dark";
import { vim } from "@replit/codemirror-vim";
export interface EditorProps {
value: string,
onUpdate(val: string): void,
onRun(): void,
height?: string,
}
export function CodeEditor(props: EditorProps) {
let containerRef: HTMLDivElement | undefined;
let view: EditorView;
const handleKeydown = (e: KeyboardEvent) => {
if (e.key === "Enter" && (e.ctrlKey || e.metaKey)) {
e.preventDefault();
e.stopImmediatePropagation();
props.onRun();
}
};
onMount(() => {
const themeConfig = EditorView.theme({
"&": { height: props.height || "300px" },
".cm-scroller": { overflow: "auto" }
});
const state = EditorState.create({
doc: props.value,
extensions: [
basicSetup,
oneDark,
vim(),
themeConfig,
EditorView.updateListener.of((update) => {
if (update.docChanged) {
props.onUpdate(update.state.doc.toString());
}
}),
],
});
view = new EditorView({
state,
parent: containerRef!,
});
containerRef?.addEventListener("keydown", handleKeydown, { capture: true });
});
onCleanup(() => {
containerRef?.removeEventListener("keydown", handleKeydown, { capture: true });
view.destroy()
});
return (
<div
ref={containerRef}
class="editor-frame"
/>
);
}

34
src/ui/Controls.tsx Normal file
View file

@ -0,0 +1,34 @@
type Props = {
// TODO
}
export function Controls(props: Props) {
return (
<aside
style={{
position: "sticky",
top: "2rem",
height: "fit-content"
}}
>
<article>
<header>Controls</header>
<fieldset>
<label>
<input type="checkbox" role="switch" checked />
Auto-evaluate
</label>
<button class="secondary outline" style={{ width: "100%" }}>
Clear All
</button>
</fieldset>
<footer>
<small>Language v0.1.0</small>
</footer>
</article>
</aside>
);
}

View file

@ -7,6 +7,7 @@ import { SourceText, sourceText } from 'src/lang/parser/source_text';
import { ParseError, parseExpr } from 'src/lang/parser/parser';
import { ShowParseError } from './ParseError';
import { Val } from './Value';
import { CodeEditor } from './CodeEditor';
namespace ReplResult {
export type Idle =
@ -55,25 +56,15 @@ export function ExprREPL() {
return (
<section>
<textarea
placeholder="+(3, 4)"
rows="5"
value={input()}
onInput={(e) => setInput(e.currentTarget.value)}
onKeyDown={(e) => {
// Check for Enter + (Ctrl or Command for Mac)
if (e.key === "Enter" && (e.ctrlKey || e.metaKey)) {
e.preventDefault(); // Stop the newline from being added
runExecution();
}
}}
<CodeEditor
value={input()}
onUpdate={setInput}
onRun={runExecution}
/>
<button onClick={runExecution}>Run</button>
<hr />
<div>
<div style={{ "margin-top": "1rem" }}>
<Switch>
<Match when={result().tag === "idle"}>
{ "" }

45
src/ui/Story.tsx Normal file
View file

@ -0,0 +1,45 @@
import { ExprREPL } from "./REPL";
type Props = {
// TODO
}
export function Story(props: Props) {
return (
<div
id="story"
style={{
display: "flex",
"flex-direction": "column",
gap: "1.5rem",
}}
>
<article>
<header><strong>Interactive REPL</strong></header>
<ExprREPL />
</article>
<article>
<header><strong>Header</strong></header>
<p>This is a place for other items</p>
</article>
<article>
<header><strong>Header</strong></header>
<p>This is a place for other items</p>
</article>
<article>
<header><strong>Header</strong></header>
<p>This is a place for other items</p>
</article>
<article>
<header><strong>Header</strong></header>
<p>This is a place for other items</p>
</article>
<article>
<header><strong>Header</strong></header>
<p>This is a place for other items</p>
</article>
</div>
);
}

View file

@ -1,3 +1,37 @@
@use "@picocss/pico/scss/pico";
// Custom styling goes here
// ===Code-Mirror vim styling===
.editor-frame {
// Pico variable for cards, usually a slightly lighter dark grey
background-color: var(--pico-card-background-color);
border: 1px solid var(--pico-border-color);
border-radius: var(--pico-border-radius);
overflow: hidden; // Ensures the editor corners don't poke out
margin-bottom: var(--pico-spacing);
transition: border-color 0.2s ease-in-out;
&:focus-within {
border-color: var(--pico-primary);
box-shadow: 0 0 0 0.125rem rgba(var(--pico-primary-rgb), 0.25);
}
// CodeMirror internal overrides
.cm-editor {
outline: none !important; // Remove default chrome/safari focus rings
font-family: var(--pico-font-family-monospace);
}
// Styling the Vim Command/Status Bar at the bottom
.cm-vim-panel {
background: #282c34 !important; // One Dark's background
color: #abb2bf !important;
border-top: 1px solid #181a1f;
font-size: 0.8rem;
padding: 4px 8px;
input {
color: var(--pico-primary) !important;
font-weight: bold;
}
}
}

View file

@ -2,7 +2,6 @@ import './index.scss';
import { render } from 'solid-js/web';
import { Program } from 'src/lang/program';
import { ProgramProvider } from './ProgramProvider';
// import App from './AppElm';
import App from './App';
export function startApp() {