Skip to content

Commit d00b2cf

Browse files
authored
feat: disable replace when configured (#24)
1 parent b5b5f62 commit d00b2cf

5 files changed

Lines changed: 238 additions & 43 deletions

File tree

‎README.md‎

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,10 @@ return {
3131
},
3232
-- TIP: find a buffer's filetype with :lua print(vim.bo.filetype)
3333
top = {
34+
{ filetype = "gitcommit", replace = false },
35+
{ filetype = "fugitive" },
3436
{ filetype = "man" },
3537
{ filetype = "help" },
36-
{ filetype = "fugitive" },
3738
},
3839
right = {
3940
{ filetype = "*", min_width = 46 },
@@ -80,16 +81,13 @@ programs.nvf.settings.vim.lazy.plugins = {
8081
event = [ "BufEnter" ];
8182
setupModule = "zen";
8283
setupOpts = {
83-
main = {
84-
width = 148;
85-
};
84+
main.width = 148;
8685
# TIP: find a buffer's filetype with :lua print(vim.bo.filetype)
8786
top = [
87+
{ filetype = "gitcommit"; replace = false; }
8888
{ filetype = "fugitive"; }
89-
{ filetype = "git"; }
9089
{ filetype = "man"; }
9190
{ filetype = "help"; }
92-
{ filetype = "gitcommit"; }
9391
];
9492
right = [
9593
{ filetype = "*"; min_width = 46; }

‎lua/zen/init.lua‎

Lines changed: 135 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
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
197198
end
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
209202
local function is_integration_open(position)
@@ -233,12 +226,113 @@ local function get_window_by_filetype(filetype)
233226
return nil
234227
end
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)
242336
end
243337

244338
local 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.",

‎tests/scripts/init_with_zen.lua‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ require("CopilotChat").setup({})
1313

1414
require("zen").setup({
1515
top = {
16+
{ filetype = "gitcommit", replace = false },
1617
{ filetype = "fugitive" },
1718
{ filetype = "man" },
1819
},
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
vim.cmd([[let &rtp.=','.getcwd()]])
2+
vim.opt.packpath:prepend("deps")
3+
4+
vim.o.columns = 240
5+
vim.o.lines = 52
6+
7+
require("mini.test").setup()
8+
9+
require("zen").setup({
10+
top = {
11+
{ filetype = "fugitive" },
12+
{ filetype = "gitcommit", replace = false },
13+
},
14+
})

‎tests/test_integrations.lua‎

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,90 @@ T["top integration"]["opening an integration should close the existing integrati
118118
})
119119
end
120120

121+
122+
for _, case in ipairs({ { name = "above", }, { name = "below", } }) do
123+
T["top integration"]["stacks " .. case.name .. " when configured " .. case.name .. " with replace=false"] = function()
124+
if case.name == "below" then
125+
child.restart({ "-u", "tests/scripts/init_with_zen_top_order.lua" })
126+
end
127+
128+
child.lua([[
129+
local tmpdir = vim.fn.tempname()
130+
vim.fn.mkdir(tmpdir, "p")
131+
vim.fn.system({ "git", "init", tmpdir })
132+
vim.fn.system({ "git", "-C", tmpdir, "config", "user.name", "Test" })
133+
vim.fn.system({ "git", "-C", tmpdir, "config", "user.email", "test@test.com" })
134+
vim.fn.system({ "git", "-C", tmpdir, "commit", "--allow-empty", "-m", "initial" })
135+
vim.fn.writefile({ "hello" }, tmpdir .. "/file")
136+
vim.cmd("edit " .. tmpdir .. "/file")
137+
]])
138+
139+
child.cmd("Git")
140+
141+
Helpers.expect.layout(child, {
142+
type = "col",
143+
children = {
144+
{ type = "leaf", filetype = "fugitive", buftype = "nowrite", width = 240, height = 25 },
145+
{
146+
type = "row",
147+
children = {
148+
{ type = "leaf", filetype = "zen-left", buftype = "nofile", width = 46, height = 24 },
149+
{ type = "leaf", filetype = "", buftype = "", width = 146, height = 24 },
150+
{ type = "leaf", filetype = "zen-right", buftype = "nofile", width = 46, height = 24 },
151+
},
152+
},
153+
},
154+
})
155+
156+
child.cmd("Git commit --allow-empty")
157+
158+
Helpers.expect.layout(child, {
159+
type = "col",
160+
children = {
161+
{
162+
type = "leaf",
163+
filetype = case.name == "above" and "gitcommit" or "fugitive",
164+
buftype = case.name == "above" and "" or "nowrite",
165+
width = 240,
166+
height = 12,
167+
},
168+
{
169+
type = "leaf",
170+
filetype = case.name == "above" and "fugitive" or "gitcommit",
171+
buftype = case.name == "above" and "nowrite" or "",
172+
width = 240,
173+
height = 12,
174+
},
175+
{
176+
type = "row",
177+
children = {
178+
{ type = "leaf", filetype = "zen-left", buftype = "nofile", width = 46, height = 24 },
179+
{ type = "leaf", filetype = "", buftype = "", width = 146, height = 24 },
180+
{ type = "leaf", filetype = "zen-right", buftype = "nofile", width = 46, height = 24 },
181+
},
182+
},
183+
},
184+
})
185+
186+
child.cmd("q")
187+
188+
Helpers.expect.layout(child, {
189+
type = "col",
190+
children = {
191+
{ type = "leaf", filetype = "fugitive", buftype = "nowrite", width = 240, height = 25 },
192+
{
193+
type = "row",
194+
children = {
195+
{ type = "leaf", filetype = "zen-left", buftype = "nofile", width = 46, height = 24 },
196+
{ type = "leaf", filetype = "", buftype = "", width = 146, height = 24 },
197+
{ type = "leaf", filetype = "zen-right", buftype = "nofile", width = 46, height = 24 },
198+
},
199+
},
200+
},
201+
})
202+
end
203+
end
204+
121205
T["bottom integration"] = MiniTest.new_set({})
122206

123207
T["bottom integration"]["opening"] = function()

0 commit comments

Comments
 (0)