diff --git a/lua/zen/init.lua b/lua/zen/init.lua index ba1f4ca..cf3351a 100644 --- a/lua/zen/init.lua +++ b/lua/zen/init.lua @@ -1,9 +1,7 @@ --- @alias Filetype string|string[] - --- @class Integration --- @field filetype Filetype --- @field min_width? number - --- @class Config --- @field main? { width: number | fun(): number; } --- @field top? Integration[] @@ -11,507 +9,330 @@ --- @field bottom? Integration[] --- @field left? Integration[] ---- @class ConfigOptions ---- @field main { width: number | fun(): number; } ---- @field top Integration[] ---- @field right Integration[] ---- @field bottom Integration[] ---- @field left Integration[] - -local default_width = 148 ---- @type ConfigOptions -local opts = { - main = { width = default_width }, +local options = { + main = { width = 148 }, top = {}, right = { { filetype = "*", min_width = 46 } }, bottom = {}, left = { { filetype = "*", min_width = 46 } }, } -local state = { - [vim.api.nvim_get_current_tabpage()] = { left = nil, right = nil }, -} +local state = { [vim.api.nvim_get_current_tabpage()] = { left = -1, right = -1 } } +local filetype_positions = {} +local min_widths = {} +local wildcard_min_widths = {} +local sibling_filetypes = {} +local in_handler = false local function get_main_width() - local width = opts.main and opts.main.width - if type(width) == "function" then - width = width() - end - if type(width) ~= "number" then - return default_width - end - return width + local width = options.main and options.main.width + if type(width) == "function" then width = width() end + return type(width) == "number" and width or options.main.width end -local function create_window(position) - if position == "left" then - vim.cmd("topleft vnew") - elseif position == "right" then - vim.cmd("botright vnew") - end - - local win_id = vim.api.nvim_get_current_win() - vim.api.nvim_win_set_width(win_id, math.floor((vim.o.columns - get_main_width()) / 2)) - vim.api.nvim_set_option_value("winfixwidth", true, { scope = "local", win = win_id }) - vim.api.nvim_set_option_value("winfixbuf", true, { scope = "local", win = win_id }) - vim.api.nvim_set_option_value("cursorline", false, { scope = "local", win = win_id }) - vim.api.nvim_set_option_value("number", false, { scope = "local", win = win_id }) - vim.api.nvim_set_option_value("relativenumber", false, { scope = "local", win = win_id }) - - local buf_id = vim.api.nvim_get_current_buf() - vim.api.nvim_set_option_value("filetype", "zen-" .. position, { buf = buf_id }) - vim.api.nvim_set_option_value("buftype", "nofile", { buf = buf_id }) - vim.api.nvim_set_option_value("buflisted", false, { buf = buf_id }) - - return vim.api.nvim_get_current_win() +local function get_padding_width() + return math.floor((vim.o.columns - get_main_width()) / 2) end ----@param target string ----@param filetype Filetype ----@return boolean -local function is_filetype(target, filetype) - if type(filetype) == "string" then - return filetype ~= "*" and filetype == target - elseif type(filetype) == "table" then - return vim.tbl_contains(filetype, target) - end - return false +local function create_side_window(position) + vim.cmd(position == "left" and "topleft vnew" or "botright vnew") + local window = vim.api.nvim_get_current_win() + vim.api.nvim_win_set_width(window, get_padding_width()) + vim.wo[window].winfixwidth = true + vim.wo[window].winfixbuf = true + vim.wo[window].cursorline = false + vim.wo[window].number = false + vim.wo[window].relativenumber = false + local buffer = vim.api.nvim_get_current_buf() + vim.bo[buffer].filetype = "zen-" .. position + vim.bo[buffer].buftype = "nofile" + vim.bo[buffer].buflisted = false + return window end ----@param position "top" | "right" | "bottom" | "left" ----@param filetype string ----@return number -local function get_min_width(position, filetype) - local wildcard_width = 0 - for _, integration in ipairs(opts[position]) do - if type(integration) == "table" then - if integration.min_width and integration.filetype ~= "*" and is_filetype(filetype, integration.filetype) then - return integration.min_width - end - local mw = integration.min_width - if integration.filetype == "*" and mw then - wildcard_width = mw +local function scan_windows() + local filetype_windows = {} + local editable_cols = {} + local editable_count = 0 + for _, window in ipairs(vim.api.nvim_list_wins()) do + if vim.api.nvim_win_get_config(window).relative ~= "" then + goto continue + end + local filetype = vim.bo[vim.api.nvim_win_get_buf(window)].filetype + filetype_windows[filetype] = window + if not filetype_positions[filetype] then + editable_count = editable_count + 1 + if vim.api.nvim_win_get_width(window) < vim.o.columns then + editable_cols[vim.api.nvim_win_get_position(window)[2]] = true end end + ::continue:: end - return wildcard_width + return filetype_windows, vim.tbl_count(editable_cols), editable_count end ----@return number -local function get_side_buffer(position) - local current_tabpage = vim.api.nvim_get_current_tabpage() - if state[current_tabpage] and state[current_tabpage][position] then - return state[current_tabpage][position] +local function close_side_window(position) + local tab = state[vim.api.nvim_get_current_tabpage()] + local window = tab and tab[position] + if not window or not vim.api.nvim_win_is_valid(window) then + return end - return -1 -end - -local function close_side_buffer(position) - local window = get_side_buffer(position) - if window and vim.api.nvim_win_is_valid(window) then - local buffer = vim.api.nvim_win_get_buf(window) - vim.api.nvim_win_close(window, true) - - if vim.api.nvim_buf_is_valid(buffer) then - vim.api.nvim_buf_delete(buffer, { force = true }) - end + local buffer = vim.api.nvim_win_get_buf(window) + vim.api.nvim_win_close(window, true) + if vim.api.nvim_buf_is_valid(buffer) then + vim.api.nvim_buf_delete(buffer, { force = true }) end end ----@param filetypes string[] ----@return boolean -local function filetypes_visible(filetypes) - for _, win_id in ipairs(vim.api.nvim_list_wins()) do - local buf_id = vim.api.nvim_win_get_buf(win_id) - if vim.api.nvim_buf_is_valid(buf_id) and vim.api.nvim_buf_is_loaded(buf_id) then - if vim.tbl_contains(filetypes, vim.api.nvim_get_option_value("filetype", { buf = buf_id })) then - return true - end - end +local function resize_side_windows() + local width = get_padding_width() + local tab = state[vim.api.nvim_get_current_tabpage()] + if not tab then + return end - return false -end - ----@param filetypes string[] ----@param type_to_remove string -local function remove_file_type(filetypes, type_to_remove) - for i, filetype in ipairs(filetypes) do - if filetype == type_to_remove then - table.remove(filetypes, i) - break - end + for _, position in ipairs({ "left", "right" }) do + if vim.api.nvim_win_is_valid(tab[position] or -1) then vim.api.nvim_win_set_width(tab[position], width) end end end ----@param buf number ----@return boolean -local function is_buff_integration(buf) - local filetype = vim.api.nvim_get_option_value("filetype", { buf = buf }) - if filetype == "zen-left" or filetype == "zen-right" then - return true - end - for _, position in ipairs({ "left", "right", "top", "bottom" }) do - for _, integration in ipairs(opts[position] or {}) do - ---@diagnostic disable-next-line: undefined-field - if type(integration) == "table" and is_filetype(filetype, integration.filetype) then - return true +-- Critical: set_config and set_height must be in separate passes; +-- interleaving them causes Neovim to redistribute heights between calls. +local function reposition_top_bottom(filetype_windows) + local heights, configs = {}, {} + for filetype, window in pairs(filetype_windows) do + local position = filetype_positions[filetype] + if (position == "top" or position == "bottom") and vim.api.nvim_win_is_valid(window) then + heights[window] = vim.api.nvim_win_get_height(window) + if vim.api.nvim_win_get_width(window) ~= vim.o.columns then + configs[window] = position end end end - return false -end - ----@return table -local function get_editable_files() - local editable_files = {} - local windows = vim.api.nvim_list_wins() - for _, win in ipairs(windows) do - local buf = vim.api.nvim_win_get_buf(win) - if not is_buff_integration(buf) then -- is an actual file - editable_files[buf] = buf - end + for window, position in pairs(configs) do + vim.api.nvim_win_set_config(window, { split = position == "top" and "above" or "below", win = -1 }) end - return editable_files -end - ----@param win_id number ----@return boolean -local function is_popup_window(win_id) - return vim.api.nvim_win_get_config(win_id).relative ~= "" -end - ----@return table -local function get_vsplits() - local vsplits = {} - local windows = vim.api.nvim_list_wins() - for _, win in ipairs(windows) do - local buf = vim.api.nvim_win_get_buf(win) - local total_width = vim.o.columns - local window_width = vim.api.nvim_win_get_width(win) - local is_popup = is_popup_window(win) - local is_integration = is_buff_integration(buf) - if not is_popup and not is_integration and window_width < total_width then - vsplits[win] = buf - end + for window, height in pairs(heights) do + if vim.api.nvim_win_is_valid(window) then vim.api.nvim_win_set_height(window, height) end end - return vsplits -end - ----@return boolean -local function is_hsplit(buf) - local win_id = vim.fn.bufwinid(buf) - local width = vim.api.nvim_win_get_width(win_id) - local height = vim.api.nvim_win_get_height(win_id) - return width > height end ----@param position "top" | "right" | "bottom" | "left" ----@return boolean -local function is_integration_open(position) - for _, integration in pairs(opts[position]) do - for _, buf in ipairs(vim.api.nvim_list_bufs()) do - if - type(integration) == "table" - and vim.api.nvim_buf_is_loaded(buf) - and is_filetype(vim.api.nvim_get_option_value("filetype", { buf = buf }), integration.filetype) - then - return true - end +local function protect_top_bottom(filetype_windows) + local protected = {} + for filetype, window in pairs(filetype_windows) do + local position = filetype_positions[filetype] + if (position == "top" or position == "bottom") and vim.api.nvim_win_is_valid(window) then + vim.wo[window].winfixheight = true + table.insert(protected, window) end end - return false + if #protected == 0 then return end + vim.api.nvim_create_autocmd("WinResized", { + once = true, + callback = function() + for _, window in ipairs(protected) do + if vim.api.nvim_win_is_valid(window) then + vim.wo[window].winfixheight = false + end + end + end, + }) end ----@param filetype string|string[] -local function get_window_by_filetype(filetype) - for _, win_id in ipairs(vim.api.nvim_list_wins()) do - local buf_id = vim.api.nvim_win_get_buf(win_id) - local buf_filetype = vim.api.nvim_get_option_value("filetype", { buf = buf_id }) - if is_filetype(buf_filetype, filetype) then - return win_id +local function has_side_window(filetype_windows, position, exclude) + for filetype in pairs(filetype_windows) do + if filetype ~= exclude and filetype_positions[filetype] == position then + return true end end - return nil -end - -local function adjust_top_bottom_window_hack(target_window, position) - if target_window then - vim.api.nvim_win_call(target_window, function() - vim.cmd("wincmd " .. position) - end) - end -end - -local function resize_side_buffers() - local new_width = math.floor((vim.o.columns - get_main_width()) / 2) - local left = vim.api.nvim_win_is_valid(get_side_buffer("left")) - if left then - vim.api.nvim_win_set_width(get_side_buffer("left"), new_width) - end - local right = vim.api.nvim_win_is_valid(get_side_buffer("right")) - if right then - vim.api.nvim_win_set_width(get_side_buffer("right"), new_width) - end + return false end ----@param filetype string|string[] -local function close(filetype) - for _, win in ipairs(vim.api.nvim_list_wins()) do - local buf = vim.api.nvim_win_get_buf(win) - if is_filetype(vim.bo[buf].filetype, filetype) then - vim.api.nvim_win_close(win, false) +local function ensure_padding(filetype_windows, exclude) + local tabpage = vim.api.nvim_get_current_tabpage() + state[tabpage] = state[tabpage] or { left = -1, right = -1 } + for _, position in ipairs({ "left", "right" }) do + if not has_side_window(filetype_windows, position, exclude) then + state[tabpage][position] = create_side_window(position) + vim.cmd(position == "left" and "wincmd l" or "wincmd h") end end end ----@param options? Config -local function setup(options) - -- Default splitting will cause your main splits to jump when opening an integration. - -- To prevent this, set `splitkeep` to either `screen` or `topline`. +--- @param config? Config +local function setup(config) vim.opt.splitkeep = "screen" - - ---@type ConfigOptions - opts = vim.tbl_extend("force", opts, options or {}) - - vim.api.nvim_create_autocmd("CursorMoved", { - -- TODO: use pattern for better perf - callback = function(args) - if is_buff_integration(args.buf) then - local buf_info = vim.fn.getbufinfo(args.buf) - - local filetype = vim.api.nvim_get_option_value("filetype", { buf = args.buf }) - for _, position in ipairs({ "right", "left" }) do - for _, integration in pairs(opts[position]) do - ---@diagnostic disable-next-line: undefined-field - if type(integration) == "table" and is_filetype(filetype, integration.filetype) then - local new_width = math.max(get_min_width(position, filetype), math.floor((vim.o.columns - get_main_width()) / 2)) - vim.api.nvim_win_set_width(buf_info[1].windows[1], new_width) - return - end - end + options = vim.tbl_extend("force", options, config or {}) + filetype_positions = { ["zen-left"] = "left", ["zen-right"] = "right" } + min_widths, wildcard_min_widths, sibling_filetypes = {}, {}, {} + for _, position in ipairs({ "top", "right", "bottom", "left" }) do + for _, integration in ipairs(options[position]) do + if type(integration) ~= "table" then + goto continue + end + if integration.filetype == "*" then + if (integration.min_width or 0) > 0 then + wildcard_min_widths[position] = integration.min_width + end + goto continue + end + local filetypes = type(integration.filetype) == "table" and integration.filetype or { integration.filetype } + for _, ft in ipairs(filetypes) do + filetype_positions[ft] = position + sibling_filetypes[ft] = filetypes + if (integration.min_width or 0) > 0 then + min_widths[position] = min_widths[position] or {} + min_widths[position][ft] = integration.min_width end end - end, - desc = "HACK: adjust the integration when opening", - }) + ::continue:: + end + end vim.api.nvim_create_autocmd({ "VimEnter", "TabNew" }, { callback = function() - -- disable when window is too small - if vim.o.columns <= get_main_width() then - return - end - - if vim.tbl_count(get_vsplits()) >= 2 then + local _, vsplit_count = scan_windows() + if vim.o.columns <= get_main_width() or vsplit_count >= 2 then return end - state[vim.api.nvim_get_current_tabpage()] = { - left = create_window("left"), - right = create_window("right"), + left = create_side_window("left"), + right = create_side_window("right"), } vim.cmd("wincmd h") end, - desc = "Restore the last buffer when opening Neovim", }) vim.api.nvim_create_autocmd("BufEnter", { callback = function() if vim.bo.filetype == "zen-left" then vim.cmd("wincmd l") - end - if vim.bo.filetype == "zen-right" then + elseif vim.bo.filetype == "zen-right" then vim.cmd("wincmd h") end end, - desc = "Prevent the cursor from moving to the side buffers.", }) vim.api.nvim_create_autocmd("QuitPre", { callback = function(args) - if is_popup_window(vim.api.nvim_get_current_win()) then + if vim.api.nvim_win_get_config(vim.api.nvim_get_current_win()).relative ~= "" then return end - if is_buff_integration(args.buf) then + if filetype_positions[vim.bo[args.buf].filetype] then return end - - local editable_count = 0 - for _, win in ipairs(vim.api.nvim_tabpage_list_wins(0)) do - if not is_popup_window(win) then - local buf = vim.api.nvim_win_get_buf(win) - if not is_buff_integration(buf) then - editable_count = editable_count + 1 - end + local editable = 0 + for _, window in ipairs(vim.api.nvim_tabpage_list_wins(0)) do + if vim.api.nvim_win_get_config(window).relative == "" and not filetype_positions[vim.bo[vim.api.nvim_win_get_buf(window)].filetype] then + editable = editable + 1 end end - if editable_count ~= 1 then - return - end - - close_side_buffer("left") - close_side_buffer("right") - - for _, position in ipairs({ "top", "right", "bottom", "left" }) do - for _, integration in pairs(opts[position]) do - if type(integration) == "table" then - close(integration.filetype) - end + if editable ~= 1 then return end + close_side_window("left") + close_side_window("right") + for _, window in ipairs(vim.api.nvim_list_wins()) do + if filetype_positions[vim.bo[vim.api.nvim_win_get_buf(window)].filetype] then + vim.api.nvim_win_close(window, false) end end end, - desc = "Close left and right buffers on quit", }) - vim.api.nvim_create_autocmd("VimResized", { + vim.api.nvim_create_autocmd("WinResized", { callback = function() - if vim.tbl_count(get_editable_files()) ~= 1 then - return + local _, vsplit_count = scan_windows() + if vsplit_count >= 2 then + close_side_window("left") + close_side_window("right") end + end, + }) - -- close when window is too small + vim.api.nvim_create_autocmd("VimResized", { + callback = function() + local filetype_windows, vsplit_count = scan_windows() if vim.o.columns <= get_main_width() then - close_side_buffer("left") - close_side_buffer("right") + close_side_window("left") + close_side_window("right") return end - - -- enable when side buffers is big enough - local left = vim.api.nvim_win_is_valid(get_side_buffer("left")) - if not left and not is_integration_open("left") then - state[vim.api.nvim_get_current_tabpage()].left = create_window("left") - vim.cmd("wincmd l") - end - - local right = vim.api.nvim_win_is_valid(get_side_buffer("right")) - if not right and not is_integration_open("right") then - state[vim.api.nvim_get_current_tabpage()].right = create_window("right") - vim.cmd("wincmd h") + if vsplit_count > 1 then + return end - resize_side_buffers() + ensure_padding(filetype_windows, nil) + reposition_top_bottom(scan_windows()) + resize_side_windows() end, - desc = "Resizes side windows after terminal has been resized, closes them if not enough space left.", }) vim.api.nvim_create_autocmd("WinClosed", { - pattern = "*", callback = function(args) - -- do not recreate when window is too small if vim.o.columns <= get_main_width() then return end - local win_id = tonumber(args.match) - if win_id == nil then + local window_id = tonumber(args.match) + if not window_id or not vim.api.nvim_win_is_valid(window_id) then return end - - if is_popup_window(win_id) then + if vim.api.nvim_win_get_config(window_id).relative ~= "" then return end - - local buf_id = vim.fn.winbufnr(win_id) - local file_type = vim.api.nvim_get_option_value("filetype", { buf = buf_id }) - local buf_type = vim.api.nvim_get_option_value("buftype", { buf = buf_id }) - -- do not recreate when multiple vsplits are active - local count = vim.tbl_count(get_vsplits()) - if buf_type == "" then - count = count - 1 - end - if count >= 2 then + local closing_buffer = vim.fn.winbufnr(window_id) + local closing_filetype = closing_buffer ~= -1 and vim.bo[closing_buffer].filetype or "" + local closing_position = filetype_positions[closing_filetype] + if closing_position == "top" or closing_position == "bottom" then + protect_top_bottom(scan_windows()) return end - - local left_file_types = { "fugitiveblame", "fyler", "undotree", "dbui", "zen-left" } - remove_file_type(left_file_types, file_type) - if not filetypes_visible(left_file_types) then - state[vim.api.nvim_get_current_tabpage()].left = create_window("left") - vim.cmd("wincmd l") + local filetype_windows, vsplit_count = scan_windows() + if closing_buffer ~= -1 and vim.bo[closing_buffer].buftype == "" then + vsplit_count = vsplit_count - 1 end - - local right_file_types = { "dapui_scopes", "neotest-summary", "zen-right" } - remove_file_type(right_file_types, file_type) - if not filetypes_visible(right_file_types) then - state[vim.api.nvim_get_current_tabpage()].right = create_window("right") - vim.cmd("wincmd h") - end - - for _, integration in pairs(opts.top) do - adjust_top_bottom_window_hack(get_window_by_filetype(integration.filetype), "K") + if vsplit_count >= 2 then + return end - for _, integration in pairs(opts.bottom) do - adjust_top_bottom_window_hack(get_window_by_filetype(integration.filetype), "J") + ensure_padding(filetype_windows, closing_filetype) + if not in_handler then + reposition_top_bottom(scan_windows()) end - resize_side_buffers() + resize_side_windows() end, - desc = "Recreate the side buffers if they are closed.", }) vim.api.nvim_create_autocmd({ "BufWinEnter", "FileType" }, { - pattern = "*", callback = function(args) - if is_popup_window(vim.api.nvim_get_current_win()) then + if vim.api.nvim_win_get_config(vim.api.nvim_get_current_win()).relative ~= "" then return end local filetype = vim.bo[args.buf].filetype - local is_integration = is_buff_integration(args.buf) - if args.event == "BufWinEnter" then - if filetype ~= "" and not is_integration and vim.tbl_count(get_vsplits()) >= 2 then - close_side_buffer("left") - close_side_buffer("right") - return - end - end - - if not is_integration then + local filetype_windows = scan_windows() + if not filetype_positions[filetype] then return end - - ---@type ("top"|"right"|"bottom"|"left")[] - local positions = { "top", "right", "bottom", "left" } - for _, position in ipairs(positions) do - for _, integration in pairs(opts[position]) do - if type(integration) == "table" and is_filetype(filetype, integration.filetype) then - close_side_buffer(position) - for _, position_inner in ipairs(positions) do - for _, integration_inner in pairs(opts[position_inner]) do - if - position_inner == position - and type(integration_inner) == "table" - and not is_filetype(filetype, integration_inner.filetype) - then - close(integration_inner.filetype) - end - end - end - - if position == "left" or position == "right" then - local min_width = get_min_width(position, filetype) - if min_width > 0 then - local win = vim.fn.bufwinid(args.buf) - if win ~= -1 then - local current_width = vim.api.nvim_win_get_width(win) - if current_width < min_width then - vim.api.nvim_win_set_width(win, min_width) - end - end - end - end - end - end + local position = filetype_positions[filetype] + if filetype == "zen-left" or filetype == "zen-right" then + reposition_top_bottom(filetype_windows) + resize_side_windows() + return end - - for _, integration in pairs(opts.top) do - if not is_hsplit(args.buf) then - adjust_top_bottom_window_hack(get_window_by_filetype(integration.filetype), "K") + in_handler = true + close_side_window(position) + local siblings = sibling_filetypes[filetype] or { filetype } + for _, window in ipairs(vim.api.nvim_list_wins()) do + local ft = vim.bo[vim.api.nvim_win_get_buf(window)].filetype + if filetype_positions[ft] == position and not vim.tbl_contains(siblings, ft) then + vim.api.nvim_win_close(window, false) end end - for _, integration in pairs(opts.bottom) do - if not is_hsplit(args.buf) then - adjust_top_bottom_window_hack(get_window_by_filetype(integration.filetype), "J") + if position == "left" or position == "right" then + local min_width = (min_widths[position] and min_widths[position][filetype]) or wildcard_min_widths[position] or 0 + local window = vim.fn.bufwinid(args.buf) + if window ~= -1 then + vim.api.nvim_win_set_width(window, math.max(min_width, get_padding_width())) end end - resize_side_buffers() + reposition_top_bottom(scan_windows()) + resize_side_windows() + in_handler = false end, - desc = "Close side buffer plugins if another plugin is already occupying that side.", }) end diff --git a/tests/scripts/init_with_zen.lua b/tests/scripts/init_with_zen.lua index 4d7afd8..2e5660a 100644 --- a/tests/scripts/init_with_zen.lua +++ b/tests/scripts/init_with_zen.lua @@ -1,5 +1,5 @@ vim.cmd([[let &rtp.=','.getcwd()]]) -vim.opt.packpath:prepend("deps") +vim.opt.packpath:prepend(vim.fn.getcwd() .. "/deps") vim.o.columns = 240 vim.o.lines = 52 diff --git a/tests/scripts/init_with_zen_min_width.lua b/tests/scripts/init_with_zen_min_width.lua index 3f2e4c1..d914c57 100644 --- a/tests/scripts/init_with_zen_min_width.lua +++ b/tests/scripts/init_with_zen_min_width.lua @@ -1,5 +1,5 @@ vim.cmd([[let &rtp.=','.getcwd()]]) -vim.opt.packpath:prepend("deps") +vim.opt.packpath:prepend(vim.fn.getcwd() .. "/deps") vim.o.columns = 240 vim.o.lines = 52 diff --git a/tests/scripts/init_with_zen_small.lua b/tests/scripts/init_with_zen_small.lua index 00090ca..558acca 100644 --- a/tests/scripts/init_with_zen_small.lua +++ b/tests/scripts/init_with_zen_small.lua @@ -1,5 +1,5 @@ vim.cmd([[let &rtp.=','.getcwd()]]) -vim.opt.packpath:prepend("deps") +vim.opt.packpath:prepend(vim.fn.getcwd() .. "/deps") vim.o.columns = 140 vim.o.lines = 52 diff --git a/tests/test_integrations.lua b/tests/test_integrations.lua index 8877e94..fe6c14f 100644 --- a/tests/test_integrations.lua +++ b/tests/test_integrations.lua @@ -36,6 +36,44 @@ T["left integration"]["opening closes zen side buffer, closing reopens it"] = fu }) end +T["left integration"]["opening a left integration preserves an existing top integration"] = function() + child.cmd("Git") + child.lua("vim.cmd('Fyler kind=split_left_most')") + + Helpers.expect.layout(child, { + type = "col", + children = { + { type = "leaf", filetype = "fugitive", buftype = "nowrite", width = 240, height = 25 }, + { + type = "row", + children = { + { type = "leaf", filetype = "fyler", buftype = "acwrite", width = 46, height = 24 }, + { type = "leaf", filetype = "", buftype = "", width = 146, height = 24 }, + { type = "leaf", filetype = "zen-right", buftype = "nofile", width = 46, height = 24 }, + }, + }, + }, + }) + + child.cmd("close") + child.lua("vim.cmd('Fyler kind=split_left_most')") + child.cmd("close") + + Helpers.expect.layout(child, { + type = "col", + children = { + { type = "leaf", filetype = "fugitive", buftype = "nowrite", width = 240, height = 25 }, + { + type = "row", + children = { + { type = "leaf", filetype = "zen-left", buftype = "nofile", width = 46, height = 24 }, + { type = "leaf", filetype = "", buftype = "", width = 146, height = 24 }, + { type = "leaf", filetype = "zen-right", buftype = "nofile", width = 46, height = 24 }, + }, + }, + }, + }) +end T["left integration"]["opening an integration should close the existing integration on the same side"] = function() child.cmd("Fyler kind=split_left_most") @@ -61,6 +99,45 @@ T["left integration"]["opening an integration should close the existing integrat }) end +T["left integration"]["opening a left integration preserves an existing bottom integration"] = function() + child.cmd("Trouble diagnostics") + child.cmd("Fyler kind=split_left_most") + + Helpers.expect.layout(child, { + type = "col", + children = { + { + type = "row", + children = { + { type = "leaf", filetype = "fyler", buftype = "acwrite", width = 46, height = 39 }, + { type = "leaf", filetype = "", buftype = "", width = 146, height = 39 }, + { type = "leaf", filetype = "zen-right", buftype = "nofile", width = 46, height = 39 }, + }, + }, + { type = "leaf", filetype = "trouble", buftype = "nofile", width = 240, height = 10 }, + }, + }) + + child.cmd("close") + child.cmd("Fyler kind=split_left_most") + child.cmd("close") + + Helpers.expect.layout(child, { + type = "col", + children = { + { + type = "row", + children = { + { type = "leaf", filetype = "zen-left", buftype = "nofile", width = 46, height = 39 }, + { type = "leaf", filetype = "", buftype = "", width = 146, height = 39 }, + { type = "leaf", filetype = "zen-right", buftype = "nofile", width = 46, height = 39 }, + }, + }, + { type = "leaf", filetype = "trouble", buftype = "nofile", width = 240, height = 10 }, + }, + }) +end + T["top integration"] = MiniTest.new_set({}) T["top integration"]["opening"] = function() @@ -118,6 +195,70 @@ T["top integration"]["opening an integration should close the existing integrati }) end +T["top integration"]["closing a stacked top split returns cursor to the integration below it"] = function() + -- initialize a real temporary git repo with user identity so that + -- "Git commit --allow-empty" works deterministically in CI + child.lua([[ + local tmpdir = vim.fn.tempname() + vim.fn.mkdir(tmpdir, "p") + vim.fn.system({ "git", "init", tmpdir }) + vim.fn.system({ "git", "-C", tmpdir, "config", "user.name", "Test" }) + vim.fn.system({ "git", "-C", tmpdir, "config", "user.email", "test@test.com" }) + vim.fn.chdir(tmpdir) + ]]) + child.cmd("Git") + + Helpers.expect.layout(child, { + type = "col", + children = { + { type = "leaf", filetype = "fugitive", buftype = "nowrite", width = 240, height = 25 }, + { + type = "row", + children = { + { type = "leaf", filetype = "zen-left", buftype = "nofile", width = 46, height = 24 }, + { type = "leaf", filetype = "", buftype = "", width = 146, height = 24 }, + { type = "leaf", filetype = "zen-right", buftype = "nofile", width = 46, height = 24 }, + }, + }, + }, + }) + + -- move cursor to fugitive window and start a commit + child.lua([[ + for _, win in ipairs(vim.api.nvim_list_wins()) do + local buf = vim.api.nvim_win_get_buf(win) + if vim.bo[buf].filetype == "fugitive" then + vim.api.nvim_set_current_win(win) + break + end + end + ]]) + child.cmd("Git commit --allow-empty") + + -- close the commit editor (abort the commit) + child.cmd("bdelete!") + + -- layout should be unchanged + Helpers.expect.layout(child, { + type = "col", + children = { + { type = "leaf", filetype = "fugitive", buftype = "nowrite", width = 240, height = 25 }, + { + type = "row", + children = { + { type = "leaf", filetype = "zen-left", buftype = "nofile", width = 46, height = 24 }, + { type = "leaf", filetype = "", buftype = "", width = 146, height = 24 }, + { type = "leaf", filetype = "zen-right", buftype = "nofile", width = 46, height = 24 }, + }, + }, + }, + }) + + -- cursor should be in the fugitive window + local ft = child.lua_get("vim.bo.filetype") + MiniTest.expect.equality(ft, "fugitive") +end + T["bottom integration"] = MiniTest.new_set({}) T["bottom integration"]["opening"] = function() @@ -177,6 +318,84 @@ end T["right integration"] = MiniTest.new_set({}) +T["right integration"]["opening a right integration preserves an existing top integration"] = function() + child.cmd("Git") + child.cmd("Neotest summary open") + + Helpers.expect.layout(child, { + type = "col", + children = { + { type = "leaf", filetype = "fugitive", buftype = "nowrite", width = 240, height = 25 }, + { + type = "row", + children = { + { type = "leaf", filetype = "zen-left", buftype = "nofile", width = 46, height = 24 }, + { type = "leaf", filetype = "", buftype = "", width = 146, height = 24 }, + { type = "leaf", filetype = "neotest-summary", buftype = "nofile", width = 46, height = 24 }, + }, + }, + }, + }) + + child.cmd("Neotest summary close") + child.cmd("Neotest summary open") + child.cmd("Neotest summary close") + + Helpers.expect.layout(child, { + type = "col", + children = { + { type = "leaf", filetype = "fugitive", buftype = "nowrite", width = 240, height = 25 }, + { + type = "row", + children = { + { type = "leaf", filetype = "zen-left", buftype = "nofile", width = 46, height = 24 }, + { type = "leaf", filetype = "", buftype = "", width = 146, height = 24 }, + { type = "leaf", filetype = "zen-right", buftype = "nofile", width = 46, height = 24 }, + }, + }, + }, + }) +end + +T["right integration"]["opening a right integration preserves an existing bottom integration"] = function() + child.cmd("Trouble diagnostics") + child.cmd("Neotest summary open") + + Helpers.expect.layout(child, { + type = "col", + children = { + { + type = "row", + children = { + { type = "leaf", filetype = "zen-left", buftype = "nofile", width = 46, height = 39 }, + { type = "leaf", filetype = "", buftype = "", width = 146, height = 39 }, + { type = "leaf", filetype = "neotest-summary", buftype = "nofile", width = 46, height = 39 }, + }, + }, + { type = "leaf", filetype = "trouble", buftype = "nofile", width = 240, height = 10 }, + }, + }) + + child.cmd("Neotest summary close") + child.cmd("Neotest summary open") + child.cmd("Neotest summary close") + + Helpers.expect.layout(child, { + type = "col", + children = { + { + type = "row", + children = { + { type = "leaf", filetype = "zen-left", buftype = "nofile", width = 46, height = 39 }, + { type = "leaf", filetype = "", buftype = "", width = 146, height = 39 }, + { type = "leaf", filetype = "zen-right", buftype = "nofile", width = 46, height = 39 }, + }, + }, + { type = "leaf", filetype = "trouble", buftype = "nofile", width = 240, height = 10 }, + }, + }) +end + T["right integration"]["opening closes zen side buffer, closing reopens it"] = function() child.cmd("Neotest summary open") @@ -184,8 +403,8 @@ T["right integration"]["opening closes zen side buffer, closing reopens it"] = f type = "row", children = { { type = "leaf", filetype = "zen-left", buftype = "nofile", width = 46, height = 50 }, - { type = "leaf", filetype = "", buftype = "", width = 142, height = 50 }, - { type = "leaf", filetype = "neotest-summary", buftype = "nofile", width = 50, height = 50 }, + { type = "leaf", filetype = "", buftype = "", width = 146, height = 50 }, + { type = "leaf", filetype = "neotest-summary", buftype = "nofile", width = 46, height = 50 }, }, }) @@ -208,8 +427,8 @@ T["right integration"]["opening an integration should close the existing integra type = "row", children = { { type = "leaf", filetype = "zen-left", buftype = "nofile", width = 46, height = 50 }, - { type = "leaf", filetype = "", buftype = "", width = 142, height = 50 }, - { type = "leaf", filetype = "neotest-summary", buftype = "nofile", width = 50, height = 50 }, + { type = "leaf", filetype = "", buftype = "", width = 146, height = 50 }, + { type = "leaf", filetype = "neotest-summary", buftype = "nofile", width = 46, height = 50 }, }, }) @@ -257,6 +476,67 @@ T["right integration"]["opening an integration with table filetype"] = function( }) end +T["combined"] = MiniTest.new_set({}) + +T["combined"]["opening a side integration preserves existing top and bottom integrations"] = function() + child.cmd("Git") + child.cmd("Trouble diagnostics") + child.cmd("Fyler kind=split_left_most") + + Helpers.expect.layout(child, { + type = "col", + children = { + { type = "leaf", filetype = "fugitive", buftype = "nowrite", width = 240, height = 25 }, + { + type = "row", + children = { + { type = "leaf", filetype = "fyler", buftype = "acwrite", width = 46, height = 13 }, + { type = "leaf", filetype = "", buftype = "", width = 146, height = 13 }, + { type = "leaf", filetype = "zen-right", buftype = "nofile", width = 46, height = 13 }, + }, + }, + { type = "leaf", filetype = "trouble", buftype = "nofile", width = 240, height = 10 }, + }, + }) + + child.cmd("close") + child.cmd("Fyler kind=split_left_most") + child.cmd("close") + + Helpers.expect.layout(child, { + type = "col", + children = { + { type = "leaf", filetype = "fugitive", buftype = "nowrite", width = 240, height = 25 }, + { + type = "row", + children = { + { type = "leaf", filetype = "zen-left", buftype = "nofile", width = 46, height = 13 }, + { type = "leaf", filetype = "", buftype = "", width = 146, height = 13 }, + { type = "leaf", filetype = "zen-right", buftype = "nofile", width = 46, height = 13 }, + }, + }, + { type = "leaf", filetype = "trouble", buftype = "nofile", width = 240, height = 10 }, + }, + }) + + child.cmd("Trouble close") + + Helpers.expect.layout(child, { + type = "col", + children = { + { type = "leaf", filetype = "fugitive", buftype = "nowrite", width = 240, height = 25 }, + { + type = "row", + children = { + { type = "leaf", filetype = "zen-left", buftype = "nofile", width = 46, height = 24 }, + { type = "leaf", filetype = "", buftype = "", width = 146, height = 24 }, + { type = "leaf", filetype = "zen-right", buftype = "nofile", width = 46, height = 24 }, + }, + }, + }, + }) +end + local min_width_child = MiniTest.new_child_neovim() T["min_width"] = MiniTest.new_set({ diff --git a/tests/test_resizing.lua b/tests/test_resizing.lua index eb7a30d..c3ceb6a 100644 --- a/tests/test_resizing.lua +++ b/tests/test_resizing.lua @@ -40,6 +40,51 @@ T["resizing above minimum width reopens the side buffers"] = function() }) end + +T["with hsplit"] = MiniTest.new_set({}) + +T["with hsplit"]["resizing below minimum width closes the side buffers"] = function() + child.cmd("edit test.lua") + child.cmd("split test2.lua") + + child.cmd("set columns-=100") + child.cmd("doautocmd VimResized") + + Helpers.expect.layout(child, { + type = "col", + children = { + { type = "leaf", filetype = "lua", buftype = "", width = 140, height = 25 }, + { type = "leaf", filetype = "lua", buftype = "", width = 140, height = 24 }, + }, + }) +end + +T["with hsplit"]["resizing above minimum width reopens the side buffers"] = function() + child.cmd("edit test.lua") + child.cmd("split test2.lua") + + child.cmd("set columns-=100") + child.cmd("doautocmd VimResized") + + child.cmd("set columns+=100") + child.cmd("doautocmd VimResized") + + Helpers.expect.layout(child, { + type = "row", + children = { + { type = "leaf", filetype = "zen-left", buftype = "nofile", width = 46, height = 50 }, + { + type = "col", + children = { + { type = "leaf", filetype = "lua", buftype = "", width = 146, height = 25 }, + { type = "leaf", filetype = "lua", buftype = "", width = 146, height = 24 }, + }, + }, + { type = "leaf", filetype = "zen-right", buftype = "nofile", width = 46, height = 50 }, + }, + }) +end + T["with top integration"] = MiniTest.new_set({}) T["with top integration"]["resizing below minimum width closes the side buffers"] = function() diff --git a/tests/test_splits.lua b/tests/test_splits.lua index 35e680f..af15bc2 100644 --- a/tests/test_splits.lua +++ b/tests/test_splits.lua @@ -13,34 +13,31 @@ local T = MiniTest.new_set({ T["vsplit"] = MiniTest.new_set({}) T["vsplit"]["opening closes zen side buffers"] = function() - child.cmd("edit test.lua") - child.cmd("vsplit test2.lua") + child.cmd("vsplit") Helpers.expect.layout(child, { type = "row", children = { - { type = "leaf", filetype = "lua", buftype = "", width = 120, height = 50 }, - { type = "leaf", filetype = "lua", buftype = "", width = 119, height = 50 }, + { type = "leaf", filetype = "", buftype = "", width = 120, height = 50 }, + { type = "leaf", filetype = "", buftype = "", width = 119, height = 50 }, }, }) end T["vsplit"]["closing reopens zen side buffers"] = function() - child.cmd("edit test.lua") - child.cmd("vsplit test2.lua") + child.cmd("vsplit") child.cmd("q") Helpers.expect.layout(child, { type = "row", children = { { type = "leaf", filetype = "zen-left", buftype = "nofile", width = 46, height = 50 }, - { type = "leaf", filetype = "lua", buftype = "", width = 146, height = 50 }, + { type = "leaf", filetype = "", buftype = "", width = 146, height = 50 }, { type = "leaf", filetype = "zen-right", buftype = "nofile", width = 46, height = 50 }, }, }) end - T["vsplit"]["closing does not reopen zen side buffers when below minimum width"] = function() child.cmd("edit test.lua") child.cmd("vsplit test2.lua") @@ -59,11 +56,112 @@ T["vsplit"]["closing does not reopen zen side buffers when below minimum width"] }) end - T["hsplit"] = MiniTest.new_set({}) T["hsplit"]["opening does not close the side buffers"] = function() - -- TODO + child.cmd("edit test.lua") + child.cmd("split test2.lua") + + Helpers.expect.layout(child, { + type = "row", + children = { + { type = "leaf", filetype = "zen-left", buftype = "nofile", width = 46, height = 50 }, + { + type = "col", + children = { + { type = "leaf", filetype = "lua", buftype = "", width = 146, height = 25 }, + { type = "leaf", filetype = "lua", buftype = "", width = 146, height = 24 }, + }, + }, + { type = "leaf", filetype = "zen-right", buftype = "nofile", width = 46, height = 50 }, + }, + }) +end + +T["hsplit"]["opening with a top integration"] = function() + child.cmd("Git") + + Helpers.expect.layout(child, { + type = "col", + children = { + { type = "leaf", filetype = "fugitive", buftype = "nowrite", width = 240, height = 25 }, + { + type = "row", + children = { + { type = "leaf", filetype = "zen-left", buftype = "nofile", width = 46, height = 24 }, + { type = "leaf", filetype = "", buftype = "", width = 146, height = 24 }, + { type = "leaf", filetype = "zen-right", buftype = "nofile", width = 46, height = 24 }, + }, + }, + }, + }) + + child.cmd("wincmd j | wincmd l") + child.cmd("split test.lua") + + Helpers.expect.layout(child, { + type = "col", + children = { + { type = "leaf", filetype = "fugitive", buftype = "nowrite", width = 240, height = 16 }, + { + type = "row", + children = { + { type = "leaf", filetype = "zen-left", buftype = "nofile", width = 46, height = 33 }, + { + type = "col", + children = { + { type = "leaf", filetype = "lua", buftype = "", width = 146, height = 16 }, + { type = "leaf", filetype = "", buftype = "", width = 146, height = 16 }, + }, + }, + { type = "leaf", filetype = "zen-right", buftype = "nofile", width = 46, height = 33 }, + }, + }, + }, + }) +end + +T["hsplit"]["closing with a top integration without layout shift"] = function() + child.cmd("split") + child.cmd("Git") + + Helpers.expect.layout(child, { + type = "col", + children = { + { type = "leaf", filetype = "fugitive", buftype = "nowrite", width = 240, height = 16 }, + { + type = "row", + children = { + { type = "leaf", filetype = "zen-left", buftype = "nofile", width = 46, height = 33 }, + { + type = "col", + children = { + { type = "leaf", filetype = "", buftype = "", width = 146, height = 16 }, + { type = "leaf", filetype = "", buftype = "", width = 146, height = 16 }, + }, + }, + { type = "leaf", filetype = "zen-right", buftype = "nofile", width = 46, height = 33 }, + }, + }, + }, + }) + + child.cmd("close") + + Helpers.expect.layout(child, { + type = "row", + children = { + { type = "leaf", filetype = "zen-left", buftype = "nofile", width = 46, height = 50 }, + { + type = "col", + children = { + { type = "leaf", filetype = "", buftype = "", width = 146, height = 25 }, + { type = "leaf", filetype = "", buftype = "", width = 146, height = 24 }, + }, + }, + { type = "leaf", filetype = "zen-right", buftype = "nofile", width = 46, height = 50 }, + }, + }) end return T diff --git a/tests/test_wincmd.lua b/tests/test_wincmd.lua new file mode 100644 index 0000000..2125915 --- /dev/null +++ b/tests/test_wincmd.lua @@ -0,0 +1,111 @@ +local Helpers = dofile("tests/scripts/helpers.lua") +local child = MiniTest.new_child_neovim() + +local T = MiniTest.new_set({ + hooks = { + pre_case = function() + child.restart({ "-u", "tests/scripts/init_with_zen.lua" }) + end, + post_once = child.stop, + }, +}) + +T["moving a window to the side does not go past zen buffers"] = function() + child.cmd("wincmd H") + + Helpers.expect.layout(child, { + type = "row", + children = { + { type = "leaf", filetype = "zen-left", buftype = "nofile", width = 46, height = 50 }, + { type = "leaf", filetype = "", buftype = "", width = 146, height = 50 }, + { type = "leaf", filetype = "zen-right", buftype = "nofile", width = 46, height = 50 }, + }, + }) + + child.cmd("wincmd L") + + Helpers.expect.layout(child, { + type = "row", + children = { + { type = "leaf", filetype = "zen-left", buftype = "nofile", width = 46, height = 50 }, + { type = "leaf", filetype = "", buftype = "", width = 146, height = 50 }, + { type = "leaf", filetype = "zen-right", buftype = "nofile", width = 46, height = 50 }, + }, + }) +end + +T["moving a window to the side does not go past integrations"] = function() + child.cmd("Fyler kind=split_left_most") + child.cmd("Neotest summary open") + + child.cmd("wincmd H") + + Helpers.expect.layout(child, { + type = "row", + children = { + { type = "leaf", filetype = "fyler", buftype = "acwrite", width = 46, height = 50 }, + { type = "leaf", filetype = "", buftype = "", width = 146, height = 50 }, + { type = "leaf", filetype = "neotest-summary", buftype = "nofile", width = 46, height = 50 }, + }, + }) + + child.cmd("wincmd L") + + Helpers.expect.layout(child, { + type = "row", + children = { + { type = "leaf", filetype = "fyler", buftype = "acwrite", width = 46, height = 50 }, + { type = "leaf", filetype = "", buftype = "", width = 146, height = 50 }, + { type = "leaf", filetype = "neotest-summary", buftype = "nofile", width = 46, height = 50 }, + }, + }) +end + +T["hsplit"] = MiniTest.new_set({}) + +T["hsplit"]["moving a window below keeps it between zen buffers"] = function() + child.cmd("edit test.lua") + child.cmd("split test.rs") + + child.cmd("wincmd J") + + Helpers.expect.layout(child, { + type = "row", + children = { + { type = "leaf", filetype = "zen-left", buftype = "nofile", width = 46, height = 50 }, + { + type = "col", + children = { + { type = "leaf", filetype = "lua", buftype = "", width = 146, height = 24 }, + { type = "leaf", filetype = "rust", buftype = "", width = 146, height = 25 }, + }, + }, + { type = "leaf", filetype = "zen-right", buftype = "nofile", width = 46, height = 50 }, + }, + }) +end + +T["hsplit"]["moving a window above keeps it between zen buffers"] = function() + child.cmd("edit test.lua") + child.cmd("split test.rs") + + child.cmd("wincmd j") + child.cmd("wincmd K") + + Helpers.expect.layout(child, { + type = "row", + children = { + { type = "leaf", filetype = "zen-left", buftype = "nofile", width = 46, height = 50 }, + { + type = "col", + children = { + { type = "leaf", filetype = "rust", buftype = "", width = 146, height = 24 }, + { type = "leaf", filetype = "lua", buftype = "", width = 146, height = 25 }, + }, + }, + { type = "leaf", filetype = "zen-right", buftype = "nofile", width = 46, height = 50 }, + }, + }) +end + +return T