implement error markers in editor

This commit is contained in:
Vyacheslav Matyukhin 2022-09-27 02:29:00 +04:00
parent 845d38e375
commit 4c56b2fd07
No known key found for this signature in database
GPG Key ID: 3D2A774C5489F96C
10 changed files with 207 additions and 63 deletions

View File

@ -5,6 +5,8 @@ import AceEditor from "react-ace";
import "ace-builds/src-noconflict/mode-golang"; import "ace-builds/src-noconflict/mode-golang";
import "ace-builds/src-noconflict/theme-github"; import "ace-builds/src-noconflict/theme-github";
import { SqLocation } from "@quri/squiggle-lang";
interface CodeEditorProps { interface CodeEditorProps {
value: string; value: string;
onChange: (value: string) => void; onChange: (value: string) => void;
@ -13,15 +15,17 @@ interface CodeEditorProps {
width?: number; width?: number;
height: number; height: number;
showGutter?: boolean; showGutter?: boolean;
errorLocations?: SqLocation[];
} }
export const CodeEditor: FC<CodeEditorProps> = ({ export const CodeEditor: FC<CodeEditorProps> = ({
value, value,
onChange, onChange,
onSubmit, onSubmit,
height,
oneLine = false, oneLine = false,
showGutter = false, showGutter = false,
height, errorLocations = [],
}) => { }) => {
const lineCount = value.split("\n").length; const lineCount = value.split("\n").length;
const id = useMemo(() => _.uniqueId(), []); const id = useMemo(() => _.uniqueId(), []);
@ -30,8 +34,11 @@ export const CodeEditor: FC<CodeEditorProps> = ({
const onSubmitRef = useRef<typeof onSubmit | null>(null); const onSubmitRef = useRef<typeof onSubmit | null>(null);
onSubmitRef.current = onSubmit; onSubmitRef.current = onSubmit;
const editorEl = useRef<AceEditor | null>(null);
return ( return (
<AceEditor <AceEditor
ref={editorEl}
value={value} value={value}
mode="golang" mode="golang"
theme="github" theme="github"
@ -59,6 +66,14 @@ export const CodeEditor: FC<CodeEditorProps> = ({
exec: () => onSubmitRef.current?.(), exec: () => onSubmitRef.current?.(),
}, },
]} ]}
markers={errorLocations?.map((location) => ({
startRow: location.start.line - 1,
startCol: location.start.column - 1,
endRow: location.end.line - 1,
endCol: location.end.column - 1,
className: "ace-error-marker",
type: "text",
}))}
/> />
); );
}; };

View File

@ -9,6 +9,7 @@ import {
import { useSquiggle } from "../lib/hooks"; import { useSquiggle } from "../lib/hooks";
import { SquiggleViewer } from "./SquiggleViewer"; import { SquiggleViewer } from "./SquiggleViewer";
import { JsImports } from "../lib/jsImports"; import { JsImports } from "../lib/jsImports";
import { getValueToRender } from "../lib/utility";
export interface SquiggleChartProps { export interface SquiggleChartProps {
/** The input string for squiggle */ /** The input string for squiggle */
@ -58,16 +59,9 @@ export interface SquiggleChartProps {
const defaultOnChange = () => {}; const defaultOnChange = () => {};
const defaultImports: JsImports = {}; const defaultImports: JsImports = {};
export const SquiggleChart: React.FC<SquiggleChartProps> = React.memo( export const splitSquiggleChartSettings = (props: SquiggleChartProps) => {
({ const {
code = "",
executionId = 0,
environment,
onChange = defaultOnChange, // defaultOnChange must be constant, don't move its definition here
height = 200,
jsImports = defaultImports,
showSummary = false, showSummary = false,
width,
logX = false, logX = false,
expY = false, expY = false,
diagramStart = 0, diagramStart = 0,
@ -80,9 +74,47 @@ export const SquiggleChart: React.FC<SquiggleChartProps> = React.memo(
title, title,
xAxisType = "number", xAxisType = "number",
distributionChartActions, distributionChartActions,
enableLocalSettings = false, } = props;
}) => {
const { result, bindings } = useSquiggle({ const distributionPlotSettings = {
showSummary,
logX,
expY,
format: tickFormat,
minX,
maxX,
color,
title,
xAxisType,
actions: distributionChartActions,
};
const chartSettings = {
start: diagramStart,
stop: diagramStop,
count: diagramCount,
};
return { distributionPlotSettings, chartSettings };
};
export const SquiggleChart: React.FC<SquiggleChartProps> = React.memo(
(props) => {
const { distributionPlotSettings, chartSettings } =
splitSquiggleChartSettings(props);
const {
code = "",
environment,
jsImports = defaultImports,
onChange = defaultOnChange, // defaultOnChange must be constant, don't move its definition here
executionId = 0,
width,
height = 200,
enableLocalSettings = false,
} = props;
const resultAndBindings = useSquiggle({
code, code,
environment, environment,
jsImports, jsImports,
@ -90,32 +122,11 @@ export const SquiggleChart: React.FC<SquiggleChartProps> = React.memo(
executionId, executionId,
}); });
const distributionPlotSettings = { const valueToRender = getValueToRender(resultAndBindings);
showSummary,
logX,
expY,
format: tickFormat,
minX,
maxX,
color,
title,
xAxisType,
actions: distributionChartActions,
};
const chartSettings = {
start: diagramStart,
stop: diagramStop,
count: diagramCount,
};
const resultToRender = resultMap(result, (value) =>
value.tag === SqValueTag.Void ? bindings.asValue() : value
);
return ( return (
<SquiggleViewer <SquiggleViewer
result={resultToRender} result={valueToRender}
width={width} width={width}
height={height} height={height}
distributionPlotSettings={distributionPlotSettings} distributionPlotSettings={distributionPlotSettings}

View File

@ -1,13 +1,21 @@
import React from "react"; import React from "react";
import { CodeEditor } from "./CodeEditor"; import { CodeEditor } from "./CodeEditor";
import { SquiggleContainer } from "./SquiggleContainer"; import { SquiggleContainer } from "./SquiggleContainer";
import { SquiggleChart, SquiggleChartProps } from "./SquiggleChart"; import {
import { useMaybeControlledValue } from "../lib/hooks"; splitSquiggleChartSettings,
SquiggleChartProps,
} from "./SquiggleChart";
import { useMaybeControlledValue, useSquiggle } from "../lib/hooks";
import { JsImports } from "../lib/jsImports";
import { defaultEnvironment, SqLocation } from "@quri/squiggle-lang";
import { SquiggleViewer } from "./SquiggleViewer";
import { getErrorLocations, getValueToRender } from "../lib/utility";
const WrappedCodeEditor: React.FC<{ const WrappedCodeEditor: React.FC<{
code: string; code: string;
setCode: (code: string) => void; setCode: (code: string) => void;
}> = ({ code, setCode }) => ( errorLocations?: SqLocation[];
}> = ({ code, setCode, errorLocations }) => (
<div className="border border-grey-200 p-2 m-4"> <div className="border border-grey-200 p-2 m-4">
<CodeEditor <CodeEditor
value={code} value={code}
@ -15,6 +23,7 @@ const WrappedCodeEditor: React.FC<{
oneLine={true} oneLine={true}
showGutter={false} showGutter={false}
height={20} height={20}
errorLocations={errorLocations}
/> />
</div> </div>
); );
@ -24,6 +33,9 @@ export type SquiggleEditorProps = SquiggleChartProps & {
onCodeChange?: (code: string) => void; onCodeChange?: (code: string) => void;
}; };
const defaultOnChange = () => {};
const defaultImports: JsImports = {};
export const SquiggleEditor: React.FC<SquiggleEditorProps> = (props) => { export const SquiggleEditor: React.FC<SquiggleEditorProps> = (props) => {
const [code, setCode] = useMaybeControlledValue({ const [code, setCode] = useMaybeControlledValue({
value: props.code, value: props.code,
@ -31,11 +43,46 @@ export const SquiggleEditor: React.FC<SquiggleEditorProps> = (props) => {
onChange: props.onCodeChange, onChange: props.onCodeChange,
}); });
let chartProps = { ...props, code }; const { distributionPlotSettings, chartSettings } =
splitSquiggleChartSettings(props);
const {
environment,
jsImports = defaultImports,
onChange = defaultOnChange, // defaultOnChange must be constant, don't move its definition here
executionId = 0,
width,
height = 200,
enableLocalSettings = false,
} = props;
const resultAndBindings = useSquiggle({
code,
environment,
jsImports,
onChange,
executionId,
});
const valueToRender = getValueToRender(resultAndBindings);
const errorLocations = getErrorLocations(resultAndBindings.result);
return ( return (
<SquiggleContainer> <SquiggleContainer>
<WrappedCodeEditor code={code} setCode={setCode} /> <WrappedCodeEditor
<SquiggleChart {...chartProps} /> code={code}
setCode={setCode}
errorLocations={errorLocations}
/>
<SquiggleViewer
result={valueToRender}
width={width}
height={height}
distributionPlotSettings={distributionPlotSettings}
chartSettings={chartSettings}
environment={environment ?? defaultEnvironment}
enableLocalSettings={enableLocalSettings}
/>
</SquiggleContainer> </SquiggleContainer>
); );
}; };

View File

@ -17,21 +17,24 @@ const StackTraceLocation: React.FC<{ location: SqLocation }> = ({
}; };
const StackTrace: React.FC<Props> = ({ error }) => { const StackTrace: React.FC<Props> = ({ error }) => {
return ( const locations = error.toLocationArray();
return locations.length ? (
<div> <div>
{error.toLocationArray().map((location, i) => ( <div>Traceback:</div>
<StackTraceLocation location={location} key={i} /> <div className="ml-4">
))} {locations.map((location, i) => (
<StackTraceLocation location={location} key={i} />
))}
</div>
</div> </div>
); ) : null;
}; };
export const SquiggleErrorAlert: React.FC<Props> = ({ error }) => { export const SquiggleErrorAlert: React.FC<Props> = ({ error }) => {
return ( return (
<ErrorAlert heading="Error"> <ErrorAlert heading="Error">
<div>{error.toString()}</div> <div className="space-y-4">
<div className="mt-4">Traceback:</div> <div>{error.toString()}</div>
<div className="ml-4">
<StackTrace error={error} /> <StackTrace error={error} />
</div> </div>
</ErrorAlert> </ErrorAlert>

View File

@ -8,7 +8,11 @@ import React, {
} from "react"; } from "react";
import { useForm, UseFormRegister, useWatch } from "react-hook-form"; import { useForm, UseFormRegister, useWatch } from "react-hook-form";
import * as yup from "yup"; import * as yup from "yup";
import { useMaybeControlledValue, useRunnerState } from "../lib/hooks"; import {
useMaybeControlledValue,
useRunnerState,
useSquiggle,
} from "../lib/hooks";
import { yupResolver } from "@hookform/resolvers/yup"; import { yupResolver } from "@hookform/resolvers/yup";
import { import {
ChartSquareBarIcon, ChartSquareBarIcon,
@ -26,7 +30,7 @@ import clsx from "clsx";
import { environment } from "@quri/squiggle-lang"; import { environment } from "@quri/squiggle-lang";
import { SquiggleChart, SquiggleChartProps } from "./SquiggleChart"; import { SquiggleChartProps } from "./SquiggleChart";
import { CodeEditor } from "./CodeEditor"; import { CodeEditor } from "./CodeEditor";
import { JsonEditor } from "./JsonEditor"; import { JsonEditor } from "./JsonEditor";
import { ErrorAlert, SuccessAlert } from "./Alert"; import { ErrorAlert, SuccessAlert } from "./Alert";
@ -40,6 +44,8 @@ import { HeadedSection } from "./ui/HeadedSection";
import { defaultTickFormat } from "../lib/distributionSpecBuilder"; import { defaultTickFormat } from "../lib/distributionSpecBuilder";
import { Button } from "./ui/Button"; import { Button } from "./ui/Button";
import { JsImports } from "../lib/jsImports"; import { JsImports } from "../lib/jsImports";
import { getErrorLocations, getValueToRender } from "../lib/utility";
import { SquiggleViewer } from "./SquiggleViewer";
type PlaygroundProps = SquiggleChartProps & { type PlaygroundProps = SquiggleChartProps & {
/** The initial squiggle string to put in the playground */ /** The initial squiggle string to put in the playground */
@ -282,7 +288,7 @@ export const SquigglePlayground: FC<PlaygroundProps> = ({
onSettingsChange?.(vars); onSettingsChange?.(vars);
}, [vars, onSettingsChange]); }, [vars, onSettingsChange]);
const env: environment = useMemo( const environment: environment = useMemo(
() => ({ () => ({
sampleCount: Number(vars.sampleCount), sampleCount: Number(vars.sampleCount),
xyPointLength: Number(vars.xyPointLength), xyPointLength: Number(vars.xyPointLength),
@ -299,26 +305,51 @@ export const SquigglePlayground: FC<PlaygroundProps> = ({
executionId, executionId,
} = useRunnerState(code); } = useRunnerState(code);
const resultAndBindings = useSquiggle({
code,
environment,
jsImports: imports,
executionId,
});
const valueToRender = getValueToRender(resultAndBindings);
const squiggleChart = const squiggleChart =
renderedCode === "" ? null : ( renderedCode === "" ? null : (
<div className="relative"> <div className="relative">
{isRunning ? ( {isRunning ? (
<div className="absolute inset-0 bg-white opacity-0 animate-semi-appear" /> <div className="absolute inset-0 bg-white opacity-0 animate-semi-appear" />
) : null} ) : null}
<SquiggleChart <SquiggleViewer
code={renderedCode} result={valueToRender}
executionId={executionId} environment={environment}
environment={env} height={vars.chartHeight || 150}
{...vars} distributionPlotSettings={{
jsImports={imports} showSummary: vars.showSummary ?? false,
logX: vars.logX ?? false,
expY: vars.expY ?? false,
format: vars.tickFormat,
minX: vars.minX,
maxX: vars.maxX,
title: vars.title,
actions: vars.distributionChartActions,
}}
chartSettings={{
start: vars.diagramStart ?? 0,
stop: vars.diagramStop ?? 10,
count: vars.diagramCount ?? 20,
}}
enableLocalSettings={true} enableLocalSettings={true}
/> />
</div> </div>
); );
const errorLocations = getErrorLocations(resultAndBindings.result);
const firstTab = vars.showEditor ? ( const firstTab = vars.showEditor ? (
<div className="border border-slate-200"> <div className="border border-slate-200">
<CodeEditor <CodeEditor
errorLocations={errorLocations}
value={code} value={code}
onChange={setCode} onChange={setCode}
onSubmit={run} onSubmit={run}

View File

@ -1,4 +1,11 @@
import { environment, SqProject, SqValue } from "@quri/squiggle-lang"; import {
environment,
result,
SqError,
SqProject,
SqRecord,
SqValue,
} from "@quri/squiggle-lang";
import { useEffect, useMemo } from "react"; import { useEffect, useMemo } from "react";
import { JsImports, jsImportsToSquiggleCode } from "../jsImports"; import { JsImports, jsImportsToSquiggleCode } from "../jsImports";
@ -10,7 +17,12 @@ type SquiggleArgs = {
onChange?: (expr: SqValue | undefined) => void; onChange?: (expr: SqValue | undefined) => void;
}; };
export const useSquiggle = (args: SquiggleArgs) => { export type ResultAndBindings = {
result: result<SqValue, SqError>;
bindings: SqRecord;
};
export const useSquiggle = (args: SquiggleArgs): ResultAndBindings => {
const result = useMemo( const result = useMemo(
() => { () => {
const project = SqProject.create(); const project = SqProject.create();

View File

@ -1,4 +1,5 @@
import { result } from "@quri/squiggle-lang"; import { result, resultMap, SqValueTag } from "@quri/squiggle-lang";
import { ResultAndBindings } from "./hooks/useSquiggle";
export function flattenResult<a, b>(x: result<a, b>[]): result<a[], b> { export function flattenResult<a, b>(x: result<a, b>[]): result<a[], b> {
if (x.length === 0) { if (x.length === 0) {
@ -35,3 +36,18 @@ export function all(arr: boolean[]): boolean {
export function some(arr: boolean[]): boolean { export function some(arr: boolean[]): boolean {
return arr.reduce((x, y) => x || y, false); return arr.reduce((x, y) => x || y, false);
} }
export function getValueToRender({ result, bindings }: ResultAndBindings) {
return resultMap(result, (value) =>
value.tag === SqValueTag.Void ? bindings.asValue() : value
);
}
export function getErrorLocations(result: ResultAndBindings["result"]) {
if (result.tag === "Error") {
const location = result.value.toLocation();
return location ? [location] : [];
} else {
return [];
}
}

View File

@ -22,3 +22,8 @@ but this line is still necessary for proper initialization of `--tw-*` variables
.ace_cursor { .ace_cursor {
border-left: 2px solid !important; border-left: 2px solid !important;
} }
.ace-error-marker {
position: absolute;
border-bottom: 1px solid red;
}

View File

@ -22,4 +22,8 @@ export class SqError {
return stackTrace ? RSError.StackTrace.toLocationArray(stackTrace) : []; return stackTrace ? RSError.StackTrace.toLocationArray(stackTrace) : [];
} }
toLocation() {
return RSError.getLocation(this._value);
}
} }

View File

@ -98,7 +98,7 @@ let rec evaluate: T.reducerFn = (expression, context): (T.value, T.context) => {
| T.ECall(fn, args) => { | T.ECall(fn, args) => {
let (lambda, _) = fn->evaluate(context) let (lambda, _) = fn->evaluate(context)
let argValues = Js.Array2.map(args, arg => { let argValues = Belt.Array.map(args, arg => {
let (argValue, _) = arg->evaluate(context) let (argValue, _) = arg->evaluate(context)
argValue argValue
}) })