@@ -6,6 +6,8 @@ local M = {}
66M .namespace = vim .api .nvim_create_namespace (' opencode_output' )
77M .debug_namespace = vim .api .nvim_create_namespace (' opencode_output_debug' )
88M .markdown_namespace = vim .api .nvim_create_namespace (' opencode_output_markdown' )
9+ M ._last_visible_bottom_by_win = {}
10+ M ._viewport_cursor_tracking_by_win = {}
911
1012local _update_depth = 0
1113local _update_buf = nil
@@ -108,6 +110,97 @@ function M.is_at_bottom(win)
108110 return cursor [1 ] >= line_count
109111end
110112
113+ --- @param win ? integer
114+ --- @return integer | nil
115+ function M .get_visible_bottom_line (win )
116+ win = win or (state .windows and state .windows .output_win )
117+ if not win or not vim .api .nvim_win_is_valid (win ) then
118+ return nil
119+ end
120+
121+ local buf = vim .api .nvim_win_get_buf (win )
122+ if not buf or not vim .api .nvim_buf_is_valid (buf ) then
123+ return nil
124+ end
125+
126+ local line_count = vim .api .nvim_buf_line_count (buf )
127+ if line_count == 0 then
128+ return nil
129+ end
130+
131+ local ok_view , view = pcall (vim .api .nvim_win_call , win , vim .fn .winsaveview )
132+ if not ok_view or type (view ) ~= ' table' then
133+ return nil
134+ end
135+
136+ local topline = math.max (1 , view .topline or 1 )
137+ local remaining_height = vim .api .nvim_win_get_height (win )
138+ for line = topline , line_count do
139+ local ok_height , result = pcall (vim .api .nvim_win_text_height , win , {
140+ start_row = line - 1 ,
141+ end_row = line - 1 ,
142+ })
143+ local line_height = ok_height and result and result .all or 1
144+ remaining_height = remaining_height - line_height
145+ if remaining_height <= 0 then
146+ return line
147+ end
148+ end
149+
150+ return line_count
151+ end
152+
153+ --- @param win ? integer
154+ function M .reset_scroll_tracking (win )
155+ if win then
156+ M ._last_visible_bottom_by_win [win ] = nil
157+ M ._viewport_cursor_tracking_by_win [win ] = nil
158+ return
159+ end
160+
161+ M ._last_visible_bottom_by_win = {}
162+ M ._viewport_cursor_tracking_by_win = {}
163+ end
164+
165+ --- @param win ? integer
166+ function M .sync_cursor_with_viewport (win )
167+ win = win or (state .windows and state .windows .output_win )
168+ if not win or not vim .api .nvim_win_is_valid (win ) then
169+ return
170+ end
171+
172+ local windows = state .windows
173+ local buf = windows and windows .output_buf
174+ if not buf or not vim .api .nvim_buf_is_valid (buf ) or vim .api .nvim_win_get_buf (win ) ~= buf then
175+ M .reset_scroll_tracking (win )
176+ return
177+ end
178+
179+ local ok_cursor , cursor = pcall (vim .api .nvim_win_get_cursor , win )
180+ local ok_count , line_count = pcall (vim .api .nvim_buf_line_count , buf )
181+ local visible_bottom = M .get_visible_bottom_line (win )
182+ if not ok_cursor or not cursor or not ok_count or not line_count or line_count == 0 or not visible_bottom then
183+ return
184+ end
185+
186+ local last_visible_bottom = M ._last_visible_bottom_by_win [win ]
187+ local tracking = M ._viewport_cursor_tracking_by_win [win ] == true
188+ local anchored_to_viewport_bottom = tracking and last_visible_bottom and cursor [1 ] == last_visible_bottom
189+
190+ if cursor [1 ] > visible_bottom or (anchored_to_viewport_bottom and cursor [1 ] ~= visible_bottom ) then
191+ M ._viewport_cursor_tracking_by_win [win ] = true
192+ pcall (vim .api .nvim_win_set_cursor , win , { math.min (visible_bottom , line_count ), 0 })
193+ local pos = state .ui .get_window_cursor (win )
194+ if pos then
195+ state .ui .set_cursor_position (' output' , pos )
196+ end
197+ elseif not anchored_to_viewport_bottom then
198+ M ._viewport_cursor_tracking_by_win [win ] = false
199+ end
200+
201+ M ._last_visible_bottom_by_win [win ] = visible_bottom
202+ end
203+
111204function M .setup (windows )
112205 window_options .set_window_option (
113206 ' winhighlight' ,
@@ -137,6 +230,8 @@ function M.setup(windows)
137230 window_options .set_window_option (' statuscolumn' , ' ' , windows .output_win , { save_original = true })
138231
139232 M .update_dimensions (windows )
233+ M .reset_scroll_tracking (windows .output_win )
234+ M ._last_visible_bottom_by_win [windows .output_win ] = M .get_visible_bottom_line (windows .output_win )
140235 M .setup_keymaps (windows )
141236end
142237
@@ -329,6 +424,7 @@ function M.close()
329424 end
330425 --- @cast state.windows { output_win : integer , output_buf : integer }
331426
427+ M .reset_scroll_tracking (state .windows .output_win )
332428 pcall (vim .api .nvim_win_close , state .windows .output_win , true )
333429 pcall (vim .api .nvim_buf_delete , state .windows .output_buf , { force = true })
334430end
@@ -378,36 +474,7 @@ function M.setup_autocmds(windows, group)
378474 group = group ,
379475 buffer = windows .output_buf ,
380476 callback = function ()
381- if not windows .output_win or not vim .api .nvim_win_is_valid (windows .output_win ) then
382- return
383- end
384-
385- local ok , cursor = pcall (vim .api .nvim_win_get_cursor , windows .output_win )
386- if not ok then
387- return
388- end
389-
390- local ok2 , line_count = pcall (vim .api .nvim_buf_line_count , windows .output_buf )
391- if not ok2 or line_count == 0 then
392- return
393- end
394-
395- if cursor [1 ] >= line_count then
396- local ok3 , view = pcall (vim .api .nvim_win_call , windows .output_win , vim .fn .winsaveview )
397- if ok3 and type (view ) == ' table' then
398- local topline = view .topline or 1
399- local win_height = vim .api .nvim_win_get_height (windows .output_win )
400- local visible_bottom = math.min (topline + win_height - 1 , line_count )
401-
402- if visible_bottom < line_count then
403- pcall (vim .api .nvim_win_set_cursor , windows .output_win , { visible_bottom , 0 })
404- local pos = state .ui .get_window_cursor (windows .output_win )
405- if pos then
406- state .ui .set_cursor_position (' output' , pos )
407- end
408- end
409- end
410- end
477+ M .sync_cursor_with_viewport (windows .output_win )
411478 end ,
412479 })
413480end
0 commit comments