Skip to content

Commit

Permalink
Major improvements
Browse files Browse the repository at this point in the history
* Validate all arguments in the public api + tests
* Rename source_win_id to just win_id
* Cast to avoid disable-next-line
* Do not use cursor relative movement when not in move mode
  • Loading branch information
MisanthropicBit committed Jun 30, 2024
1 parent 0b4dec3 commit 3c40c88
Show file tree
Hide file tree
Showing 7 changed files with 209 additions and 78 deletions.
176 changes: 105 additions & 71 deletions lua/winmove/init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,12 @@ local float = require("winmove.float")
local highlight = require("winmove.highlight")
local layout = require("winmove.layout")
local message = require("winmove.message")
local _mode = require("winmove.mode")
local resize = require("winmove.resize")
local str = require("winmove.util.str")
local winutil = require("winmove.winutil")

winmove.Mode = require("winmove.mode")
winmove.Mode = _mode.Mode
winmove.ResizeAnchor = require("winmove.resize").anchor

local api = vim.api
Expand All @@ -34,6 +35,18 @@ local state = {
saved_keymaps = nil,
}

---@param value any
---@return boolean
local function is_nonnegative_number(value)
return type(value) == "number" and value >= 0
end

---@param value any
---@return boolean
local function is_valid_direction(value)
return value == "h" or value == "j" or value == "k" or value == "l"
end

--- Set current state
---@param mode winmove.Mode
---@param win_id integer
Expand Down Expand Up @@ -71,20 +84,20 @@ end
---@alias winmove.VerticalDirection "k" | "j"
---@alias winmove.Direction winmove.HorizontalDirection | winmove.VerticalDirection

---@param source_win_id integer
---@param win_id integer
---@param func function
---@param ... any
local function wincall(source_win_id, func, ...)
local function wincall(win_id, func, ...)
-- NOTE: Using vim.api.nvim_win_call seems to trigger 'textlock' or leaves
-- nvim in a weird state where the process exists with either code 134 or
-- 139 so we are instead using 'wincall_no_events'. This might also happen
-- because we would close the window inside the vim.api.nvim_win_call call
-- when moving the window to another tab
local cur_win_id = api.nvim_get_current_win()
local is_same_window_id = cur_win_id == source_win_id
local is_same_window_id = cur_win_id == win_id

if not is_same_window_id then
winutil.wincall_no_events(api.nvim_set_current_win, source_win_id)
winutil.wincall_no_events(api.nvim_set_current_win, win_id)
end

winutil.wincall_no_events(func, ...)
Expand All @@ -94,17 +107,17 @@ local function wincall(source_win_id, func, ...)
end
end

---@param source_win_id integer
---@param win_id integer
---@param target_win_id integer
---@param dir winmove.Direction
---@param vertical boolean
local function move_window_to_tab(source_win_id, target_win_id, dir, vertical)
local source_buffer = api.nvim_win_get_buf(source_win_id)
local function move_window_to_tab(win_id, target_win_id, dir, vertical)
local source_buffer = api.nvim_win_get_buf(win_id)

-- https://github.com/neovim/neovim/issues/18283
highlight.unhighlight_window(source_win_id)
highlight.unhighlight_window(win_id)

if not winutil.wincall_no_events(api.nvim_win_close, source_win_id, false) then
if not winutil.wincall_no_events(api.nvim_win_close, win_id, false) then
return
end

Expand All @@ -129,20 +142,20 @@ local function move_window_to_tab(source_win_id, target_win_id, dir, vertical)
end
end

---@param source_win_id integer
---@param win_id integer
---@param dir winmove.Direction
---@param behaviour winmove.AtEdge
---@param split_into boolean
---@return boolean
---@return integer?
---@return winmove.Direction
local function handle_edge(source_win_id, dir, behaviour, split_into)
local function handle_edge(win_id, dir, behaviour, split_into)
if behaviour == false then
return false, nil, dir
elseif behaviour == at_edge.Wrap then
local new_target_win_id = layout.get_wraparound_neighbor(dir)

if new_target_win_id == source_win_id then
if new_target_win_id == win_id then
-- If we get the same window it is full width/height because we
-- wrap around to the same window
return false, nil, dir
Expand All @@ -159,7 +172,7 @@ local function handle_edge(source_win_id, dir, behaviour, split_into)
end

---@cast dir winmove.HorizontalDirection
local target_win_id, reldir = layout.get_target_window_in_tab(source_win_id, dir)
local target_win_id, reldir = layout.get_target_window_in_tab(win_id, dir)
local final_dir = reldir ---@type winmove.Direction
local vertical = false

Expand All @@ -168,7 +181,7 @@ local function handle_edge(source_win_id, dir, behaviour, split_into)
vertical = true
end

move_window_to_tab(source_win_id, target_win_id, final_dir, vertical)
move_window_to_tab(win_id, target_win_id, final_dir, vertical)

return false, target_win_id, dir
else
Expand Down Expand Up @@ -203,10 +216,10 @@ local function can_move(win_id, mode)
end

--- Move a window in a given direction
---@param source_win_id integer
---@param win_id integer
---@param dir winmove.Direction
local function move_window(source_win_id, dir)
if not can_move(source_win_id, winmove.Mode.Move) then
local function move_window(win_id, dir)
if not can_move(win_id, winmove.Mode.Move) then
return
end

Expand All @@ -216,8 +229,7 @@ local function move_window(source_win_id, dir)
if target_win_id == nil then
local edge_type = winutil.is_horizontal(dir) and "horizontal" or "vertical"
local behaviour = config.at_edge[edge_type]
local proceed, new_target_win_id, new_dir =
handle_edge(source_win_id, dir, behaviour, false)
local proceed, new_target_win_id, new_dir = handle_edge(win_id, dir, behaviour, false)

if not proceed then
return
Expand All @@ -227,31 +239,37 @@ local function move_window(source_win_id, dir)
dir = new_dir
end

---@diagnostic disable-next-line:param-type-mismatch
if not layout.are_siblings(source_win_id, target_win_id) then
---@diagnostic disable-next-line:param-type-mismatch
dir = layout.get_sibling_relative_dir(source_win_id, target_win_id, dir)
---@cast target_win_id -nil

if not layout.are_siblings(win_id, target_win_id) then
dir = layout.get_sibling_relative_dir(win_id, target_win_id, dir, winmove.current_mode())
end

winutil.wincall_no_events(
vim.fn.win_splitmove,
source_win_id,
win_id,
target_win_id,
{ vertical = winutil.is_horizontal(dir), rightbelow = dir == "j" or dir == "l" }
)
end

--- Move a window in a given direction
---@param source_win_id integer
---@param win_id integer
---@param dir winmove.Direction
function winmove.move_window(source_win_id, dir)
wincall(source_win_id, move_window, source_win_id, dir)
function winmove.move_window(win_id, dir)
vim.validate({
win_id = { win_id, is_nonnegative_number, "a non-negative number" },
dir = { dir, is_valid_direction, "a valid direction" },
})

wincall(win_id, move_window, win_id, dir)
end

--
--- Split a window into another window in a given direction
---@param source_win_id integer
---@param win_id integer
---@param dir winmove.Direction
local function split_into(source_win_id, dir)
local function split_into(win_id, dir)
if winutil.window_count() == 1 then
return
end
Expand All @@ -262,7 +280,7 @@ local function split_into(source_win_id, dir)
if target_win_id == nil then
local edge_type = winutil.is_horizontal(dir) and "horizontal" or "vertical"
local behaviour = config.at_edge[edge_type]
local proceed, new_target_win_id, new_dir = handle_edge(source_win_id, dir, behaviour, true)
local proceed, new_target_win_id, new_dir = handle_edge(win_id, dir, behaviour, true)

if not proceed then
return
Expand All @@ -278,32 +296,45 @@ local function split_into(source_win_id, dir)
}

---@diagnostic disable-next-line:param-type-mismatch
if layout.are_siblings(source_win_id, target_win_id) then
if layout.are_siblings(win_id, target_win_id) then
---@diagnostic disable-next-line:param-type-mismatch
local reldir = layout.get_sibling_relative_dir(source_win_id, target_win_id, dir)
local reldir = layout.get_sibling_relative_dir(win_id, target_win_id, dir)

split_options.vertical = not split_options.vertical
split_options.rightbelow = reldir == "l" or reldir == "j"
end

winutil.wincall_no_events(vim.fn.win_splitmove, source_win_id, target_win_id, split_options)
winutil.wincall_no_events(vim.fn.win_splitmove, win_id, target_win_id, split_options)
end

--- Split a window into another window in a given direction
---@param source_win_id integer
---@param win_id integer
---@param dir winmove.Direction
function winmove.split_into(source_win_id, dir)
wincall(source_win_id, split_into, source_win_id, dir)
function winmove.split_into(win_id, dir)
vim.validate({
win_id = { win_id, is_nonnegative_number, "a non-negative number" },
dir = { dir, is_valid_direction, "a valid direction" },
})

wincall(win_id, split_into, win_id, dir)
end

---@param dir winmove.Direction
local function move_window_far(dir)
vim.cmd("wincmd " .. dir:upper())
end

---@diagnostic disable-next-line:unused-local
--- Move a window as far as possible in a direction
---@param source_win_id integer
---@param win_id integer
---@param dir winmove.Direction
function winmove.move_window_far(source_win_id, dir)
wincall(source_win_id, function()
vim.cmd("wincmd " .. dir:upper())
end)
function winmove.move_window_far(win_id, dir)
vim.validate({
win_id = { win_id, is_nonnegative_number, "a non-negative number" },
dir = { dir, is_valid_direction, "a valid direction" },
})

wincall(win_id, move_window_far, dir)
end

local function toggle_mode()
Expand All @@ -321,30 +352,32 @@ local function move_mode_key_handler(keys)
---@type integer
local win_id = state.win_id

-- Make sure to call the local functions to avoid ignoring window and buffer
-- events in move mode
if keys == keymaps.left then
winmove.move_window(win_id, "h")
move_window(win_id, "h")
elseif keys == keymaps.down then
winmove.move_window(win_id, "j")
move_window(win_id, "j")
elseif keys == keymaps.up then
winmove.move_window(win_id, "k")
move_window(win_id, "k")
elseif keys == keymaps.right then
winmove.move_window(win_id, "l")
move_window(win_id, "l")
elseif keys == keymaps.split_left then
winmove.split_into(win_id, "h")
split_into(win_id, "h")
elseif keys == keymaps.split_down then
winmove.split_into(win_id, "j")
split_into(win_id, "j")
elseif keys == keymaps.split_up then
winmove.split_into(win_id, "k")
split_into(win_id, "k")
elseif keys == keymaps.split_right then
winmove.split_into(win_id, "l")
split_into(win_id, "l")
elseif keys == keymaps.far_left then
winmove.move_window_far(win_id, "h")
move_window_far("h")
elseif keys == keymaps.far_down then
winmove.move_window_far(win_id, "j")
move_window_far("j")
elseif keys == keymaps.far_up then
winmove.move_window_far(win_id, "k")
move_window_far("k")
elseif keys == keymaps.far_right then
winmove.move_window_far(win_id, "l")
move_window_far("l")
elseif keys == keymaps.resize_mode then
toggle_mode()
end
Expand Down Expand Up @@ -520,7 +553,9 @@ local function restore_keymaps(mode)
end
end

local function create_mode_autocmds(mode, source_win_id)
---@param mode winmove.Mode
---@param win_id integer
local function create_mode_autocmds(mode, win_id)
-- Clear any existing autocommands
for _, autocmd in ipairs(autocmds) do
pcall(api.nvim_del_autocmd, autocmd)
Expand All @@ -532,11 +567,11 @@ local function create_mode_autocmds(mode, source_win_id)
autocmds,
api.nvim_create_autocmd("WinEnter", {
callback = function()
local win_id = api.nvim_get_current_win()
local cur_win_id = api.nvim_get_current_win()

-- Do not stop the current mode if we are entering the window we are
-- moving/resizing or if we are entering the help window
if win_id ~= source_win_id and not float.is_help_window(win_id) then
if cur_win_id ~= win_id and not float.is_help_window(cur_win_id) then
stop_mode(mode)
return true
end
Expand All @@ -553,12 +588,12 @@ local function create_mode_autocmds(mode, source_win_id)
autocmds,
api.nvim_create_autocmd("WinNew", {
callback = function()
local win_id = api.nvim_get_current_win()
local cur_win_id = api.nvim_get_current_win()

-- Clear the winhighlight option if the winmove highlighting
-- has leaked into the new window
if highlight.has_winmove_highlight(win_id, mode) then
highlight.unhighlight_window(win_id)
if highlight.has_winmove_highlight(cur_win_id, mode) then
highlight.unhighlight_window(cur_win_id)
end

return true
Expand Down Expand Up @@ -589,10 +624,8 @@ start_mode = function(mode)
return
end

local titlecase_mode = str.titlecase(mode)

if winmove.current_mode() == mode then
message.error(titlecase_mode .. " mode already activated")
message.error(str.titlecase(mode) .. " mode already activated")
return
end

Expand Down Expand Up @@ -643,17 +676,11 @@ end
local sorted_modes = vim.tbl_values(winmove.Mode)
table.sort(sorted_modes)

local check_mode_error_message = ("valid mode (%s)"):format(table.concat(sorted_modes, ", "))

---@param mode any
---@return boolean
local function is_valid_mode(mode)
return mode == winmove.Mode.Move or mode == winmove.Mode.Resize
end
local check_mode_error_message = ("a valid mode (%s)"):format(table.concat(sorted_modes, ", "))

---@param mode winmove.Mode
function winmove.start_mode(mode)
vim.validate({ mode = { mode, is_valid_mode, check_mode_error_message } })
vim.validate({ mode = { mode, _mode.is_valid_mode, check_mode_error_message } })

start_mode(mode)
end
Expand All @@ -667,6 +694,13 @@ end
---@param count integer
---@param anchor winmove.ResizeAnchor?
function winmove.resize_window(win_id, dir, count, anchor)
vim.validate({
win_id = { win_id, is_nonnegative_number, "a non-negative number" },
dir = { dir, is_valid_direction, "a valid direction" },
count = { count, is_nonnegative_number, "a non-negative number" },
anchor = { anchor, resize.is_valid_anchor, "a valid anchor" },
})

winutil.win_id_context_call(win_id, function()
resize.resize_window(win_id, dir, count, anchor)
end)
Expand Down
Loading

0 comments on commit 3c40c88

Please sign in to comment.