diff --git a/packages/components/src/components/CodeEditor.tsx b/packages/components/src/components/CodeEditor.tsx index 15802131..ec2fdee7 100644 --- a/packages/components/src/components/CodeEditor.tsx +++ b/packages/components/src/components/CodeEditor.tsx @@ -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 = ({ 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 = ({ const onSubmitRef = useRef(null); onSubmitRef.current = onSubmit; + const editorEl = useRef(null); + return ( = ({ 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", + }))} /> ); }; diff --git a/packages/components/src/components/SquiggleChart.tsx b/packages/components/src/components/SquiggleChart.tsx index 4dd57a1f..2e126d4c 100644 --- a/packages/components/src/components/SquiggleChart.tsx +++ b/packages/components/src/components/SquiggleChart.tsx @@ -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 = 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 = 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 = 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 = 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 ( void; -}> = ({ code, setCode }) => ( + errorLocations?: SqLocation[]; +}> = ({ code, setCode, errorLocations }) => (
); @@ -24,6 +33,9 @@ export type SquiggleEditorProps = SquiggleChartProps & { onCodeChange?: (code: string) => void; }; +const defaultOnChange = () => {}; +const defaultImports: JsImports = {}; + export const SquiggleEditor: React.FC = (props) => { const [code, setCode] = useMaybeControlledValue({ value: props.code, @@ -31,11 +43,46 @@ export const SquiggleEditor: React.FC = (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 ( - - + + ); }; diff --git a/packages/components/src/components/SquiggleErrorAlert.tsx b/packages/components/src/components/SquiggleErrorAlert.tsx index e9dd4245..fb4721eb 100644 --- a/packages/components/src/components/SquiggleErrorAlert.tsx +++ b/packages/components/src/components/SquiggleErrorAlert.tsx @@ -17,21 +17,24 @@ const StackTraceLocation: React.FC<{ location: SqLocation }> = ({ }; const StackTrace: React.FC = ({ error }) => { - return ( + const locations = error.toLocationArray(); + return locations.length ? (
- {error.toLocationArray().map((location, i) => ( - - ))} +
Traceback:
+
+ {locations.map((location, i) => ( + + ))} +
- ); + ) : null; }; export const SquiggleErrorAlert: React.FC = ({ error }) => { return ( -
{error.toString()}
-
Traceback:
-
+
+
{error.toString()}
diff --git a/packages/components/src/components/SquigglePlayground.tsx b/packages/components/src/components/SquigglePlayground.tsx index 8039bbe0..de467918 100644 --- a/packages/components/src/components/SquigglePlayground.tsx +++ b/packages/components/src/components/SquigglePlayground.tsx @@ -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 = ({ 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 = ({ executionId, } = useRunnerState(code); + const resultAndBindings = useSquiggle({ + code, + environment, + jsImports: imports, + executionId, + }); + + const valueToRender = getValueToRender(resultAndBindings); + const squiggleChart = renderedCode === "" ? null : (
{isRunning ? (
) : null} -
); + const errorLocations = getErrorLocations(resultAndBindings.result); + const firstTab = vars.showEditor ? (
void; }; -export const useSquiggle = (args: SquiggleArgs) => { +export type ResultAndBindings = { + result: result; + bindings: SqRecord; +}; + +export const useSquiggle = (args: SquiggleArgs): ResultAndBindings => { const result = useMemo( () => { const project = SqProject.create(); diff --git a/packages/components/src/lib/utility.ts b/packages/components/src/lib/utility.ts index d4d3661b..c3ce08a4 100644 --- a/packages/components/src/lib/utility.ts +++ b/packages/components/src/lib/utility.ts @@ -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(x: result[]): result { 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 []; + } +} diff --git a/packages/components/src/styles/main.css b/packages/components/src/styles/main.css index 987c3714..257c5712 100644 --- a/packages/components/src/styles/main.css +++ b/packages/components/src/styles/main.css @@ -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; +} diff --git a/packages/squiggle-lang/src/js/SqError.ts b/packages/squiggle-lang/src/js/SqError.ts index 5025e671..5e599e51 100644 --- a/packages/squiggle-lang/src/js/SqError.ts +++ b/packages/squiggle-lang/src/js/SqError.ts @@ -22,4 +22,8 @@ export class SqError { return stackTrace ? RSError.StackTrace.toLocationArray(stackTrace) : []; } + + toLocation() { + return RSError.getLocation(this._value); + } } diff --git a/packages/squiggle-lang/src/rescript/Reducer/Reducer_Expression/Reducer_Expression.res b/packages/squiggle-lang/src/rescript/Reducer/Reducer_Expression/Reducer_Expression.res index d33d7ba3..10e026a3 100644 --- a/packages/squiggle-lang/src/rescript/Reducer/Reducer_Expression/Reducer_Expression.res +++ b/packages/squiggle-lang/src/rescript/Reducer/Reducer_Expression/Reducer_Expression.res @@ -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 })