33--- @class Integration
44--- @field filetype Filetype
55--- @field min_width ? number
6+ --- @field replace ? boolean
67
78--- @class Config
89--- @field main ? { width : number | fun (): number ; }
@@ -196,14 +197,6 @@ local function get_vsplits()
196197 return vsplits
197198end
198199
199- --- @return boolean
200- local function is_hsplit (buf )
201- local win_id = vim .fn .bufwinid (buf )
202- local width = vim .api .nvim_win_get_width (win_id )
203- local height = vim .api .nvim_win_get_height (win_id )
204- return width > height
205- end
206-
207200--- @param position " top" | " right" | " bottom" | " left"
208201--- @return boolean
209202local function is_integration_open (position )
@@ -233,12 +226,113 @@ local function get_window_by_filetype(filetype)
233226 return nil
234227end
235228
236- local function adjust_top_bottom_window_hack (target_window , position )
237- if target_window then
238- vim .api .nvim_win_call (target_window , function ()
239- vim .cmd (" wincmd " .. position )
240- end )
229+ --- @param position " top" | " bottom"
230+ --- @return number[]
231+ local function get_ordered_stack_windows (position )
232+ local windows = {}
233+ for _ , integration in ipairs (opts [position ]) do
234+ if type (integration ) == " table" and integration .filetype ~= " *" then
235+ local win = get_window_by_filetype (integration .filetype )
236+ if win then
237+ table.insert (windows , win )
238+ end
239+ end
240+ end
241+ return windows
242+ end
243+
244+ --- Stacks the open `top`/`bottom` integration windows on top of each other,
245+ --- full width, in the order they appear in the config: for `top`, the first
246+ --- configured integration ends up closest to the very top edge of the screen
247+ --- and the last one closest to the main area; for `bottom` it is the mirror
248+ --- (first closest to the main area, last closest to the very bottom edge).
249+ --- @param position " top" | " bottom"
250+ local function reposition_stack (position )
251+ local windows = get_ordered_stack_windows (position )
252+ if # windows == 0 then
253+ return
254+ end
255+
256+ -- already full width and in the desired relative order (top-to-bottom):
257+ -- nothing to do, since re-splitting would needlessly renegotiate heights
258+ local in_order = true
259+ for _ , win in ipairs (windows ) do
260+ if vim .api .nvim_win_get_width (win ) ~= vim .o .columns then
261+ in_order = false
262+ break
263+ end
264+ end
265+ if in_order then
266+ for i = 2 , # windows do
267+ if vim .api .nvim_win_get_position (windows [i ])[1 ] < vim .api .nvim_win_get_position (windows [i - 1 ])[1 ] then
268+ in_order = false
269+ break
270+ end
271+ end
272+ end
273+ if in_order then
274+ return
275+ end
276+
277+ -- splitting a window relative to another renegotiates both windows'
278+ -- heights, so snapshot and restore them once the whole stack is in order
279+ local heights = {}
280+ for _ , win in ipairs (windows ) do
281+ heights [win ] = vim .api .nvim_win_get_height (win )
282+ end
283+
284+ if position == " top" then
285+ vim .api .nvim_win_set_config (windows [1 ], { split = " above" , win = - 1 })
286+ for i = 2 , # windows do
287+ vim .api .nvim_win_set_config (windows [i ], { split = " below" , win = windows [i - 1 ] })
288+ end
289+ else
290+ vim .api .nvim_win_set_config (windows [# windows ], { split = " below" , win = - 1 })
291+ for i = # windows - 1 , 1 , - 1 do
292+ vim .api .nvim_win_set_config (windows [i ], { split = " above" , win = windows [i + 1 ] })
293+ end
294+ end
295+
296+ for _ , win in ipairs (windows ) do
297+ vim .api .nvim_win_set_height (win , heights [win ])
298+ vim .api .nvim_win_set_width (win , vim .o .columns )
299+ end
300+ end
301+
302+ --- @param position " top" | " bottom"
303+ --- @return number height , number count combined height and number of open stack windows
304+ local function stack_metrics (position )
305+ local windows = get_ordered_stack_windows (position )
306+ local height = 0
307+ for _ , win in ipairs (windows ) do
308+ height = height + vim .api .nvim_win_get_height (win )
241309 end
310+ return height , # windows
311+ end
312+
313+ --- Closing a stacked `top`/`bottom` window frees its content plus its one
314+ --- statusline row, but Neovim may hand that freed row to the main area instead
315+ --- of the surviving stack window. Given the stack's height/count from before
316+ --- the close, grow the survivor closest to the main area back to the height the
317+ --- stack should now have (its old height plus the closed window's freed row).
318+ --- @param position " top" | " bottom"
319+ --- @param before_height number stack height captured before the close
320+ --- @param before_count number stack window count captured before the close
321+ local function reclaim_stack_height (position , before_height , before_count )
322+ local windows = get_ordered_stack_windows (position )
323+ -- nothing to do if this stack didn't lose a window, or lost them all
324+ if # windows == 0 or # windows >= before_count then
325+ return
326+ end
327+
328+ local height = stack_metrics (position )
329+ local deficit = before_height + 1 - height
330+ if deficit <= 0 then
331+ return
332+ end
333+
334+ local boundary = position == " top" and windows [# windows ] or windows [1 ]
335+ vim .api .nvim_win_set_height (boundary , vim .api .nvim_win_get_height (boundary ) + deficit )
242336end
243337
244338local function resize_side_buffers ()
@@ -408,6 +502,19 @@ local function setup(options)
408502 return
409503 end
410504
505+ -- `WinClosed` fires just before the window is removed, so the stacks
506+ -- still have their pre-close heights here. Capture them and reclaim
507+ -- any slack from `WinResized`, which fires once the close has settled.
508+ local top_height , top_count = stack_metrics (" top" )
509+ local bottom_height , bottom_count = stack_metrics (" bottom" )
510+ vim .api .nvim_create_autocmd (" WinResized" , {
511+ once = true ,
512+ callback = function ()
513+ reclaim_stack_height (" top" , top_height , top_count )
514+ reclaim_stack_height (" bottom" , bottom_height , bottom_count )
515+ end ,
516+ })
517+
411518 local buf_id = vim .fn .winbufnr (win_id )
412519 local file_type = vim .api .nvim_get_option_value (" filetype" , { buf = buf_id })
413520 local buf_type = vim .api .nvim_get_option_value (" buftype" , { buf = buf_id })
@@ -434,12 +541,8 @@ local function setup(options)
434541 vim .cmd (" wincmd h" )
435542 end
436543
437- for _ , integration in pairs (opts .top ) do
438- adjust_top_bottom_window_hack (get_window_by_filetype (integration .filetype ), " K" )
439- end
440- for _ , integration in pairs (opts .bottom ) do
441- adjust_top_bottom_window_hack (get_window_by_filetype (integration .filetype ), " J" )
442- end
544+ reposition_stack (" top" )
545+ reposition_stack (" bottom" )
443546 resize_side_buffers ()
444547 end ,
445548 desc = " Recreate the side buffers if they are closed." ,
@@ -471,14 +574,17 @@ local function setup(options)
471574 for _ , integration in pairs (opts [position ]) do
472575 if type (integration ) == " table" and is_filetype (filetype , integration .filetype ) then
473576 close_side_buffer (position )
474- for _ , position_inner in ipairs (positions ) do
475- for _ , integration_inner in pairs (opts [position_inner ]) do
476- if
477- position_inner == position
478- and type (integration_inner ) == " table"
479- and not is_filetype (filetype , integration_inner .filetype )
480- then
481- close (integration_inner .filetype )
577+ if integration .replace ~= false then
578+ for _ , position_inner in ipairs (positions ) do
579+ for _ , integration_inner in pairs (opts [position_inner ]) do
580+ if
581+ position_inner == position
582+ and type (integration_inner ) == " table"
583+ and integration_inner .replace ~= false
584+ and not is_filetype (filetype , integration_inner .filetype )
585+ then
586+ close (integration_inner .filetype )
587+ end
482588 end
483589 end
484590 end
@@ -499,16 +605,8 @@ local function setup(options)
499605 end
500606 end
501607
502- for _ , integration in pairs (opts .top ) do
503- if not is_hsplit (args .buf ) then
504- adjust_top_bottom_window_hack (get_window_by_filetype (integration .filetype ), " K" )
505- end
506- end
507- for _ , integration in pairs (opts .bottom ) do
508- if not is_hsplit (args .buf ) then
509- adjust_top_bottom_window_hack (get_window_by_filetype (integration .filetype ), " J" )
510- end
511- end
608+ reposition_stack (" top" )
609+ reposition_stack (" bottom" )
512610 resize_side_buffers ()
513611 end ,
514612 desc = " Close side buffer plugins if another plugin is already occupying that side." ,
0 commit comments