@@ -157,22 +157,56 @@ function ensureCursor(): Row | undefined {
157157 return r ;
158158}
159159
160- // Land the cursor on a specific rendered line (display space) and center it — used by the
161- // blockers jump list. Retries once across two frames so a just-triggered collapsed-region
162- // expansion (which rerenders) has laid out its rows.
160+ // Land the cursor on a specific rendered line (display space) and center it. Context rows merge
161+ // to a single entry (additions primary, deletions twin in `alt`), so fall back to a line-only
162+ // match before giving up.
163+ function landAt ( side : Side , line : number ) : boolean {
164+ const list = rows ( ) ;
165+ const r = list . find ( ( x ) => matches ( x , side , line ) ) ?? list . find ( ( x ) => x . line === line ) ;
166+ if ( ! r ) return false ;
167+ cur = { side : r . side , line : r . line } ;
168+ paint ( r ) ;
169+ r . el . scrollIntoView ( { block : "center" } ) ;
170+ return true ;
171+ }
172+
173+ // Jump used by the blockers list. Retries once across two frames so a just-triggered
174+ // collapsed-region expansion (which rerenders) has laid out its rows.
163175export function cursorJumpTo ( side : Side , line : number ) {
164- const land = ( ) => {
165- const list = rows ( ) ;
166- // Context rows merge to a single entry (additions primary, deletions twin in `alt`), so
167- // fall back to a line-only match before giving up.
168- const r = list . find ( ( x ) => matches ( x , side , line ) ) ?? list . find ( ( x ) => x . line === line ) ;
169- if ( ! r ) return false ;
170- cur = { side : r . side , line : r . line } ;
171- paint ( r ) ;
172- r . el . scrollIntoView ( { block : "center" } ) ;
173- return true ;
174- } ;
175- if ( ! land ( ) ) requestAnimationFrame ( ( ) => requestAnimationFrame ( ( ) => land ( ) ) ) ;
176+ if ( ! landAt ( side , line ) )
177+ requestAnimationFrame ( ( ) => requestAnimationFrame ( ( ) => landAt ( side , line ) ) ) ;
178+ }
179+
180+ // ── Go to line ───────────────────────────────────────────────────────────────
181+ // Typing digits in the diff accumulates a line number (shown as the goline pill); ↵ or a short
182+ // idle pause commits the jump, Esc cancels. Commit only moves the cursor — the existing ↵ /
183+ // ⇧Y / r bindings take over from the landed line, so the jump composes with every verb.
184+
185+ let golineTimer : ReturnType < typeof setTimeout > | undefined ;
186+
187+ export function golineActive ( ) : boolean {
188+ return ! ! S . golineBuffer ;
189+ }
190+
191+ export function golineDigit ( d : string ) {
192+ if ( ! S . golineBuffer && d === "0" ) return ; // a leading 0 can't start a real line number
193+ S . golineBuffer += d ;
194+ clearTimeout ( golineTimer ) ;
195+ golineTimer = setTimeout ( golineCommit , 800 ) ;
196+ }
197+
198+ export function golineCancel ( ) {
199+ S . golineBuffer = "" ;
200+ clearTimeout ( golineTimer ) ;
201+ }
202+
203+ export function golineCommit ( ) {
204+ const n = parseInt ( S . golineBuffer , 10 ) ;
205+ golineCancel ( ) ;
206+ if ( ! Number . isFinite ( n ) ) return ;
207+ // Prefer the additions/new side — the number a reviewer reads off the gutter. No retry:
208+ // goline never triggers an expansion, so a miss means the line isn't rendered.
209+ if ( ! landAt ( "additions" , n ) ) toast ( `Line ${ n } isn't visible in this diff` ) ;
176210}
177211
178212export function cursorMoveLine ( dir : 1 | - 1 ) {
0 commit comments