implement error markers in editor
This commit is contained in:
parent
845d38e375
commit
4c56b2fd07
|
@ -5,6 +5,8 @@ import AceEditor from "react-ace";
|
|||
import "ace-builds/src-noconflict/mode-golang";
|
||||
import "ace-builds/src-noconflict/theme-github";
|
||||
|
||||
import { SqLocation } from "@quri/squiggle-lang";
|
||||
|
||||
interface CodeEditorProps {
|
||||
value: string;
|
||||
onChange: (value: string) => void;
|
||||
|
@ -13,15 +15,17 @@ interface CodeEditorProps {
|
|||
width?: number;
|
||||
height: number;
|
||||
showGutter?: boolean;
|
||||
errorLocations?: SqLocation[];
|
||||
}
|
||||
|
||||
export const CodeEditor: FC<CodeEditorProps> = ({
|
||||
value,
|
||||
onChange,
|
||||
onSubmit,
|
||||
height,
|
||||
oneLine = false,
|
||||
showGutter = false,
|
||||
height,
|
||||
errorLocations = [],
|
||||
}) => {
|
||||
const lineCount = value.split("\n").length;
|
||||
const id = useMemo(() => _.uniqueId(), []);
|
||||
|
@ -30,8 +34,11 @@ export const CodeEditor: FC<CodeEditorProps> = ({
|
|||
const onSubmitRef = useRef<typeof onSubmit | null>(null);
|
||||
onSubmitRef.current = onSubmit;
|
||||
|
||||
const editorEl = useRef<AceEditor | null>(null);
|
||||
|
||||
return (
|
||||
<AceEditor
|
||||
ref={editorEl}
|
||||
value={value}
|
||||
mode="golang"
|
||||
theme="github"
|
||||
|
@ -59,6 +66,14 @@ export const CodeEditor: FC<CodeEditorProps> = ({
|
|||
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",
|
||||
}))}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -9,6 +9,7 @@ import {
|
|||
import { useSquiggle } from "../lib/hooks";
|
||||
import { SquiggleViewer } from "./SquiggleViewer";
|
||||
import { JsImports } from "../lib/jsImports";
|
||||
import { getValueToRender } from "../lib/utility";
|
||||
|
||||
export interface SquiggleChartProps {
|
||||
/** The input string for squiggle */
|
||||
|
@ -58,16 +59,9 @@ export interface SquiggleChartProps {
|
|||
const defaultOnChange = () => {};
|
||||
const defaultImports: JsImports = {};
|
||||
|
||||
export const SquiggleChart: React.FC<SquiggleChartProps> = React.memo(
|
||||
({
|
||||
code = "",
|
||||
executionId = 0,
|
||||
environment,
|
||||
onChange = defaultOnChange, // defaultOnChange must be constant, don't move its definition here
|
||||
height = 200,
|
||||
jsImports = defaultImports,
|
||||
export const splitSquiggleChartSettings = (props: SquiggleChartProps) => {
|
||||
const {
|
||||
showSummary = false,
|
||||
width,
|
||||
logX = false,
|
||||
expY = false,
|
||||
diagramStart = 0,
|
||||
|
@ -80,9 +74,47 @@ export const SquiggleChart: React.FC<SquiggleChartProps> = React.memo(
|
|||
title,
|
||||
xAxisType = "number",
|
||||
distributionChartActions,
|
||||
enableLocalSettings = false,
|
||||
}) => {
|
||||
const { result, bindings } = useSquiggle({
|
||||
} = props;
|
||||
|
||||
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,
|
||||
environment,
|
||||
jsImports,
|
||||
|
@ -90,32 +122,11 @@ export const SquiggleChart: React.FC<SquiggleChartProps> = React.memo(
|
|||
executionId,
|
||||
});
|
||||
|
||||
const distributionPlotSettings = {
|
||||
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
|
||||
);
|
||||
const valueToRender = getValueToRender(resultAndBindings);
|
||||
|
||||
return (
|
||||
<SquiggleViewer
|
||||
result={resultToRender}
|
||||
result={valueToRender}
|
||||
width={width}
|
||||
height={height}
|
||||
distributionPlotSettings={distributionPlotSettings}
|
||||
|
|
|
@ -1,13 +1,21 @@
|
|||
import React from "react";
|
||||
import { CodeEditor } from "./CodeEditor";
|
||||
import { SquiggleContainer } from "./SquiggleContainer";
|
||||
import { SquiggleChart, SquiggleChartProps } from "./SquiggleChart";
|
||||
import { useMaybeControlledValue } from "../lib/hooks";
|
||||
import {
|
||||
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<{
|
||||
code: string;
|
||||
setCode: (code: string) => void;
|
||||
}> = ({ code, setCode }) => (
|
||||
errorLocations?: SqLocation[];
|
||||
}> = ({ code, setCode, errorLocations }) => (
|
||||
<div className="border border-grey-200 p-2 m-4">
|
||||
<CodeEditor
|
||||
value={code}
|
||||
|
@ -15,6 +23,7 @@ const WrappedCodeEditor: React.FC<{
|
|||
oneLine={true}
|
||||
showGutter={false}
|
||||
height={20}
|
||||
errorLocations={errorLocations}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
@ -24,6 +33,9 @@ export type SquiggleEditorProps = SquiggleChartProps & {
|
|||
onCodeChange?: (code: string) => void;
|
||||
};
|
||||
|
||||
const defaultOnChange = () => {};
|
||||
const defaultImports: JsImports = {};
|
||||
|
||||
export const SquiggleEditor: React.FC<SquiggleEditorProps> = (props) => {
|
||||
const [code, setCode] = useMaybeControlledValue({
|
||||
value: props.code,
|
||||
|
@ -31,11 +43,46 @@ export const SquiggleEditor: React.FC<SquiggleEditorProps> = (props) => {
|
|||
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 (
|
||||
<SquiggleContainer>
|
||||
<WrappedCodeEditor code={code} setCode={setCode} />
|
||||
<SquiggleChart {...chartProps} />
|
||||
<WrappedCodeEditor
|
||||
code={code}
|
||||
setCode={setCode}
|
||||
errorLocations={errorLocations}
|
||||
/>
|
||||
<SquiggleViewer
|
||||
result={valueToRender}
|
||||
width={width}
|
||||
height={height}
|
||||
distributionPlotSettings={distributionPlotSettings}
|
||||
chartSettings={chartSettings}
|
||||
environment={environment ?? defaultEnvironment}
|
||||
enableLocalSettings={enableLocalSettings}
|
||||
/>
|
||||
</SquiggleContainer>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -17,21 +17,24 @@ const StackTraceLocation: React.FC<{ location: SqLocation }> = ({
|
|||
};
|
||||
|
||||
const StackTrace: React.FC<Props> = ({ error }) => {
|
||||
return (
|
||||
const locations = error.toLocationArray();
|
||||
return locations.length ? (
|
||||
<div>
|
||||
{error.toLocationArray().map((location, i) => (
|
||||
<StackTraceLocation location={location} key={i} />
|
||||
))}
|
||||
<div>Traceback:</div>
|
||||
<div className="ml-4">
|
||||
{locations.map((location, i) => (
|
||||
<StackTraceLocation location={location} key={i} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
) : null;
|
||||
};
|
||||
|
||||
export const SquiggleErrorAlert: React.FC<Props> = ({ error }) => {
|
||||
return (
|
||||
<ErrorAlert heading="Error">
|
||||
<div>{error.toString()}</div>
|
||||
<div className="mt-4">Traceback:</div>
|
||||
<div className="ml-4">
|
||||
<div className="space-y-4">
|
||||
<div>{error.toString()}</div>
|
||||
<StackTrace error={error} />
|
||||
</div>
|
||||
</ErrorAlert>
|
||||
|
|
|
@ -8,7 +8,11 @@ import React, {
|
|||
} from "react";
|
||||
import { useForm, UseFormRegister, useWatch } from "react-hook-form";
|
||||
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 {
|
||||
ChartSquareBarIcon,
|
||||
|
@ -26,7 +30,7 @@ import clsx from "clsx";
|
|||
|
||||
import { environment } from "@quri/squiggle-lang";
|
||||
|
||||
import { SquiggleChart, SquiggleChartProps } from "./SquiggleChart";
|
||||
import { SquiggleChartProps } from "./SquiggleChart";
|
||||
import { CodeEditor } from "./CodeEditor";
|
||||
import { JsonEditor } from "./JsonEditor";
|
||||
import { ErrorAlert, SuccessAlert } from "./Alert";
|
||||
|
@ -40,6 +44,8 @@ import { HeadedSection } from "./ui/HeadedSection";
|
|||
import { defaultTickFormat } from "../lib/distributionSpecBuilder";
|
||||
import { Button } from "./ui/Button";
|
||||
import { JsImports } from "../lib/jsImports";
|
||||
import { getErrorLocations, getValueToRender } from "../lib/utility";
|
||||
import { SquiggleViewer } from "./SquiggleViewer";
|
||||
|
||||
type PlaygroundProps = SquiggleChartProps & {
|
||||
/** The initial squiggle string to put in the playground */
|
||||
|
@ -282,7 +288,7 @@ export const SquigglePlayground: FC<PlaygroundProps> = ({
|
|||
onSettingsChange?.(vars);
|
||||
}, [vars, onSettingsChange]);
|
||||
|
||||
const env: environment = useMemo(
|
||||
const environment: environment = useMemo(
|
||||
() => ({
|
||||
sampleCount: Number(vars.sampleCount),
|
||||
xyPointLength: Number(vars.xyPointLength),
|
||||
|
@ -299,26 +305,51 @@ export const SquigglePlayground: FC<PlaygroundProps> = ({
|
|||
executionId,
|
||||
} = useRunnerState(code);
|
||||
|
||||
const resultAndBindings = useSquiggle({
|
||||
code,
|
||||
environment,
|
||||
jsImports: imports,
|
||||
executionId,
|
||||
});
|
||||
|
||||
const valueToRender = getValueToRender(resultAndBindings);
|
||||
|
||||
const squiggleChart =
|
||||
renderedCode === "" ? null : (
|
||||
<div className="relative">
|
||||
{isRunning ? (
|
||||
<div className="absolute inset-0 bg-white opacity-0 animate-semi-appear" />
|
||||
) : null}
|
||||
<SquiggleChart
|
||||
code={renderedCode}
|
||||
executionId={executionId}
|
||||
environment={env}
|
||||
{...vars}
|
||||
jsImports={imports}
|
||||
<SquiggleViewer
|
||||
result={valueToRender}
|
||||
environment={environment}
|
||||
height={vars.chartHeight || 150}
|
||||
distributionPlotSettings={{
|
||||
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}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
const errorLocations = getErrorLocations(resultAndBindings.result);
|
||||
|
||||
const firstTab = vars.showEditor ? (
|
||||
<div className="border border-slate-200">
|
||||
<CodeEditor
|
||||
errorLocations={errorLocations}
|
||||
value={code}
|
||||
onChange={setCode}
|
||||
onSubmit={run}
|
||||
|
|
|
@ -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 { JsImports, jsImportsToSquiggleCode } from "../jsImports";
|
||||
|
||||
|
@ -10,7 +17,12 @@ type SquiggleArgs = {
|
|||
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 project = SqProject.create();
|
||||
|
|
|
@ -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> {
|
||||
if (x.length === 0) {
|
||||
|
@ -35,3 +36,18 @@ export function all(arr: boolean[]): boolean {
|
|||
export function some(arr: boolean[]): boolean {
|
||||
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 [];
|
||||
}
|
||||
}
|
||||
|
|
|
@ -22,3 +22,8 @@ but this line is still necessary for proper initialization of `--tw-*` variables
|
|||
.ace_cursor {
|
||||
border-left: 2px solid !important;
|
||||
}
|
||||
|
||||
.ace-error-marker {
|
||||
position: absolute;
|
||||
border-bottom: 1px solid red;
|
||||
}
|
||||
|
|
|
@ -22,4 +22,8 @@ export class SqError {
|
|||
|
||||
return stackTrace ? RSError.StackTrace.toLocationArray(stackTrace) : [];
|
||||
}
|
||||
|
||||
toLocation() {
|
||||
return RSError.getLocation(this._value);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -98,7 +98,7 @@ let rec evaluate: T.reducerFn = (expression, context): (T.value, T.context) => {
|
|||
|
||||
| T.ECall(fn, args) => {
|
||||
let (lambda, _) = fn->evaluate(context)
|
||||
let argValues = Js.Array2.map(args, arg => {
|
||||
let argValues = Belt.Array.map(args, arg => {
|
||||
let (argValue, _) = arg->evaluate(context)
|
||||
argValue
|
||||
})
|
||||
|
|
Loading…
Reference in New Issue
Block a user