Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 14 additions & 3 deletions lua/opencode/ui/output_window.lua
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,19 @@ function M.get_visible_top_line(win)
return (ok and line and line > 0) and line or nil
end

---@param win integer
---@param topline integer
function M.restore_view_topline(win, topline)
vim.api.nvim_win_call(win, function()
local view = vim.fn.winsaveview()
view.topline = math.max(1, topline)
view.skipcol = 0
vim.fn.winrestview(view)
-- Apply the restored viewport before later line('w$') reads inspect it.
vim.cmd('redraw')
end)
end

---@param win? integer
function M.reset_scroll_tracking(win)
if win then
Expand Down Expand Up @@ -763,9 +776,7 @@ function M.setup_autocmds(windows, group)
if rendered and rendered.line_start then
local restored_top = math.max(1, rendered.line_start + anchor_offset)
pcall(vim.api.nvim_win_set_cursor, windows.output_win, { restored_top, 0 })
pcall(vim.api.nvim_win_call, windows.output_win, function()
vim.cmd('normal! zt')
end)
pcall(M.restore_view_topline, windows.output_win, restored_top)
return
end
end
Expand Down
44 changes: 7 additions & 37 deletions lua/opencode/ui/renderer/scroll.lua
Original file line number Diff line number Diff line change
Expand Up @@ -3,31 +3,6 @@ local output_window = require('opencode.ui.output_window')

local M = {}

local function with_window_event_autocmds_ignored(fn)
local previous = vim.o.eventignore
local ignored = {
WinEnter = true,
WinLeave = true,
BufEnter = true,
}

for event in previous:gmatch('[^,]+') do
if event ~= '' then
ignored[event] = true
end
end

local events = vim.tbl_keys(ignored)
table.sort(events)
vim.o.eventignore = table.concat(events, ',')

local ok, err = pcall(fn)
vim.o.eventignore = previous
if not ok then
error(err)
end
end

---@param win integer
---@return boolean
local function window_wraps(win)
Expand All @@ -44,6 +19,12 @@ local function get_text_width(win)
return math.max(1, width - textoff)
end

---@param win integer
---@param line integer
local function restore_view_with_line_at_bottom(win, line)
output_window.restore_view_topline(win, line - vim.api.nvim_win_get_height(win) + 1)
end

---@param buf integer
---@param win integer
---@param target_line integer
Expand Down Expand Up @@ -123,18 +104,7 @@ function M.scroll_win_to_bottom(win, buf)
end

if needs_bottom_align then
local windows = state.windows
if windows and vim.api.nvim_get_current_win() == windows.input_win then
with_window_event_autocmds_ignored(function()
vim.api.nvim_win_call(win, function()
vim.cmd('normal! zb')
end)
end)
else
vim.api.nvim_win_call(win, function()
vim.cmd('normal! zb')
end)
end
restore_view_with_line_at_bottom(win, target_line)
end

output_window._prev_line_count_by_win[win] = line_count
Expand Down
16 changes: 16 additions & 0 deletions tests/replay/lazy_render_scroll_spec.lua
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,17 @@ describe('replay lazy-render upward loading', function()
assert.are.equal(1, output_window.get_visible_top_line(win))
assert.are.equal(5, vim.api.nvim_win_get_cursor(win)[1])

local modechanged_count = 0
local group = vim.api.nvim_create_augroup('OpencodeLazyRenderViewRegression', { clear = true })
vim.api.nvim_create_autocmd('ModeChanged', {
group = group,
pattern = 'i:n',
callback = function()
modechanged_count = modechanged_count + 1
end,
})
local cmd_stub = require('luassert.stub')(vim, 'cmd')

vim.api.nvim_exec_autocmds('WinScrolled', {
buffer = state.windows.output_buf,
modeline = false,
Expand All @@ -123,5 +134,10 @@ describe('replay lazy-render upward loading', function()
end)

assert.is_true(loaded, 'Expected viewport-at-top WinScrolled to load older replayed messages')
assert.equals(0, modechanged_count)
assert.stub(cmd_stub).was_not_called_with('normal! zt')

cmd_stub:revert()
pcall(vim.api.nvim_del_augroup_by_id, group)
end)
end)
66 changes: 43 additions & 23 deletions tests/unit/cursor_tracking_spec.lua
Original file line number Diff line number Diff line change
Expand Up @@ -474,7 +474,7 @@ describe('renderer.scroll_to_bottom', function()
cmd_stub:revert()
end)

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

local cmd_stub = stub(vim, 'cmd').invokes(function(cmd)
if cmd == 'normal! zb' then
vim.api.nvim_win_call(win, function()
vim.fn.winrestview({ topline = 36 })
end)
return
end
return vim.api.nvim_cmd(vim.api.nvim_parse_cmd(cmd, {}), {})
end)
local cmd_stub = stub(vim, 'cmd')

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

local cursor = vim.api.nvim_win_get_cursor(win)
assert.equals(40, cursor[1])
assert.stub(cmd_stub).was_called_with('normal! zb')
assert.equals(40, output_window.get_visible_bottom_line(win))
assert.stub(cmd_stub).was_not_called_with('normal! zb')
cmd_stub:revert()
end)

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

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

local cmd_stub = stub(vim, 'cmd').invokes(function(cmd)
if cmd == 'normal! zb' then
return
end
return vim.api.nvim_cmd(vim.api.nvim_parse_cmd(cmd, {}), {})
end)
local cmd_stub = stub(vim, 'cmd')

local scroll = require('opencode.ui.renderer.scroll')
scroll.scroll_win_to_bottom(win, buf)
Expand All @@ -528,7 +516,7 @@ describe('renderer.scroll_to_bottom', function()
local cursor = vim.api.nvim_win_get_cursor(win)
assert.equals(1, cursor[1])
assert.equals(#longer_line - 1, cursor[2])
assert.stub(cmd_stub).was_called_with('normal! zb')
assert.stub(cmd_stub).was_not_called_with('normal! zb')
cmd_stub:revert()
end)

Expand All @@ -545,26 +533,58 @@ describe('renderer.scroll_to_bottom', function()
state.ui.set_windows({ output_win = win, output_buf = buf, input_win = input_win, input_buf = input_buf })
vim.api.nvim_set_current_win(input_win)

local winleave_count = 0
local events = {
BufEnter = 0,
ModeChanged = 0,
WinEnter = 0,
WinLeave = 0,
}
local group = vim.api.nvim_create_augroup('OpencodeScrollImeRegression', { clear = true })
vim.api.nvim_create_autocmd('WinEnter', {
group = group,
callback = function()
events.WinEnter = events.WinEnter + 1
end,
})
vim.api.nvim_create_autocmd('WinLeave', {
group = group,
buffer = input_buf,
callback = function()
winleave_count = winleave_count + 1
events.WinLeave = events.WinLeave + 1
end,
})
vim.api.nvim_create_autocmd('BufEnter', {
group = group,
callback = function()
events.BufEnter = events.BufEnter + 1
end,
})
vim.api.nvim_create_autocmd('ModeChanged', {
group = group,
pattern = 'i:n',
callback = function()
events.ModeChanged = events.ModeChanged + 1
end,
})

local cmd_stub = require('luassert.stub')(vim, 'cmd')

vim.api.nvim_win_set_height(win, 5)
vim.api.nvim_win_set_cursor(win, { 1, 0 })
config.values.ui.output.always_scroll_to_bottom = true

renderer.scroll_to_bottom()

assert.equals(input_win, vim.api.nvim_get_current_win())
assert.equals(0, winleave_count)
assert.same({
BufEnter = 0,
ModeChanged = 0,
WinEnter = 0,
WinLeave = 0,
}, events)
assert.equals(50, vim.api.nvim_win_get_cursor(win)[1])
assert.stub(cmd_stub).was_not_called_with('normal! zb')

cmd_stub:revert()
config.values.ui.output.always_scroll_to_bottom = false
pcall(vim.api.nvim_del_augroup_by_id, group)
end)
Expand Down
Loading