From 5dc32e2c3b1cac049d21edb344e6505c7435d009 Mon Sep 17 00:00:00 2001 From: Moritz Wilksch Date: Wed, 30 Sep 2026 19:20:38 +0000 Subject: [PATCH] fix(review): comment on the cursor's line when its number is pressed The keyboard cursor holds a one-line selection, and the viewer toggles a lone selected line off when its number is pressed again, so that press opened nothing. It now opens the composer like a press on any other line. --- src/client/review/ReviewPane.tsx | 7 ++++++- test/client/ReviewPane.test.ts | 21 +++++++++++++++++++++ 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/src/client/review/ReviewPane.tsx b/src/client/review/ReviewPane.tsx index 23fb3a11..2a55b87d 100644 --- a/src/client/review/ReviewPane.tsx +++ b/src/client/review/ReviewPane.tsx @@ -780,9 +780,14 @@ export function ReviewPane() { // Clicking a line's content puts the cursor there (the number column starts a range selection instead). // Fires after onTokenClick on the same click, so it must not close the popover that click opened. onLineClick: (props: OnLineClickProps | OnDiffLineClickProps, ctx: { item: { id: string } }) => { - if (props.numberColumn) return; const side = 'annotationSide' in props ? props.annotationSide : 'additions'; const range = { start: props.lineNumber, side, end: props.lineNumber, endSide: side }; + if (props.numberColumn) { + // The viewer toggles off a lone selected line pressed again, which is often the cursor's line; + // here that press comments on it like any other line's. + if (!viewerRef.current?.getSelectedLines()) void openDraft({ id: ctx.item.id, range }); + return; + } setSelection({ id: ctx.item.id, range }); setActivePath(pathFromItemId(ctx.item.id)); }, diff --git a/test/client/ReviewPane.test.ts b/test/client/ReviewPane.test.ts index 6c61e507..f8d2a481 100644 --- a/test/client/ReviewPane.test.ts +++ b/test/client/ReviewPane.test.ts @@ -31,6 +31,7 @@ const captureOptions = vi.fn<(options: CodeViewOptions) => void>(); const captureItems = vi.fn<(items: unknown[]) => void>(); const primeDiffHighlightCache = vi.fn(async (_diff: unknown) => {}); const scrollTo = vi.fn(); +let viewerSelection: unknown = null; vi.mock('@pierre/diffs/react', () => ({ useWorkerPool: () => ({ primeDiffHighlightCache }), CodeView: forwardRef(function CodeView( @@ -50,6 +51,7 @@ vi.mock('@pierre/diffs/react', () => ({ () => ({ getInstance: () => ({ getRenderedItems: () => rendered, render: renderViewer, instanceChanged }), getItem: (id: string) => rendered.find((r) => r.id === id)?.element ?? null, + getSelectedLines: () => viewerSelection, scrollTo, }), [], @@ -188,6 +190,7 @@ beforeEach(() => { (globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true; installSearchHighlights.mockClear(); rendered = []; + viewerSelection = null; useStore.setState({ snapshot: null, loaded: {}, @@ -878,6 +881,24 @@ describe('ReviewPane scroller effects', () => { expect(installSearchHighlights.mock.calls[0]![1]).toBe(host.querySelector('.codeview')); }); + it("opens a comment on the cursor's line when its number is pressed, which the viewer reads as unselecting it", async () => { + await act(() => root.render(createElement(ReviewPane))); + await act(() => useStore.setState({ snapshot: snap(changed), draft: null })); + const onLineClick = captureOptions.mock.calls.at(-1)![0].onLineClick as (props: unknown, ctx: unknown) => void; + const ctx = { item: { id: 'diff:a.txt@0' } }; + const press = { lineNumber: 2, annotationSide: 'additions', numberColumn: true }; + const range = { start: 2, side: 'additions', end: 2, endSide: 'additions' }; + + // A press that left a selection already opened the composer through onLineSelectionEnd. + viewerSelection = { id: ctx.item.id, range }; + await act(() => onLineClick(press, ctx)); + expect(useStore.getState().draft).toBeNull(); + + viewerSelection = null; + await act(() => onLineClick(press, ctx)); + expect(useStore.getState().draft).toEqual({ path: 'a.txt', selection: { id: ctx.item.id, range } }); + }); + it('toggles collapse from the file header while only the checkbox changes viewed state', async () => { await act(() => root.render(createElement(ReviewPane))); const file = { kind: 'file' as const, file: { name: 'a.txt', contents: 'x' } };