Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
603 changes: 603 additions & 0 deletions claude-notes/plans/2026-08-21-preview-click-to-editor-scroll.md

Large diffs are not rendered by default.

291 changes: 291 additions & 0 deletions claude-notes/plans/2026-08-22-click-align-editor-y.md

Large diffs are not rendered by default.

4 changes: 4 additions & 0 deletions hub-client/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ WASM rebuild is needed for a changelog-only edit.

-->

### 2026-08-22

- [`0de96cd7a`](https://github.com/quarto-dev/q2/commits/0de96cd7a): Clicking in either preview pane now lines the source editor up with what you clicked: the first line of that block's source sits at the same height on screen as the block itself, so the two panes read side by side. Previously the q2-preview did nothing at all when you clicked a block, and the HTML preview scrolled your line to the middle of the editor. Selecting text that came from an included file also no longer moves your cursor to an unrelated line of the file you are editing.

### 2026-08-21

- [`837de60b`](https://github.com/quarto-dev/q2/commits/837de60b): Fix deleted projects reappearing on the next page load — deleting a project now records a tombstone that syncs with the project set, so the on-load reconciler purges the stale local copy instead of resurrecting it, on every browser.
Expand Down
911 changes: 911 additions & 0 deletions hub-client/e2e/q2-preview-click-to-editor-scroll.spec.ts

Large diffs are not rendered by default.

10 changes: 8 additions & 2 deletions hub-client/src/components/render/Preview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -250,7 +250,7 @@ export default function Preview({
);

// Scroll synchronization between editor and preview
const { handlePreviewScroll, handlePreviewClick } = useScrollSync({
const { handlePreviewScroll, handlePreviewClick, revealEditorLine } = useScrollSync({
editorRef,
scrollPreviewToLine: (line: number) => {
doubleBufferedIframeRef.current?.scrollToLine(line);
Expand All @@ -262,11 +262,17 @@ export default function Preview({
editorHasFocusRef,
});

// Selection synchronization between preview and editor
// Selection synchronization between preview and editor. Threads
// `revealEditorLine` from the `useScrollSync` call above (2026-08-22
// click-align-editor-y plan, Phase 2, decision A7) rather than
// re-implementing the alignment arithmetic here — that keeps the reveal
// bracketed by `useScrollSync`'s own `isSyncingRef`, the flag its
// ratio-sync overwrite-race guard actually reads.
const { handlePreviewSelection } = useSelectionSync({
editorRef,
previewRef: doubleBufferedIframeRef,
enabled: scrollSyncEnabled && editorReady,
revealEditorLine,
});

// Set scroll sync via runtime metadata when the prop changes
Expand Down
16 changes: 11 additions & 5 deletions hub-client/src/components/render/ReactPreview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -522,12 +522,18 @@ export default function ReactPreview({

// Scroll sync (editor ↔ preview). Mirrors `Preview.tsx`: the q2-preview
// iframe exposes `scrollToLine` / `getScrollRatio` via this handle, and
// forwards its own scroll/click events through `handlePreviewScroll` /
// `handlePreviewClick`. Only the q2-preview format wires this; other
// React formats (q2-debug, slides) leave the handle unattached.
// forwards its own scroll event through `handlePreviewScroll`. Preview→
// editor clicks use `revealEditorLine` instead of `handlePreviewClick`'s
// scroll-ratio path (click-to-editor-scroll, D1/D3): q2-preview's click
// opens an inline editor in the preview itself, so this is a plain
// scroll-into-view keyed off `data-loc`, not a focus-gated ratio match.
// `handlePreviewClick` stays in `useScrollSync` for the HTML preview
// (`Preview.tsx`), which keeps wiring it unchanged. Only the q2-preview
// format wires this hook at all; other React formats (q2-debug, slides)
// leave the handle unattached.
const previewScrollRef = useRef<Q2PreviewIframeHandle>(null);

const { handlePreviewScroll, handlePreviewClick, handleAstRendered, scrollToLineDeferred } = useScrollSync({
const { handlePreviewScroll, revealEditorLine, handleAstRendered, scrollToLineDeferred } = useScrollSync({
editorRef,
scrollPreviewToLine: (line: number) => {
previewScrollRef.current?.scrollToLine(line);
Expand Down Expand Up @@ -872,7 +878,7 @@ export default function ReactPreview({
nestedEditBuffers={nestedEditBuffers}
scrollHandleRef={previewScrollRef}
onPreviewScroll={handlePreviewScroll}
onPreviewClick={handlePreviewClick}
onPreviewClickAtLine={revealEditorLine}
onAstRendered={handleAstRendered}
/>
) : previewState === 'ERROR_AT_START' && currentError ? (
Expand Down
13 changes: 10 additions & 3 deletions hub-client/src/components/render/ReactRenderer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,14 @@ interface ReactRendererProps {
*/
scrollHandleRef?: Ref<Q2PreviewIframeHandle>;
onPreviewScroll?: () => void;
onPreviewClick?: () => void;
/**
* Preview→editor click sync (click-to-editor-scroll): called with the
* resolved source line when a click in the preview resolves one via
* `lineForClickTarget`. Forwarded to `Q2PreviewIframe` as `onClickAtLine`.
* The second argument, `hostY`, is the clicked block's top edge in
* host-page coordinates, used to align (not just reveal) the line.
*/
onPreviewClickAtLine?: (line: number, hostY?: number) => void;
onAstRendered?: () => void;
}

Expand Down Expand Up @@ -165,7 +172,7 @@ function ReactRenderer({
nestedEditBuffers,
scrollHandleRef,
onPreviewScroll,
onPreviewClick,
onPreviewClickAtLine,
onAstRendered,
}: ReactRendererProps) {
// Stable wrappers for Q2PreviewIframe props that are useEffect dependencies.
Expand Down Expand Up @@ -334,7 +341,7 @@ function ReactRenderer({
onSlideChange={onSlideChange}
scrollHandleRef={scrollHandleRef}
onScroll={onPreviewScroll}
onClick={onPreviewClick}
onClickAtLine={onPreviewClickAtLine}
onAstRendered={onAstRendered}
/>
</div>
Expand Down
Loading
Loading