diff --git a/packages/components/src/components/CodeEditor/index.tsx b/packages/components/src/components/CodeEditor/index.tsx index 8f213f5a71..10d1e27823 100644 --- a/packages/components/src/components/CodeEditor/index.tsx +++ b/packages/components/src/components/CodeEditor/index.tsx @@ -15,7 +15,7 @@ export type CodeEditorProps = { showGutter?: boolean; lineWrapping?: boolean; errors?: SqError[]; - sourceId?: string; + sourceId: string; fontSize?: number; project: SqProject; }; diff --git a/packages/components/src/components/CodeEditor/languageSupport/autocomplete.tsx b/packages/components/src/components/CodeEditor/languageSupport/autocomplete.tsx index 6e8f89e6d8..5e7702dda5 100644 --- a/packages/components/src/components/CodeEditor/languageSupport/autocomplete.tsx +++ b/packages/components/src/components/CodeEditor/languageSupport/autocomplete.tsx @@ -11,27 +11,38 @@ import { SqProject } from "@quri/squiggle-lang"; import { FnDocumentationFromName } from "../../ui/FnDocumentation.js"; -export function getNameNodes(tree: Tree, from: number) { +type NameNode = { + node: SyntaxNode; + type: "function" | "variable"; +}; + +export function getNameNodes(tree: Tree, from: number): NameNode[] { const cursor = tree.cursorAt(from, -1); - const nameNodes: SyntaxNode[] = []; + const nameNodes: NameNode[] = []; // We walk up and backwards through the tree, looking for nodes that have names. let direction: "start" | "sibling" | "parent" | undefined = "start"; while (1) { - // Only for sibling nodes; `foo = { }` shouldn't autocomplete `foo`. - if (cursor.type.is("Binding") && direction === "sibling") { - const nameNode = cursor.node.getChild("VariableName"); - if (nameNode) { - nameNodes.push(nameNode); + if (cursor.type.is("Statement") && direction === "sibling") { + // Only for sibling nodes; `foo = { }` shouldn't autocomplete `foo`. + + // Unwrap decorated statements. + let node: SyntaxNode | null = cursor.node; + while (node && node.type.is("DecoratedStatement")) { + node = node.getChild("Statement"); } - // Only for sibling nodes; Squiggle doesn't support recursive calls. - } else if (cursor.type.is("FunDeclaration") && direction === "sibling") { - const nameNode = cursor.node.getChild("FunctionName"); - if (nameNode) { - nameNodes.push(nameNode); + + const nameNode = node?.getChild("VariableName"); + if (node && nameNode) { + nameNodes.push({ + node: nameNode, + type: node?.type.is("DefunStatement") ? "function" : "variable", + }); } - } else if (cursor.type.is("FunDeclaration") && direction !== "sibling") { + } else if (cursor.type.is("DefunStatement") && direction !== "sibling") { + // Function declaration that's a parent, let's autocomplete its parameter names. + // Note that we also allow `direction === "start"`, to handle `f(foo) = foo` correctly. const parameterNodes = cursor.node.getChild("LambdaArgs")?.getChildren("LambdaParameter") ?? []; @@ -39,11 +50,14 @@ export function getNameNodes(tree: Tree, from: number) { for (const parameter of parameterNodes) { const nameNode = parameter.getChild("LambdaParameterName"); if (nameNode) { - nameNodes.push(nameNode); + nameNodes.push({ + node: nameNode, + // Is there a more specific type? There's no "parameter" type in CodeMirror. + // https://codemirror.net/docs/ref/#autocomplete.Completion.type + type: "variable", + }); } } - } else if (cursor.type.is("Decorator") && direction !== "sibling") { - // TODO } // Move to the next node and store the direction that we used. @@ -106,7 +120,7 @@ export function makeCompletionSource(project: SqProject) { snippetCompletion("|${args}| ${body}", { label: "|", detail: "lambda function", - type: "syntax", + type: "text", }), ], }; @@ -114,16 +128,18 @@ export function makeCompletionSource(project: SqProject) { } { - const identifier = cmpl.tokenBefore(["AccessExpr", "IdentifierExpr"]); + const identifier = cmpl.tokenBefore(["AccessExpr", "Identifier"]); if (identifier) { const { from } = identifier; const nameNodes = getNameNodes(tree, from); - const localCompletions = nameNodes.map((node): Completion => { - const name = cmpl.state.doc.sliceString(node.from, node.to); - const type = node.type.is("FunctionName") ? "function" : "variable"; + const localCompletions = nameNodes.map((nameNode): Completion => { + const name = cmpl.state.doc.sliceString( + nameNode.node.from, + nameNode.node.to + ); return { label: name, - type, + type: nameNode.type, }; }); diff --git a/packages/components/src/components/CodeEditor/languageSupport/highlightingStyle.ts b/packages/components/src/components/CodeEditor/languageSupport/highlightingStyle.ts index e3ba235aa7..c6bbe4c9c0 100644 --- a/packages/components/src/components/CodeEditor/languageSupport/highlightingStyle.ts +++ b/packages/components/src/components/CodeEditor/languageSupport/highlightingStyle.ts @@ -51,10 +51,7 @@ export const lightThemeHighlightingStyle = HighlightStyle.define([ ], color: numbers, }, - { - tag: [tags.escape], - color: escapes, - }, + { tag: tags.escape, color: escapes }, { tag: [ tags.operator, @@ -67,7 +64,7 @@ export const lightThemeHighlightingStyle = HighlightStyle.define([ fontWeight: "bold", color: operators, }, - { tag: [tags.meta, tags.comment], color: comments }, + { tag: tags.comment, color: comments }, { tag: tags.strong, fontWeight: "bold" }, { tag: tags.emphasis, fontStyle: "italic" }, { tag: tags.strikethrough, textDecoration: "line-through" }, diff --git a/packages/components/src/components/CodeEditor/languageSupport/squiggle.grammar b/packages/components/src/components/CodeEditor/languageSupport/squiggle.grammar index b5b8f13797..00b4cfd169 100644 --- a/packages/components/src/components/CodeEditor/languageSupport/squiggle.grammar +++ b/packages/components/src/components/CodeEditor/languageSupport/squiggle.grammar @@ -40,7 +40,7 @@ commaSep { // when trailing comma is allowed commaSep1 { "" | content ("," content?)* } -Binding { export? VariableName { identifier } "=" expression } +LetStatement { export? VariableName { identifier } "=" expression } LambdaParameter { LambdaParameterName { identifier } (":" expression)? @@ -50,7 +50,7 @@ LambdaArgs { () | LambdaParameter ("," LambdaParameter)* } -FunDeclaration { export? FunctionName { identifier } ~callOrDeclaration "(" LambdaArgs ")" "=" expression } +DefunStatement { export? VariableName { identifier } ~callOrDeclaration "(" LambdaArgs ")" "=" expression } Decorator { "@" DecoratorName { identifier } @@ -60,38 +60,40 @@ Decorator { ) } -statement { - Decorator* - ( - Binding - | FunDeclaration - ) +statement[@isGroup="Statement"] { + LetStatement + | DefunStatement + | DecoratedStatement { Decorator statement } +} + +expression { + expressionWithoutParens + | ( "(" expression ")" ) } -expression[@isGroup="Expression"] { +expressionWithoutParens[@isGroup="Expression"] { String - | Boolean + | Boolean { @specialize[@name="Boolean"] } | Number - | BlockExpr { "{" blockContent "}" } - | DictExpr { + | Block { "{" blockContent "}" } + | Dict { "{" commaSep1< - Entry { Field[@dynamicPrecedence=1] { expression } ~inheritAmbig ":" expression } + KeyValue { Field[@dynamicPrecedence=1] { expression } ~inheritAmbig ":" expression } | InheritEntry { Field[@dynamicPrecedence=0] { identifier } ~inheritAmbig } > "}" } - | LambdaExpr { "{" ArgsOpen { "|" } LambdaArgs "|" blockContent "}" } - | IfExpr { if expression then expression !else else expression } - | ParenExpr { "(" expression ")" } - | IdentifierExpr { identifier } + | Lambda { "{" ArgsOpen { "|" } LambdaArgs "|" blockContent "}" } + | Identifier { identifier } | AccessExpr { expression !deref "." Field { identifier } } - | CallExpr { expression ~callOrDeclaration !call "(" commaSep ")" } - | TernaryExpr { expression !logop LogicOp<"?"> expression LogicOp<":"> expression } - | KVAccessExpr { expression !call ("[" Key { expression } "]") } - | ArrayExpr { "[" commaSep1 "]" } - | UnaryExpr { !unary (ArithOp<"-"> | ArithOp<"!"> | DotArithOp<".-">) expression } - | LogicExpr { + | Call { expression ~callOrDeclaration !call "(" commaSep ")" } + | TernaryC { expression !logop LogicOp<"?"> expression LogicOp<":"> expression } + | TernaryIfThenElse { if expression then expression !else else expression } + | BracketLookup { expression !call ("[" Key { expression } "]") } + | Array { "[" commaSep1 "]" } + | UnaryCall { !unary (ArithOp<"-"> | ArithOp<"!"> | DotArithOp<".-">) expression } + | InfixCall { expression !or LogicOp<"||"> expression | expression !and LogicOp<"&&"> expression | expression !rel LogicOp<">"> expression @@ -99,25 +101,21 @@ expression[@isGroup="Expression"] { | expression !rel LogicOp<"<="> expression | expression !rel LogicOp<">="> expression | expression !rel LogicOp<"=="> expression - } - | ControlExpr { - expression !control ControlOp<"->"> expression - } - | ArithExpr { - expression !times ( ArithOp<"*"> | DotArithOp<".*"> ) expression + | expression !times ( ArithOp<"*"> | DotArithOp<".*"> ) expression | expression !times ( ArithOp<"/"> | DotArithOp<"./"> ) expression | expression !exp ( ArithOp<"^"> | DotArithOp<".^"> ) expression | expression !plus ( ArithOp<"+"> | DotArithOp<".+"> ) expression | expression !plus ( ArithOp<"-"> | DotArithOp<".-"> ) expression | expression !plus @extend[@name="ArithOp"] expression } + | Pipe { + expression !control ControlOp<"->"> expression + } } +// use `@extend` instead of `@specialize`, because keywords are valid variable names in Squiggle, for now. +kw { @extend[@name={term}] } - -Boolean { @specialize[@name="Boolean"] } - -kw { @specialize[@name={term}] } if { kw<"if"> } then { kw<"then"> } else { kw<"else"> } diff --git a/packages/components/src/components/CodeEditor/languageSupport/squiggle.ts b/packages/components/src/components/CodeEditor/languageSupport/squiggle.ts index e264dd0c1b..8cb2d8c914 100644 --- a/packages/components/src/components/CodeEditor/languageSupport/squiggle.ts +++ b/packages/components/src/components/CodeEditor/languageSupport/squiggle.ts @@ -15,15 +15,9 @@ import { parser } from "./generated/squiggle.js"; const parserWithMetadata = parser.configure({ props: [ styleTags({ - if: t.keyword, - then: t.keyword, - else: t.keyword, - import: t.keyword, - export: t.keyword, - as: t.keyword, + "if then else import export as": t.keyword, Equals: t.definitionOperator, - ArithOp: t.arithmeticOperator, LogicOp: t.logicOperator, ControlOp: t.controlOperator, @@ -38,40 +32,36 @@ const parserWithMetadata = parser.configure({ Boolean: t.bool, Number: t.integer, String: t.string, - Comment: t.comment, - Void: t.escape, + LineComment: t.lineComment, + BlockComment: t.blockComment, Escape: t.escape, - FunctionName: t.function(t.variableName), - DecoratorName: t.variableName, "Decorator/*/String": t.comment, At: t.keyword, - LambdaSyntax: t.blockComment, - VariableName: t.constant(t.variableName), - IdentifierExpr: t.variableName, + Identifier: t.variableName, Field: t.variableName, LambdaParameterName: t.variableName, }), foldNodeProp.add({ - LambdaExpr: (context) => ({ + Lambda: (context) => ({ from: context.getChild("NonEmptyProgram")?.from || 0, to: context.getChild("NonEmptyProgram")?.to || 0, }), - BlockExpr: foldInside, - DictExpr: foldInside, - ArrayExpr: foldInside, + Block: foldInside, + Dict: foldInside, + Array: foldInside, }), indentNodeProp.add({ - DictExpr: (context) => + Dict: (context) => context.baseIndent + (context.textAfter === "}" ? 0 : context.unit), - BlockExpr: (context) => + Block: (context) => context.baseIndent + (context.textAfter === "}" ? 0 : context.unit), - LambdaExpr: (context) => + Lambda: (context) => context.baseIndent + (context.textAfter === "}" ? 0 : context.unit), - ArrayExpr: (context) => + Array: (context) => context.baseIndent + (context.textAfter === "]" ? 0 : context.unit), }), ], diff --git a/packages/components/src/components/CodeEditor/useSquiggleEditorView.ts b/packages/components/src/components/CodeEditor/useSquiggleEditorView.ts index 74cd10f729..a5845deaf6 100644 --- a/packages/components/src/components/CodeEditor/useSquiggleEditorView.ts +++ b/packages/components/src/components/CodeEditor/useSquiggleEditorView.ts @@ -110,7 +110,10 @@ export function useSquiggleEditorExtensions( const formatExtension = useFormatSquiggleExtension(); const errorsExtension = useErrorsExtension(view, params.errors); - const tooltipsExtension = useTooltipsExtension(); + const tooltipsExtension = useTooltipsExtension(view, { + project: params.project, + sourceId: params.sourceId, + }); const highPrioritySquiggleExtensions = [ submitExtension, // works only if listed before `builtinExtensions` diff --git a/packages/components/src/components/CodeEditor/useTooltipsExtension.tsx b/packages/components/src/components/CodeEditor/useTooltipsExtension.tsx index d6a44d6df9..0572584d8e 100644 --- a/packages/components/src/components/CodeEditor/useTooltipsExtension.tsx +++ b/packages/components/src/components/CodeEditor/useTooltipsExtension.tsx @@ -1,66 +1,213 @@ +import { syntaxTree } from "@codemirror/language"; import { EditorView, hoverTooltip, repositionTooltips } from "@codemirror/view"; -import { FC, useEffect } from "react"; +import { SyntaxNode } from "@lezer/common"; +import { FC, PropsWithChildren, useEffect } from "react"; import { createRoot } from "react-dom/client"; -import { getFunctionDocumentation } from "@quri/squiggle-lang"; +import { + getFunctionDocumentation, + SqProject, + SqValue, +} from "@quri/squiggle-lang"; +import { valueHasContext } from "../../lib/utility.js"; +import { SquiggleValueChart } from "../SquiggleViewer/SquiggleValueChart.js"; +import { + InnerViewerProvider, + useViewerContext, +} from "../SquiggleViewer/ViewerProvider.js"; import { FnDocumentation } from "../ui/FnDocumentation.js"; +import { useReactiveExtension } from "./codemirrorHooks.js"; type Hover = NonNullable>; -const HoverTooltip: FC<{ hover: Hover; view: EditorView }> = ({ - hover, +const TooltipBox: FC> = ({ view, + children, }) => { useEffect(() => { - // https://codemirror.net/docs/ref/#view.repositionTooltips need to be called on each render. + // https://codemirror.net/docs/ref/#view.repositionTooltips needs to be called on each render. repositionTooltips(view); }); return ( -
- +
+ {children}
); }; +const ValueTooltip: FC<{ value: SqValue; view: EditorView }> = ({ + value, + view, +}) => { + const { globalSettings } = useViewerContext(); + + if (valueHasContext(value)) { + return ( + +
+ {/* Force a standalone ephemeral ViewerProvider, so that we won't sync up collapsed state with the top-level viewer */} + + + +
+
+ ); + } else { + return null; // shouldn't happen + } +}; + +const HoverTooltip: FC<{ hover: Hover; view: EditorView }> = ({ + hover, + view, +}) => ( + +
+ +
+
+); + // Based on https://codemirror.net/examples/tooltip/#hover-tooltips // See also: https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_hover -const wordHover = hoverTooltip((view, pos, side) => { - const { doc } = view.state; - const { from, to, text } = doc.lineAt(pos); - let start = pos, - end = pos; - while (start > from && /[\w.]/.test(text[start - from - 1])) start--; - while (end < to && /[\w.]/.test(text[end - from])) end++; - if ((start === pos && side < 0) || (end === pos && side > 0)) return null; - - const token = text.slice(start - from, end - from); - const hover = getFunctionDocumentation(token); - if (!hover) { - return null; - } +function buildWordHoverExtension({ + project, + sourceId, +}: { + project: SqProject; + sourceId: string; +}) { + return hoverTooltip((view, pos, side) => { + const { doc } = view.state; - return { - pos: start, - end, - above: true, - create() { - const dom = document.createElement("div"); - const root = createRoot(dom); - root.render(); - return { dom }; - }, - }; -}); + const tree = syntaxTree(view.state); + const cursor = tree.cursorAt(pos, side); + + const getText = (node: SyntaxNode) => doc.sliceString(node.from, node.to); + + const createBuiltinTooltip = (node: SyntaxNode) => { + const hover = getFunctionDocumentation(getText(node)); + if (!hover) { + return null; + } + + return { + pos: node.from, + end: node.to, + above: true, + create() { + const dom = document.createElement("div"); + const root = createRoot(dom); + root.render(); + return { dom }; + }, + }; + }; + + const createTopLevelVariableNameTooltip = ( + node: SyntaxNode, + value: SqValue + ) => { + return { + pos: node.from, + end: node.to, + above: true, + create() { + const dom = document.createElement("div"); + const root = createRoot(dom); + root.render(); + return { dom }; + }, + }; + }; + + switch (cursor.name) { + case "Identifier": + if (getText(cursor.node).match(/^[A-Z]/)) { + // TODO - expand the namespace to the identifier, or just show the namespace documentation + return null; + } + // TODO - check that the identifier is not overwritten by a local variable + return createBuiltinTooltip(cursor.node); + case "Field": + // `Namespace.function`; go up to fully identified name. + if (!cursor.parent()) { + return null; + } + return createBuiltinTooltip(cursor.node); + case "VariableName": { + const node = cursor.node; + + // Let's find the statement that declares this variable. + if (!cursor.parent()) { + return null; + } + // Ascend through decorated statements. + while (cursor.type.is("Statement") && cursor.parent()); + + // Is this a top-level variable? + if (!cursor.type.is("Program")) { + return null; + } + + const name = getText(node); + + const bindings = project.getBindings(sourceId); + if (!bindings.ok) return null; + + const value = bindings.value.get(name); + if (!value) return null; + + // Should be LetStatement or DefunStatement + const valueAst = value.context?.valueAst; + + if ( + valueAst && + (valueAst.type === "LetStatement" || + valueAst.type === "DefunStatement") && + // If these don't match then variable was probably shadowed by a later statement and we can't show its value. + // Or it could be caused by code rot, if we change the logic of how `valueAst` is computed, or add another statement type in AST. + // TODO - if we can prove that the variable was shadowed, show the tooltip pointing to the latest assignment. + valueAst.variable.location.start.offset === node.from && + valueAst.variable.location.end.offset === node.to + ) { + return createTopLevelVariableNameTooltip(node, value); + } + } + } + + return null; + }); +} const tooltipTheme = EditorView.baseTheme({ ".cm-tooltip-hover": { backgroundColor: "white !important", border: "0 !important", }, + ".cm-tooltip-section": { + height: "100%", // necessary for scrolling, see also: "h-full" in `TooltipBox` + }, }); -export function useTooltipsExtension() { - return [wordHover, tooltipTheme]; +export function useTooltipsExtension( + view: EditorView | undefined, + { + project, + sourceId, + }: { + project: SqProject; + sourceId: string; + } +) { + return useReactiveExtension( + view, + () => [buildWordHoverExtension({ project, sourceId }), tooltipTheme], + [project, sourceId] + ); } diff --git a/packages/components/src/components/CodeEditor/useViewNodeExtension.ts b/packages/components/src/components/CodeEditor/useViewNodeExtension.ts index b9981d2aa3..f65a69574f 100644 --- a/packages/components/src/components/CodeEditor/useViewNodeExtension.ts +++ b/packages/components/src/components/CodeEditor/useViewNodeExtension.ts @@ -9,12 +9,12 @@ export function useViewNodeExtension( view: EditorView | undefined, { project, - onViewValuePath, sourceId, + onViewValuePath, }: { project: SqProject; + sourceId: string; onViewValuePath?: (path: SqValuePath) => void; - sourceId?: string; } ) { const viewCurrentPosition = useCallback(() => { diff --git a/packages/components/src/components/SquiggleEditor.tsx b/packages/components/src/components/SquiggleEditor.tsx index 825d38cf43..9382eb9762 100644 --- a/packages/components/src/components/SquiggleEditor.tsx +++ b/packages/components/src/components/SquiggleEditor.tsx @@ -32,7 +32,7 @@ export const SquiggleEditor: FC = ({ const runnerState = useRunnerState(code); - const [squiggleOutput, { project, isRunning }] = useSquiggle({ + const [squiggleOutput, { project, isRunning, sourceId }] = useSquiggle({ code: runnerState.renderedCode, executionId: runnerState.executionId, ...(propsProject ? { project: propsProject, continues } : { environment }), @@ -60,6 +60,7 @@ export const SquiggleEditor: FC = ({ showGutter={false} errors={errors} project={project} + sourceId={sourceId} ref={editorRef} onSubmit={() => runnerState.run()} /> diff --git a/packages/components/src/components/SquiggleViewer/ValueWithContextViewer.tsx b/packages/components/src/components/SquiggleViewer/ValueWithContextViewer.tsx index e6303359eb..08bf4387b0 100644 --- a/packages/components/src/components/SquiggleViewer/ValueWithContextViewer.tsx +++ b/packages/components/src/components/SquiggleViewer/ValueWithContextViewer.tsx @@ -9,7 +9,6 @@ import { CommentIcon, TextTooltip } from "@quri/ui"; import { MarkdownViewer } from "../../lib/MarkdownViewer.js"; import { SqValueWithContext } from "../../lib/utility.js"; -import { leftWidgetMargin } from "../../widgets/utils.js"; import { ErrorBoundary } from "../ErrorBoundary.js"; import { CollapsedIcon, ExpandedIcon } from "./icons.js"; import { SquiggleValueChart } from "./SquiggleValueChart.js"; @@ -26,6 +25,7 @@ import { useRegisterAsItemViewer, useToggleCollapsed, useViewerContext, + useViewerType, } from "./ViewerProvider.js"; const CommentIconForValue: FC<{ value: SqValueWithContext }> = ({ value }) => { @@ -73,7 +73,6 @@ const WithComment: FC> = ({ value, children }) => {
@@ -119,6 +118,7 @@ export const ValueWithContextViewer: FC = ({ const toggleCollapsed_ = useToggleCollapsed(); const focus = useFocus(); + const viewerType = useViewerType(); const { itemStore } = useViewerContext(); const itemState = itemStore.getStateOrInitialize(value); @@ -130,6 +130,8 @@ export const ValueWithContextViewer: FC = ({ const header = props.header ?? (isRoot ? "hide" : "show"); const collapsible = header === "hide" ? false : props.collapsible ?? true; const size = props.size ?? "normal"; + const enableDropdownMenu = viewerType !== "tooltip"; + const enableFocus = viewerType !== "tooltip"; const toggleCollapsed = () => { toggleCollapsed_(path); @@ -141,7 +143,12 @@ export const ValueWithContextViewer: FC = ({ // In that case, the output would look broken (empty). const isOpen = !collapsible || !itemState.collapsed; - const _focus = () => focus(path); + const _focus = () => { + if (!enableFocus) { + return; + } + focus(path); + }; const triangleToggle = () => { const Icon = itemState.collapsed ? CollapsedIcon : ExpandedIcon; @@ -185,11 +192,15 @@ export const ValueWithContextViewer: FC = ({ const headerClasses = () => { if (header === "large") { - return clsx("text-md font-bold ml-1", headerColor); + return clsx("text-md font-bold", headerColor); } else if (isRoot) { return "text-sm text-stone-600 font-semibold"; } else { - return clsx("text-sm cursor-pointer hover:underline", headerColor); + return clsx( + "text-sm", + enableFocus && "cursor-pointer hover:underline", + headerColor + ); } }; @@ -243,9 +254,11 @@ export const ValueWithContextViewer: FC = ({ )} {!isOpen && }
-
- -
+ {enableDropdownMenu && ( +
+ +
+ )} )} {isOpen && ( diff --git a/packages/components/src/components/SquiggleViewer/ViewerProvider.tsx b/packages/components/src/components/SquiggleViewer/ViewerProvider.tsx index 1f469a8905..6ff3b4bc83 100644 --- a/packages/components/src/components/SquiggleViewer/ViewerProvider.tsx +++ b/packages/components/src/components/SquiggleViewer/ViewerProvider.tsx @@ -29,6 +29,8 @@ import { shouldBeginCollapsed, } from "./utils.js"; +type ViewerType = "normal" | "tooltip"; + export type SquiggleViewerHandle = { viewValuePath(path: SqValuePath): void; }; @@ -165,6 +167,7 @@ type ViewerContextShape = { setFocused: (value: SqValuePath | undefined) => void; editor?: CodeEditorHandle; itemStore: ItemStore; + viewerType: ViewerType; initialized: boolean; handle: SquiggleViewerHandle; }; @@ -175,6 +178,7 @@ export const ViewerContext = createContext({ setFocused: () => undefined, editor: undefined, itemStore: new ItemStore(), + viewerType: "normal", handle: { viewValuePath: () => {}, }, @@ -303,14 +307,25 @@ export function useMergedSettings(path: SqValuePath) { return result; } +export function useViewerType() { + const { viewerType } = useViewerContext(); + return viewerType; +} + type Props = PropsWithChildren<{ partialPlaygroundSettings: PartialPlaygroundSettings; editor?: CodeEditorHandle; + viewerType?: ViewerType; }>; export const InnerViewerProvider = forwardRef( ( - { partialPlaygroundSettings: unstablePlaygroundSettings, editor, children }, + { + partialPlaygroundSettings: unstablePlaygroundSettings, + editor, + viewerType = "normal", + children, + }, ref ) => { const [itemStore] = useState(() => new ItemStore()); @@ -346,6 +361,7 @@ export const InnerViewerProvider = forwardRef( focused, setFocused, itemStore, + viewerType, handle, initialized: true, }} diff --git a/packages/components/src/components/SquiggleViewer/index.tsx b/packages/components/src/components/SquiggleViewer/index.tsx index 706121d1a7..9234592592 100644 --- a/packages/components/src/components/SquiggleViewer/index.tsx +++ b/packages/components/src/components/SquiggleViewer/index.tsx @@ -50,7 +50,7 @@ const FocusedNavigation: FC<{ : 0; return ( -
+
{!rootPath?.items.length && ( )} @@ -84,37 +84,22 @@ const SquiggleViewerWithoutProvider: FC = ({ value }) => { focusedItem = getSubvalueByPath(value, focused); } - const body = () => { - if (focused) { - if (focusedItem) { - return ( -
- -
- ); - } else { - return ; - } - } else { - return ; - } - }; - - return ( -
- {focused && ( - + + {focusedItem ? ( + + ) : ( + )} - {body()}
+ ) : ( + ); }; diff --git a/packages/components/src/widgets/ArrayWidget.tsx b/packages/components/src/widgets/ArrayWidget.tsx index d5e7175cf3..8514568635 100644 --- a/packages/components/src/widgets/ArrayWidget.tsx +++ b/packages/components/src/widgets/ArrayWidget.tsx @@ -1,10 +1,11 @@ import { useMemo } from "react"; +import { DocumentTextIcon } from "@quri/ui"; + import { ValueViewer } from "../components/SquiggleViewer/ValueViewer.js"; +import { SqValueWithContext } from "../lib/utility.js"; import { widgetRegistry } from "./registry.js"; import { SqTypeWithCount } from "./SqTypeWithCount.js"; -import { SqValueWithContext } from "../lib/utility.js"; -import { DocumentTextIcon } from "@quri/ui"; function isNotebook(value: SqValueWithContext) { return Boolean(value.tags.notebook()); diff --git a/packages/components/src/widgets/BoolWidget.tsx b/packages/components/src/widgets/BoolWidget.tsx index 49584c50b2..e84b3b0557 100644 --- a/packages/components/src/widgets/BoolWidget.tsx +++ b/packages/components/src/widgets/BoolWidget.tsx @@ -1,17 +1,9 @@ -import { clsx } from "clsx"; - import { widgetRegistry } from "./registry.js"; -import { leftWidgetMargin } from "./utils.js"; widgetRegistry.register("Bool", { Preview: (value) => value.value.toString(), Chart: (value) => ( -
+
{value.value.toString()}
), diff --git a/packages/components/src/widgets/DurationWidget.tsx b/packages/components/src/widgets/DurationWidget.tsx index 69745341f4..64b3891683 100644 --- a/packages/components/src/widgets/DurationWidget.tsx +++ b/packages/components/src/widgets/DurationWidget.tsx @@ -1,11 +1,8 @@ -import { clsx } from "clsx"; - import { SqDurationValue } from "@quri/squiggle-lang"; import { NumberShower } from "../components/NumberShower.js"; import { formatNumber } from "../lib/d3/index.js"; import { widgetRegistry } from "./registry.js"; -import { leftWidgetMargin } from "./utils.js"; const showDuration = (duration: SqDurationValue) => { const numberFormat = duration.tags.numberFormat(); @@ -21,9 +18,7 @@ widgetRegistry.register("Duration", { Preview: (value) => showDuration(value), Chart: (value) => { return ( -
- {showDuration(value)} -
+
{showDuration(value)}
); }, }); diff --git a/packages/components/src/widgets/NumberWidget.tsx b/packages/components/src/widgets/NumberWidget.tsx index e772a04d90..bf375bce6e 100644 --- a/packages/components/src/widgets/NumberWidget.tsx +++ b/packages/components/src/widgets/NumberWidget.tsx @@ -3,9 +3,9 @@ import { clsx } from "clsx"; import { SqNumberValue } from "@quri/squiggle-lang"; import { NumberShower } from "../components/NumberShower.js"; +import { useViewerType } from "../components/SquiggleViewer/ViewerProvider.js"; import { formatNumber } from "../lib/d3/index.js"; import { widgetRegistry } from "./registry.js"; -import { leftWidgetMargin } from "./utils.js"; const showNumber = (value: SqNumberValue) => { const numberFormat = value.tags.numberFormat(); @@ -18,9 +18,17 @@ const showNumber = (value: SqNumberValue) => { widgetRegistry.register("Number", { Preview: (value) => showNumber(value), - Chart: (value) => ( -
- {showNumber(value)} -
- ), + Chart: (value) => { + const viewerType = useViewerType(); + return ( +
+ {showNumber(value)} +
+ ); + }, }); diff --git a/packages/components/test/autocompletion.test.ts b/packages/components/test/autocompletion.test.ts index 4e4a147c7d..5a840e7442 100644 --- a/packages/components/test/autocompletion.test.ts +++ b/packages/components/test/autocompletion.test.ts @@ -17,8 +17,8 @@ bar = 6 const nodes = getNameNodes(tree, code.length); expect(nodes.length).toBe(2); - expect(getText(code, nodes[0])).toBe("bar"); // code is traversed backwards - expect(getText(code, nodes[1])).toBe("foo"); + expect(getText(code, nodes[0].node)).toBe("bar"); // code is traversed backwards + expect(getText(code, nodes[1].node)).toBe("foo"); }); test("Parameter names", () => { @@ -28,8 +28,8 @@ myFun(arg1, arg2) = 1+`; const nodes = getNameNodes(tree, code.length); expect(nodes.length).toBe(2); - expect(getText(code, nodes[0])).toBe("arg1"); - expect(getText(code, nodes[1])).toBe("arg2"); + expect(getText(code, nodes[0].node)).toBe("arg1"); + expect(getText(code, nodes[1].node)).toBe("arg2"); }); test("Parameter names at the start of function body", () => { @@ -39,8 +39,8 @@ myFun(arg1, arg2) = `; const nodes = getNameNodes(tree, code.length); expect(nodes.length).toBe(2); - expect(getText(code, nodes[0])).toBe("arg1"); - expect(getText(code, nodes[1])).toBe("arg2"); + expect(getText(code, nodes[0].node)).toBe("arg1"); + expect(getText(code, nodes[1].node)).toBe("arg2"); }); test("Don't suggest current binding", () => { @@ -51,7 +51,7 @@ bar = {`; const nodes = getNameNodes(tree, code.length); expect(nodes.length).toBe(1); - expect(getText(code, nodes[0])).toBe("foo"); + expect(getText(code, nodes[0].node)).toBe("foo"); }); test("Don't suggest parameters of sibling functions", () => { @@ -62,8 +62,8 @@ fun2(arg2) =`; const nodes = getNameNodes(tree, code.length); expect(nodes.length).toBe(2); - expect(getText(code, nodes[0])).toBe("arg2"); - expect(getText(code, nodes[1])).toBe("fun1"); + expect(getText(code, nodes[0].node)).toBe("arg2"); + expect(getText(code, nodes[1].node)).toBe("fun1"); }); test("Don't suggest bindings declared later", () => { @@ -78,6 +78,6 @@ bar = 2 const nodes = getNameNodes(tree, code1.length); expect(nodes.length).toBe(1); - expect(getText(code, nodes[0])).toBe("foo"); + expect(getText(code, nodes[0].node)).toBe("foo"); }); }); diff --git a/packages/components/test/grammar.test.ts b/packages/components/test/grammar.test.ts index 2e9e9c023b..4e0d5fca6e 100644 --- a/packages/components/test/grammar.test.ts +++ b/packages/components/test/grammar.test.ts @@ -3,7 +3,13 @@ import { parser } from "../src/components/CodeEditor/languageSupport/generated/s describe("Lezer grammar", () => { test("Basic", () => { expect(parser.parse("2+2").toString()).toBe( - "Program(ArithExpr(Number,ArithOp,Number))" + "Program(InfixCall(Number,ArithOp,Number))" + ); + }); + + test("Parens", () => { + expect(parser.parse("((2))").toString()).toBe( + 'Program("(","(",Number,")",")")' ); }); @@ -16,7 +22,7 @@ bar = 6` ) .toString() ).toBe( - "Program(Binding(VariableName,Equals,Number),Binding(VariableName,Equals,Number))" + "Program(LetStatement(VariableName,Equals,Number),LetStatement(VariableName,Equals,Number))" ); }); @@ -30,7 +36,7 @@ foo + bar` ) .toString() ).toBe( - "Program(Binding(VariableName,Equals,Number),Binding(VariableName,Equals,Number),ArithExpr(IdentifierExpr,ArithOp,IdentifierExpr))" + "Program(LetStatement(VariableName,Equals,Number),LetStatement(VariableName,Equals,Number),InfixCall(Identifier,ArithOp,Identifier))" ); }); @@ -43,7 +49,7 @@ foo(5)` ) .toString() ).toBe( - 'Program(FunDeclaration(FunctionName,"(",LambdaArgs(LambdaParameter(LambdaParameterName)),")",Equals,IdentifierExpr),CallExpr(IdentifierExpr,"(",Argument(Number),")"))' + 'Program(DefunStatement(VariableName,"(",LambdaArgs(LambdaParameter(LambdaParameterName)),")",Equals,Identifier),Call(Identifier,"(",Argument(Number),")"))' ); }); @@ -57,7 +63,7 @@ bar" ` ) .toString() - ).toBe("Program(Binding(VariableName,Equals,String))"); + ).toBe("Program(LetStatement(VariableName,Equals,String))"); }); test("Decorators", () => { @@ -70,7 +76,13 @@ x = 5 ) .toString() ).toBe( - 'Program(Decorator(At,DecoratorName,"(",Argument(String),")"),Binding(VariableName,Equals,Number))' + 'Program(DecoratedStatement(Decorator(At,DecoratorName,"(",Argument(String),")"),LetStatement(VariableName,Equals,Number)))' + ); + }); + + test("Pipe", () => { + expect(parser.parse("5 -> max(6)").toString()).toBe( + 'Program(Pipe(Number,ControlOp,Call(Identifier,"(",Argument(Number),")")))' ); }); }); diff --git a/packages/prettier-plugin/src/printer.ts b/packages/prettier-plugin/src/printer.ts index 21abf6d740..077650b773 100644 --- a/packages/prettier-plugin/src/printer.ts +++ b/packages/prettier-plugin/src/printer.ts @@ -347,8 +347,6 @@ export function createSquigglePrinter( node.kind === "C" ? " : " : " else ", path.call(print, "falseExpression"), ]; - case "Void": - return "()"; case "UnitValue": return [typedPath(node).call(print, "value"), node.unit]; case "lineComment": diff --git a/packages/squiggle-lang/src/ast/parse.ts b/packages/squiggle-lang/src/ast/parse.ts index 27f37c3230..e7374655b9 100644 --- a/packages/squiggle-lang/src/ast/parse.ts +++ b/packages/squiggle-lang/src/ast/parse.ts @@ -109,8 +109,6 @@ function nodeToString(node: ASTNode): string { return `'${node.value}'`; // TODO - quote? case "Ternary": return sExpr([node.condition, node.trueExpression, node.falseExpression]); - case "Void": - return "()"; case "UnitValue": // S-expression; we should migrate to S-expressions for other branches too, for easier testing. return sExpr([node.value, node.unit]); diff --git a/packages/squiggle-lang/src/ast/peggyHelpers.ts b/packages/squiggle-lang/src/ast/peggyHelpers.ts index 7eacf26198..fdad2743fd 100644 --- a/packages/squiggle-lang/src/ast/peggyHelpers.ts +++ b/packages/squiggle-lang/src/ast/peggyHelpers.ts @@ -163,8 +163,6 @@ type NodeString = N<"String", { value: string }>; type NodeBoolean = N<"Boolean", { value: boolean }>; -type NodeVoid = N<"Void", object>; - export type ASTNode = | NodeArray | NodeDict @@ -188,8 +186,7 @@ export type ASTNode = | NodeTernary | NodeKeyValue | NodeString - | NodeBoolean - | NodeVoid; + | NodeBoolean; export function nodeCall( fn: ASTNode, @@ -440,10 +437,6 @@ export function nodeTernary( }; } -export function nodeVoid(location: LocationRange): NodeVoid { - return { type: "Void", location }; -} - export type ASTCommentNode = { type: "lineComment" | "blockComment"; value: string; diff --git a/packages/squiggle-lang/src/ast/peggyParser.peggy b/packages/squiggle-lang/src/ast/peggyParser.peggy index a1c616f08e..d74f96e7f0 100644 --- a/packages/squiggle-lang/src/ast/peggyParser.peggy +++ b/packages/squiggle-lang/src/ast/peggyParser.peggy @@ -253,10 +253,6 @@ basicLiteral / number / boolean / variable - / voidLiteral - -voidLiteral 'void' - = "()" {return h.nodeVoid(location());} variable = dollarIdentifierWithModule / dollarIdentifier diff --git a/packages/squiggle-lang/src/expression/compile.ts b/packages/squiggle-lang/src/expression/compile.ts index 7aea1c9085..8a8f23e0fc 100644 --- a/packages/squiggle-lang/src/expression/compile.ts +++ b/packages/squiggle-lang/src/expression/compile.ts @@ -331,8 +331,6 @@ function compileToContent( } case "String": return [expression.eValue(vString(ast.value)), context]; - case "Void": - return [expression.eVoid(), context]; case "Identifier": { const offset = context.nameToPos.get(ast.value); if (offset === undefined) { diff --git a/packages/squiggle-lang/src/expression/index.ts b/packages/squiggle-lang/src/expression/index.ts index 9fad3be875..1324c13b81 100644 --- a/packages/squiggle-lang/src/expression/index.ts +++ b/packages/squiggle-lang/src/expression/index.ts @@ -4,7 +4,6 @@ */ import { ASTNode } from "../ast/parse.js"; import { Value } from "../value/index.js"; -import { vVoid } from "../value/VVoid.js"; export type LambdaExpressionParameter = { name: string; @@ -173,11 +172,6 @@ export const eTernary = ( }, }); -export const eVoid = (): ExpressionContent => ({ - type: "Value", - value: vVoid(), -}); - // Converts the expression to String. Useful for tests. export function expressionToString(expression: Expression): string { switch (expression.type) {