Skip to content

Commit 533f0f2

Browse files
authored
fix(ui): avoid forced scroll-to-bottom while reading older output (#273)
* fix(ui): avoid forced scroll-to-bottom while reading older output * refactor(ui): only retrieve cursor position if necessary * test: add unit tests for scroll_to_bottom behavior
1 parent 5ce7f13 commit 533f0f2

2 files changed

Lines changed: 61 additions & 6 deletions

File tree

lua/opencode/ui/renderer.lua

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -326,9 +326,14 @@ function M.scroll_to_bottom(force)
326326
-- Always scroll on initial render
327327
if prev_line_count == 0 then
328328
should_scroll = true
329-
-- Scroll if user is at bottom (respects manual scroll position)
330-
elseif output_window.viewport_at_bottom then
329+
-- Respect explicit config to always follow output
330+
elseif config.ui.output.always_scroll_to_bottom then
331331
should_scroll = true
332+
-- Scroll if user is at bottom (respects manual scroll position)
333+
else
334+
local ok_cursor, cursor = pcall(vim.api.nvim_win_get_cursor, state.windows.output_win)
335+
local cursor_row = ok_cursor and cursor[1] or 1
336+
should_scroll = cursor_row >= prev_line_count
332337
end
333338
end
334339

@@ -338,10 +343,6 @@ function M.scroll_to_bottom(force)
338343
vim.api.nvim_win_call(state.windows.output_win, function()
339344
vim.cmd('normal! zb')
340345
end)
341-
output_window.viewport_at_bottom = true
342-
else
343-
-- User has scrolled up, don't scroll
344-
output_window.viewport_at_bottom = false
345346
end
346347
end
347348

tests/unit/cursor_tracking_spec.lua

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,3 +185,57 @@ describe('output_window.is_at_bottom', function()
185185
assert.is_true(output_window.is_at_bottom(win))
186186
end)
187187
end)
188+
189+
describe('renderer.scroll_to_bottom', function()
190+
local renderer = require('opencode.ui.renderer')
191+
local output_window = require('opencode.ui.output_window')
192+
local buf, win
193+
194+
before_each(function()
195+
config.setup({})
196+
buf = vim.api.nvim_create_buf(false, true)
197+
local lines = {}
198+
for i = 1, 50 do
199+
lines[i] = 'line ' .. i
200+
end
201+
vim.api.nvim_buf_set_lines(buf, 0, -1, false, lines)
202+
203+
win = vim.api.nvim_open_win(buf, true, {
204+
relative = 'editor', width = 80, height = 10, row = 0, col = 0,
205+
})
206+
207+
state.windows = { output_win = win, output_buf = buf }
208+
renderer._prev_line_count = 50
209+
end)
210+
211+
after_each(function()
212+
pcall(vim.api.nvim_win_close, win, true)
213+
pcall(vim.api.nvim_buf_delete, buf, { force = true })
214+
state.windows = nil
215+
renderer._prev_line_count = 0
216+
output_window.viewport_at_bottom = nil
217+
end)
218+
219+
it('does not force-scroll when user cursor is above previous bottom', function()
220+
vim.api.nvim_win_set_cursor(win, { 10, 0 })
221+
output_window.viewport_at_bottom = true
222+
223+
vim.api.nvim_buf_set_lines(buf, -1, -1, false, { 'line 51' })
224+
renderer.scroll_to_bottom()
225+
226+
local cursor = vim.api.nvim_win_get_cursor(win)
227+
assert.equals(10, cursor[1])
228+
end)
229+
230+
it('still scrolls when always_scroll_to_bottom is enabled', function()
231+
config.values.ui.output.always_scroll_to_bottom = true
232+
vim.api.nvim_win_set_cursor(win, { 10, 0 })
233+
234+
vim.api.nvim_buf_set_lines(buf, -1, -1, false, { 'line 51' })
235+
renderer.scroll_to_bottom()
236+
237+
local cursor = vim.api.nvim_win_get_cursor(win)
238+
assert.equals(51, cursor[1])
239+
config.values.ui.output.always_scroll_to_bottom = false
240+
end)
241+
end)

0 commit comments

Comments
 (0)