Skip to content

Commit f78500e

Browse files
committed
refactor(ui): simplify output window scroll tracking
Restore ed0c078 scroll behavior Replace the sticky `_was_at_bottom_by_win` flag with a new approach that tracks the previous line count per window. The `is_at_bottom` logic now compares the cursor position to the previous and current line counts, making auto-scroll behavior more robust and less dependent on viewport events. This also removes redundant comments and unused code, and ensures scroll tracking state is reset consistently.
1 parent 3c6995a commit f78500e

2 files changed

Lines changed: 18 additions & 32 deletions

File tree

lua/opencode/ui/output_window.lua

Lines changed: 10 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ M.debug_namespace = vim.api.nvim_create_namespace('opencode_output_debug')
88
M.markdown_namespace = vim.api.nvim_create_namespace('opencode_output_markdown')
99
M._last_visible_bottom_by_win = {}
1010
M._was_at_bottom_by_win = {}
11+
M._prev_line_count_by_win = {}
1112

1213
local _update_depth = 0
1314
local _update_buf = nil
@@ -81,19 +82,9 @@ function M.buffer_valid(windows)
8182
return windows and windows.output_buf and vim.api.nvim_buf_is_valid(windows.output_buf)
8283
end
8384

84-
---Check if the output window viewport is scrolled to the bottom of the buffer.
85-
---Returns true if the output window should continue auto-scrolling to follow
86-
---new content. Uses the viewport position (visible bottom line) rather than
87-
---the cursor, so that mouse-wheel scrolling—which moves the viewport but not
88-
---the cursor—correctly stops the tail-follow behavior.
89-
---
90-
---The `_was_at_bottom_by_win` flag is the persistent signal: it is set to
91-
---`true` by `scroll_win_to_bottom` and cleared to `false` by
92-
---`sync_cursor_with_viewport` whenever the viewport is scrolled away from the
93-
---buffer's last line. Reading a sticky flag (rather than the live viewport
94-
---position) lets callers like `renderer.scroll_to_bottom()` that run *after*
95-
---a buffer write still return the correct answer even though the viewport has
96-
---not yet caught up to the newly appended lines.
85+
---Check if the cursor in the output window is at (or was at) the bottom of
86+
---the buffer, using the same logic as the original implementation.
87+
---Returns true if the window should continue auto-scrolling.
9788
---@param win? integer Window ID, defaults to state.windows.output_win
9889
---@return boolean
9990
function M.is_at_bottom(win)
@@ -116,18 +107,13 @@ function M.is_at_bottom(win)
116107
return true
117108
end
118109

119-
-- Prefer the sticky flag when it has been set by scroll/WinScrolled events.
120-
-- Fall back to a live viewport check on the very first call (flag is nil).
121-
if M._was_at_bottom_by_win[win] ~= nil then
122-
return M._was_at_bottom_by_win[win] == true
123-
end
124-
125-
local visible_bottom = M.get_visible_bottom_line(win)
126-
if not visible_bottom then
110+
local ok2, cursor = pcall(vim.api.nvim_win_get_cursor, win)
111+
if not ok2 then
127112
return true
128113
end
129114

130-
return visible_bottom >= line_count
115+
local prev_line_count = M._prev_line_count_by_win[win] or line_count
116+
return cursor[1] >= prev_line_count or cursor[1] >= line_count
131117
end
132118

133119
---@param win? integer
@@ -146,11 +132,13 @@ function M.reset_scroll_tracking(win)
146132
if win then
147133
M._last_visible_bottom_by_win[win] = nil
148134
M._was_at_bottom_by_win[win] = nil
135+
M._prev_line_count_by_win[win] = nil
149136
return
150137
end
151138

152139
M._last_visible_bottom_by_win = {}
153140
M._was_at_bottom_by_win = {}
141+
M._prev_line_count_by_win = {}
154142
end
155143

156144
---@param win? integer
@@ -174,12 +162,6 @@ function M.sync_cursor_with_viewport(win)
174162
end
175163

176164
M._last_visible_bottom_by_win[win] = visible_bottom
177-
178-
-- Update the sticky at-bottom flag based on whether the viewport now shows
179-
-- the last line. This is the key mechanism: when the user scrolls up (mouse
180-
-- or keyboard), WinScrolled fires here and clears the flag so that the next
181-
-- `is_at_bottom()` call returns false and streaming stops following the tail.
182-
M._was_at_bottom_by_win[win] = visible_bottom >= line_count
183165
end
184166

185167
---@param windows OpencodeWindowState

lua/opencode/ui/renderer/scroll.lua

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
local config = require('opencode.config')
21
local state = require('opencode.state')
32
local output_window = require('opencode.ui.output_window')
43

@@ -15,8 +14,6 @@ function M.get_output_win()
1514
end
1615

1716
---Move the cursor in `win` to the last line of `buf` and scroll so it's visible.
18-
---Also marks the window as "at bottom" so that the next is_at_bottom() call
19-
---returns true even when the buffer grew past the current viewport.
2017
---@param win integer
2118
---@param buf integer
2219
function M.scroll_win_to_bottom(win, buf)
@@ -29,7 +26,7 @@ function M.scroll_win_to_bottom(win, buf)
2926
vim.api.nvim_win_call(win, function()
3027
vim.cmd('normal! zb')
3128
end)
32-
output_window._was_at_bottom_by_win[win] = true
29+
output_window._prev_line_count_by_win[win] = line_count
3330
end
3431

3532
---@param buf integer|nil
@@ -44,6 +41,13 @@ function M.pre_flush(buf)
4441
return nil
4542
end
4643

44+
-- Snapshot the current line count before the buffer write so that
45+
-- is_at_bottom() can compare cursor position against it after the write.
46+
local ok, line_count = pcall(vim.api.nvim_buf_line_count, buf)
47+
if ok and line_count and line_count > 0 then
48+
output_window._prev_line_count_by_win[win] = line_count
49+
end
50+
4751
return {
4852
win = win,
4953
follow = output_window.is_at_bottom(win),

0 commit comments

Comments
 (0)