From 14b492377179ebe408a6972c1a9a14b2f8c7444d Mon Sep 17 00:00:00 2001 From: Vyacheslav Matyukhin Date: Fri, 19 Jan 2024 23:09:18 -0600 Subject: [PATCH 1/3] simplify some code --- .../CodeEditor/useTooltipsExtension.tsx | 32 +++++++------------ 1 file changed, 12 insertions(+), 20 deletions(-) diff --git a/packages/components/src/components/CodeEditor/useTooltipsExtension.tsx b/packages/components/src/components/CodeEditor/useTooltipsExtension.tsx index a49926e1de..47844b489d 100644 --- a/packages/components/src/components/CodeEditor/useTooltipsExtension.tsx +++ b/packages/components/src/components/CodeEditor/useTooltipsExtension.tsx @@ -1,7 +1,7 @@ import { syntaxTree } from "@codemirror/language"; import { EditorView, hoverTooltip, repositionTooltips } from "@codemirror/view"; import { SyntaxNode } from "@lezer/common"; -import { FC, PropsWithChildren, useEffect } from "react"; +import { FC, PropsWithChildren, ReactNode, useEffect } from "react"; import { getFunctionDocumentation, @@ -73,6 +73,15 @@ const HoverTooltip: FC<{ hover: Hover; view: EditorView }> = ({ ); +function nodeTooltip(syntaxNode: SyntaxNode, reactNode: ReactNode) { + return { + pos: syntaxNode.from, + end: syntaxNode.to, + above: true, + create: () => reactAsDom(reactNode), + }; +} + // 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 function buildWordHoverExtension({ @@ -96,24 +105,7 @@ function buildWordHoverExtension({ return null; } - return { - pos: node.from, - end: node.to, - above: true, - create: () => reactAsDom(), - }; - }; - - const createTopLevelVariableNameTooltip = ( - node: SyntaxNode, - value: SqValue - ) => { - return { - pos: node.from, - end: node.to, - above: true, - create: () => reactAsDom(), - }; + return nodeTooltip(node, ); }; switch (cursor.name) { @@ -170,7 +162,7 @@ function buildWordHoverExtension({ valueAst.variable.location.start.offset === node.from && valueAst.variable.location.end.offset === node.to ) { - return createTopLevelVariableNameTooltip(node, value); + return nodeTooltip(node, ); } } } From ec260c6a0f36b87cdb3b5eda8502a92380e65148 Mon Sep 17 00:00:00 2001 From: Vyacheslav Matyukhin Date: Sat, 20 Jan 2024 13:52:00 -0600 Subject: [PATCH 2/3] tooltips on import strings (injectable through renderImportTooltip) --- .../src/components/CodeEditor/index.tsx | 6 ++- .../CodeEditor/useSquiggleEditorView.ts | 1 + .../CodeEditor/useTooltipsExtension.tsx | 45 ++++++++++------ .../LeftPlaygroundPanel/index.tsx | 14 +++-- .../components/SquigglePlayground/index.tsx | 7 ++- packages/hub/src/app/RootLayout.tsx | 6 +-- .../[slug]/EditSquiggleSnippetModel.tsx | 14 +++++ .../SquiggleModelExportPage.tsx | 1 + .../RelativeValuesModelLayout.tsx | 2 +- packages/hub/src/components/EntityCard.tsx | 2 +- .../ReactRoot.tsx} | 4 +- .../components/views/ListView/sidebar.tsx | 6 +-- .../src/squiggle/components/ImportTooltip.tsx | 52 +++++++++++++++++++ .../hub/src/squiggle/components/linker.ts | 2 +- packages/versioned-components/src/index.ts | 1 + packages/versioned-components/src/versions.ts | 13 +++++ 16 files changed, 142 insertions(+), 34 deletions(-) rename packages/hub/src/{app/ClientApp.tsx => components/ReactRoot.tsx} (70%) create mode 100644 packages/hub/src/squiggle/components/ImportTooltip.tsx diff --git a/packages/components/src/components/CodeEditor/index.tsx b/packages/components/src/components/CodeEditor/index.tsx index 10d1e27823..2bee915c83 100644 --- a/packages/components/src/components/CodeEditor/index.tsx +++ b/packages/components/src/components/CodeEditor/index.tsx @@ -1,4 +1,4 @@ -import { forwardRef, useImperativeHandle } from "react"; +import { forwardRef, ReactNode, useImperativeHandle } from "react"; import { SqError, SqProject, SqValuePath } from "@quri/squiggle-lang"; @@ -18,6 +18,10 @@ export type CodeEditorProps = { sourceId: string; fontSize?: number; project: SqProject; + renderImportTooltip?: (params: { + project: SqProject; + importId: string; + }) => ReactNode; }; export type CodeEditorHandle = { diff --git a/packages/components/src/components/CodeEditor/useSquiggleEditorView.ts b/packages/components/src/components/CodeEditor/useSquiggleEditorView.ts index a5845deaf6..62739aff23 100644 --- a/packages/components/src/components/CodeEditor/useSquiggleEditorView.ts +++ b/packages/components/src/components/CodeEditor/useSquiggleEditorView.ts @@ -113,6 +113,7 @@ export function useSquiggleEditorExtensions( const tooltipsExtension = useTooltipsExtension(view, { project: params.project, sourceId: params.sourceId, + renderImportTooltip: params.renderImportTooltip, }); const highPrioritySquiggleExtensions = [ diff --git a/packages/components/src/components/CodeEditor/useTooltipsExtension.tsx b/packages/components/src/components/CodeEditor/useTooltipsExtension.tsx index 47844b489d..ddf8b42cf9 100644 --- a/packages/components/src/components/CodeEditor/useTooltipsExtension.tsx +++ b/packages/components/src/components/CodeEditor/useTooltipsExtension.tsx @@ -3,11 +3,7 @@ import { EditorView, hoverTooltip, repositionTooltips } from "@codemirror/view"; import { SyntaxNode } from "@lezer/common"; import { FC, PropsWithChildren, ReactNode, useEffect } from "react"; -import { - getFunctionDocumentation, - SqProject, - SqValue, -} from "@quri/squiggle-lang"; +import { getFunctionDocumentation, SqValue } from "@quri/squiggle-lang"; import { valueHasContext } from "../../lib/utility.js"; import { SquiggleValueChart } from "../SquiggleViewer/SquiggleValueChart.js"; @@ -17,6 +13,7 @@ import { } from "../SquiggleViewer/ViewerProvider.js"; import { FnDocumentation } from "../ui/FnDocumentation.js"; import { useReactiveExtension } from "./codemirrorHooks.js"; +import { CodeEditorProps } from "./index.js"; import { reactAsDom } from "./utils.js"; type Hover = NonNullable>; @@ -84,13 +81,11 @@ function nodeTooltip(syntaxNode: SyntaxNode, reactNode: ReactNode) { // 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 -function buildWordHoverExtension({ +function buildTooltipExtension({ project, sourceId, -}: { - project: SqProject; - sourceId: string; -}) { + renderImportTooltip, +}: Pick) { return hoverTooltip((view, pos, side) => { const { doc } = view.state; @@ -109,6 +104,23 @@ function buildWordHoverExtension({ }; switch (cursor.name) { + case "String": { + // Is this an import? + const stringNode = cursor.node; + if (!cursor.parent()) { + return null; + } + if (cursor.type.is("Import") && renderImportTooltip) { + const importId = getText(stringNode).slice(1, -1); + return nodeTooltip( + stringNode, + + {renderImportTooltip({ project, importId })} + + ); + } + break; + } case "Identifier": if (getText(cursor.node).match(/^[A-Z]/)) { // TODO - expand the namespace to the identifier, or just show the namespace documentation @@ -186,14 +198,15 @@ export function useTooltipsExtension( { project, sourceId, - }: { - project: SqProject; - sourceId: string; - } + renderImportTooltip, + }: Pick ) { return useReactiveExtension( view, - () => [buildWordHoverExtension({ project, sourceId }), tooltipTheme], - [project, sourceId] + () => [ + buildTooltipExtension({ project, sourceId, renderImportTooltip }), + tooltipTheme, + ], + [project, sourceId, renderImportTooltip] ); } diff --git a/packages/components/src/components/SquigglePlayground/LeftPlaygroundPanel/index.tsx b/packages/components/src/components/SquigglePlayground/LeftPlaygroundPanel/index.tsx index 6a713423ed..75e3814f2e 100644 --- a/packages/components/src/components/SquigglePlayground/LeftPlaygroundPanel/index.tsx +++ b/packages/components/src/components/SquigglePlayground/LeftPlaygroundPanel/index.tsx @@ -7,7 +7,7 @@ import { useRef, } from "react"; -import { SqProject, SqValuePath } from "@quri/squiggle-lang"; +import { SqProject } from "@quri/squiggle-lang"; import { AdjustmentsVerticalIcon, Bars3CenterLeftIcon, @@ -25,7 +25,11 @@ import { useUncontrolledCode, } from "../../../lib/hooks/index.js"; import { altKey, getErrors } from "../../../lib/utility.js"; -import { CodeEditor, CodeEditorHandle } from "../../CodeEditor/index.js"; +import { + CodeEditor, + CodeEditorHandle, + CodeEditorProps, +} from "../../CodeEditor/index.js"; import { PlaygroundSettings } from "../../PlaygroundSettings.js"; import { PanelWithToolbar } from "../../ui/PanelWithToolbar/index.js"; import { ToolbarItem } from "../../ui/PanelWithToolbar/ToolbarItem.js"; @@ -54,8 +58,7 @@ type Props = { /* Allows to inject extra items to the left panel's dropdown menu. */ renderExtraDropdownItems?: RenderExtraControls; renderExtraModal?: Parameters[0]["renderModal"]; - onViewValuePath?: (path: SqValuePath) => void; -}; +} & Pick; // for interactions with this component from outside export type LeftPlaygroundPanelHandle = { @@ -164,8 +167,9 @@ export const LeftPlaygroundPanel = forwardRef( showGutter={true} lineWrapping={props.settings.editorSettings.lineWrapping} onChange={setCode} - onViewValuePath={props.onViewValuePath} onSubmit={runnerState.run} + onViewValuePath={props.onViewValuePath} + renderImportTooltip={props.renderImportTooltip} /> ); diff --git a/packages/components/src/components/SquigglePlayground/index.tsx b/packages/components/src/components/SquigglePlayground/index.tsx index 9974fb8574..b487e9b3ab 100644 --- a/packages/components/src/components/SquigglePlayground/index.tsx +++ b/packages/components/src/components/SquigglePlayground/index.tsx @@ -53,7 +53,10 @@ export type SquigglePlaygroundProps = { height?: CSSProperties["height"]; } & Pick< Parameters[0], - "renderExtraControls" | "renderExtraDropdownItems" | "renderExtraModal" + | "renderExtraControls" + | "renderExtraDropdownItems" + | "renderExtraModal" + | "renderImportTooltip" > & PartialPlaygroundSettings; @@ -77,6 +80,7 @@ export const SquigglePlayground: React.FC = ( renderExtraControls, renderExtraDropdownItems, renderExtraModal, + renderImportTooltip, height = 500, sourceId, ...defaultSettings @@ -155,6 +159,7 @@ export const SquigglePlayground: React.FC = ( renderExtraDropdownItems={renderExtraDropdownItems} renderExtraModal={renderExtraModal} onViewValuePath={(path) => rightPanelRef.current?.viewValuePath(path)} + renderImportTooltip={renderImportTooltip} ref={leftPanelRef} /> ); diff --git a/packages/hub/src/app/RootLayout.tsx b/packages/hub/src/app/RootLayout.tsx index 14201b9241..3164cc4e8c 100644 --- a/packages/hub/src/app/RootLayout.tsx +++ b/packages/hub/src/app/RootLayout.tsx @@ -11,7 +11,7 @@ import { isModelRoute, isModelSubroute } from "@/routes"; import { PageFooter } from "../components/layout/RootLayout/PageFooter"; import { PageMenu } from "../components/layout/RootLayout/PageMenu"; -import { ClientApp } from "./ClientApp"; +import { ReactRoot } from "../components/ReactRoot"; import { RootLayoutQuery } from "@/__generated__/RootLayoutQuery.graphql"; @@ -53,8 +53,8 @@ export const RootLayout: FC< }> > = ({ session, children }) => { return ( - + {children} - + ); }; diff --git a/packages/hub/src/app/models/[owner]/[slug]/EditSquiggleSnippetModel.tsx b/packages/hub/src/app/models/[owner]/[slug]/EditSquiggleSnippetModel.tsx index 90c49d769c..bf073575b7 100644 --- a/packages/hub/src/app/models/[owner]/[slug]/EditSquiggleSnippetModel.tsx +++ b/packages/hub/src/app/models/[owner]/[slug]/EditSquiggleSnippetModel.tsx @@ -1,3 +1,4 @@ +import { useSession } from "next-auth/react"; import { BaseSyntheticEvent, FC, useMemo, useState } from "react"; import { FormProvider, useFieldArray, useForm } from "react-hook-form"; import { graphql, useFragment } from "react-relay"; @@ -22,13 +23,16 @@ import { VersionedSquigglePlayground, versionSupportsDropdownMenu, versionSupportsExports, + versionSupportsImportTooltip, } from "@quri/versioned-squiggle-components"; import { EditModelExports } from "@/components/exports/EditModelExports"; +import { ReactRoot } from "@/components/ReactRoot"; import { FormModal } from "@/components/ui/FormModal"; import { useAvailableHeight } from "@/hooks/useAvailableHeight"; import { useMutationForm } from "@/hooks/useMutationForm"; import { extractFromGraphqlErrorUnion } from "@/lib/graphqlHelpers"; +import { ImportTooltip } from "@/squiggle/components/ImportTooltip"; import { serializeSourceId, squiggleHubLinker, @@ -122,6 +126,8 @@ export const EditSquiggleSnippetModel: FC = ({ modelRef, forceVersionPicker, }) => { + const { data: session } = useSession(); + const model = useFragment( graphql` fragment EditSquiggleSnippetModel on Model { @@ -375,6 +381,14 @@ export const EditSquiggleSnippetModel: FC = ({ }; } + if (versionSupportsImportTooltip.props(playgroundProps)) { + playgroundProps.renderImportTooltip = ({ importId }) => ( + + + + ); + } + return (
onSubmit()}> diff --git a/packages/hub/src/app/models/[owner]/[slug]/exports/[variableName]/SquiggleModelExportPage.tsx b/packages/hub/src/app/models/[owner]/[slug]/exports/[variableName]/SquiggleModelExportPage.tsx index c2c606abeb..b5cd39e739 100644 --- a/packages/hub/src/app/models/[owner]/[slug]/exports/[variableName]/SquiggleModelExportPage.tsx +++ b/packages/hub/src/app/models/[owner]/[slug]/exports/[variableName]/SquiggleModelExportPage.tsx @@ -35,6 +35,7 @@ const VersionedSquiggleModelExportPage: FC< const project = new squiggleLang.SqProject({ linker: squiggleHubLinker, }); + const rootPath = new squiggleLang.SqValuePath({ root: "bindings", items: [{ type: "string", value: variableName }], diff --git a/packages/hub/src/app/models/[owner]/[slug]/relative-values/[variableName]/RelativeValuesModelLayout.tsx b/packages/hub/src/app/models/[owner]/[slug]/relative-values/[variableName]/RelativeValuesModelLayout.tsx index 474e9f3ace..47dc57de9e 100644 --- a/packages/hub/src/app/models/[owner]/[slug]/relative-values/[variableName]/RelativeValuesModelLayout.tsx +++ b/packages/hub/src/app/models/[owner]/[slug]/relative-values/[variableName]/RelativeValuesModelLayout.tsx @@ -154,7 +154,7 @@ export const RelativeValuesModelLayout: FC<
-
+
{variableName}
diff --git a/packages/hub/src/components/EntityCard.tsx b/packages/hub/src/components/EntityCard.tsx index 77b6c6f836..840328abe2 100644 --- a/packages/hub/src/components/EntityCard.tsx +++ b/packages/hub/src/components/EntityCard.tsx @@ -36,7 +36,7 @@ export const EntityCard: FC = ({
-
+
{showOwner ? ownerName + "/" : ""} {slug}
diff --git a/packages/hub/src/app/ClientApp.tsx b/packages/hub/src/components/ReactRoot.tsx similarity index 70% rename from packages/hub/src/app/ClientApp.tsx rename to packages/hub/src/components/ReactRoot.tsx index 7b58e2dd6a..2da58e883b 100644 --- a/packages/hub/src/app/ClientApp.tsx +++ b/packages/hub/src/components/ReactRoot.tsx @@ -9,7 +9,9 @@ import { WithToasts } from "@quri/ui"; import { ErrorBoundary } from "@/components/ErrorBoundary"; import { getCurrentEnvironment } from "@/relay/environment"; -export const ClientApp: FC> = ({ +// This component is used in the app's root layout to configure all common providers and wrappers. +// It's also useful when you want to mount a separate React root. One example is CodeMirror tooltips, which are mounted as separate DOM elements. +export const ReactRoot: FC> = ({ session, children, }) => { diff --git a/packages/hub/src/relative-values/components/views/ListView/sidebar.tsx b/packages/hub/src/relative-values/components/views/ListView/sidebar.tsx index cbfaa74e12..8f1b6afb8d 100644 --- a/packages/hub/src/relative-values/components/views/ListView/sidebar.tsx +++ b/packages/hub/src/relative-values/components/views/ListView/sidebar.tsx @@ -24,7 +24,7 @@ const TableRow: React.FC = ({ label, number }) => (
{label}
-
+
@@ -124,9 +124,7 @@ export const ItemSideBar: FC = ({ return (
- - value - + value ( {numeratorItem.name} diff --git a/packages/hub/src/squiggle/components/ImportTooltip.tsx b/packages/hub/src/squiggle/components/ImportTooltip.tsx new file mode 100644 index 0000000000..a8e5139029 --- /dev/null +++ b/packages/hub/src/squiggle/components/ImportTooltip.tsx @@ -0,0 +1,52 @@ +import clsx from "clsx"; +import { FC } from "react"; +import { graphql, useLazyLoadQuery } from "react-relay"; + +import { ModelCard } from "@/models/components/ModelCard"; + +import { parseSourceId } from "./linker"; + +import { ImportTooltipQuery } from "@/__generated__/ImportTooltipQuery.graphql"; + +type Props = { + importId: string; +}; + +export const ImportTooltip: FC = ({ importId }) => { + const { owner, slug } = parseSourceId(importId); + + const { model } = useLazyLoadQuery( + graphql` + query ImportTooltipQuery($input: QueryModelInput!) { + model(input: $input) { + __typename + ... on Model { + ...ModelCard + } + } + } + `, + { input: { owner, slug } } + ); + + if (model.__typename !== "Model") { + return ( +
+ {"Couldn't load "} + {owner}/{slug} +
+ ); + } + + return ( +
from squiggle-components instead? + "text-base" + )} + > + +
+ ); +}; diff --git a/packages/hub/src/squiggle/components/linker.ts b/packages/hub/src/squiggle/components/linker.ts index 84f47a4b3b..2a946eae46 100644 --- a/packages/hub/src/squiggle/components/linker.ts +++ b/packages/hub/src/squiggle/components/linker.ts @@ -13,7 +13,7 @@ type ParsedSourceId = { const PREFIX = "hub"; -function parseSourceId(sourceId: string): ParsedSourceId { +export function parseSourceId(sourceId: string): ParsedSourceId { const match = sourceId.match(/^(\w+):([\w-]+)\/([\w-]+)$/); if (!match) { diff --git a/packages/versioned-components/src/index.ts b/packages/versioned-components/src/index.ts index e9c58c3ace..fe68bd3088 100644 --- a/packages/versioned-components/src/index.ts +++ b/packages/versioned-components/src/index.ts @@ -10,6 +10,7 @@ export { squiggleVersions, versionSupportsDropdownMenu, versionSupportsExports, + versionSupportsImportTooltip, versionSupportsSquiggleChart, } from "./versions.js"; diff --git a/packages/versioned-components/src/versions.ts b/packages/versioned-components/src/versions.ts index 747b61498c..c497ae8271 100644 --- a/packages/versioned-components/src/versions.ts +++ b/packages/versioned-components/src/versions.ts @@ -65,3 +65,16 @@ function excludeVersions< export const versionSupportsDropdownMenu = excludeVersions(["0.8.5"]); export const versionSupportsExports = excludeVersions(["0.8.5", "0.8.6"]); export const versionSupportsSquiggleChart = excludeVersions(["0.8.5", "0.8.6"]); +export const versionSupportsImportTooltip = excludeVersions([ + "0.8.5", + "0.8.6", + "0.9.0", + "0.9.2", +]); + +export const versionSupportsSqPathV2 = excludeVersions([ + "0.8.5", + "0.8.6", + "0.9.0", + "0.9.2", +]); From 440b7f8f8fcc1b702d0d312cb15c5519f01e9c59 Mon Sep 17 00:00:00 2001 From: Vyacheslav Matyukhin Date: Sat, 20 Jan 2024 23:09:04 +0300 Subject: [PATCH 3/3] Create grumpy-owls-clean.md --- .changeset/grumpy-owls-clean.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/grumpy-owls-clean.md diff --git a/.changeset/grumpy-owls-clean.md b/.changeset/grumpy-owls-clean.md new file mode 100644 index 0000000000..08d4914c0f --- /dev/null +++ b/.changeset/grumpy-owls-clean.md @@ -0,0 +1,5 @@ +--- +"@quri/squiggle-components": patch +--- + +Tooltips for import strings can be injected with `renderImportTooltip` prop