From 697d0acf15281268b7042b3c042373c0ab33bfb4 Mon Sep 17 00:00:00 2001 From: Martin Larsson Date: Fri, 5 Jul 2024 22:03:24 +0200 Subject: [PATCH] Create simple window management in neovim, you can swap buffers in windows, resize windows and enter a resizing mode. Also add some utility functionality to support this and update the rest of the config to use these new utility functions --- home/.config/nvim/init.lua | 106 +-------- home/.config/nvim/lua/plugs/leap.lua | 18 +- home/.config/nvim/lua/utils.lua | 63 +++++- home/.config/nvim/lua/window_management.lua | 227 ++++++++++++++++++++ 4 files changed, 296 insertions(+), 118 deletions(-) create mode 100644 home/.config/nvim/lua/window_management.lua diff --git a/home/.config/nvim/init.lua b/home/.config/nvim/init.lua index f25cb55..7a6809f 100644 --- a/home/.config/nvim/init.lua +++ b/home/.config/nvim/init.lua @@ -12,7 +12,6 @@ local function setup_yank_highlight() }) end, group = yank_autocommand, - pattern = "*", }) end @@ -31,107 +30,8 @@ require("lazy").setup("plugs") -- Initialize the sticky terminal window at the bottom require("terminal").setup() +-- Initialize the custom window management functionality +require("window_management").setup() + -- See ":help vim.highlight.on_yank()" setup_yank_highlight() - --- Extract to plugin later -local function is_floating_window(window) - return vim.api.nvim_win_get_config(window).relative ~= "" -end - -local function is_window_resizable(window) - local config = vim.api.nvim_win_get_config(window) - return not config.winfixwidth and not config.winfixheight -end - -local function get_total_num_windows_open() - return #vim.api.nvim_tabpage_list_wins(0) -end - -local function get_adjacent_window(dir_char) - assert(dir_char == "h" or dir_char == "j" or dir_char == "k" or dir_char == "l", "Invalid direction character") - local current_window = vim.api.nvim_get_current_win() - assert(current_window, "Current window is nil") - - vim.cmd("wincmd " .. dir_char) - local new_window = vim.api.nvim_get_current_win() - vim.api.nvim_set_current_win(current_window) - - assert(vim.api.nvim_get_current_win() == current_window, "Cursor moved to a different window") - return new_window -end - -local function is_current_window_at_edge(dir_char) - return vim.api.nvim_get_current_win() == get_adjacent_window(dir_char) -end - -local function swap_buffer_between_windows(window_a, window_b) - assert(window_a and window_b, "Invalid window") - - local buffer_a = vim.api.nvim_win_get_buf(window_a) - local buffer_b = vim.api.nvim_win_get_buf(window_b) - vim.api.nvim_win_set_buf(window_a, buffer_b) - vim.api.nvim_win_set_buf(window_b, buffer_a) - - assert(vim.api.nvim_win_get_buf(window_a) == buffer_b and vim.api.nvim_win_get_buf(window_b) == buffer_a, - "Failed to swap buffers") -end - -local function swap_window(dir_char) - assert(dir_char == "h" or dir_char == "j" or dir_char == "k" or dir_char == "l", "Invalid direction character") - local current_window = vim.api.nvim_get_current_win() - assert(current_window, "Current window is nil") - - local required_num_windows = 2 - if get_total_num_windows_open() < required_num_windows then - return - end - - if is_floating_window(current_window) then - return - end - - if is_current_window_at_edge(dir_char) then - return - end - - if not is_window_resizable(current_window) then - return - end - - local adjacent_window = get_adjacent_window(dir_char) - if not is_window_resizable(adjacent_window) then - return - end - - swap_buffer_between_windows(current_window, adjacent_window) -end - -require("utils").add_keymaps({ - n = { - -- ["Left"] = { - ["h"] = { - cmd = function() - swap_window("h") - end - }, - -- ["Down"] = { - ["j"] = { - cmd = function() - swap_window("j") - end - }, - -- ["Up"] = { - ["k"] = { - cmd = function() - swap_window("k") - end - }, - -- ["Right"] = { - ["l"] = { - cmd = function() - swap_window("l") - end - }, - }, -}) diff --git a/home/.config/nvim/lua/plugs/leap.lua b/home/.config/nvim/lua/plugs/leap.lua index 88cc85f..4b68565 100644 --- a/home/.config/nvim/lua/plugs/leap.lua +++ b/home/.config/nvim/lua/plugs/leap.lua @@ -17,15 +17,6 @@ local function get_open_buffers_with_inlay_hints() return buffers end -local function add_leap_autocmd(pattern, callback) - vim.api.nvim_create_autocmd("User", { - pattern = pattern, - callback = function() - callback() - end, - }) -end - return { "ggandor/leap.nvim", dependencies = { @@ -37,7 +28,7 @@ return { local autocmds = { { - pattern = "LeapEnter", + event_name = "LeapEnter", cb = function() local open_buffers = get_open_buffers_with_inlay_hints() set_inlay_hints_active(open_buffers, false) @@ -45,15 +36,18 @@ return { end }, { - pattern = "LeapLeave", + event_name = "LeapLeave", cb = function() set_inlay_hints_active(buffers_without_inlay_hints, true) end }, } + local utils = require("utils") + local leap_augroup_name = "LeapEvents" + vim.api.nvim_create_augroup(leap_augroup_name, { clear = true }) for _, cmd in ipairs(autocmds) do - add_leap_autocmd(cmd.pattern, cmd.cb) + utils.create_user_event_cb(cmd.event_name, cmd.cb, leap_augroup_name) end require("utils").add_keymaps({ diff --git a/home/.config/nvim/lua/utils.lua b/home/.config/nvim/lua/utils.lua index 0860db5..8ece6c9 100644 --- a/home/.config/nvim/lua/utils.lua +++ b/home/.config/nvim/lua/utils.lua @@ -1,9 +1,66 @@ local M = {} +local function is_single_keymap_table(map_table) + assert(map_table) + return map_table.n or map_table.t or map_table.i or map_table.v +end + +function M.create_user_event_cb(event_name, function_callback, augroup) + assert(event_name and event_name ~= "", "Event name must be provided") + assert(function_callback and type(function_callback) == "function", "Callback must be a valid function") + + local cmd = { + callback = function_callback, + pattern = event_name, + } + + if augroup then + cmd.group = augroup + end + + vim.api.nvim_create_autocmd("User", cmd) +end + +function M.broadcast_event(event_name) + vim.api.nvim_command("doautocmd User " .. event_name) +end + function M.add_keymaps(maps) - for mode, entries in pairs(maps) do - for code, info in pairs(entries) do - vim.keymap.set(mode, code, info.cmd, info.opts) + assert(maps) + + local function set_keymaps(map_table) + for mode, entries in pairs(map_table) do + for code, info in pairs(entries) do + vim.keymap.set(mode, code, info.cmd, info.opts) + end + end + end + + if is_single_keymap_table(maps) then + set_keymaps(maps) + else + for _, map_table in pairs(maps) do + set_keymaps(map_table) + end + end +end + +function M.remove_keymaps(maps) + assert(maps) + + local function del_keymaps(map_table) + for mode, entries in pairs(map_table) do + for code, _ in pairs(entries) do + vim.keymap.del(mode, code) + end + end + end + + if is_single_keymap_table(maps) then + del_keymaps(maps) + else + for _, map_table in pairs(maps) do + del_keymaps(map_table) end end end diff --git a/home/.config/nvim/lua/window_management.lua b/home/.config/nvim/lua/window_management.lua new file mode 100644 index 0000000..85f79c5 --- /dev/null +++ b/home/.config/nvim/lua/window_management.lua @@ -0,0 +1,227 @@ +local utils = require("utils") + +local M = {} + +local is_in_resizing_mode = false +local on_resize_mode_enter_event = "ResizeModeEnter" +local on_resize_mode_exit_event = "ResizeModeExit" + +local function is_floating_window(window) + assert(window, "Invalid window") + + return vim.api.nvim_win_get_config(window).relative ~= "" +end + +local function is_window_resizable(window) + assert(window, "Invalid window") + + local config = vim.api.nvim_win_get_config(window) + return not config.winfixwidth and not config.winfixheight +end + +local function get_total_num_windows_open() + return #vim.api.nvim_tabpage_list_wins(0) +end + +local function get_adjacent_window(dir_char) + assert(dir_char == "h" or dir_char == "j" or dir_char == "k" or dir_char == "l", "Invalid direction character") + + local current_window = vim.api.nvim_get_current_win() + if not current_window then + return + end + + vim.cmd("wincmd " .. dir_char) + local new_window = vim.api.nvim_get_current_win() + vim.api.nvim_set_current_win(current_window) + + assert(vim.api.nvim_get_current_win() == current_window, "Cursor moved to a different window") + return new_window +end + +local function is_current_window_at_edge(dir_char) + return vim.api.nvim_get_current_win() == get_adjacent_window(dir_char) +end + +local function swap_buffer_between_windows(window_a, window_b) + assert(window_a and window_b, "Invalid window") + + local buffer_a = vim.api.nvim_win_get_buf(window_a) + local buffer_b = vim.api.nvim_win_get_buf(window_b) + vim.api.nvim_win_set_buf(window_a, buffer_b) + vim.api.nvim_win_set_buf(window_b, buffer_a) + + assert(vim.api.nvim_win_get_buf(window_a) == buffer_b and vim.api.nvim_win_get_buf(window_b) == buffer_a, + "Failed to swap buffers") +end + +local function swap_window(dir_char) + assert(dir_char == "h" or dir_char == "j" or dir_char == "k" or dir_char == "l", "Invalid direction character") + + local required_num_windows = 2 + if get_total_num_windows_open() < required_num_windows then + return + end + + local current_window = vim.api.nvim_get_current_win() + if not current_window then + return + end + + if is_floating_window(current_window) then + return + end + + if is_current_window_at_edge(dir_char) then + return + end + + if not is_window_resizable(current_window) then + return + end + + local adjacent_window = get_adjacent_window(dir_char) + if not is_window_resizable(adjacent_window) then + return + end + + swap_buffer_between_windows(current_window, adjacent_window) + vim.api.nvim_set_current_win(adjacent_window) + + assert(vim.api.nvim_get_current_win() == adjacent_window, "Failed to swap windows") +end + +local function resize_window(window, dir_char) + assert(is_in_resizing_mode, "Not in resizing mode") + assert(window, "Invalid window") + assert(dir_char == "h" or dir_char == "j" or dir_char == "k" or dir_char == "l", "Invalid direction character") + + local default_resize_units = 5 + local tot_resize_units = default_resize_units * vim.v.count1 + if dir_char == "h" then + vim.cmd("vertical resize -" .. tot_resize_units) + elseif dir_char == "j" then + vim.cmd("resize " .. tot_resize_units) + elseif dir_char == "k" then + vim.cmd("resize -" .. tot_resize_units) + elseif dir_char == "l" then + vim.cmd("vertical resize " .. tot_resize_units) + end +end + +local function exit_resizing_mode() + assert(is_in_resizing_mode, "Not in resizing mode") + + is_in_resizing_mode = false + utils.broadcast_event(on_resize_mode_exit_event) + + assert(not is_in_resizing_mode, "Failed to exit resizing mode") +end + +local function enter_resizing_mode() + assert(not is_in_resizing_mode, "Already in resizing mode") + + local current_window = vim.api.nvim_get_current_win() + if not current_window then + return + end + + if is_floating_window(current_window) then + return + end + + if not is_window_resizable(current_window) then + return + end + + is_in_resizing_mode = true + utils.broadcast_event(on_resize_mode_enter_event) + + assert(is_in_resizing_mode, "Failed to enter resizing mode") +end + +function M.autosize_windows() + vim.api.nvim_command("wincmd =") +end + +function M.is_in_resizing_mode() + return is_in_resizing_mode +end + +function M.setup() + local resizing_mode_keymaps = { + n = { + [""] = { + cmd = function() resize_window(vim.api.nvim_get_current_win(), "h") end + }, + [""] = { + cmd = function() resize_window(vim.api.nvim_get_current_win(), "j") end + }, + [""] = { + cmd = function() resize_window(vim.api.nvim_get_current_win(), "k") end + }, + [""] = { + cmd = function() resize_window(vim.api.nvim_get_current_win(), "l") end + }, + [""] = { + cmd = function() exit_resizing_mode() end + }, + [""] = { + cmd = function() exit_resizing_mode() end + }, + } + } + local enter_resizing_mode_keymaps = { + n = { + [""] = { + cmd = function() enter_resizing_mode() end + } + }, + } + local window_shifting_keymaps = { + n = { + [""] = { + cmd = function() + swap_window("h") + end + }, + [""] = { + cmd = function() + swap_window("j") + end + }, + [""] = { + cmd = function() + swap_window("k") + end + }, + [""] = { + cmd = function() + swap_window("l") + end + }, + }, + } + + utils.add_keymaps({ + window_shifting_keymaps, + enter_resizing_mode_keymaps + }) + + local function on_resize_mode_enter() + utils.remove_keymaps(enter_resizing_mode_keymaps) + utils.add_keymaps(resizing_mode_keymaps) + end + + local function on_resize_mode_exit() + utils.remove_keymaps(resizing_mode_keymaps) + utils.add_keymaps(enter_resizing_mode_keymaps) + end + + local window_management_augroup = "WindowManagementEvents" + vim.api.nvim_create_augroup(window_management_augroup, { clear = true }) + utils.create_user_event_cb(on_resize_mode_enter_event, on_resize_mode_enter, window_management_augroup) + utils.create_user_event_cb(on_resize_mode_exit_event, on_resize_mode_exit, window_management_augroup) +end + +return M