Skip to content

Commit f1526ca

Browse files
committed
Avoid normal commands during output scroll restore
1 parent af18aec commit f1526ca

4 files changed

Lines changed: 71 additions & 68 deletions

File tree

lua/opencode/ui/output_window.lua

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,19 @@ function M.get_visible_top_line(win)
201201
return (ok and line and line > 0) and line or nil
202202
end
203203

204+
---@param win integer
205+
---@param topline integer
206+
function M.restore_view_topline(win, topline)
207+
vim.api.nvim_win_call(win, function()
208+
local view = vim.fn.winsaveview()
209+
view.topline = math.max(1, topline)
210+
view.skipcol = 0
211+
vim.fn.winrestview(view)
212+
vim.fn.line('w$')
213+
vim.cmd('redraw')
214+
end)
215+
end
216+
204217
---@param win? integer
205218
function M.reset_scroll_tracking(win)
206219
if win then
@@ -763,9 +776,7 @@ function M.setup_autocmds(windows, group)
763776
if rendered and rendered.line_start then
764777
local restored_top = math.max(1, rendered.line_start + anchor_offset)
765778
pcall(vim.api.nvim_win_set_cursor, windows.output_win, { restored_top, 0 })
766-
pcall(vim.api.nvim_win_call, windows.output_win, function()
767-
vim.cmd('normal! zt')
768-
end)
779+
pcall(M.restore_view_topline, windows.output_win, restored_top)
769780
return
770781
end
771782
end

lua/opencode/ui/renderer/scroll.lua

Lines changed: 7 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -3,32 +3,6 @@ local output_window = require('opencode.ui.output_window')
33

44
local M = {}
55

6-
local function with_window_event_autocmds_ignored(fn)
7-
local previous = vim.o.eventignore
8-
local ignored = {
9-
ModeChanged = true,
10-
WinEnter = true,
11-
WinLeave = true,
12-
BufEnter = true,
13-
}
14-
15-
for event in previous:gmatch('[^,]+') do
16-
if event ~= '' then
17-
ignored[event] = true
18-
end
19-
end
20-
21-
local events = vim.tbl_keys(ignored)
22-
table.sort(events)
23-
vim.o.eventignore = table.concat(events, ',')
24-
25-
local ok, err = pcall(fn)
26-
vim.o.eventignore = previous
27-
if not ok then
28-
error(err)
29-
end
30-
end
31-
326
---@param win integer
337
---@return boolean
348
local function window_wraps(win)
@@ -45,6 +19,12 @@ local function get_text_width(win)
4519
return math.max(1, width - textoff)
4620
end
4721

22+
---@param win integer
23+
---@param line integer
24+
local function restore_view_with_line_at_bottom(win, line)
25+
output_window.restore_view_topline(win, line - vim.api.nvim_win_get_height(win) + 1)
26+
end
27+
4828
---@param buf integer
4929
---@param win integer
5030
---@param target_line integer
@@ -124,18 +104,7 @@ function M.scroll_win_to_bottom(win, buf)
124104
end
125105

126106
if needs_bottom_align then
127-
local windows = state.windows
128-
if windows and vim.api.nvim_get_current_win() == windows.input_win then
129-
with_window_event_autocmds_ignored(function()
130-
vim.api.nvim_win_call(win, function()
131-
vim.cmd('normal! zb')
132-
end)
133-
end)
134-
else
135-
vim.api.nvim_win_call(win, function()
136-
vim.cmd('normal! zb')
137-
end)
138-
end
107+
restore_view_with_line_at_bottom(win, target_line)
139108
end
140109

141110
output_window._prev_line_count_by_win[win] = line_count

tests/replay/lazy_render_scroll_spec.lua

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,17 @@ describe('replay lazy-render upward loading', function()
113113
assert.are.equal(1, output_window.get_visible_top_line(win))
114114
assert.are.equal(5, vim.api.nvim_win_get_cursor(win)[1])
115115

116+
local modechanged_count = 0
117+
local group = vim.api.nvim_create_augroup('OpencodeLazyRenderViewRegression', { clear = true })
118+
vim.api.nvim_create_autocmd('ModeChanged', {
119+
group = group,
120+
pattern = 'i:n',
121+
callback = function()
122+
modechanged_count = modechanged_count + 1
123+
end,
124+
})
125+
local cmd_stub = require('luassert.stub')(vim, 'cmd')
126+
116127
vim.api.nvim_exec_autocmds('WinScrolled', {
117128
buffer = state.windows.output_buf,
118129
modeline = false,
@@ -123,5 +134,10 @@ describe('replay lazy-render upward loading', function()
123134
end)
124135

125136
assert.is_true(loaded, 'Expected viewport-at-top WinScrolled to load older replayed messages')
137+
assert.equals(0, modechanged_count)
138+
assert.stub(cmd_stub).was_not_called_with('normal! zt')
139+
140+
cmd_stub:revert()
141+
pcall(vim.api.nvim_del_augroup_by_id, group)
126142
end)
127143
end)

tests/unit/cursor_tracking_spec.lua

Lines changed: 34 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -474,7 +474,7 @@ describe('renderer.scroll_to_bottom', function()
474474
cmd_stub:revert()
475475
end)
476476

477-
it('uses zb when the followed bottom line is below the viewport', function()
477+
it('bottom-aligns when the followed bottom line is below the viewport', function()
478478
local lines = {}
479479
for i = 1, 40 do
480480
lines[i] = 'line ' .. i
@@ -483,26 +483,19 @@ describe('renderer.scroll_to_bottom', function()
483483
vim.api.nvim_win_set_height(win, 5)
484484
vim.api.nvim_win_set_cursor(win, { 1, 0 })
485485

486-
local cmd_stub = stub(vim, 'cmd').invokes(function(cmd)
487-
if cmd == 'normal! zb' then
488-
vim.api.nvim_win_call(win, function()
489-
vim.fn.winrestview({ topline = 36 })
490-
end)
491-
return
492-
end
493-
return vim.api.nvim_cmd(vim.api.nvim_parse_cmd(cmd, {}), {})
494-
end)
486+
local cmd_stub = stub(vim, 'cmd')
495487

496488
local scroll = require('opencode.ui.renderer.scroll')
497489
scroll.scroll_win_to_bottom(win, buf)
498490

499491
local cursor = vim.api.nvim_win_get_cursor(win)
500492
assert.equals(40, cursor[1])
501-
assert.stub(cmd_stub).was_called_with('normal! zb')
493+
assert.equals(40, output_window.get_visible_bottom_line(win))
494+
assert.stub(cmd_stub).was_not_called_with('normal! zb')
502495
cmd_stub:revert()
503496
end)
504497

505-
it('uses zb when a wrapped bottom line grows past the last screen row', function()
498+
it('keeps the visual end selected when a wrapped bottom line grows past the last screen row', function()
506499
local long_line = string.rep('x', 80)
507500
local longer_line = string.rep('x', 180)
508501

@@ -511,12 +504,7 @@ describe('renderer.scroll_to_bottom', function()
511504
vim.api.nvim_set_option_value('wrap', true, { win = win, scope = 'local' })
512505
vim.api.nvim_buf_set_lines(buf, 0, -1, false, { long_line })
513506

514-
local cmd_stub = stub(vim, 'cmd').invokes(function(cmd)
515-
if cmd == 'normal! zb' then
516-
return
517-
end
518-
return vim.api.nvim_cmd(vim.api.nvim_parse_cmd(cmd, {}), {})
519-
end)
507+
local cmd_stub = stub(vim, 'cmd')
520508

521509
local scroll = require('opencode.ui.renderer.scroll')
522510
scroll.scroll_win_to_bottom(win, buf)
@@ -528,7 +516,7 @@ describe('renderer.scroll_to_bottom', function()
528516
local cursor = vim.api.nvim_win_get_cursor(win)
529517
assert.equals(1, cursor[1])
530518
assert.equals(#longer_line - 1, cursor[2])
531-
assert.stub(cmd_stub).was_called_with('normal! zb')
519+
assert.stub(cmd_stub).was_not_called_with('normal! zb')
532520
cmd_stub:revert()
533521
end)
534522

@@ -545,21 +533,36 @@ describe('renderer.scroll_to_bottom', function()
545533
state.ui.set_windows({ output_win = win, output_buf = buf, input_win = input_win, input_buf = input_buf })
546534
vim.api.nvim_set_current_win(input_win)
547535

548-
local winleave_count = 0
549-
local modechanged_count = 0
536+
local events = {
537+
BufEnter = 0,
538+
ModeChanged = 0,
539+
WinEnter = 0,
540+
WinLeave = 0,
541+
}
550542
local group = vim.api.nvim_create_augroup('OpencodeScrollImeRegression', { clear = true })
543+
vim.api.nvim_create_autocmd('WinEnter', {
544+
group = group,
545+
callback = function()
546+
events.WinEnter = events.WinEnter + 1
547+
end,
548+
})
551549
vim.api.nvim_create_autocmd('WinLeave', {
552550
group = group,
553-
buffer = input_buf,
554551
callback = function()
555-
winleave_count = winleave_count + 1
552+
events.WinLeave = events.WinLeave + 1
553+
end,
554+
})
555+
vim.api.nvim_create_autocmd('BufEnter', {
556+
group = group,
557+
callback = function()
558+
events.BufEnter = events.BufEnter + 1
556559
end,
557560
})
558561
vim.api.nvim_create_autocmd('ModeChanged', {
559562
group = group,
560563
pattern = 'i:n',
561564
callback = function()
562-
modechanged_count = modechanged_count + 1
565+
events.ModeChanged = events.ModeChanged + 1
563566
end,
564567
})
565568

@@ -578,10 +581,14 @@ describe('renderer.scroll_to_bottom', function()
578581
renderer.scroll_to_bottom()
579582

580583
assert.equals(input_win, vim.api.nvim_get_current_win())
581-
assert.equals(0, winleave_count)
582-
assert.equals(0, modechanged_count)
584+
assert.same({
585+
BufEnter = 0,
586+
ModeChanged = 0,
587+
WinEnter = 0,
588+
WinLeave = 0,
589+
}, events)
583590
assert.equals(50, vim.api.nvim_win_get_cursor(win)[1])
584-
assert.stub(cmd_stub).was_called_with('normal! zb')
591+
assert.stub(cmd_stub).was_not_called_with('normal! zb')
585592

586593
cmd_stub:revert()
587594
config.values.ui.output.always_scroll_to_bottom = false

0 commit comments

Comments
 (0)