Skip to content

Commit bdbd9ef

Browse files
feat: improve scroll at bottom to support scrolling without focus (#104)
1 parent 3fd6e82 commit bdbd9ef

File tree

4 files changed

+58
-10
lines changed

4 files changed

+58
-10
lines changed

lua/opencode/config.lua

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,7 @@ M.defaults = {
102102
tools = {
103103
show_output = true,
104104
},
105+
always_scroll_to_bottom = false,
105106
},
106107
input = {
107108
text = {

lua/opencode/types.lua

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,7 @@
114114
---@class OpencodeUIOutputConfig
115115
---@field tools { show_output: boolean }
116116
---@field rendering OpencodeUIOutputRenderingConfig
117+
---@field always_scroll_to_bottom boolean
117118

118119
---@class OpencodeContextConfig
119120
---@field enabled boolean

lua/opencode/ui/output_window.lua

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ local config = require('opencode.config')
33

44
local M = {}
55
M.namespace = vim.api.nvim_create_namespace('opencode_output')
6+
M.viewport_at_bottom = true
67

78
function M.create_buf()
89
local output_buf = vim.api.nvim_create_buf(false, true)
@@ -27,6 +28,36 @@ function M.mounted(windows)
2728
return windows and windows.output_buf and windows.output_win and vim.api.nvim_win_is_valid(windows.output_win)
2829
end
2930

31+
---Check if the output window is currently at the bottom
32+
---@param win? integer Window ID, defaults to state.windows.output_win
33+
---@return boolean true if at bottom, false otherwise
34+
function M.is_at_bottom(win)
35+
if config.ui.output.always_scroll_to_bottom then
36+
return true
37+
end
38+
39+
win = win or (state.windows and state.windows.output_win)
40+
41+
if not win or not vim.api.nvim_win_is_valid(win) then
42+
return true
43+
end
44+
45+
if not state.windows or not state.windows.output_buf then
46+
return true
47+
end
48+
49+
local ok, line_count = pcall(vim.api.nvim_buf_line_count, state.windows.output_buf)
50+
if not ok or not line_count or line_count == 0 then
51+
return true
52+
end
53+
54+
local botline = vim.fn.line('w$', win)
55+
56+
-- Consider at bottom if bottom visible line is at or near the end
57+
-- Use -1 tolerance for wrapped lines
58+
return botline >= line_count - 1
59+
end
60+
3061
function M.setup(windows)
3162
vim.api.nvim_set_option_value('winhighlight', config.ui.window_highlight, { win = windows.output_win })
3263
vim.api.nvim_set_option_value('wrap', true, { win = windows.output_win })
@@ -177,13 +208,23 @@ function M.setup_autocmds(windows, group)
177208
state.subscribe('current_permission', function()
178209
require('opencode.keymap').toggle_permission_keymap(windows.output_buf)
179210
end)
211+
212+
-- Track scroll position when window is scrolled
213+
vim.api.nvim_create_autocmd('WinScrolled', {
214+
group = group,
215+
buffer = windows.output_buf,
216+
callback = function()
217+
M.viewport_at_bottom = M.is_at_bottom(windows.output_win)
218+
end,
219+
})
180220
end
181221

182222
function M.clear()
183223
M.set_lines({})
184224
-- clear extmarks in all namespaces as I've seen RenderMarkdown leave some
185225
-- extmarks behind
186226
M.clear_extmarks(0, -1, true)
227+
M.viewport_at_bottom = true
187228
end
188229

189230
return M

lua/opencode/ui/renderer.lua

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ function M.reset()
5050
require('opencode.api').respond_to_permission('reject')
5151
end
5252
state.current_permission = nil
53+
5354
trigger_on_data_rendered()
5455
end
5556

@@ -224,26 +225,30 @@ function M.scroll_to_bottom()
224225
return
225226
end
226227

227-
local botline = vim.fn.line('w$', state.windows.output_win)
228-
local cursor = vim.api.nvim_win_get_cursor(state.windows.output_win)
229-
local cursor_row = cursor[1] or 0
230-
local is_focused = vim.api.nvim_get_current_win() == state.windows.output_win
231-
232228
local prev_line_count = M._prev_line_count or 0
233229

234230
---@cast line_count integer
235231
M._prev_line_count = line_count
236232

237-
local was_at_bottom = (botline >= prev_line_count) or prev_line_count == 0
238-
239233
trigger_on_data_rendered()
240234

241-
if is_focused and cursor_row < prev_line_count - 1 then
242-
return
235+
-- Determine if we should scroll to bottom
236+
local should_scroll = false
237+
238+
-- Always scroll on initial render
239+
if prev_line_count == 0 then
240+
should_scroll = true
241+
-- Scroll if user is at bottom (respects manual scroll position)
242+
elseif output_window.viewport_at_bottom then
243+
should_scroll = true
243244
end
244245

245-
if was_at_bottom or not is_focused then
246+
if should_scroll then
246247
vim.api.nvim_win_set_cursor(state.windows.output_win, { line_count, 0 })
248+
output_window.viewport_at_bottom = true
249+
else
250+
-- User has scrolled up, don't scroll
251+
output_window.viewport_at_bottom = false
247252
end
248253
end
249254

0 commit comments

Comments
 (0)