Skip to content

Commit

Permalink
Merge pull request #2983 from quantified-uncertainty/select-items
Browse files Browse the repository at this point in the history
Select items and keyboard nav
  • Loading branch information
berekuk authored Jan 25, 2024
2 parents c7a955b + da41d3e commit e6dde34
Show file tree
Hide file tree
Showing 20 changed files with 604 additions and 187 deletions.
6 changes: 6 additions & 0 deletions .changeset/strong-balloons-rush.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@quri/squiggle-lang": patch
"@quri/squiggle-components": patch
---

Adds simple keyboard navigation for Viewer
6 changes: 3 additions & 3 deletions packages/components/src/components/CodeEditor/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,20 +26,20 @@ export type CodeEditorProps = {

export type CodeEditorHandle = {
format(): void;
scrollTo(position: number): void;
scrollTo(position: number, focus: boolean): void;
};

export const CodeEditor = forwardRef<CodeEditorHandle, CodeEditorProps>(
function CodeEditor(props, ref) {
const { view, ref: editorRef } = useSquiggleEditorView(props);

const scrollTo = (position: number) => {
const scrollTo = (position: number, focus) => {
if (!view) return;
view.dispatch({
selection: { anchor: position },
scrollIntoView: true,
});
view.focus();
focus && view.focus();
};

useImperativeHandle(ref, () => ({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ const ValueTooltip: FC<{ value: SqValue; view: EditorView }> = ({
<InnerViewerProvider
partialPlaygroundSettings={globalSettings}
viewerType="tooltip"
rootValue={value}
>
<SquiggleValueChart value={value} settings={globalSettings} />
</InnerViewerProvider>
Expand Down
2 changes: 1 addition & 1 deletion packages/components/src/components/SquiggleErrorAlert.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ const LocationLine: FC<{
const { editor } = useViewerContext();

const findInEditor = () => {
editor?.scrollTo(location.start.offset);
editor?.scrollTo(location.start.offset, true);
};

return (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,28 @@ type Props = {
isRunning: boolean;
};

export function modeToValue(
mode: ViewerMode,
output: SqOutputResult
): SqValue | undefined {
if (!output.ok) {
return;
}
const sqOutput = output.value;
switch (mode) {
case "Result":
return sqOutput.result;
case "Variables":
return sqOutput.bindings.asValue();
case "Imports":
return sqOutput.imports.asValue();
case "Exports":
return sqOutput.exports.asValue();
case "AST":
return;
}
}

export const ViewerBody: FC<Props> = ({ output, mode, isRunning }) => {
if (!output.ok) {
return <SquiggleErrorAlert error={output.value} />;
Expand All @@ -28,20 +50,8 @@ export const ViewerBody: FC<Props> = ({ output, mode, isRunning }) => {
</pre>
);
}
let usedValue: SqValue | undefined;
switch (mode) {
case "Result":
usedValue = output.value.result;
break;
case "Variables":
usedValue = sqOutput.bindings.asValue();
break;
case "Imports":
usedValue = sqOutput.imports.asValue();
break;
case "Exports":
usedValue = sqOutput.exports.asValue();
}

const usedValue = modeToValue(mode, output);

if (!usedValue) {
return null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {
} from "../SquiggleViewer/ViewerProvider.js";
import { Layout } from "./Layout.js";
import { RenderingIndicator } from "./RenderingIndicator.js";
import { ViewerBody } from "./ViewerBody.js";
import { modeToValue, ViewerBody } from "./ViewerBody.js";
import { ViewerMenu } from "./ViewerMenu.js";

type Props = {
Expand Down Expand Up @@ -51,6 +51,7 @@ export const SquiggleOutputViewer = forwardRef<SquiggleViewerHandle, Props>(
partialPlaygroundSettings={settings}
editor={editor}
ref={viewerRef}
rootValue={modeToValue(mode, output) || undefined}
>
<Layout
menu={<ViewerMenu mode={mode} setMode={setMode} output={output} />}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ const ItemSettingsModal: FC<Props> = ({

const { itemStore } = useContext(ViewerContext);
const resetScroll = () => {
itemStore.scrollToPath(path);
itemStore.scrollViewerToPath(path);
};

return (
Expand Down
168 changes: 168 additions & 0 deletions packages/components/src/components/SquiggleViewer/SqViewNode.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
import { SqValue, SqValuePath } from "@quri/squiggle-lang";

import { getChildrenValues, TraverseCalculatorEdge } from "./utils.js";

//We might want to bring this into the SquiggleLang library. The ``traverseCalculatorEdge`` part is awkward though.
class SqValueNode {
constructor(
public root: SqValue,
public path: SqValuePath,
public traverseCalculatorEdge: TraverseCalculatorEdge
) {
this.isEqual = this.isEqual.bind(this);
}

uid() {
return this.path.uid();
}

isEqual(other: SqValueNode): boolean {
return this.uid() === other.uid();
}

sqValue(): SqValue | undefined {
return this.root.getSubvalueByPath(this.path, this.traverseCalculatorEdge);
}

parent() {
const parentPath = this.path.parent();
return parentPath
? new SqValueNode(this.root, parentPath, this.traverseCalculatorEdge)
: undefined;
}

children() {
const value = this.sqValue();
if (!value) {
return [];
}
return getChildrenValues(value)
.map((childValue) => {
const path = childValue.context?.path;
return path
? new SqValueNode(this.root, path, this.traverseCalculatorEdge)
: undefined;
})
.filter((a): a is NonNullable<typeof a> => a !== undefined);
}

lastChild(): SqValueNode | undefined {
return this.children().at(-1);
}

siblings() {
return this.parent()?.children() ?? [];
}

prevSibling() {
const index = this.getParentIndex();
const isRootOrError = index === -1;
const isFirstSibling = index === 0;
if (isRootOrError || isFirstSibling) {
return undefined;
}
return this.siblings()[index - 1];
}

nextSibling() {
const index = this.getParentIndex();
const isRootOrError = index === -1;
const isLastSibling = index === this.siblings().length - 1;
if (isRootOrError || isLastSibling) {
return undefined;
}
return this.siblings()[index + 1];
}

getParentIndex() {
return this.siblings().findIndex(this.isEqual);
}
}

type GetIsCollapsed = (path: SqValuePath) => boolean;
type Params = { getIsCollapsed: GetIsCollapsed };

//This is split from SqValueNode because it handles more specialized logic for viewing open/closed nodes in the Viewer. It works for lists of nodes - we'll need new logic for tabular data.
export class SqListViewNode {
constructor(
public node: SqValueNode,
public params: Params
) {
this.make = this.make.bind(this);
}

static make(
root: SqValue,
path: SqValuePath,
traverseCalculatorEdge: TraverseCalculatorEdge,
getIsCollapsed: GetIsCollapsed
) {
const node = new SqValueNode(root, path, traverseCalculatorEdge);
return new SqListViewNode(node, { getIsCollapsed });
}

make(node: SqValueNode) {
return new SqListViewNode(node, this.params);
}

// A helper function to make a node or undefined
makeU(node: SqValueNode | undefined) {
return node ? new SqListViewNode(node, this.params) : undefined;
}

value(): SqValue | undefined {
return this.node.sqValue();
}
isRoot() {
return this.node.path.isRoot();
}
parent() {
return this.makeU(this.node.parent());
}
children() {
return this.node.children().map(this.make);
}
lastChild() {
return this.makeU(this.node.lastChild());
}
siblings() {
return this.node.siblings().map(this.make);
}
prevSibling() {
return this.makeU(this.node.prevSibling());
}
nextSibling() {
return this.makeU(this.node.nextSibling());
}
private isCollapsed() {
return this.params.getIsCollapsed(this.node.path);
}

private hasVisibleChildren() {
return !this.isCollapsed() && this.children().length > 0;
}

private lastVisibleSubChild(): SqListViewNode | undefined {
if (this.hasVisibleChildren()) {
const lastChild = this.lastChild();
return lastChild?.lastVisibleSubChild() || lastChild;
} else {
return this;
}
}

private nextAvailableSibling(): SqListViewNode | undefined {
return this.nextSibling() || this.parent()?.nextAvailableSibling();
}

next(): SqListViewNode | undefined {
return this.hasVisibleChildren()
? this.children()[0]
: this.nextAvailableSibling();
}

prev(): SqListViewNode | undefined {
const prevSibling = this.prevSibling();
return prevSibling ? prevSibling.lastVisibleSubChild() : this.parent();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import { clsx } from "clsx";
import { FC } from "react";

import {
CodeBracketIcon,
Cog8ToothIcon,
CommandLineIcon,
Dropdown,
Expand All @@ -19,60 +18,37 @@ import { valueToHeadingString } from "../../widgets/utils.js";
import { CollapsedIcon, ExpandedIcon } from "./icons.js";
import { getChildrenValues } from "./utils.js";
import {
useFocus,
useHasLocalSettings,
useIsFocused,
useIsZoomedIn,
useSetCollapsed,
useUnfocus,
useViewerContext,
useZoomIn,
useZoomOut,
} from "./ViewerProvider.js";

const FindInEditorItem: FC<{ value: SqValueWithContext }> = ({ value }) => {
const { editor } = useViewerContext();
const closeDropdown = useCloseDropdown();

if (!editor || value.context.path.isRoot()) {
return null;
}

const findInEditor = () => {
const location = value.context.findLocation();
editor?.scrollTo(location.start.offset);
closeDropdown();
};

return (
<DropdownMenuActionItem
title="Show in Editor"
icon={CodeBracketIcon}
onClick={findInEditor}
/>
);
};

const FocusItem: FC<{ value: SqValueWithContext }> = ({ value }) => {
const { path } = value.context;
const isFocused = useIsFocused(path);
const focus = useFocus();
const unfocus = useUnfocus();
const isFocused = useIsZoomedIn(path);
const zoomIn = useZoomIn();
const zoomOut = useZoomOut();
if (path.isRoot()) {
return null;
}

if (isFocused) {
return (
<DropdownMenuActionItem
title="Unfocus"
title="Zoom Out"
icon={FocusIcon}
onClick={unfocus}
onClick={zoomOut}
/>
);
} else {
return (
<DropdownMenuActionItem
title="Focus"
title="Zoom In"
icon={FocusIcon}
onClick={() => focus(path)}
onClick={() => zoomIn(path)}
/>
);
}
Expand Down Expand Up @@ -153,7 +129,6 @@ export const SquiggleValueMenu: FC<{
{widgetHeading && (
<DropdownMenuHeader>{widgetHeading}</DropdownMenuHeader>
)}
<FindInEditorItem value={value} />
<FocusItem value={value} />
<SetChildrenCollapsedStateItem
value={value}
Expand Down
Loading

2 comments on commit e6dde34

@vercel
Copy link

@vercel vercel bot commented on e6dde34 Jan 25, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@vercel
Copy link

@vercel vercel bot commented on e6dde34 Jan 25, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.