diff --git a/packages/components/src/components/CodeEditor/gutter/focusGutterExtension.tsx b/packages/components/src/components/CodeEditor/gutter/focusGutterExtension.tsx index 906ec627cf..49ae9325bd 100644 --- a/packages/components/src/components/CodeEditor/gutter/focusGutterExtension.tsx +++ b/packages/components/src/components/CodeEditor/gutter/focusGutterExtension.tsx @@ -176,6 +176,7 @@ export function getMarkers( return markers; } +// TODO: This seems to be broken now. Only finding a small subset of the lines that should be highlighted. export function focusGutterExtension(): Extension { const markersFacet = Facet.define< RangeSet, diff --git a/packages/components/src/components/CodeEditor/gutter/gutterExtension.tsx b/packages/components/src/components/CodeEditor/gutter/gutterExtension.tsx index 65bee3d397..20414a3b3d 100644 --- a/packages/components/src/components/CodeEditor/gutter/gutterExtension.tsx +++ b/packages/components/src/components/CodeEditor/gutter/gutterExtension.tsx @@ -7,7 +7,6 @@ import { import { showGutterFacet } from "../fields.js"; import { extensionFromFacets } from "../utils.js"; -import { focusGutterExtension } from "./focusGutterExtension.js"; export function gutterExtension(initialShowGutter: boolean) { return [ @@ -19,7 +18,6 @@ export function gutterExtension(initialShowGutter: boolean) { ? [ highlightActiveLine(), highlightActiveLineGutter(), - focusGutterExtension(), lineNumbers(), foldGutter(), ] diff --git a/packages/components/src/components/CodeEditor/index.tsx b/packages/components/src/components/CodeEditor/index.tsx index 56da26a40c..7e99cc08a9 100644 --- a/packages/components/src/components/CodeEditor/index.tsx +++ b/packages/components/src/components/CodeEditor/index.tsx @@ -1,3 +1,5 @@ +import { StateEffect, StateField } from "@codemirror/state"; +import { Decoration, DecorationSet, EditorView } from "@codemirror/view"; import { forwardRef, ReactNode, useImperativeHandle } from "react"; import { SqLocation, SqProject, SqValuePath } from "@quri/squiggle-lang"; @@ -6,6 +8,38 @@ import { Simulation } from "../../lib/hooks/useSimulator.js"; import { formatSquiggle } from "./formatSquiggleExtension.js"; import { useSquiggleEditorView } from "./useSquiggleEditorView.js"; +const flashHighlight = StateEffect.define<{ + from: number; + to: number; +} | null>(); + +const flashHighlightField = StateField.define({ + create() { + return Decoration.none; + }, + update(highlights, tr) { + highlights = highlights.map(tr.changes); + for (let e of tr.effects) { + if (e.is(flashHighlight)) { + if (e.value) { + highlights = highlights.update({ + add: [ + Decoration.mark({ class: "cm-flash-highlight" }).range( + e.value.from, + e.value.to + ), + ], + }); + } else { + highlights = Decoration.none; + } + } + } + return highlights; + }, + provide: (f) => EditorView.decorations.from(f), +}); + export type CodeEditorProps = { defaultValue: string; onChange: (value: string) => void; @@ -37,13 +71,18 @@ export const CodeEditor = forwardRef( function CodeEditor(props, ref) { const { view, ref: editorRef } = useSquiggleEditorView(props); - const scrollTo = (location: SqLocation, focus: boolean) => { + const scrollTo = (location: SqLocation, flash: boolean) => { if (!view) return; + view.dispatch({ - selection: { anchor: location.start.offset, head: location.end.offset }, scrollIntoView: true, + effects: true + ? flashHighlight.of({ + from: location.start.offset, + to: location.end.offset, + }) + : undefined, }); - focus && view.focus(); }; useImperativeHandle(ref, () => ({ @@ -57,3 +96,11 @@ export const CodeEditor = forwardRef( ); } ); + +const style = document.createElement("style"); +style.textContent = ` + .cm-flash-highlight { + background-color: #ff9999; + } +`; +document.head.appendChild(style); diff --git a/packages/components/src/components/CodeEditor/useSquiggleEditorView.ts b/packages/components/src/components/CodeEditor/useSquiggleEditorView.ts index 7a119e8f4b..1d7cfe6f5b 100644 --- a/packages/components/src/components/CodeEditor/useSquiggleEditorView.ts +++ b/packages/components/src/components/CodeEditor/useSquiggleEditorView.ts @@ -17,8 +17,10 @@ import { syntaxHighlighting, } from "@codemirror/language"; import { highlightSelectionMatches, searchKeymap } from "@codemirror/search"; -import { EditorState } from "@codemirror/state"; +import { EditorState, StateField } from "@codemirror/state"; import { + Decoration, + DecorationSet, drawSelection, dropCursor, EditorView, @@ -32,7 +34,7 @@ import { useConfigureCodemirrorView, } from "./codemirrorHooks.js"; import { errorsExtension } from "./errorsExtension.js"; -import { showGutterFacet, useReactPropsField } from "./fields.js"; +import { useReactPropsField } from "./fields.js"; import { formatSquiggleExtension } from "./formatSquiggleExtension.js"; import { gutterExtension } from "./gutter/gutterExtension.js"; import { CodeEditorProps } from "./index.js"; @@ -46,14 +48,34 @@ import { themeExtension } from "./themeExtension.js"; import { tooltipsExtension } from "./tooltips/index.js"; import { viewNodeExtension } from "./viewNodeExtension.js"; -// eslint-disable-next-line @typescript-eslint/no-unused-vars -function debugExtension() { - // Print state or specific fields on changes. - return EditorView.updateListener.of(({ state }) => { - // eslint-disable-next-line no-console - console.log(state.facet(showGutterFacet)); - }); -} +const flashHighlightField = StateField.define({ + create() { + return Decoration.none; + }, + update(highlights, tr) { + console.log("Update flash highlight field", highlights, tr.effects); + highlights = highlights.map(tr.changes); + let newHighlight: DecorationSet | null = null; + for (let e of tr.effects) { + if (e.value && "from" in e.value && "to" in e.value) { + // Create a new highlight, replacing any existing one + newHighlight = Decoration.set([ + Decoration.mark({ class: "cm-flash-highlight" }).range( + e.value.from, + e.value.to + ), + ]); + } else if (e.value === null) { + // Remove all highlights when e.value is null + newHighlight = Decoration.none; + } + } + // If a new highlight was created or all highlights were removed, use it + // Otherwise, keep the existing highlights + return newHighlight !== null ? newHighlight : highlights; + }, + provide: (f) => EditorView.decorations.from(f), +}); export function useSquiggleEditorExtensions( view: EditorView | undefined, @@ -87,6 +109,7 @@ export function useSquiggleEditorExtensions( // uncomment for local debugging: // debugExtension(), highlightSpecialChars(), + flashHighlightField, history(), drawSelection(), dropCursor(), @@ -128,6 +151,7 @@ export function useSquiggleEditorExtensions( height: params.height, }), tooltipsExtension(), + flashHighlightField, ]; return [ diff --git a/packages/components/src/components/SquiggleViewer/ValueViewer/WithContext.tsx b/packages/components/src/components/SquiggleViewer/ValueViewer/WithContext.tsx index 83443eb342..b8f12512be 100644 --- a/packages/components/src/components/SquiggleViewer/ValueViewer/WithContext.tsx +++ b/packages/components/src/components/SquiggleViewer/ValueViewer/WithContext.tsx @@ -183,7 +183,7 @@ export const ValueWithContextViewer: FC = ({ ? "mb-2 px-0.5 py-1 focus:bg-indigo-50" : "focus:bg-indigo-100" )} - onFocus={(_) => { + onMouseEnter={(_) => { scrollEditorToPath(); }} onKeyDown={(event) => {