Skip to content

Commit 4025de5

Browse files
committed
diff: the wheel over a list longer than its window moves the list (ui.scroll's pointer), and the dialog opens as tall as its content (ui.open's rows)
1 parent f714e95 commit 4025de5

8 files changed

Lines changed: 138 additions & 20 deletions

File tree

‎mods/diff/hooks/register.ts‎

Lines changed: 51 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,10 @@
1-
import type { On, ResultOf, SessionMessage, Timer } from 'claude-code'
1+
import type {
2+
On,
3+
PaneOpenArgs,
4+
ResultOf,
5+
SessionMessage,
6+
Timer,
7+
} from 'claude-code'
28

39
import Ask from './ask'
410
import Backend from './backend'
@@ -35,6 +41,7 @@ export function register(on: On) {
3541
let probing: Promise<boolean> | null = null
3642
let sessionStartMs = 0
3743
let isPaneOpen = false
44+
let dialogRows: number | null = null
3845
let hasAutoOpened = false
3946
let columns: number | null = null
4047
let shownSessionId: string | null = null
@@ -141,7 +148,31 @@ export function register(on: On) {
141148
return true
142149
}
143150

151+
function dialogPane(): PaneOpenArgs {
152+
return {
153+
id: Names.PANE_ID,
154+
title: Names.PANE_TITLE,
155+
holdToasts: true,
156+
closeOnEscape: true,
157+
rows: Views.dialogRowsOf(model),
158+
}
159+
}
160+
161+
function fitDialog(engine: Host) {
162+
const rows = Views.dialogRowsOf(model)
163+
164+
const isStale =
165+
isPaneOpen && model.isFullscreen === false && rows !== dialogRows
166+
167+
if (isStale) {
168+
dialogRows = rows
169+
void engine.openPane(dialogPane()).catch(() => undefined)
170+
}
171+
}
172+
144173
function redraw(engine: Host) {
174+
fitDialog(engine)
175+
145176
if (timers.has('redraw')) {
146177
return
147178
}
@@ -352,12 +383,13 @@ export function register(on: On) {
352383
place: { ...model.place, top: 0, listStart: 0 },
353384
}
354385

355-
await engine.openPane({
356-
id: Names.PANE_ID,
357-
title: Names.PANE_TITLE,
358-
holdToasts: true,
359-
...(isDialog ? ({ focus: true, closeOnEscape: true } as const) : {}),
360-
})
386+
dialogRows = isDialog ? Views.dialogRowsOf(model) : null
387+
388+
await engine.openPane(
389+
isDialog
390+
? { ...dialogPane(), focus: true }
391+
: { id: Names.PANE_ID, title: Names.PANE_TITLE, holdToasts: true },
392+
)
361393

362394
isPaneOpen = true
363395

@@ -666,14 +698,10 @@ export function register(on: On) {
666698
model = { ...model, dialogView: 'list' }
667699
redraw(host)
668700

701+
dialogRows = Views.dialogRowsOf(model)
702+
669703
void host
670-
.openPane({
671-
id: Names.PANE_ID,
672-
title: Names.PANE_TITLE,
673-
focus: true,
674-
closeOnEscape: true,
675-
holdToasts: true,
676-
})
704+
.openPane({ ...dialogPane(), focus: true })
677705
.catch(() => undefined)
678706

679707
return { deny: 'back to the file list' }
@@ -708,7 +736,15 @@ export function register(on: On) {
708736
return next(e)
709737
}
710738

711-
model = { ...model, place: Views.bodyScrolledBy(model, e) }
739+
const isOverList = Views.isWheelOverList(model, e)
740+
741+
model = {
742+
...model,
743+
place: isOverList
744+
? Views.listScrolledBy(model, e.by)
745+
: Views.bodyScrolledBy(model, e),
746+
}
747+
712748
host.invalidate()
713749

714750
return {}

‎mods/diff/hooks/views/body/body-scrolled-by.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@ import type PaneState from '../../pane-state'
55
import Plan from './plan'
66

77
/**
8-
* The docked pane's place after one of the person's scrolls: a wheel tick
9-
* or arrow three rows, a page key the window's rows, Home and End the ends.
8+
* The docked pane's place after one of the person's scrolls of the body:
9+
* a wheel tick or arrow three rows, a page key the window, Home/End an end.
1010
*
1111
* Told apart by the rows the move asks for against the pane's body and
1212
* tree, as the engine sizes each; clamped to the body's extent.

‎mods/diff/hooks/views/body/index.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
export * from './body-scrolled-by.js'
22
export * from './draw-window.jsx'
3+
export * from './is-wheel-over-list.js'
34
export * from './list-block-of.jsx'
45
export * from './list-scrolled-by.js'
56
export * from './place-at-file.js'
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import type { UiScrollInput } from 'claude-code'
2+
3+
import Limits from '../../limits'
4+
import type PaneState from '../../pane-state'
5+
import Plan from './plan'
6+
7+
/**
8+
* Whether a scroll is a wheel tick over the docked list block while the
9+
* list overflows its window: it moves the list then, as ReplDiffSidebar's.
10+
*
11+
* @param model the pane's state
12+
* @param scroll the move: the cell the wheel was over, absent for keys
13+
* @returns whether the list takes it
14+
*/
15+
export function isWheelOverList(
16+
model: PaneState.PaneModel,
17+
scroll: Pick<UiScrollInput, 'pointer'>,
18+
): boolean {
19+
const plan = Plan.dockPlanOf(model)
20+
const { listTop, listEnd } = Plan.bodyLayoutOf(model, plan)
21+
const row = scroll.pointer?.row ?? -1
22+
23+
return (
24+
row >= listTop &&
25+
row < listEnd &&
26+
plan.rows.length > Limits.MAX_SUMMARY_ROWS
27+
)
28+
}

‎mods/diff/hooks/views/body/plan/body-layout-of.ts‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import Segments from './segments'
55

66
/**
77
* The docked pane's scrolling body laid out under its pinned rows: the
8-
* segments, their rows, the window's rows, where each file's block starts.
8+
* segments and their rows, the window's rows, the files' and list's rows.
99
*
1010
* The pinned rows are counted as sidebarPane draws them (header, base or
1111
* turn line, todo bar, list block); read from the box last drawn (`place`),
@@ -54,11 +54,15 @@ export function bodyLayoutOf(
5454
extent += Segments.segmentRowsOf(segment)
5555
}
5656

57+
const listTop = Limits.PANE_TOP_PAD_ROWS + headRows + 1
58+
5759
return {
5860
segments,
5961
extent,
6062
visibleRows,
6163
maxTop: Math.max(0, extent - visibleRows),
6264
tops,
65+
listTop,
66+
listEnd: hasList ? listTop + listRows - 2 : listTop,
6367
}
6468
}
Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
11
import type { Segment } from './segment'
22

33
/**
4-
* The docked pane's scrolling body laid out: its segments, their rows in
5-
* all, the window's rows, its last top, the row each file's block starts.
4+
* The docked pane's scrolling body laid out: its segments and their rows,
5+
* the window, each file's first row, the list block's rows (end exclusive).
66
*/
77
export type BodyLayout = {
88
segments: readonly Segment[]
99
extent: number
1010
visibleRows: number
1111
maxTop: number
1212
tops: ReadonlyMap<string, number>
13+
listTop: number
14+
listEnd: number
1315
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import Limits from '../../limits'
2+
import PaneState from '../../pane-state'
3+
import { dialogEntriesOf } from './dialog-entries-of'
4+
import { dialogWindowOf } from './dialog-window-of'
5+
6+
/**
7+
* The rows the dialog's content takes as dialogPane draws it, for the
8+
* `rows` an inline open asks for: title, blank, count, picker, body, hints.
9+
*
10+
* The body is the message line, the list's window with its `more files`
11+
* rows, or the file's name, rule and hunk lines (their wraps not counted:
12+
* the layout caps a long file anyway and its arrows scroll it).
13+
*
14+
* @param model the pane's state
15+
* @returns the rows wanted
16+
*/
17+
export function dialogRowsOf(model: PaneState.PaneModel): number {
18+
const entries = dialogEntriesOf(model)
19+
20+
const hasCount =
21+
PaneState.pickedTurnOf(model) !== undefined || model.data !== null
22+
23+
const isPaged = entries.length > Limits.MAX_VISIBLE_FILES
24+
const picked = entries.find(entry => entry.path === model.selectedPath)
25+
const isDetail = model.dialogView === 'detail' && picked !== undefined
26+
27+
const start = dialogWindowOf(
28+
model,
29+
entries.map(entry => entry.path),
30+
)
31+
32+
const lines = (picked?.body?.hunks ?? []).reduce(
33+
(sum, hunk) => sum + 1 + hunk.lines.length,
34+
0,
35+
)
36+
37+
const listed = Math.min(entries.length - start, Limits.MAX_VISIBLE_FILES)
38+
39+
const isEmpty = entries.length === 0
40+
const listRows = isEmpty ? 1 : listed + (isPaged ? 2 : 0)
41+
const bodyRows = isDetail ? 2 + Math.max(1, lines) : listRows
42+
43+
return (
44+
2 + Number(hasCount) + Number(model.turns.length > 0) + 2 + bodyRows + 1
45+
)
46+
}

‎mods/diff/hooks/views/dialog-pane/index.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
export * from './dialog-entries-of'
22
export * from './dialog-pane.jsx'
3+
export * from './dialog-rows-of.js'
34
export * from './dialog-window-of'
45
export * from './list-body-of'
56
export * from './message-pane-of'

0 commit comments

Comments
 (0)