FunctinDigith and focusTarget scrolling
This commit is contained in:
parent
8e4dcb5de7
commit
b0280b9d74
10 changed files with 378 additions and 91 deletions
|
|
@ -1,6 +1,5 @@
|
||||||
import { createSignal } from 'solid-js';
|
|
||||||
import { Scrowl } from './Scrowl';
|
import { Scrowl } from './Scrowl';
|
||||||
import { Controls } from './Controls';
|
import { Sidebar } from './Sidebar';
|
||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
return (
|
return (
|
||||||
|
|
@ -14,7 +13,7 @@ export default function App() {
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Scrowl />
|
<Scrowl />
|
||||||
<Controls />
|
<Sidebar />
|
||||||
</main>
|
</main>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { spawnFunctionDraft } from "./scrowlStore";
|
import { spawnNewFunctionDraftDigith } from "./scrowlStore";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
// TODO
|
// TODO
|
||||||
|
|
@ -6,20 +6,14 @@ type Props = {
|
||||||
|
|
||||||
export function Controls(props: Props) {
|
export function Controls(props: Props) {
|
||||||
return (
|
return (
|
||||||
<aside
|
<aside>
|
||||||
style={{
|
|
||||||
position: "sticky",
|
|
||||||
top: "2rem",
|
|
||||||
height: "fit-content"
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<article>
|
<article>
|
||||||
<header>Controls</header>
|
<header>Controls</header>
|
||||||
|
|
||||||
<div style={{ display: "flex", gap: "0.5rem", "flex-wrap": "wrap" }}>
|
<div style={{ display: "flex", gap: "0.5rem", "flex-wrap": "wrap" }}>
|
||||||
<button
|
<button
|
||||||
class="outline secondary"
|
class="outline secondary"
|
||||||
onClick={spawnFunctionDraft}
|
onClick={spawnNewFunctionDraftDigith}
|
||||||
style={{ padding: "2px 8px", "font-size": "0.8rem", width: "auto" }}
|
style={{ padding: "2px 8px", "font-size": "0.8rem", width: "auto" }}
|
||||||
>
|
>
|
||||||
+fn
|
+fn
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import { FunctionName } from "src/lang/expr"
|
import { FunctionName } from "src/lang/expr";
|
||||||
|
import { DigithId } from "./scrowlStore";
|
||||||
|
|
||||||
export type Digith =
|
export type Digith =
|
||||||
| Digith.Repl
|
| Digith.Repl
|
||||||
|
|
@ -7,10 +8,12 @@ export type Digith =
|
||||||
|
|
||||||
export namespace Digith {
|
export namespace Digith {
|
||||||
export type Repl = {
|
export type Repl = {
|
||||||
|
id: DigithId,
|
||||||
tag: "repl",
|
tag: "repl",
|
||||||
}
|
}
|
||||||
|
|
||||||
export type NewFunctionDraft = {
|
export type NewFunctionDraft = {
|
||||||
|
id: DigithId,
|
||||||
tag: "new-fn-draft",
|
tag: "new-fn-draft",
|
||||||
raw_name: string,
|
raw_name: string,
|
||||||
raw_parameters: string,
|
raw_parameters: string,
|
||||||
|
|
@ -18,11 +21,11 @@ export namespace Digith {
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Function = {
|
export type Function = {
|
||||||
|
id: DigithId,
|
||||||
tag: "fn",
|
tag: "fn",
|
||||||
name: FunctionName,
|
name: FunctionName,
|
||||||
raw_parameters: string,
|
raw_parameters: string,
|
||||||
raw_body: string,
|
raw_body: string,
|
||||||
is_loaded: boolean
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,12 +2,74 @@ import { createSignal, For, Match, Show, Switch } from "solid-js";
|
||||||
import { Digith } from "../Digith";
|
import { Digith } from "../Digith";
|
||||||
import { useProgram } from "../ProgramProvider";
|
import { useProgram } from "../ProgramProvider";
|
||||||
import { CodeEditor } from "../CodeEditor";
|
import { CodeEditor } from "../CodeEditor";
|
||||||
import { ParseError, parseExpr, parseFunctionName, parseFunctionParameters } from "src/lang/parser/parser";
|
import { ParseError } from "src/lang/parser/parser";
|
||||||
import { sourceText, SourceText } from "src/lang/parser/source_text";
|
import { sourceText, SourceText } from "src/lang/parser/source_text";
|
||||||
import { Expr, FunctionName, ProductPattern } from "src/lang/expr";
|
|
||||||
import { ShowParseError } from "../ParseError";
|
import { ShowParseError } from "../ParseError";
|
||||||
import { Program } from "src/lang/program";
|
import { Program } from "src/lang/program";
|
||||||
import { V, Validation, letValidate } from "../validation";
|
import { V, letValidate } from "../validation";
|
||||||
|
import { ProgramErrorDisplay, validateExprRaw, validateParamsRaw } from "./Helpers";
|
||||||
|
import { updateDigith } from "../scrowlStore";
|
||||||
|
|
||||||
|
type UpdateFnError =
|
||||||
|
| { tag: "Parse", field: "params" | "body", err: ParseError, src: SourceText }
|
||||||
|
| { tag: "Program", err: Program.Error }
|
||||||
|
|
||||||
|
const validator = letValidate(
|
||||||
|
(input: { raw_params: string, raw_body: string }) => ({
|
||||||
|
parameters: V.elseErr(validateParamsRaw(input.raw_params), err => ({ tag: "Parse", field: "params", err, src: sourceText(input.raw_params) })),
|
||||||
|
body: V.elseErr(validateExprRaw(input.raw_body), err => ({ tag: "Parse", field: "body", err, src: sourceText(input.raw_body) })),
|
||||||
|
}),
|
||||||
|
(fields, input) => {
|
||||||
|
return V.ok({
|
||||||
|
parameters: fields.parameters,
|
||||||
|
body: fields.body,
|
||||||
|
raw_parameters: input.raw_params,
|
||||||
|
raw_body: input.raw_body
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const fieldLabels: Record<string, string> = {
|
||||||
|
params: "Parameters",
|
||||||
|
body: "Function Body"
|
||||||
|
};
|
||||||
|
|
||||||
|
function SingleErrorDisplay(props: { error: UpdateFnError }) {
|
||||||
|
return (
|
||||||
|
<div style={{ "margin-bottom": "1rem" }}>
|
||||||
|
<Switch>
|
||||||
|
<Match
|
||||||
|
when={props.error.tag === "Parse" ? (props.error as Extract<UpdateFnError, { tag: "Parse" }>) : undefined}
|
||||||
|
>
|
||||||
|
{(err) => (
|
||||||
|
<article style={{ border: "1px solid var(--pico-del-color)", padding: "0.5rem 1rem" }}>
|
||||||
|
<header style={{ "margin-bottom": "0.5rem", color: "var(--pico-del-color)", "font-weight": "bold" }}>
|
||||||
|
{fieldLabels[err().field]} Error
|
||||||
|
</header>
|
||||||
|
<ShowParseError text={err().src} err={err().err} />
|
||||||
|
</article>
|
||||||
|
)}
|
||||||
|
</Match>
|
||||||
|
|
||||||
|
<Match
|
||||||
|
when={props.error.tag === "Program" ? (props.error as Extract<UpdateFnError, { tag: "Program" }>) : undefined}
|
||||||
|
>
|
||||||
|
{(err) => ( <ProgramErrorDisplay error={err().err} />)}
|
||||||
|
</Match>
|
||||||
|
</Switch>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ErrorListDisplay(props: { errors: UpdateFnError[] }) {
|
||||||
|
return (
|
||||||
|
<div style={{ "margin-top": "2rem", "border-top": "1px solid var(--pico-muted-border-color)", "padding-top": "1rem" }}>
|
||||||
|
<For each={props.errors}>
|
||||||
|
{(error) => <SingleErrorDisplay error={error} />}
|
||||||
|
</For>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// TODO: What about renaming?
|
// TODO: What about renaming?
|
||||||
export function FunctionDigith(props: { function: Digith.Function }) {
|
export function FunctionDigith(props: { function: Digith.Function }) {
|
||||||
|
|
@ -16,16 +78,97 @@ export function FunctionDigith(props: { function: Digith.Function }) {
|
||||||
const [params, setParams] = createSignal(props.function.raw_parameters);
|
const [params, setParams] = createSignal(props.function.raw_parameters);
|
||||||
const [body, setBody] = createSignal(props.function.raw_body);
|
const [body, setBody] = createSignal(props.function.raw_body);
|
||||||
|
|
||||||
const handleCommit = () => {
|
const [errors, setErrors] = createSignal<UpdateFnError[]>([]);
|
||||||
// TODO: Update the old function with new code
|
|
||||||
console.log(`Committing ${props.function.name} to the Program...`);
|
const isDirty = () =>
|
||||||
};
|
params() !== props.function.raw_parameters ||
|
||||||
|
body() !== props.function.raw_body;
|
||||||
|
|
||||||
|
function handleRedefine() {
|
||||||
|
setErrors([]);
|
||||||
|
|
||||||
|
const validRes = validator({ raw_params: params(), raw_body: body() });
|
||||||
|
if (validRes.tag === "errors") {
|
||||||
|
setErrors(validRes.errors as UpdateFnError[]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const updateData = validRes.value;
|
||||||
|
|
||||||
|
const progRes = Program.updateFunction(program, props.function.name, {
|
||||||
|
parameters: updateData.parameters,
|
||||||
|
body: updateData.body,
|
||||||
|
raw_parameters: updateData.raw_parameters,
|
||||||
|
raw_body: updateData.raw_body
|
||||||
|
});
|
||||||
|
|
||||||
|
if (progRes.tag === "error") {
|
||||||
|
setErrors([{ tag: "Program", err: progRes.error }]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// reloading the digith
|
||||||
|
updateDigith(props.function.id, {
|
||||||
|
...props.function,
|
||||||
|
raw_parameters: updateData.raw_parameters,
|
||||||
|
raw_body: updateData.raw_body
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<article>
|
<article>
|
||||||
<header><strong>Fn</strong></header>
|
<header>
|
||||||
|
<strong>Fn</strong>
|
||||||
|
|
||||||
|
{/* Dirty Indicator / Status */}
|
||||||
|
<div>
|
||||||
|
<Show when={isDirty()} fallback={<span style={{color: "var(--pico-muted-color)"}}>Synced</span>}>
|
||||||
|
<span style={{color: "var(--pico-primary)"}}>● Unsaved Changes</span>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="grid">
|
||||||
|
<label>
|
||||||
|
Name
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={props.function.name}
|
||||||
|
disabled
|
||||||
|
style={{ opacity: 0.7, cursor: "not-allowed" }}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
Parameters
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={params()}
|
||||||
|
onInput={(e) => setParams(e.currentTarget.value)}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<label>Body</label>
|
||||||
|
<CodeEditor
|
||||||
|
value={body()}
|
||||||
|
onUpdate={setBody}
|
||||||
|
onRun={handleRedefine}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<footer style={{ display: "flex", "align-items": "center", gap: "1rem" }}>
|
||||||
|
<button
|
||||||
|
onClick={handleRedefine}
|
||||||
|
disabled={!isDirty()}
|
||||||
|
class={isDirty() ? "" : "secondary outline"}
|
||||||
|
>
|
||||||
|
Redefine
|
||||||
|
</button>
|
||||||
|
|
||||||
|
</footer>
|
||||||
|
|
||||||
|
<Show when={errors().length > 0}>
|
||||||
|
<ErrorListDisplay errors={ errors() } />
|
||||||
|
</Show>
|
||||||
|
|
||||||
<div>TODO: Fn under construction</div>
|
|
||||||
</article>
|
</article>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ import { ShowParseError } from "../ParseError";
|
||||||
import { Program } from "src/lang/program";
|
import { Program } from "src/lang/program";
|
||||||
import { V, Validation, letValidate } from "../validation";
|
import { V, Validation, letValidate } from "../validation";
|
||||||
import { ProgramErrorDisplay, validateExprRaw, validateNameRaw, validateParamsRaw } from "./Helpers";
|
import { ProgramErrorDisplay, validateExprRaw, validateNameRaw, validateParamsRaw } from "./Helpers";
|
||||||
|
import { spawnFunctionDigith } from "../scrowlStore";
|
||||||
|
|
||||||
|
|
||||||
type NewFnError =
|
type NewFnError =
|
||||||
|
|
@ -20,6 +21,29 @@ const fieldLabels: Record<string, string> = {
|
||||||
body: "Function Body"
|
body: "Function Body"
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type Input = {
|
||||||
|
raw_name: string,
|
||||||
|
raw_params: string,
|
||||||
|
raw_body: string,
|
||||||
|
}
|
||||||
|
|
||||||
|
const validator: Validation<Input, Program.CreateFunction, NewFnError> = letValidate(
|
||||||
|
(input: Input) =>({
|
||||||
|
name: V.elseErr(validateNameRaw(input.raw_name), err => ({ tag: "Parse", field: "name", err, src: sourceText(input.raw_name) })),
|
||||||
|
parameters: V.elseErr(validateParamsRaw(input.raw_params), err => ({ tag: "Parse", field: "params", err, src: sourceText(input.raw_params) })),
|
||||||
|
body: V.elseErr(validateExprRaw(input.raw_body), err => ({ tag: "Parse", field: "body", err, src: sourceText(input.raw_body) })),
|
||||||
|
}),
|
||||||
|
(fields, input) => {
|
||||||
|
const createFunction: Program.CreateFunction = {
|
||||||
|
name: fields.name,
|
||||||
|
parameters: fields.parameters,
|
||||||
|
body: fields.body,
|
||||||
|
raw_parameters: input.raw_params,
|
||||||
|
raw_body: input.raw_body,
|
||||||
|
};
|
||||||
|
return V.ok(createFunction);
|
||||||
|
})
|
||||||
|
|
||||||
export function SingleErrorDisplay(props: { error: NewFnError }) {
|
export function SingleErrorDisplay(props: { error: NewFnError }) {
|
||||||
return (
|
return (
|
||||||
<div style={{ "margin-bottom": "1rem" }}>
|
<div style={{ "margin-bottom": "1rem" }}>
|
||||||
|
|
@ -65,52 +89,30 @@ export function NewFunctionDraftDigith(props: { draft: Digith.NewFunctionDraft }
|
||||||
const [params, setParams] = createSignal(props.draft.raw_parameters);
|
const [params, setParams] = createSignal(props.draft.raw_parameters);
|
||||||
const [body, setBody] = createSignal(props.draft.raw_body);
|
const [body, setBody] = createSignal(props.draft.raw_body);
|
||||||
|
|
||||||
const [validResult, setValidResult] = createSignal<V<void, NewFnError> | null>(null);
|
const [errors, setErrors] = createSignal<NewFnError[]>([]);
|
||||||
|
|
||||||
type Input = {
|
|
||||||
raw_name: string,
|
|
||||||
raw_params: string,
|
|
||||||
raw_body: string,
|
|
||||||
}
|
|
||||||
|
|
||||||
const validator: Validation<Input, void, NewFnError> = letValidate((input: Input) => ({
|
|
||||||
name: V.elseErr(validateNameRaw(input.raw_name), err => ({ tag: "Parse", field: "name", err, src: sourceText(input.raw_name) })),
|
|
||||||
parameters: V.elseErr(validateParamsRaw(input.raw_params), err => ({ tag: "Parse", field: "params", err, src: sourceText(input.raw_params) })),
|
|
||||||
body: V.elseErr(validateExprRaw(input.raw_body), err => ({ tag: "Parse", field: "body", err, src: sourceText(input.raw_body) })),
|
|
||||||
}),
|
|
||||||
(fields, input) => {
|
|
||||||
const createFunction: Program.CreateFunction = {
|
|
||||||
name: fields.name,
|
|
||||||
parameters: fields.parameters,
|
|
||||||
body: fields.body,
|
|
||||||
raw_parameters: input.raw_name,
|
|
||||||
raw_body: input.raw_body,
|
|
||||||
};
|
|
||||||
|
|
||||||
const regResult = Program.registerFunction(program, createFunction);
|
|
||||||
if (regResult.tag === "ok") {
|
|
||||||
// TODO: Side effects? Not sure about this. Ideally validator would be pure... but it is nice that we can return errors here.
|
|
||||||
// But then again... these are not really normal errors, right? or? But that's probably a misuse...
|
|
||||||
// For now we just return Ok
|
|
||||||
return V.ok(undefined);
|
|
||||||
} else {
|
|
||||||
return V.error({ tag: "Program", err: regResult.error });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// TODO: There's something wrong with this, it doesn't trigger when expected... WTF
|
|
||||||
function handleCommit() {
|
function handleCommit() {
|
||||||
const result = validator({ raw_name: name(), raw_params: params(), raw_body: body() });
|
setErrors([]);
|
||||||
setValidResult(result);
|
const validRes = validator({ raw_name: name(), raw_params: params(), raw_body: body() });
|
||||||
if (result.tag === "ok") {
|
if (validRes.tag === "errors") {
|
||||||
// Handle success closure here if needed
|
setErrors(validRes.errors as NewFnError[]);
|
||||||
console.log("Function created successfully!");
|
return;
|
||||||
}
|
}
|
||||||
|
const createFunction = validRes.value;
|
||||||
|
|
||||||
|
const programRes = Program.registerFunction(program, createFunction);
|
||||||
|
if (programRes.tag === "error") {
|
||||||
|
setErrors([{ tag: "Program", err: programRes.error }]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const fnName = programRes.value;
|
||||||
|
|
||||||
|
spawnFunctionDigith(program, fnName, props.draft.id);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<article>
|
<article>
|
||||||
<header><strong>Fn Draft</strong></header>
|
<header><strong>Fn (Draft)</strong></header>
|
||||||
|
|
||||||
<div class="grid">
|
<div class="grid">
|
||||||
<label>
|
<label>
|
||||||
|
|
@ -144,10 +146,8 @@ export function NewFunctionDraftDigith(props: { draft: Digith.NewFunctionDraft }
|
||||||
<button class="primary" onClick={handleCommit}>Commit</button>
|
<button class="primary" onClick={handleCommit}>Commit</button>
|
||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
<Show when={validResult()?.tag === "errors"}>
|
<Show when={errors().length > 0}>
|
||||||
<ErrorListDisplay
|
<ErrorListDisplay errors={errors()} />
|
||||||
errors={(validResult() as { tag: "errors", errors: NewFnError[] }).errors}
|
|
||||||
/>
|
|
||||||
</Show>
|
</Show>
|
||||||
</article>
|
</article>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
0
src/ui/FunctionList.tsx
Normal file
0
src/ui/FunctionList.tsx
Normal file
10
src/ui/ProgramMeta.tsx
Normal file
10
src/ui/ProgramMeta.tsx
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
|
||||||
|
export function ProgramMeta(props: {}) {
|
||||||
|
return (
|
||||||
|
<aside>
|
||||||
|
<article>
|
||||||
|
<header>Controls</header>
|
||||||
|
</article>
|
||||||
|
</aside>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { For, Match, Switch } from "solid-js";
|
import { createEffect, For, Match, onMount, Switch } from "solid-js";
|
||||||
import { ExprREPL } from "./REPL";
|
import { ExprREPL } from "./REPL";
|
||||||
import { scrowl } from "./scrowlStore";
|
import { clearFocus, DigithId, scrowl } from "./scrowlStore";
|
||||||
import { NewFunctionDraftDigith } from "./Function/NewFunctionDraftDigith";
|
import { NewFunctionDraftDigith } from "./Function/NewFunctionDraftDigith";
|
||||||
import { FunctionDigith } from "./Function/FunctionDigith";
|
import { FunctionDigith } from "./Function/FunctionDigith";
|
||||||
import { Digith } from "./Digith";
|
import { Digith } from "./Digith";
|
||||||
|
|
@ -13,11 +13,7 @@ import { Digith } from "./Digith";
|
||||||
// - Thinking of a prosthesis that's an extension of your body doing your bidding without you knowing exactly how.
|
// - Thinking of a prosthesis that's an extension of your body doing your bidding without you knowing exactly how.
|
||||||
// - digital
|
// - digital
|
||||||
|
|
||||||
type Props = {
|
export function Scrowl() {
|
||||||
// TODO
|
|
||||||
}
|
|
||||||
|
|
||||||
export function Scrowl(props: Props) {
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
id="scrowl"
|
id="scrowl"
|
||||||
|
|
@ -29,22 +25,24 @@ export function Scrowl(props: Props) {
|
||||||
>
|
>
|
||||||
<For each={scrowl.digiths}>
|
<For each={scrowl.digiths}>
|
||||||
{(digith) => (
|
{(digith) => (
|
||||||
<Switch>
|
<DigithWrapper id={digith.id}>
|
||||||
<Match when={digith.tag === 'repl'}>
|
<Switch>
|
||||||
<article class="digith">
|
<Match when={digith.tag === 'repl'}>
|
||||||
<header><strong>REPL</strong></header>
|
<article class="digith">
|
||||||
<ExprREPL />
|
<header><strong>REPL</strong></header>
|
||||||
</article>
|
<ExprREPL />
|
||||||
</Match>
|
</article>
|
||||||
|
</Match>
|
||||||
<Match when={digith.tag === 'new-fn-draft'}>
|
|
||||||
<NewFunctionDraftDigith draft={digith as Digith.NewFunctionDraft} />
|
<Match when={digith.tag === 'new-fn-draft'}>
|
||||||
</Match>
|
<NewFunctionDraftDigith draft={digith as Digith.NewFunctionDraft} />
|
||||||
|
</Match>
|
||||||
|
|
||||||
<Match when={digith.tag === 'fn'}>
|
<Match when={digith.tag === 'fn'}>
|
||||||
<FunctionDigith function={digith as Digith.Function} />
|
<FunctionDigith function={digith as Digith.Function} />
|
||||||
</Match>
|
</Match>
|
||||||
</Switch>
|
</Switch>
|
||||||
|
</DigithWrapper>
|
||||||
)}
|
)}
|
||||||
</For>
|
</For>
|
||||||
|
|
||||||
|
|
@ -52,3 +50,31 @@ export function Scrowl(props: Props) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// For focusing on a Digith
|
||||||
|
// This works basically in two ways:
|
||||||
|
// - We have the list of digiths, and each time any of them is mounted, there's `attemptScroll` check done on all of them.
|
||||||
|
// - Whenever `scrowl.focusTarget` changes, it reactively broadcasts the change to each of the digith,
|
||||||
|
// and each digith asks itself, "Am I focus-target?".
|
||||||
|
function DigithWrapper(props: { id: DigithId, children: any }) {
|
||||||
|
let ref: HTMLDivElement | undefined;
|
||||||
|
|
||||||
|
// The Logic: Run this whenever 'scrowl.focusTarget' changes OR when this component mounts
|
||||||
|
const attemptScroll = () => {
|
||||||
|
if (scrowl.focusTarget === props.id && ref) {
|
||||||
|
ref.scrollIntoView({ behavior: "smooth", block: "start" });
|
||||||
|
setTimeout(clearFocus, 0); // This sets asynchronously the focus-target to null.
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
onMount(attemptScroll);
|
||||||
|
createEffect(attemptScroll);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={ref}
|
||||||
|
style={{ "scroll-margin-top": "2rem" }}
|
||||||
|
>
|
||||||
|
{props.children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
|
||||||
19
src/ui/Sidebar.tsx
Normal file
19
src/ui/Sidebar.tsx
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
import { Controls } from "./Controls";
|
||||||
|
import { ProgramMeta } from "./ProgramMeta";
|
||||||
|
|
||||||
|
export function Sidebar() {
|
||||||
|
return (
|
||||||
|
<aside style={{
|
||||||
|
display: "flex",
|
||||||
|
"flex-direction": "column",
|
||||||
|
gap: "2rem", // Space between Controls and the List
|
||||||
|
position: "sticky",
|
||||||
|
top: "2rem",
|
||||||
|
"max-height": "calc(100vh - 4rem)",
|
||||||
|
"overflow-y": "auto" // Allow the list to scroll if it gets too long
|
||||||
|
}}>
|
||||||
|
<Controls />
|
||||||
|
<ProgramMeta />
|
||||||
|
</aside>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -1,21 +1,114 @@
|
||||||
import { createStore } from "solid-js/store";
|
import { createStore } from "solid-js/store";
|
||||||
import { Digith } from "./Digith";
|
import { Digith } from "./Digith";
|
||||||
|
import { Program } from "src/lang/program";
|
||||||
|
import { FunctionName } from "src/lang/expr";
|
||||||
|
|
||||||
|
export type DigithId = number;
|
||||||
|
|
||||||
export type Scrowl = {
|
export type Scrowl = {
|
||||||
digiths: Digith[];
|
digiths: Digith[],
|
||||||
|
nextId: DigithId,
|
||||||
|
focusTarget: DigithId | null, // 99.99999% of time this is null, but it flickers briefly - and on that flicker focus can happen.
|
||||||
|
}
|
||||||
|
|
||||||
|
export namespace Scrowl {
|
||||||
|
export type Error =
|
||||||
|
| Program.Error
|
||||||
|
|
||||||
|
export type Result<T> =
|
||||||
|
| { tag: "ok", value: T }
|
||||||
|
| { tag: "error", error: Error }
|
||||||
|
|
||||||
|
export namespace Result {
|
||||||
|
export function ok<T>(value: T): Result<T> { return { tag: "ok", value } }
|
||||||
|
export function error<T>(error: Error): Result<T> { return { tag: "error", error } }
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const [scrowl, setScrowl] = createStore<Scrowl>({
|
export const [scrowl, setScrowl] = createStore<Scrowl>({
|
||||||
digiths: [{ tag: 'repl' }]
|
digiths: [{ tag: 'repl', id: 0 }],
|
||||||
|
nextId: 1,
|
||||||
|
focusTarget: null,
|
||||||
});
|
});
|
||||||
|
|
||||||
export function spawnFunctionDraft() {
|
function prependDigith(newDigith: Digith) {
|
||||||
|
setScrowl("digiths", (prev) => [newDigith, ...prev]);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function updateDigith(targetId: DigithId, newDigith: Digith) {
|
||||||
|
setScrowl("digiths", (items) =>
|
||||||
|
items.map((item) => item.id === targetId ? newDigith : item)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function closeDigith(targetId: DigithId) {
|
||||||
|
setScrowl("digiths", (prev) => prev.filter((d) => d.id !== targetId));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function closeAllDigiths() {
|
||||||
|
setScrowl("digiths", []);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function requestFocus(id: DigithId) {
|
||||||
|
setScrowl("focusTarget", id);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function clearFocus() {
|
||||||
|
setScrowl("focusTarget", null);
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateId(): DigithId {
|
||||||
|
const id = scrowl.nextId;
|
||||||
|
setScrowl("nextId", (prev) => prev + 1);
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function spawnNewFunctionDraftDigith() {
|
||||||
|
const id = generateId();
|
||||||
|
|
||||||
const newDraft: Digith = {
|
const newDraft: Digith = {
|
||||||
|
id,
|
||||||
tag: 'new-fn-draft',
|
tag: 'new-fn-draft',
|
||||||
raw_name: '',
|
raw_name: '',
|
||||||
raw_parameters: '',
|
raw_parameters: '',
|
||||||
raw_body: '',
|
raw_body: '',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
requestFocus(id);
|
||||||
setScrowl("digiths", (prev) => [newDraft, ...prev]);
|
setScrowl("digiths", (prev) => [newDraft, ...prev]);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export function spawnFunctionDigith(program: Program, name: FunctionName, targetId?: DigithId): Scrowl.Result<Digith.Function> {
|
||||||
|
const lookupRes = Program.getFunction(program, name);
|
||||||
|
if (lookupRes.tag === "error") {
|
||||||
|
return Scrowl.Result.error(lookupRes.error);
|
||||||
|
}
|
||||||
|
const fnDef = lookupRes.value;
|
||||||
|
|
||||||
|
// TODO: Maybe consider representing some read-only Digith for primitive (it would just display the name, it wouldn't have code).
|
||||||
|
if (fnDef.tag === "primitive") {
|
||||||
|
return Scrowl.Result.error({ tag: "CannotEditPrimitiveFunction", name });
|
||||||
|
}
|
||||||
|
const userDef = fnDef.def;
|
||||||
|
const id = targetId ?? generateId();
|
||||||
|
|
||||||
|
const newDigith: Digith.Function = {
|
||||||
|
id: id,
|
||||||
|
tag: "fn",
|
||||||
|
|
||||||
|
name: userDef.name,
|
||||||
|
raw_parameters: userDef.raw_parameters,
|
||||||
|
raw_body: userDef.raw_body,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (targetId === undefined) {
|
||||||
|
prependDigith(newDigith);
|
||||||
|
} else {
|
||||||
|
// Swap with old function draft.
|
||||||
|
updateDigith(targetId, newDigith);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Scrowl.Result.ok(newDigith);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue