UI and a Lisp experiment

This commit is contained in:
Yura Dupyn 2026-04-25 01:10:49 +02:00
parent 38ff06ea45
commit f55b437037
24 changed files with 2746 additions and 89 deletions

127
src/ui/App.tsx Normal file
View file

@ -0,0 +1,127 @@
import { createMemo, createSignal, Show } from 'solid-js';
import { sourceText } from 'source-region';
import type { CodePointSpan, SourceRegion, SourceText } from 'source-region';
import { parseDocument } from '../parser';
import type { ParseError } from '../parser';
import type { ConcreteSyntax } from '../syntax';
import { spanLabel } from './format';
import { PaneHeader, PaneSplitter } from './Pane';
import { SourceGrid } from './SourceGrid';
import type { SourceGridAnnotation } from './SourceGrid';
import { ErrorList, ExpressionList } from './SyntaxPane';
import type { HoverTarget } from './types';
type ParsedDocument = {
source: SourceText;
region: SourceRegion;
values: ConcreteSyntax[];
errors: ParseError[];
};
const SAMPLE_INPUT = `(define square (_ x) (mul x x))
(add 1 2)
(define pyth (_ x y) (+ (square x) (square y)))
foo ) @@@ (bar 1)
(nested (list 123 abc_9 name-with-dash))`;
export function App() {
const [input, setInput] = createSignal(SAMPLE_INPUT);
const [hovered, setHovered] = createSignal<HoverTarget | undefined>();
const [leftWidth, setLeftWidth] = createSignal(420);
const [middleWidth, setMiddleWidth] = createSignal(420);
const parsed = createMemo<ParsedDocument>(() => {
const source = sourceText(input());
const region = source.fullRegion();
const result = parseDocument(region);
return { source, region, values: result.values, errors: result.errors };
});
return (
<main
class="app-shell"
style={{
"--left-width": `${leftWidth()}px`,
"--middle-width": `${middleWidth()}px`,
}}
>
<section class="pane input-pane">
<PaneHeader title="Source" detail={`${input().length} UTF-16 units`} />
<textarea
class="source-input"
spellcheck={false}
value={input()}
onInput={(event) => {
setInput(event.currentTarget.value);
setHovered(undefined);
}}
/>
</section>
<PaneSplitter
label="Resize source and structure panes"
onDrag={(delta) => {
setLeftWidth((width) => clamp(width + delta, 280, 760));
}}
/>
<section class="pane structure-pane">
<PaneHeader
title="Structure"
detail={`${parsed().values.length} expressions, ${parsed().errors.length} errors`}
/>
<Show
when={parsed().errors.length > 0}
fallback={
<ExpressionList
values={parsed().values}
onHover={setHovered}
/>
}
>
<ErrorList
errors={parsed().errors}
values={parsed().values}
onHover={setHovered}
/>
</Show>
</section>
<PaneSplitter
label="Resize structure and source grid panes"
onDrag={(delta) => {
setMiddleWidth((width) => clamp(width + delta, 260, 760));
}}
/>
<section class="pane source-pane">
<PaneHeader
title="Source Grid"
detail={hovered() ? spanLabel(hovered()!.span) : "nothing hovered"}
/>
<SourceGrid
source={parsed().source}
region={parsed().region}
annotations={hovered() ? [hoverAnnotation(hovered()!)] : []}
/>
</section>
</main>
);
}
function hoverAnnotation(target: HoverTarget): SourceGridAnnotation {
return {
id: "hovered",
span: target.span,
label: target.label,
cellClass: "annotation-hovered",
markerClass: "annotation-hovered-marker",
};
}
function clamp(value: number, min: number, max: number): number {
return Math.max(min, Math.min(max, value));
}