262 lines
7.6 KiB
TypeScript
262 lines
7.6 KiB
TypeScript
import { For, Show } from 'solid-js';
|
|
import type { JSX } from 'solid-js';
|
|
import type { CodePointSpan } from 'source-region';
|
|
import type {
|
|
ConcreteError,
|
|
ConcreteErrorNode,
|
|
ConcreteInfo,
|
|
List,
|
|
ListItem,
|
|
Expr as SyntaxExpr,
|
|
} from '../syntax';
|
|
import type { PartialConcreteSyntax } from '../parser';
|
|
import { Expr } from '../syntax';
|
|
import { errorDetail, errorTitle, spanLabel } from './format';
|
|
import type { HoverTarget } from './types';
|
|
|
|
type PartialExpr = SyntaxExpr<ConcreteInfo, ConcreteError>;
|
|
type PartialList = List<ConcreteInfo, ConcreteError>;
|
|
type PartialListItem = ListItem<ConcreteInfo, ConcreteError>;
|
|
|
|
export function StructureTree(props: {
|
|
program: PartialConcreteSyntax;
|
|
isValid: boolean;
|
|
errorCount: number;
|
|
onHover: (target: HoverTarget | undefined) => void;
|
|
}) {
|
|
return (
|
|
<div class="structure-tree">
|
|
<HoverBlock
|
|
class="syntax-node program-node"
|
|
label="program"
|
|
span={props.program.span}
|
|
onHover={props.onHover}
|
|
>
|
|
<div class="node-header">
|
|
<span class={props.isValid ? "status-dot status-valid" : "status-dot status-invalid"} />
|
|
<span class="node-kind">program</span>
|
|
<span class="item-meta">
|
|
{props.isValid ? "valid" : "invalid"} · {props.program.expressions.length} expressions · {props.errorCount} errors
|
|
</span>
|
|
</div>
|
|
|
|
<Show when={props.program.error}>
|
|
{(error) => <ConcreteErrorView error={error()} label="program error" onHover={props.onHover} />}
|
|
</Show>
|
|
|
|
<div class="list-children">
|
|
<For each={props.program.expressions}>
|
|
{(expr, index) => (
|
|
<ExprView
|
|
expr={expr}
|
|
label={`expression ${index() + 1}`}
|
|
onHover={props.onHover}
|
|
/>
|
|
)}
|
|
</For>
|
|
</div>
|
|
</HoverBlock>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function ExprView(props: {
|
|
expr: PartialExpr;
|
|
label: string;
|
|
onHover: (target: HoverTarget | undefined) => void;
|
|
}) {
|
|
switch (props.expr.tag) {
|
|
case "number":
|
|
case "identifier":
|
|
return (
|
|
<HoverBlock
|
|
class="syntax-node literal-node"
|
|
label={`${props.label}: ${props.expr.tag}`}
|
|
span={props.expr.span}
|
|
onHover={props.onHover}
|
|
>
|
|
<div class="node-header">
|
|
<span class="node-kind">{props.expr.tag}</span>
|
|
<span class="node-value">{Expr.show(props.expr)}</span>
|
|
</div>
|
|
</HoverBlock>
|
|
);
|
|
|
|
case "error-number":
|
|
case "error-identifier":
|
|
case "error-expression":
|
|
return (
|
|
<HoverBlock
|
|
class="syntax-node syntax-error-node"
|
|
label={`${props.label}: ${props.expr.tag}`}
|
|
span={props.expr.span}
|
|
onHover={props.onHover}
|
|
>
|
|
<div class="node-header">
|
|
<span class="status-dot status-invalid" />
|
|
<span class="node-kind">{props.expr.tag}</span>
|
|
<span class="item-meta">{spanLabel(props.expr.span)}</span>
|
|
</div>
|
|
<ConcreteErrorView error={props.expr.error} label={props.expr.tag} onHover={props.onHover} />
|
|
</HoverBlock>
|
|
);
|
|
|
|
case "list":
|
|
return <ListView list={props.expr} label={props.label} onHover={props.onHover} />;
|
|
}
|
|
}
|
|
|
|
function ListView(props: {
|
|
list: PartialList;
|
|
label: string;
|
|
onHover: (target: HoverTarget | undefined) => void;
|
|
}) {
|
|
return (
|
|
<HoverBlock
|
|
class={props.list.error ? "syntax-node list-node syntax-error-node" : "syntax-node list-node"}
|
|
label={`${props.label}: list`}
|
|
span={props.list.span}
|
|
onHover={props.onHover}
|
|
>
|
|
<div class="node-header">
|
|
<Show when={props.list.error}>
|
|
<span class="status-dot status-invalid" />
|
|
</Show>
|
|
<span class="node-kind">{listLabel(props.list.open.tag)}</span>
|
|
<span class="item-meta">{props.list.items.length} items · {delimiterLabel(props.list)}</span>
|
|
</div>
|
|
|
|
<div class="delimiter-row">
|
|
<SpanChip label={props.list.open.tag} span={props.list.open.span} onHover={props.onHover} />
|
|
<Show when={props.list.close}>
|
|
{(close) => <SpanChip label={close().tag} span={close().span} onHover={props.onHover} />}
|
|
</Show>
|
|
</div>
|
|
|
|
<Show when={props.list.error}>
|
|
{(error) => <ConcreteErrorView error={error()} label="list error" onHover={props.onHover} />}
|
|
</Show>
|
|
|
|
<div class="list-children">
|
|
<For each={props.list.items}>
|
|
{(item, index) => (
|
|
<ListItemView
|
|
item={item}
|
|
label={`${props.label}.${index() + 1}`}
|
|
onHover={props.onHover}
|
|
/>
|
|
)}
|
|
</For>
|
|
</div>
|
|
</HoverBlock>
|
|
);
|
|
}
|
|
|
|
function ListItemView(props: {
|
|
item: PartialListItem;
|
|
label: string;
|
|
onHover: (target: HoverTarget | undefined) => void;
|
|
}) {
|
|
if (props.item.tag === "error-list-separator") {
|
|
return (
|
|
<HoverBlock
|
|
class="syntax-node syntax-error-node"
|
|
label={`${props.label}: separator`}
|
|
span={props.item.span}
|
|
onHover={props.onHover}
|
|
>
|
|
<div class="node-header">
|
|
<span class="status-dot status-invalid" />
|
|
<span class="node-kind">error-list-separator</span>
|
|
<span class="item-meta">{spanLabel(props.item.span)}</span>
|
|
</div>
|
|
<ConcreteErrorView error={props.item.error} label="separator error" onHover={props.onHover} />
|
|
</HoverBlock>
|
|
);
|
|
}
|
|
|
|
return <ExprView expr={props.item} label={props.label} onHover={props.onHover} />;
|
|
}
|
|
|
|
function ConcreteErrorView(props: {
|
|
error: ConcreteError;
|
|
label: string;
|
|
onHover: (target: HoverTarget | undefined) => void;
|
|
}) {
|
|
return (
|
|
<div class="concrete-error-list">
|
|
<For each={props.error}>
|
|
{(node, index) => (
|
|
<ConcreteErrorNodeView
|
|
node={node}
|
|
label={`${props.label} ${index() + 1}`}
|
|
onHover={props.onHover}
|
|
/>
|
|
)}
|
|
</For>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function ConcreteErrorNodeView(props: {
|
|
node: ConcreteErrorNode;
|
|
label: string;
|
|
onHover: (target: HoverTarget | undefined) => void;
|
|
}) {
|
|
return (
|
|
<div class="concrete-error">
|
|
<div class="error-title">{errorTitle(props.node.error)}</div>
|
|
<div class="item-meta">{errorDetail(props.node.error)}</div>
|
|
<div class="span-chip-row">
|
|
<SpanChip label="focus" span={props.node.span} onHover={props.onHover} />
|
|
<Show when={props.node.panickedOver}>
|
|
{(span) => <SpanChip label="panicked over" span={span()} onHover={props.onHover} />}
|
|
</Show>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function SpanChip(props: {
|
|
label: string;
|
|
span: CodePointSpan;
|
|
onHover: (target: HoverTarget | undefined) => void;
|
|
}) {
|
|
return (
|
|
<button
|
|
class="span-chip"
|
|
type="button"
|
|
onMouseEnter={() => props.onHover({ label: props.label, span: props.span })}
|
|
onMouseLeave={() => props.onHover(undefined)}
|
|
>
|
|
<span>{props.label}</span>
|
|
<span>{spanLabel(props.span)}</span>
|
|
</button>
|
|
);
|
|
}
|
|
|
|
function HoverBlock(props: {
|
|
class: string;
|
|
label: string;
|
|
span: CodePointSpan;
|
|
onHover: (target: HoverTarget | undefined) => void;
|
|
children: JSX.Element;
|
|
}) {
|
|
return (
|
|
<div
|
|
class={props.class}
|
|
onMouseEnter={() => props.onHover({ label: props.label, span: props.span })}
|
|
onMouseLeave={() => props.onHover(undefined)}
|
|
>
|
|
{props.children}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function listLabel(tag: string): string {
|
|
return tag === "open-bracket" ? "square-list" : "round-list";
|
|
}
|
|
|
|
function delimiterLabel(list: PartialList): string {
|
|
return list.close ? `${list.open.tag} / ${list.close.tag}` : `${list.open.tag} / missing close`;
|
|
}
|