@@ -220,6 +220,13 @@ describe('output_window.is_at_bottom', function()
220220 assert .is_true (output_window .is_at_bottom (win ))
221221 end )
222222
223+ it (' treats a trailing blank line as padding, not content bottom' , function ()
224+ vim .api .nvim_buf_set_lines (buf , 0 , - 1 , false , { ' line 1' , ' line 2' , ' ' })
225+ vim .api .nvim_win_set_cursor (win , { 2 , 0 })
226+
227+ assert .is_true (output_window .is_at_bottom (win ))
228+ end )
229+
223230 it (' returns false when cursor is not on last line' , function ()
224231 -- cursor not at last line
225232 vim .api .nvim_win_set_cursor (win , { 25 , 0 })
@@ -364,6 +371,7 @@ describe('renderer.scroll_to_bottom', function()
364371 local renderer = require (' opencode.ui.renderer' )
365372 local ctx = require (' opencode.ui.renderer.ctx' )
366373 local output_window = require (' opencode.ui.output_window' )
374+ local stub = require (' luassert.stub' )
367375 local buf , win
368376
369377 before_each (function ()
@@ -437,6 +445,90 @@ describe('renderer.scroll_to_bottom', function()
437445 assert .equals (3 , cursor [1 ])
438446 assert .equals (# longer_line - 1 , cursor [2 ])
439447 end )
448+
449+ it (' scrolls to the last non-empty line when buffer ends with padding' , function ()
450+ vim .api .nvim_buf_set_lines (buf , 0 , - 1 , false , { ' line 1' , ' line 2' , ' ' })
451+
452+ local scroll = require (' opencode.ui.renderer.scroll' )
453+ scroll .scroll_win_to_bottom (win , buf )
454+
455+ local cursor = vim .api .nvim_win_get_cursor (win )
456+ assert .equals (2 , cursor [1 ])
457+ end )
458+
459+ it (' skips zb when the followed bottom line is already visible' , function ()
460+ vim .api .nvim_buf_set_lines (buf , 0 , - 1 , false , { ' line 1' , ' line 2' , ' line 3' })
461+ vim .api .nvim_win_set_height (win , 10 )
462+ vim .api .nvim_set_option_value (' wrap' , false , { win = win , scope = ' local' })
463+ vim .api .nvim_win_set_cursor (win , { 1 , 0 })
464+
465+ local cmd_stub = stub (vim , ' cmd' )
466+ local scroll = require (' opencode.ui.renderer.scroll' )
467+ scroll .scroll_win_to_bottom (win , buf )
468+
469+ local cursor = vim .api .nvim_win_get_cursor (win )
470+ assert .equals (3 , cursor [1 ])
471+ assert .stub (cmd_stub ).was_not_called_with (' normal! zb' )
472+ cmd_stub :revert ()
473+ end )
474+
475+ it (' uses zb when the followed bottom line is below the viewport' , function ()
476+ local lines = {}
477+ for i = 1 , 40 do
478+ lines [i ] = ' line ' .. i
479+ end
480+ vim .api .nvim_buf_set_lines (buf , 0 , - 1 , false , lines )
481+ vim .api .nvim_win_set_height (win , 5 )
482+ vim .api .nvim_win_set_cursor (win , { 1 , 0 })
483+
484+ local cmd_stub = stub (vim , ' cmd' ).invokes (function (cmd )
485+ if cmd == ' normal! zb' then
486+ vim .api .nvim_win_call (win , function ()
487+ vim .fn .winrestview ({ topline = 36 })
488+ end )
489+ return
490+ end
491+ return vim .api .nvim_cmd (vim .api .nvim_parse_cmd (cmd , {}), {})
492+ end )
493+
494+ local scroll = require (' opencode.ui.renderer.scroll' )
495+ scroll .scroll_win_to_bottom (win , buf )
496+
497+ local cursor = vim .api .nvim_win_get_cursor (win )
498+ assert .equals (40 , cursor [1 ])
499+ assert .stub (cmd_stub ).was_called_with (' normal! zb' )
500+ cmd_stub :revert ()
501+ end )
502+
503+ it (' uses zb when a wrapped bottom line grows past the last screen row' , function ()
504+ local long_line = string.rep (' x' , 80 )
505+ local longer_line = string.rep (' x' , 180 )
506+
507+ vim .api .nvim_win_set_width (win , 20 )
508+ vim .api .nvim_win_set_height (win , 5 )
509+ vim .api .nvim_set_option_value (' wrap' , true , { win = win , scope = ' local' })
510+ vim .api .nvim_buf_set_lines (buf , 0 , - 1 , false , { long_line })
511+
512+ local cmd_stub = stub (vim , ' cmd' ).invokes (function (cmd )
513+ if cmd == ' normal! zb' then
514+ return
515+ end
516+ return vim .api .nvim_cmd (vim .api .nvim_parse_cmd (cmd , {}), {})
517+ end )
518+
519+ local scroll = require (' opencode.ui.renderer.scroll' )
520+ scroll .scroll_win_to_bottom (win , buf )
521+ cmd_stub :clear ()
522+
523+ vim .api .nvim_buf_set_lines (buf , 0 , 1 , false , { longer_line })
524+ scroll .scroll_win_to_bottom (win , buf )
525+
526+ local cursor = vim .api .nvim_win_get_cursor (win )
527+ assert .equals (1 , cursor [1 ])
528+ assert .equals (# longer_line - 1 , cursor [2 ])
529+ assert .stub (cmd_stub ).was_called_with (' normal! zb' )
530+ cmd_stub :revert ()
531+ end )
440532end )
441533
442534describe (' ui.focus_input' , function ()
0 commit comments