diff --git a/README.md b/README.md
index 8a9b3bd..27c51f0 100644
--- a/README.md
+++ b/README.md
@@ -1,7 +1,7 @@
winmove.nvim
-
Easily move windows around
+
Easily move and swap windows
@@ -65,27 +65,46 @@ active during modes.
```lua
require('winmove').configure({
- highlights = {
- move = "Search", -- Highlight group for move mode
- },
- wrap_around = true, -- Wrap around edges when moving windows
keymaps = {
help = "?", -- Open floating window with help for the current mode
help_close = "q", -- Close the floating help window
quit = "q", -- Quit current mode
+ toggle_mode = "", -- Toggle between modes when in a mode
+ },
+ modes = {
move = {
- left = "h", -- Move window left
- down = "j", -- Move window down
- up = "k", -- Move window up
- right = "l", -- Move window right
- far_left = "H", -- Move window far left and maximize it
- far_down = "J", -- Move window down and maximize it
- far_up = "K", -- Move window up and maximize it
- far_right = "L", -- Move window right and maximize it
- split_left = "sh", -- Create a split with the window on the left
- split_down = "sj", -- Create a split with the window below
- split_up = "sk", -- Create a split with the window above
- split_right = "sl", -- Create a split with the window on the right
+ highlight = "Visual", -- Highlight group for move mode
+ at_edge = {
+ horizontal = at_edge.AtEdge.None, -- Behaviour at horizontal edges
+ vertical = at_edge.AtEdge.None, -- Behaviour at vertical edges
+ },
+ keymaps = {
+ left = "h", -- Move window left
+ down = "j", -- Move window down
+ up = "k", -- Move window up
+ right = "l", -- Move window right
+ far_left = "H", -- Move window far left and maximize it
+ far_down = "J", -- Move window down and maximize it
+ far_up = "K", -- Move window up and maximize it
+ far_right = "L", -- Move window right and maximize it
+ split_left = "sh", -- Create a split with the window on the left
+ split_down = "sj", -- Create a split with the window below
+ split_up = "sk", -- Create a split with the window above
+ split_right = "sl", -- Create a split with the window on the right
+ },
+ },
+ swap = {
+ highlight = "Substitute", -- Highlight group for swap mode
+ at_edge = {
+ horizontal = at_edge.AtEdge.None, -- Behaviour at horizontal edges
+ vertical = at_edge.AtEdge.None, -- Behaviour at vertical edges
+ },
+ keymaps = {
+ left = "h", -- Swap left
+ down = "j", -- Swap down
+ up = "k", -- Swap up
+ right = "l", -- Swap right
+ },
},
},
})
@@ -125,7 +144,7 @@ Get the current version of `winmove`.
#### `winmove.current_mode`
-Check which mode is currently active. Returns `"move"` or `nil`.
+Check which mode is currently active. Returns `"move"`, `"swap"`, or `nil`.
#### `winmove.start_mode`
@@ -137,6 +156,7 @@ winmove.start_mode(mode)
-- Example:
winmove.start_mode(winmove.Mode.Move)
+winmove.start_mode("swap")
winmove.start_mode("move")
```
@@ -184,6 +204,36 @@ winmove.move_window_far(win_id, dir)
winmove.move_window_far(1000, "h")
```
+#### `winmove.swap_window_in_direction`
+
+Swap a window in a given direction (does not need to be the current window).
+
+```lua
+---@param win_id integer
+---@param dir winmove.Direction
+winmove.swap_window_in_direction(win_id, dir)
+
+-- Example:
+winmove.swap_window_in_direction(1000, "j")
+winmove.swap_window_in_direction(1000, "l")
+```
+
+#### `winmove.swap_window`
+
+Swap a window (does not need to be the current window). When called the first
+time, highlights the selected window for swapping. When called the second time
+with another window will swap the two selected windows.
+
+```lua
+---@param win_id integer
+---@param dir winmove.Direction
+winmove.swap_window(win_id, dir)
+
+-- Example:
+winmove.swap_window(1000)
+winmove.swap_window(1000)
+```
+
## Contributing
See [here](/CONTRIBUTING.md).
diff --git a/lua/winmove/at_edge.lua b/lua/winmove/at_edge.lua
index 558c245..300a274 100644
--- a/lua/winmove/at_edge.lua
+++ b/lua/winmove/at_edge.lua
@@ -1,5 +1,22 @@
+local at_edge = {}
+
---@enum winmove.AtEdge
-return {
+at_edge.AtEdge = {
+ None = "none",
Wrap = "wrap",
MoveToTab = "move_to_tab",
}
+
+---@param value any
+---@return boolean
+function at_edge.is_valid_behaviour(value)
+ if type(value) ~= "string" then
+ return false
+ end
+
+ return value == at_edge.AtEdge.None
+ or value == at_edge.AtEdge.Wrap
+ or value == at_edge.AtEdge.MoveToTab
+end
+
+return at_edge
diff --git a/lua/winmove/config.lua b/lua/winmove/config.lua
index e77a306..cd212f8 100644
--- a/lua/winmove/config.lua
+++ b/lua/winmove/config.lua
@@ -5,85 +5,132 @@ local message = require("winmove.message")
local config_loaded = false
+---@class winmove.ConfigCommonKeymaps
+---@field help string
+---@field help_close string
+---@field quit string
+---@field toggle_mode string
+
---@class winmove.ConfigMoveModeKeymaps
----@field left string
----@field down string
----@field up string
----@field right string
----@field far_left string
----@field far_down string
----@field far_up string
----@field far_right string
+---@field left string
+---@field down string
+---@field up string
+---@field right string
+---@field far_left string
+---@field far_down string
+---@field far_up string
+---@field far_right string
---@field split_left string
---@field split_down string
---@field split_up string
---@field split_right string
----@class winmove.ConfigModeKeymaps
----@field help string
----@field help_close string
----@field quit string
----@field move winmove.ConfigMoveModeKeymaps
+---@class winmove.ConfigMoveMode
+---@field highlight winmove.Highlight
+---@field at_edge winmove.AtEdgeConfig
+---@field keymaps winmove.ConfigMoveModeKeymaps
+
+---@class winmove.ConfigSwapModeKeymaps
+---@field left string
+---@field down string
+---@field up string
+---@field right string
----@class winmove.Highlights
----@field move string?
+---@class winmove.ConfigSwapMode
+---@field highlight winmove.Highlight
+---@field at_edge winmove.AtEdgeConfig
+---@field keymaps winmove.ConfigSwapModeKeymaps
+
+---@class winmove.ConfigModes
+---@field move winmove.ConfigMoveMode
+---@field swap winmove.ConfigSwapMode
---@class winmove.AtEdgeConfig
----@field horizontal false | winmove.AtEdge
----@field vertical false | winmove.AtEdge
+---@field horizontal winmove.AtEdge
+---@field vertical winmove.AtEdge
---@class winmove.Config
----@field highlights winmove.Highlights
----@field at_edge winmove.AtEdgeConfig
----@field keymaps winmove.ConfigModeKeymaps
+---@field keymaps winmove.ConfigCommonKeymaps
+---@field modes winmove.ConfigModes
---@type winmove.Config
local default_config = {
- highlights = {
- move = "Visual",
- },
- at_edge = {
- horizontal = at_edge.MoveToTab,
- vertical = at_edge.Wrap,
- },
keymaps = {
help = "?",
help_close = "q",
quit = "q",
+ toggle_mode = "",
+ },
+ modes = {
move = {
- left = "h",
- down = "j",
- up = "k",
- right = "l",
- far_left = "H",
- far_down = "J",
- far_up = "K",
- far_right = "L",
- split_left = "sh",
- split_down = "sj",
- split_up = "sk",
- split_right = "sl",
+ highlight = "Visual",
+ at_edge = {
+ horizontal = at_edge.AtEdge.None,
+ vertical = at_edge.AtEdge.None,
+ },
+ keymaps = {
+ left = "h",
+ down = "j",
+ up = "k",
+ right = "l",
+ far_left = "H",
+ far_down = "J",
+ far_up = "K",
+ far_right = "L",
+ split_left = "sh",
+ split_down = "sj",
+ split_up = "sk",
+ split_right = "sl",
+ },
+ },
+ swap = {
+ highlight = "Substitute",
+ at_edge = {
+ horizontal = at_edge.AtEdge.None,
+ vertical = at_edge.AtEdge.None,
+ },
+ keymaps = {
+ left = "h",
+ down = "j",
+ up = "k",
+ right = "l",
+ },
},
},
}
local mapping_descriptions = {
- help = "Show help",
- help_close = "Close help",
- quit = "Quit current mode",
- move = {
- left = "Move a window left",
- down = "Move a window down",
- up = "Move a window up",
- right = "Move a window right",
- far_left = "Move a window far left and maximize it",
- far_down = "Move a window far down and maximize it",
- far_up = "Move a window far up and maximize it",
- far_right = "Move a window far right and maximize it",
- split_left = "Split a window left into another window",
- split_down = "Split a window down into another window",
- split_up = "Split a window up into another window",
- split_right = "Split a window right into another window",
+ keymaps = {
+ help = "Show help",
+ help_close = "Close help",
+ quit = "Quit current mode",
+ toggle_mode = "Toggle between modes",
+ },
+ modes = {
+ move = {
+ keymaps = {
+ left = "Move a window left",
+ down = "Move a window down",
+ up = "Move a window up",
+ right = "Move a window right",
+ far_left = "Move a window far left and maximize it",
+ far_down = "Move a window far down and maximize it",
+ far_up = "Move a window far up and maximize it",
+ far_right = "Move a window far right and maximize it",
+ split_left = "Split a window left into another window",
+ split_down = "Split a window down into another window",
+ split_up = "Split a window up into another window",
+ split_right = "Split a window right into another window",
+ },
+ },
+ swap = {
+ keymaps = {
+ left = "Swap window left",
+ down = "Swap window down",
+ up = "Swap window up",
+ right = "Swap window right",
+ },
+ },
},
}
@@ -94,9 +141,9 @@ local mapping_descriptions = {
function config.get_keymap_description(name, mode)
if mode == nil then
---@diagnostic disable-next-line:return-type-mismatch
- return mapping_descriptions[name]
+ return mapping_descriptions.keymaps[name]
else
- return mapping_descriptions[mode][name]
+ return mapping_descriptions.modes[mode].keymaps[name]
end
end
@@ -111,93 +158,114 @@ local function is_non_empty_string(value)
return type(value) == "string" and #value > 0
end
---- Validate keys in a table
----@param specs table
----@return fun(tbl: table): boolean, any?
-local function validate_keys(specs)
- return function(tbl)
- if not tbl then
- return true
- end
+---@param object table
+---@param schema table
+---@return table
+local function validate_schema(object, schema)
+ local errors = {}
- for _, spec in ipairs(specs) do
- local key = spec[1]
- local expected = spec[2]
+ for key, value in pairs(schema) do
+ if type(value) == "string" then
+ local ok, err = pcall(vim.validate, { [key] = { object[key], value } })
- local validated, error = pcall(vim.validate, {
- [key] = { tbl[key], expected, spec[3] },
- })
+ if not ok then
+ table.insert(errors, err)
+ end
+ elseif type(value) == "table" then
+ if type(object) ~= "table" then
+ table.insert(errors, "Expected a table at key " .. key)
+ else
+ if vim.is_callable(value[1]) then
+ local ok, err = pcall(vim.validate, {
+ [key] = { object[key], value[1], value[2] },
+ })
- if not validated then
- return validated, error
+ if not ok then
+ table.insert(errors, err)
+ end
+ else
+ vim.list_extend(errors, validate_schema(object[key], value))
+ end
end
end
-
- return true
end
+
+ return errors
end
+local expected_non_empty_string = "Expected a non-empty string"
+
+local horizontal_validator = {
+ at_edge.is_valid_behaviour,
+ "valid behaviour at horizontal edge",
+}
+
+local vertical_validator = {
+ function(value)
+ return value == at_edge.AtEdge.None or value == at_edge.AtEdge.Wrap
+ end,
+ "valid behaviour at vertical edge",
+}
+
+local non_empty_string_validator = { is_non_empty_string, expected_non_empty_string }
+
--- Validate a config
---@param _config winmove.Config
---@return boolean
---@return any?
function config.validate(_config)
- local expected_non_empty_string = "Expected a non-empty string"
+ -- TODO: Validate superfluous keys
-- stylua: ignore start
- return pcall(vim.validate, {
- highlights = {
- _config.highlights,
- validate_keys({
- { "move", "string" },
- }),
+ local config_schema = {
+ keymaps = {
+ help = "string",
+ help_close = "string",
+ quit = "string",
+ toggle_mode = "string",
},
- at_edge = {
- _config.at_edge,
- validate_keys({
- {
- "horizontal",
- function(value)
- return value == false or value == at_edge.Wrap or value == at_edge.MoveToTab
- end,
- "valid behaviour at horizontal edge",
+ modes = {
+ move = {
+ highlight = "string",
+ at_edge = {
+ horizontal = horizontal_validator,
+ vertical = vertical_validator,
},
- {
- "vertical",
- function(value)
- return value == false or value == at_edge.Wrap
- end,
- "valid behaviour at vertical edge",
+ keymaps = {
+ left = non_empty_string_validator,
+ down = non_empty_string_validator,
+ up = non_empty_string_validator,
+ right = non_empty_string_validator,
+ far_left = non_empty_string_validator,
+ far_down = non_empty_string_validator,
+ far_up = non_empty_string_validator,
+ far_right = non_empty_string_validator,
+ split_left = non_empty_string_validator,
+ split_down = non_empty_string_validator,
+ split_up = non_empty_string_validator,
+ split_right = non_empty_string_validator,
},
- }),
- },
- keymaps = {
- _config.keymaps,
- validate_keys({
- { "help", "string" },
- { "help_close", "string" },
- { "quit", "string" },
- }),
- },
- ["keymaps.move"] = {
- _config.keymaps.move,
- validate_keys({
- { "left", is_non_empty_string, expected_non_empty_string },
- { "down", is_non_empty_string, expected_non_empty_string },
- { "up", is_non_empty_string, expected_non_empty_string },
- { "right", is_non_empty_string, expected_non_empty_string },
- { "far_left", is_non_empty_string, expected_non_empty_string },
- { "far_down", is_non_empty_string, expected_non_empty_string },
- { "far_up", is_non_empty_string, expected_non_empty_string },
- { "far_right", is_non_empty_string, expected_non_empty_string },
- { "split_left", is_non_empty_string, expected_non_empty_string },
- { "split_down", is_non_empty_string, expected_non_empty_string },
- { "split_up", is_non_empty_string, expected_non_empty_string },
- { "split_right", is_non_empty_string, expected_non_empty_string },
- }),
+ },
+ swap = {
+ highlight = "string",
+ at_edge = {
+ horizontal = horizontal_validator,
+ vertical = vertical_validator,
+ },
+ keymaps = {
+ left = non_empty_string_validator,
+ down = non_empty_string_validator,
+ up = non_empty_string_validator,
+ right = non_empty_string_validator,
+ },
+ },
},
- })
+ }
-- stylua: ignore end
+
+ local errors = validate_schema(_config, config_schema)
+
+ return #errors == 0, errors
end
---@type winmove.Config
@@ -216,7 +284,7 @@ function config.configure(user_config)
local ok, error = config.validate(_user_config)
if not ok then
- message.error("Errors found in config: " .. error)
+ message.error("Errors found in config: " .. table.concat(error, "\n"))
else
config_loaded = true
end
diff --git a/lua/winmove/float.lua b/lua/winmove/float.lua
index 705386a..af88df7 100644
--- a/lua/winmove/float.lua
+++ b/lua/winmove/float.lua
@@ -108,7 +108,7 @@ end
---@param mode winmove.Mode
function float.open(mode)
local lines = {}
- local keymaps = config.keymaps[mode]
+ local keymaps = config.modes[mode].keymaps
local max_widths = {}
local spacing = (" "):rep(4)
diff --git a/lua/winmove/highlight.lua b/lua/winmove/highlight.lua
index 0837cab..5f387b9 100644
--- a/lua/winmove/highlight.lua
+++ b/lua/winmove/highlight.lua
@@ -3,6 +3,8 @@
local highlight = {}
+---@alias winmove.Highlight string
+
local config = require("winmove.config")
local str = require("winmove.util.str")
@@ -11,6 +13,7 @@ local api = vim.api
-- Window higlights per mode
local win_highlights = {
move = nil,
+ swap = nil,
}
---@type string?
@@ -40,7 +43,7 @@ local highlight_groups = {
---@param groups string[]
local function generate_highlights(mode, groups)
local highlights = {}
- local color = config.highlights[mode]
+ local color = config.modes[mode].highlight
for _, group in ipairs(groups) do
local titlecase_mode = str.titlecase(mode)
@@ -85,7 +88,7 @@ end
---@param win_id integer
---@param mode winmove.Mode
-function highlight.has_winmove_highlight(win_id, mode)
+function highlight.has_highlight(win_id, mode)
if not api.nvim_win_is_valid(win_id) then
return
end
diff --git a/lua/winmove/init.lua b/lua/winmove/init.lua
index eaf6105..b8fc4ae 100644
--- a/lua/winmove/init.lua
+++ b/lua/winmove/init.lua
@@ -8,75 +8,21 @@ local highlight = require("winmove.highlight")
local layout = require("winmove.layout")
local message = require("winmove.message")
local _mode = require("winmove.mode")
+local State = require("winmove.state")
local str = require("winmove.util.str")
+local swap = require("winmove.swap")
+local validators = require("winmove.validators")
local winutil = require("winmove.winutil")
winmove.Mode = _mode.Mode
+winmove.AtEdge = at_edge.AtEdge
local api = vim.api
local winmove_version = "0.1.0"
local augroup = api.nvim_create_augroup("Winmove", { clear = true })
local autocmds = {}
-
----@class winmove.State
----@field mode winmove.Mode?
----@field win_id integer?
----@field bufnr integer?
----@field saved_keymaps table?
-
----@type winmove.State
-local state = {
- mode = nil,
- win_id = nil,
- bufnr = nil,
- 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
-local function win_id_validator(value)
- return { value, is_nonnegative_number, "a non-negative number" }
-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
-
----@param value any
-local function dir_validator(value)
- return { value, is_valid_direction, "a valid direction" }
-end
-
---- Set current state
----@param mode winmove.Mode
----@param win_id integer
----@param bufnr integer
----@param saved_keymaps table
-local function set_state(mode, win_id, bufnr, saved_keymaps)
- state.mode = mode
- state.win_id = win_id
- state.bufnr = bufnr
- state.saved_keymaps = saved_keymaps
-end
-
-local function update_state(changes)
- state = vim.tbl_extend("force", state, changes)
-end
-
-local function reset_state()
- state.mode = nil
- state.win_id = nil
- state.bufnr = nil
- state.saved_keymaps = nil
-end
+local state = State.new()
---@type fun(mode: winmove.Mode)
local start_mode
@@ -92,29 +38,6 @@ end
---@alias winmove.VerticalDirection "k" | "j"
---@alias winmove.Direction winmove.HorizontalDirection | winmove.VerticalDirection
----@param win_id integer
----@param func function
----@param ... any
-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 == win_id
-
- if not is_same_window_id then
- winutil.wincall_no_events(api.nvim_set_current_win, win_id)
- end
-
- winutil.wincall_no_events(func, ...)
-
- if not is_same_window_id then
- winutil.wincall_no_events(api.nvim_set_current_win, cur_win_id)
- end
-end
-
---@param win_id integer
---@param target_win_id integer
---@param dir winmove.Direction
@@ -142,11 +65,11 @@ local function move_window_to_tab(win_id, target_win_id, dir, vertical)
local new_win_id = api.nvim_get_current_win()
local mode = winmove.Mode.Move
- highlight.highlight_window(new_win_id, mode)
-
if winmove.current_mode() == winmove.Mode.Move then
+ highlight.highlight_window(new_win_id, mode)
+
-- Update state with the new window
- update_state({ win_id = new_win_id })
+ state:update({ win_id = new_win_id })
end
end
@@ -157,15 +80,15 @@ end
---@return boolean
---@return integer?
---@return winmove.Direction
-local function handle_edge(win_id, dir, behaviour, split_into)
- if behaviour == false then
+local function handle_edge(win_id, dir, mode, behaviour, split_into)
+ if behaviour == winmove.AtEdge.None then
return false, nil, dir
- elseif behaviour == at_edge.Wrap then
- local new_target_win_id = layout.get_wraparound_neighbor(dir)
+ elseif behaviour == winmove.AtEdge.Wrap then
+ local new_target_win_id = layout.get_wraparound_neighbor(win_id, dir)
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
+ -- wrapped around to the same window
return false, nil, dir
end
@@ -173,12 +96,18 @@ local function handle_edge(win_id, dir, behaviour, split_into)
dir = winutil.reverse_direction(dir)
return true, target_win_id, dir
- elseif behaviour == at_edge.MoveToTab then
+ elseif behaviour == winmove.AtEdge.MoveToTab then
if winutil.window_count() == 1 and vim.fn.tabpagenr("$") == 1 then
-- Only one window and one tab, do not proceed
return false, nil, dir
end
+ if not winutil.is_horizontal(dir) then
+ -- Do not try to move to a tab vertically even if the user selected
+ -- that behaviour by mistake
+ return false, nil, dir
+ end
+
---@cast dir winmove.HorizontalDirection
local target_win_id, reldir = layout.get_target_window_in_tab(win_id, dir)
local final_dir = reldir ---@type winmove.Direction
@@ -189,7 +118,12 @@ local function handle_edge(win_id, dir, behaviour, split_into)
vertical = true
end
- move_window_to_tab(win_id, target_win_id, final_dir, vertical)
+ if mode == winmove.Mode.Move then
+ -- TODO: Refactor so we do not need to call this here
+ move_window_to_tab(win_id, target_win_id, final_dir, vertical)
+ elseif mode == winmove.Mode.Swap then
+ return true, target_win_id, dir
+ end
return false, target_win_id, dir
else
@@ -200,17 +134,19 @@ end
---@param win_id integer
---@param mode winmove.Mode
---@return boolean
-local function can_move(win_id, mode)
- local at_edge_horizontal = config.at_edge.horizontal
+local function can_move_or_swap(win_id, mode)
+ local at_edge_horizontal = config.modes.move.at_edge.horizontal
- if at_edge_horizontal == at_edge.MoveToTab then
+ if at_edge_horizontal == winmove.AtEdge.MoveToTab then
if winutil.window_count() == 1 and vim.fn.tabpagenr("$") == 1 then
- message.error("Only one window and tab")
+ ---@cast mode string
+ message.error(("Cannot %s window, only one window and tab"):format(mode:lower()))
return false
end
- elseif at_edge_horizontal == at_edge.Wrap then
+ elseif at_edge_horizontal == winmove.AtEdge.Wrap then
if winutil.window_count() == 1 then
- message.error("Only one window")
+ ---@cast mode string
+ message.error(("Cannot %s window, only one window"):format(mode:lower()))
return false
end
end
@@ -223,30 +159,45 @@ local function can_move(win_id, mode)
return true
end
---- Move a window in a given direction
---@param win_id integer
---@param dir winmove.Direction
-local function move_window(win_id, dir)
- if not can_move(win_id, winmove.Mode.Move) then
- return
- end
-
- local target_win_id = layout.get_neighbor(dir)
+---@param mode winmove.Mode
+---@param split_into boolean
+---@return boolean
+---@return integer?
+---@return winmove.Direction
+local function find_target_win_id(win_id, dir, mode, split_into)
+ local target_win_id = layout.get_neighbor(win_id, dir)
-- No neighbor, handle configured behaviour at edges
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(win_id, dir, behaviour, false)
+ local behaviour = config.modes[mode].at_edge[edge_type]
+ local proceed, new_target_win_id, new_dir =
+ handle_edge(win_id, dir, mode, behaviour, split_into)
- if not proceed then
- return
- end
+ return proceed, new_target_win_id, new_dir
+ end
+
+ return true, target_win_id, dir
+end
- target_win_id = new_target_win_id
- dir = new_dir
+--- Move a window in a given direction
+---@param win_id integer
+---@param dir winmove.Direction
+local function move_window(win_id, dir)
+ if not can_move_or_swap(win_id, winmove.Mode.Move) then
+ return
+ end
+
+ local proceed, target_win_id, new_dir =
+ find_target_win_id(win_id, dir, winmove.Mode.Move, false)
+
+ if not proceed then
+ return
end
+ dir = new_dir
---@cast target_win_id -nil
if not layout.are_siblings(win_id, target_win_id) then
@@ -266,14 +217,13 @@ end
---@param dir winmove.Direction
function winmove.move_window(win_id, dir)
vim.validate({
- win_id = win_id_validator(win_id),
- dir = dir_validator(dir),
+ win_id = validators.win_id_validator(win_id),
+ dir = validators.dir_validator(dir),
})
- wincall(win_id, move_window, win_id, dir)
+ winutil.wincall(win_id, move_window, win_id, dir)
end
---
--- Split a window into another window in a given direction
---@param win_id integer
---@param dir winmove.Direction
@@ -282,30 +232,21 @@ local function split_into(win_id, dir)
return
end
- local target_win_id = layout.get_neighbor(dir)
-
- -- No neighbor, handle configured behaviour at edges
- 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(win_id, dir, behaviour, true)
-
- if not proceed then
- return
- end
+ local proceed, target_win_id, new_dir = find_target_win_id(win_id, dir, winmove.Mode.Move, true)
- target_win_id = new_target_win_id
- dir = new_dir
+ if not proceed then
+ return
end
+ dir = new_dir
+ ---@cast target_win_id -nil
+
local split_options = {
vertical = winutil.is_horizontal(dir),
rightbelow = dir == "h" or dir == "k",
}
- ---@diagnostic disable-next-line:param-type-mismatch
if layout.are_siblings(win_id, target_win_id) then
- ---@diagnostic disable-next-line:param-type-mismatch
local reldir =
layout.get_sibling_relative_dir(win_id, target_win_id, dir, winmove.current_mode())
@@ -321,16 +262,11 @@ end
---@param dir winmove.Direction
function winmove.split_into(win_id, dir)
vim.validate({
- win_id = win_id_validator(win_id),
- dir = dir_validator(dir),
+ win_id = validators.win_id_validator(win_id),
+ dir = validators.dir_validator(dir),
})
- wincall(win_id, split_into, win_id, dir)
-end
-
----@param dir winmove.Direction
-local function move_window_far(dir)
- vim.cmd("wincmd " .. dir:upper())
+ winutil.wincall(win_id, split_into, win_id, dir)
end
---@diagnostic disable-next-line:unused-local
@@ -339,19 +275,81 @@ end
---@param dir winmove.Direction
function winmove.move_window_far(win_id, dir)
vim.validate({
- win_id = win_id_validator(win_id),
- dir = dir_validator(dir),
+ win_id = validators.win_id_validator(win_id),
+ dir = validators.dir_validator(dir),
+ })
+
+ winutil.wincall(win_id, function()
+ vim.cmd("wincmd " .. dir:upper())
+ end, dir)
+end
+
+---@param win_id integer
+---@param dir winmove.Direction
+local function swap_window_in_direction(win_id, dir)
+ local mode = winmove.Mode.Swap
+
+ if not can_move_or_swap(win_id, mode) then
+ return
+ end
+
+ local proceed, target_win_id, _ = find_target_win_id(win_id, dir, mode, false)
+
+ if not proceed then
+ return
+ end
+
+ ---@cast target_win_id -nil
+ swap.swap_window_in_direction(win_id, target_win_id)
+
+ if winmove.current_mode() == winmove.Mode.Swap then
+ -- Seems the winhighlight bug can also leak into other windows when
+ -- switching: https://github.com/neovim/neovim/issues/18283
+ highlight.unhighlight_window(target_win_id)
+ highlight.unhighlight_window(win_id)
+ highlight.highlight_window(target_win_id, mode)
+
+ -- Update state with the new window
+ state:update({ win_id = target_win_id })
+ end
+end
+
+---@param win_id integer
+---@param dir winmove.Direction
+function winmove.swap_window_in_direction(win_id, dir)
+ vim.validate({
+ win_id = validators.win_id_validator(win_id),
+ dir = validators.dir_validator(dir),
})
- wincall(win_id, move_window_far, dir)
+ winutil.wincall_no_events(function()
+ swap_window_in_direction(win_id, dir)
+ end)
+end
+
+---@param win_id integer
+function winmove.swap_window(win_id)
+ vim.validate({ win_id = validators.win_id_validator(win_id) })
+
+ winutil.wincall_no_events(function()
+ swap.swap_window(win_id)
+ end)
+end
+
+local function toggle_mode()
+ local mode = winmove.current_mode()
+ local new_mode = mode == winmove.Mode.Move and winmove.Mode.Swap or winmove.Mode.Move
+
+ stop_mode(mode)
+ start_mode(new_mode)
end
---@param keys string
local function move_mode_key_handler(keys)
- local keymaps = config.keymaps.move
+ local keymaps = config.modes.move.keymaps
---@type integer
- local win_id = state.win_id
+ local win_id = state:get("win_id")
if keys == keymaps.left then
move_window(win_id, "h")
@@ -370,13 +368,30 @@ local function move_mode_key_handler(keys)
elseif keys == keymaps.split_right then
split_into(win_id, "l")
elseif keys == keymaps.far_left then
- move_window_far("h")
+ winmove.move_window_far(win_id, "h")
elseif keys == keymaps.far_down then
- move_window_far("j")
+ winmove.move_window_far(win_id, "j")
elseif keys == keymaps.far_up then
- move_window_far("k")
+ winmove.move_window_far(win_id, "k")
elseif keys == keymaps.far_right then
- move_window_far("l")
+ winmove.move_window_far(win_id, "l")
+ end
+end
+
+---@param keys string
+local function swap_mode_key_handler(keys)
+ ---@type integer
+ local win_id = state:get("win_id")
+ local keymaps = config.modes.swap.keymaps
+
+ if keys == keymaps.left then
+ winmove.swap_window_in_direction(win_id, "h")
+ elseif keys == keymaps.down then
+ winmove.swap_window_in_direction(win_id, "j")
+ elseif keys == keymaps.up then
+ winmove.swap_window_in_direction(win_id, "k")
+ elseif keys == keymaps.right then
+ winmove.swap_window_in_direction(win_id, "l")
end
end
@@ -450,10 +465,15 @@ local function safe_call_autorestore_mode(func, ...)
end
end
+local mode_key_handlers = {
+ [winmove.Mode.Move] = move_mode_key_handler,
+ [winmove.Mode.Swap] = swap_mode_key_handler,
+}
+
---@param mode winmove.Mode
---@return fun(keys: string)
local function create_mode_key_handler(mode)
- local handler = move_mode_key_handler
+ local handler = mode_key_handlers[mode]
return function(keys)
local ok, err = pcall(handler, keys)
@@ -474,7 +494,7 @@ local function set_keymaps(win_id, bufnr, mode)
local saved_buf_keymaps = {}
local handler = create_mode_key_handler(mode)
- for name, map in pairs(config.keymaps[mode]) do
+ for name, map in pairs(config.modes[mode].keymaps) do
local description = config.get_keymap_description(name, mode)
set_mode_keymap(win_id, bufnr, map, handler, description)
@@ -505,29 +525,40 @@ local function set_keymaps(win_id, bufnr, mode)
config.get_keymap_description("quit")
)
+ set_mode_keymap(
+ win_id,
+ bufnr,
+ config.keymaps.toggle_mode,
+ safe_call_autorestore_mode(toggle_mode),
+ config.get_keymap_description("toggle_mode")
+ )
+
return saved_buf_keymaps
end
--- Delete mode keymaps and restore previous buffer keymaps
---@param mode winmove.Mode
local function restore_keymaps(mode)
- if not api.nvim_buf_is_valid(state.bufnr) then
+ local bufnr = state:get("bufnr")
+
+ if not api.nvim_buf_is_valid(bufnr) then
return
end
-- Remove winmove keymaps in protected calls since the buffer might have
-- been deleted but the buffer can still be marked as valid
- for _, map in pairs(config.keymaps[mode]) do
- pcall(api.nvim_buf_del_keymap, state.bufnr, "n", map)
+ for _, map in pairs(config.modes[mode].keymaps) do
+ pcall(api.nvim_buf_del_keymap, bufnr, "n", map)
end
- pcall(api.nvim_buf_del_keymap, state.bufnr, "n", config.keymaps.help)
- pcall(api.nvim_buf_del_keymap, state.bufnr, "n", config.keymaps.quit)
+ for _, map in pairs(config.keymaps) do
+ pcall(api.nvim_buf_del_keymap, bufnr, "n", map)
+ end
-- Restore old keymaps
- for _, keymap in ipairs(state.saved_keymaps) do
+ for _, keymap in ipairs(state:get("saved_keymaps")) do
vim.keymap.set("n", keymap.lhs, keymap.rhs, {
- buffer = state.bufnr,
+ buffer = bufnr,
expr = keymap.expr,
callback = keymap.callback,
noremap = keymap.noremap,
@@ -548,23 +579,27 @@ local function create_mode_autocmds(mode, win_id)
autocmds = {}
- table.insert(
- autocmds,
- api.nvim_create_autocmd("WinEnter", {
- callback = function()
- local cur_win_id = api.nvim_get_current_win()
-
- -- Do not stop the current mode if we are entering the window
- -- we are moving or if we are entering the help window
- if cur_win_id ~= win_id and not float.is_help_window(cur_win_id) then
- stop_mode(mode)
- return true
- end
- end,
- group = augroup,
- desc = "Quits " .. mode .. " when leaving the window",
- })
- )
+ -- TODO: Do these actually trigger when we ignore them?
+
+ if mode ~= winmove.Mode.Swap then
+ table.insert(
+ autocmds,
+ api.nvim_create_autocmd("WinEnter", {
+ callback = function()
+ local cur_win_id = api.nvim_get_current_win()
+
+ -- Do not stop the current mode if we are entering the window
+ -- we are moving or if we are entering the help window
+ if cur_win_id ~= win_id and not float.is_help_window(cur_win_id) then
+ stop_mode(mode)
+ return true
+ end
+ end,
+ group = augroup,
+ desc = "Quits " .. mode .. " mode when leaving the window",
+ })
+ )
+ end
-- If we enter a new window, unhighlight the window since there is a bug
-- where the winhighlight option can leak into other windows:
@@ -577,7 +612,7 @@ local function create_mode_autocmds(mode, win_id)
-- Clear the winhighlight option if the winmove highlighting
-- has leaked into the new window
- if highlight.has_winmove_highlight(cur_win_id, mode) then
+ if highlight.has_highlight(cur_win_id, mode) then
highlight.unhighlight_window(cur_win_id)
end
@@ -596,7 +631,7 @@ local function create_mode_autocmds(mode, win_id)
return true
end,
group = augroup,
- desc = "Quits " .. mode .. " when entering insert mode",
+ desc = "Quits " .. mode .. " mode when entering insert mode",
})
)
end
@@ -605,7 +640,7 @@ end
start_mode = function(mode)
local cur_win_id = api.nvim_get_current_win()
- if not can_move(cur_win_id, mode) then
+ if not can_move_or_swap(cur_win_id, mode) then
return
end
@@ -618,7 +653,12 @@ start_mode = function(mode)
local saved_buf_keymaps = set_keymaps(cur_win_id, bufnr, mode)
highlight.highlight_window(cur_win_id, mode)
- set_state(mode, cur_win_id, bufnr, saved_buf_keymaps)
+ state:update({
+ mode = mode,
+ win_id = cur_win_id,
+ bufnr = bufnr,
+ saved_keymaps = saved_buf_keymaps,
+ })
api.nvim_exec_autocmds("User", {
pattern = "WinmoveModeStart",
@@ -641,7 +681,7 @@ stop_mode = function(mode)
return
end
- local unhighlight_ok = pcall(highlight.unhighlight_window, state.win_id)
+ local unhighlight_ok = pcall(highlight.unhighlight_window, state:get("win_id"))
if not unhighlight_ok then
message.error("Failed to unhighlight window when stopping mode")
@@ -653,7 +693,7 @@ stop_mode = function(mode)
message.error("Failed to restore keymaps when stopping mode")
end
- reset_state()
+ state:reset()
for _, autocmd in ipairs(autocmds) do
pcall(api.nvim_del_autocmd, autocmd)
@@ -685,7 +725,7 @@ function winmove.stop_mode()
end
function winmove.current_mode()
- return state.mode
+ return state:get("mode")
end
function winmove.configure(user_config)
diff --git a/lua/winmove/layout.lua b/lua/winmove/layout.lua
index fb5fe50..e257c84 100644
--- a/lua/winmove/layout.lua
+++ b/lua/winmove/layout.lua
@@ -38,41 +38,55 @@ local function window_bounding_box(win_id)
end
--- Returns the possible window handle of a neighbor
+---@param win_id integer
---@param dir winmove.Direction
---@return integer?
-function layout.get_neighbor(dir)
- local neighbor = vim.fn.winnr(dir)
- local cur_win_nr = vim.fn.winnr()
+function layout.get_neighbor(win_id, dir)
+ local result = nil
+
+ winutil.wincall(win_id, function()
+ local neighbor = vim.fn.winnr(dir)
+ local cur_win_nr = vim.fn.winnr()
- return cur_win_nr ~= neighbor and vim.fn.win_getid(neighbor) or nil
+ result = cur_win_nr ~= neighbor and vim.fn.win_getid(neighbor) or nil
+ end)
+
+ return result
end
--- Get the neighbor on the opposite side of the screen if the current window
--- was to wrap around
+---@param win_id integer
---@param dir winmove.Direction
---@return integer?
-function layout.get_wraparound_neighbor(dir)
+function layout.get_wraparound_neighbor(win_id, dir)
if winutil.window_count() == 1 then
return nil
end
- local count = 1
- local opposite_dir = winutil.reverse_direction(dir)
- local prev_win_nr = vim.fn.winnr()
- local neighbor = nil
+ local result = nil
- while count <= winutil.window_count() do
- neighbor = vim.fn.winnr(("%d%s"):format(count, opposite_dir))
+ winutil.wincall(win_id, function()
+ local count = 1
+ local opposite_dir = winutil.reverse_direction(dir)
+ local prev_win_nr = vim.fn.winnr()
+ local neighbor = nil
- if neighbor == prev_win_nr then
- break
+ while count <= winutil.window_count() do
+ neighbor = vim.fn.winnr(("%d%s"):format(count, opposite_dir))
+
+ if neighbor == prev_win_nr then
+ break
+ end
+
+ count = count + 1
+ prev_win_nr = neighbor
end
- count = count + 1
- prev_win_nr = neighbor
- end
+ result = vim.fn.win_getid(neighbor)
+ end)
- return vim.fn.win_getid(neighbor)
+ return result
end
--- Determine if two windows are siblings in the same row or column
diff --git a/lua/winmove/mode.lua b/lua/winmove/mode.lua
index 94dee07..ee00613 100644
--- a/lua/winmove/mode.lua
+++ b/lua/winmove/mode.lua
@@ -3,12 +3,17 @@ local mode = {}
---@enum winmove.Mode
mode.Mode = {
Move = "move",
+ Swap = "swap",
}
---@param value any
---@return boolean
function mode.is_valid_mode(value)
- return value == mode.Mode.Move
+ if type(value) ~= "string" then
+ return false
+ end
+
+ return value == mode.Mode.Move or value == mode.Mode.Swap
end
return mode
diff --git a/lua/winmove/state.lua b/lua/winmove/state.lua
new file mode 100644
index 0000000..b1c5ab9
--- /dev/null
+++ b/lua/winmove/state.lua
@@ -0,0 +1,51 @@
+---@class winmove.State
+---@field mode winmove.Mode?
+---@field win_id integer?
+---@field bufnr integer?
+---@field saved_keymaps table?
+local State = {}
+
+State.__index = State
+
+---@return winmove.State
+function State.new()
+ return setmetatable({
+ mode = nil,
+ win_id = nil,
+ bufnr = nil,
+ saved_keymaps = nil,
+ }, State)
+end
+
+---@param key string
+---@return unknown
+function State:get(key)
+ return self[key]
+end
+
+---@param changes winmove.State
+function State:update(changes)
+ self.mode = changes.mode or self.mode
+ self.win_id = changes.win_id or self.win_id
+ self.bufnr = changes.bufnr or self.bufnr
+ self.saved_keymaps = changes.saved_keymaps or self.saved_keymaps
+end
+
+function State:reset()
+ self.mode = nil
+ self.win_id = nil
+ self.bufnr = nil
+ self.saved_keymaps = nil
+end
+
+---@return string
+function State:__tostring()
+ return ("State(%s)"):format(vim.inspect({
+ mode = self.mode,
+ win_id = self.win_id,
+ bufnr = self.bufnr,
+ saved_keymaps = self.saved_keymaps,
+ }))
+end
+
+return State
diff --git a/lua/winmove/swap.lua b/lua/winmove/swap.lua
new file mode 100644
index 0000000..6daebe7
--- /dev/null
+++ b/lua/winmove/swap.lua
@@ -0,0 +1,67 @@
+local swap = {}
+
+local highlight = require("winmove.highlight")
+local message = require("winmove.message")
+local mode = require("winmove.mode")
+
+---@type integer?
+local selected_window
+
+---@param win_id1 integer
+---@param win_id2 integer
+local function swap_windows(win_id1, win_id2)
+ local buf1 = vim.api.nvim_win_get_buf(win_id1)
+ local buf2 = vim.api.nvim_win_get_buf(win_id2)
+
+ -- Save views before switching buffers
+ vim.api.nvim_set_current_win(win_id1)
+ local view1 = vim.fn.winsaveview()
+
+ vim.api.nvim_set_current_win(win_id2)
+ local view2 = vim.fn.winsaveview()
+
+ -- Set buffers and restore views
+ vim.api.nvim_win_set_buf(win_id2, buf1)
+ vim.fn.winrestview(view1)
+
+ vim.api.nvim_set_current_win(win_id1)
+ vim.api.nvim_win_set_buf(win_id1, buf2)
+ vim.fn.winrestview(view2)
+end
+
+--- Selects a window for swapping. If no window has been selected already, selects
+--- it, otherwise swaps the window with the previously selected window
+---@param win_id integer
+function swap.swap_window(win_id)
+ -- Normalize window id
+ win_id = win_id == 0 and vim.api.nvim_get_current_win() or win_id
+
+ if not selected_window then
+ selected_window = win_id
+ highlight.highlight_window(win_id, mode.Mode.Swap)
+ return
+ end
+
+ if not vim.api.nvim_win_is_valid(selected_window) then
+ selected_window = nil
+ message.error("Previously selected window is not valid anymore")
+ return
+ elseif win_id == selected_window then
+ selected_window = nil
+ message.error("Cannot swap selected window with itself")
+ return
+ end
+
+ highlight.unhighlight_window(selected_window)
+ swap_windows(win_id, selected_window)
+ selected_window = nil
+end
+
+---@param win_id integer
+---@param target_win_id integer
+function swap.swap_window_in_direction(win_id, target_win_id)
+ swap_windows(win_id, target_win_id)
+ vim.api.nvim_set_current_win(target_win_id)
+end
+
+return swap
diff --git a/lua/winmove/validators.lua b/lua/winmove/validators.lua
new file mode 100644
index 0000000..796f920
--- /dev/null
+++ b/lua/winmove/validators.lua
@@ -0,0 +1,25 @@
+local validators = {}
+
+---@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
+
+---@param value any
+function validators.win_id_validator(value)
+ return { value, is_nonnegative_number, "a non-negative number" }
+end
+
+---@param value any
+function validators.dir_validator(value)
+ return { value, is_valid_direction, "a valid direction" }
+end
+
+return validators
diff --git a/lua/winmove/winutil.lua b/lua/winmove/winutil.lua
index b06a46b..3926109 100644
--- a/lua/winmove/winutil.lua
+++ b/lua/winmove/winutil.lua
@@ -3,6 +3,26 @@ local winutil = {}
local compat = require("winmove.compat")
local message = require("winmove.message")
+local events = {
+ "WinEnter",
+ "WinLeave",
+ "WinNew",
+ "WinScrolled",
+ "WinClosed",
+ "BufWinEnter",
+ "BufWinLeave",
+ "BufEnter",
+ "BufLeave",
+}
+
+if compat.has("nvim-0.8.2") then
+ table.insert(events, "WinResized")
+end
+
+function winutil.get_ignored_events()
+ return events
+end
+
---@return integer
function winutil.window_count()
return vim.fn.winnr("$")
@@ -23,23 +43,7 @@ end
function winutil.wincall_no_events(func, ...)
local saved_eventignore = vim.opt_global.eventignore:get()
- local events = {
- "WinEnter",
- "WinLeave",
- "WinNew",
- "WinScrolled",
- "WinClosed",
- "BufWinEnter",
- "BufWinLeave",
- "BufEnter",
- "BufLeave",
- }
-
- if compat.has("nvim-0.8.2") then
- table.insert(events, "WinResized")
- end
-
- vim.opt_global.eventignore = events
+ vim.opt_global.eventignore = winutil.get_ignored_events()
-- Do a protected call so that we restore 'eventignore' in case it fails
local ok, error = pcall(func, ...)
@@ -53,20 +57,27 @@ function winutil.wincall_no_events(func, ...)
return ok
end
---- Call a function in the context of a window without triggering any window/buffer events
---@param win_id integer
---@param func function
---@param ... any
----@return boolean
-function winutil.win_id_context_call(win_id, func, ...)
- local args = { ... }
- local ok
+function winutil.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 = vim.api.nvim_get_current_win()
+ local is_same_window_id = cur_win_id == win_id
+
+ if not is_same_window_id then
+ winutil.wincall_no_events(vim.api.nvim_set_current_win, win_id)
+ end
- vim.api.nvim_win_call(win_id, function()
- ok = winutil.wincall_no_events(func, unpack(args))
- end)
+ winutil.wincall_no_events(func, ...)
- return ok
+ if not is_same_window_id then
+ winutil.wincall_no_events(vim.api.nvim_set_current_win, cur_win_id)
+ end
end
---@param dir winmove.Direction
diff --git a/tests/basic_movement_spec.lua b/tests/basic_movement_spec.lua
index 04001b2..8bf0103 100644
--- a/tests/basic_movement_spec.lua
+++ b/tests/basic_movement_spec.lua
@@ -11,9 +11,13 @@ local make_layout = test_helpers.make_layout
describe("basic movements", function()
config.configure({
- at_edge = {
- horizontal = at_edge.Wrap,
- vertical = at_edge.Wrap,
+ modes = {
+ move = {
+ at_edge = {
+ horizontal = at_edge.AtEdge.Wrap,
+ vertical = at_edge.AtEdge.Wrap,
+ },
+ },
},
})
@@ -138,7 +142,7 @@ describe("basic movements", function()
assert
.stub(vim.notify).was
- .called_with("[winmove.nvim]: Only one window", vim.log.levels.ERROR)
+ .called_with("[winmove.nvim]: Cannot move window, only one window", vim.log.levels.ERROR)
end)
---@diagnostic disable-next-line: undefined-field
diff --git a/tests/config_spec.lua b/tests/config_spec.lua
index b76650f..2e5b069 100644
--- a/tests/config_spec.lua
+++ b/tests/config_spec.lua
@@ -7,22 +7,44 @@ describe("config", function()
it("handles invalid configs", function()
local invalid_configs = {
{
- highlights = {
- move = true,
+ modes = {
+ move = {
+ highlight = true,
+ },
+ },
+ },
+ {
+ modes = {
+ swap = {
+ at_edge = 2,
+ },
},
},
{
- at_edge = 2,
+ modes = {
+ move = {
+ at_edge = {
+ vertical = at_edge.AtEdge.MoveToTab,
+ },
+ },
+ },
},
{
- at_edge = {
- horizontal = at_edge.MoveToTab,
- vertical = at_edge.MoveToTab,
+ modes = {
+ swap = {
+ at_edge = {
+ vertical = at_edge.AtEdge.MoveToTab,
+ },
+ },
},
},
{
- at_edge = {
- vertical = true,
+ modes = {
+ move = {
+ at_edge = {
+ vertical = true,
+ },
+ },
},
},
{
@@ -31,16 +53,20 @@ describe("config", function()
},
},
{
- keymaps = {
- move = {
- left = 12.5,
+ modes = {
+ swap = {
+ keymaps = {
+ left = 12.5,
+ },
},
},
},
{
- keymaps = {
- move = {
- left = "",
+ modes = {
+ swap = {
+ keymaps = {
+ left = "",
+ },
},
},
},
@@ -64,30 +90,46 @@ describe("config", function()
it("throws no errors for a valid config", function()
local ok = config.configure({
- highlights = {
- move = "Title",
- },
- at_edge = {
- horizontal = at_edge.Wrap,
- vertical = false,
- },
keymaps = {
help = "_",
help_close = "z",
quit = "i",
+ toggle_mode = "",
+ },
+ modes = {
move = {
- left = "",
- down = "",
- up = "",
- right = "",
- far_left = "U",
- far_down = "I",
- far_up = "O",
- far_right = "P",
- split_left = "ef",
- split_down = "nv",
- split_up = "qp",
- split_right = "vn",
+ highlight = "Title",
+ at_edge = {
+ horizontal = at_edge.AtEdge.Wrap,
+ vertical = at_edge.AtEdge.None,
+ },
+ keymaps = {
+ left = "",
+ down = "",
+ up = "",
+ right = "",
+ far_left = "U",
+ far_down = "I",
+ far_up = "O",
+ far_right = "P",
+ split_left = "ef",
+ split_down = "nv",
+ split_up = "qp",
+ split_right = "vn",
+ },
+ },
+ swap = {
+ highlight = "Question",
+ at_edge = {
+ horizontal = at_edge.AtEdge.MoveToTab,
+ vertical = at_edge.AtEdge.None,
+ },
+ keymaps = {
+ left = "",
+ down = "",
+ up = "",
+ right = "",
+ },
},
},
})
diff --git a/tests/custom_highlights_spec.lua b/tests/custom_highlights_spec.lua
index 5dafb2d..4fbde34 100644
--- a/tests/custom_highlights_spec.lua
+++ b/tests/custom_highlights_spec.lua
@@ -42,9 +42,12 @@ describe("custom highlights", function()
it("uses a custom highlight for move mode", function()
vim.cmd(("hi link %s %s"):format("CustomWinmoveMoveMode", "Title"))
+ ---@diagnostic disable-next-line: missing-fields
config.configure({
- highlights = {
- move = "CustomWinmoveMoveMode",
+ modes = {
+ move = {
+ highlight = "CustomWinmoveMoveMode",
+ },
},
})
@@ -77,4 +80,46 @@ describe("custom highlights", function()
winmove.stop_mode()
end)
end)
+
+ it("uses a custom highlight for swap mode", function()
+ vim.cmd(("hi link %s %s"):format("CustomWinmoveSwapMode", "Repeat"))
+
+ ---@diagnostic disable-next-line: missing-fields
+ config.configure({
+ modes = {
+ swap = {
+ highlight = "CustomWinmoveSwapMode",
+ },
+ },
+ })
+
+ given(function()
+ make_layout({
+ "row",
+ { "leaf", "leaf" },
+ })
+
+ assert.matches_winlayout(vim.fn.winlayout(), {
+ "row",
+ {
+ { "leaf" },
+ { "leaf" },
+ },
+ })
+
+ winmove.start_mode(winmove.Mode.Swap)
+
+ local win_id = vim.api.nvim_get_current_win()
+
+ assert.are.same(vim.wo[win_id].winhighlight, get_expected_winhighlight("WinmoveSwap"))
+
+ for _, group in ipairs(highlight.groups()) do
+ local linked_group = vim.api.nvim_get_hl(0, { name = "WinmoveSwap" .. group }).link
+
+ assert.are.same(linked_group, "CustomWinmoveSwapMode")
+ end
+
+ winmove.stop_mode()
+ end)
+ end)
end)
diff --git a/tests/init_spec.lua b/tests/init_spec.lua
index e4c95d2..f80967c 100644
--- a/tests/init_spec.lua
+++ b/tests/init_spec.lua
@@ -18,7 +18,7 @@ describe("init", function()
assert.has_error(function()
---@diagnostic disable-next-line: param-type-mismatch
winmove.start_mode("hello")
- end, "mode: expected a valid mode (move), got hello")
+ end, "mode: expected a valid mode (move, swap), got hello")
end)
it("fails to stop mode if no mode is currently active", function()
@@ -67,4 +67,23 @@ describe("init", function()
winmove.move_window_far(1000, 1)
end, "dir: expected a valid direction, got 1")
end)
+
+ it("validates arguments of swap_window_in_direction", function()
+ assert.has_error(function()
+ ---@diagnostic disable-next-line: param-type-mismatch
+ winmove.swap_window_in_direction(true, "j")
+ end, "win_id: expected a non-negative number, got true")
+
+ assert.has_error(function()
+ ---@diagnostic disable-next-line: param-type-mismatch
+ winmove.swap_window_in_direction(1000, 1)
+ end, "dir: expected a valid direction, got 1")
+ end)
+
+ it("validates arguments of swap_window", function()
+ assert.has_error(function()
+ ---@diagnostic disable-next-line: param-type-mismatch
+ winmove.swap_window(true)
+ end, "win_id: expected a non-negative number, got true")
+ end)
end)
diff --git a/tests/mode_mappings_spec.lua b/tests/mode_mappings_spec.lua
index d380c4b..1bcb30b 100644
--- a/tests/mode_mappings_spec.lua
+++ b/tests/mode_mappings_spec.lua
@@ -27,7 +27,7 @@ describe("mode mappings", function()
local keymaps = test_helpers.get_buf_mapped_keymaps(vim.api.nvim_get_current_buf())
- for name, lhs in pairs(config.keymaps.move) do
+ for name, lhs in pairs(config.modes.move.keymaps) do
compare_keymap("move", name, keymaps[lhs] or keymaps[lhs:upper()])
end
@@ -35,6 +35,22 @@ describe("mode mappings", function()
end)
end)
+ it("sets buffer-only mode mappings when entering swap mode", function()
+ given(function()
+ vim.cmd("new") -- Create another buffer to activate swap mode
+
+ winmove.start_mode(winmove.Mode.Swap)
+
+ local keymaps = test_helpers.get_buf_mapped_keymaps(vim.api.nvim_get_current_buf())
+
+ for name, lhs in pairs(config.modes.swap.keymaps) do
+ compare_keymap("swap", name, keymaps[lhs] or keymaps[lhs:upper()])
+ end
+
+ winmove.stop_mode()
+ end)
+ end)
+
it("restores mappings after exiting a mode", function()
given(function()
vim.cmd("new") -- Create another buffer to activate move mode
diff --git a/tests/move_between_tabs_spec.lua b/tests/move_between_tabs_spec.lua
index 3640a9a..fc081d2 100644
--- a/tests/move_between_tabs_spec.lua
+++ b/tests/move_between_tabs_spec.lua
@@ -10,10 +10,17 @@ local given = vader.given
local make_layout = test_helpers.make_layout
describe("moving between tabs", function()
- -- Ensure default configuration
+ ---@diagnostic disable-next-line: missing-fields
config.configure({
- at_edge = {
- horizontal = at_edge.MoveToTab,
+ ---@diagnostic disable-next-line: missing-fields
+ modes = {
+ ---@diagnostic disable-next-line: missing-fields
+ move = {
+ ---@diagnostic disable-next-line: missing-fields
+ at_edge = {
+ horizontal = at_edge.AtEdge.MoveToTab,
+ },
+ },
},
})
@@ -705,7 +712,9 @@ describe("moving between tabs", function()
assert.matches_winlayout(vim.fn.winlayout(), { "leaf", win_id })
- assert.stub(message.error).was.called_with("Only one window and tab")
+ assert
+ .stub(message.error).was
+ .called_with("Cannot move window, only one window and tab")
---@diagnostic disable-next-line: undefined-field
message.error:revert()
@@ -717,7 +726,9 @@ describe("moving between tabs", function()
stub(message, "error")
winmove.start_mode(winmove.Mode.Move)
- assert.stub(message.error).was.called_with("Only one window and tab")
+ assert
+ .stub(message.error).was
+ .called_with("Cannot move window, only one window and tab")
---@diagnostic disable-next-line: undefined-field
message.error:revert()
diff --git a/tests/no_win_events_spec.lua b/tests/no_win_events_spec.lua
index 27a733a..815c438 100644
--- a/tests/no_win_events_spec.lua
+++ b/tests/no_win_events_spec.lua
@@ -1,27 +1,24 @@
local winmove = require("winmove")
-local compat = require("winmove.compat")
+local winutil = require("winmove.winutil")
local vader = require("winmove.util.vader")
local given = vader.given
describe("no window events", function()
- local events = {
- "WinEnter",
- "WinLeave",
- "WinNew",
- "WinScrolled",
- "WinClosed",
- "BufWinEnter",
- "BufWinLeave",
- "BufEnter",
- "BufLeave",
- }
-
- if compat.has("nvim-0.8.2") then
- table.insert(events, "WinResized")
- end
-
+ local events = winutil.get_ignored_events()
local triggers = {}
+ local autocmd_ids = {}
+
+ before_each(function()
+ triggers = {}
+ autocmd_ids = {}
+ end)
+
+ after_each(function()
+ for _, autocmd_id in ipairs(autocmd_ids) do
+ pcall(vim.api.nvim_del_autocmd, autocmd_id)
+ end
+ end)
it("does not trigger any window events when moving", function()
given(function()
@@ -29,11 +26,13 @@ describe("no window events", function()
-- Set up autocmds *after* splitting
for _, event in ipairs(events) do
- vim.api.nvim_create_autocmd(event, {
+ local autocmd_id = vim.api.nvim_create_autocmd(event, {
callback = function()
triggers[event] = true
end,
})
+
+ table.insert(autocmd_ids, autocmd_id)
end
winmove.move_window(vim.api.nvim_get_current_win(), "h")
@@ -41,4 +40,25 @@ describe("no window events", function()
assert.are.same(triggers, {})
end)
end)
+
+ it("does not trigger any window events when swapping", function()
+ given(function()
+ vim.cmd.vnew()
+
+ -- Set up autocmds *after* splitting
+ for _, event in ipairs(events) do
+ local autocmd_id = vim.api.nvim_create_autocmd(event, {
+ callback = function()
+ triggers[event] = true
+ end,
+ })
+
+ table.insert(autocmd_ids, autocmd_id)
+ end
+
+ winmove.swap_window_in_direction(vim.api.nvim_get_current_win(), "h")
+
+ assert.are.same(triggers, {})
+ end)
+ end)
end)
diff --git a/tests/split_into_spec.lua b/tests/split_into_spec.lua
index 8008ffa..b90f444 100644
--- a/tests/split_into_spec.lua
+++ b/tests/split_into_spec.lua
@@ -1,4 +1,5 @@
local winmove = require("winmove")
+local at_edge = require("winmove.at_edge")
local config = require("winmove.config")
local vader = require("winmove.util.vader")
local test_helpers = require("winmove.util.test_helpers")
@@ -9,16 +10,18 @@ local make_layout = test_helpers.make_layout
describe("split_into", function()
-- Ensure default configuration
config.configure({
- at_edge = {
- horizontal = false,
- vertical = false,
- },
- keymaps = {
+ modes = {
move = {
- split_left = "sh",
- split_down = "sj",
- split_up = "sk",
- split_right = "sl",
+ at_edge = {
+ horizontal = at_edge.AtEdge.None,
+ vertical = at_edge.AtEdge.None,
+ },
+ keymaps = {
+ split_left = "sh",
+ split_down = "sj",
+ split_up = "sk",
+ split_right = "sl",
+ },
},
},
})
diff --git a/tests/state_spec.lua b/tests/state_spec.lua
new file mode 100644
index 0000000..a37cb1d
--- /dev/null
+++ b/tests/state_spec.lua
@@ -0,0 +1,31 @@
+local State = require("winmove.state")
+
+describe("State", function()
+ it("updates and resets state, gets properties", function()
+ local state = State.new()
+
+ assert.are.same(state:get("mode"), nil)
+ assert.are.same(state:get("win_id"), nil)
+ assert.are.same(state:get("bufnr"), nil)
+ assert.are.same(state:get("saved_keymaps"), nil)
+
+ state:update({
+ mode = "move",
+ win_id = 1000,
+ bufnr = 3,
+ saved_keymaps = {},
+ })
+
+ assert.are.same(state:get("mode"), "move")
+ assert.are.same(state:get("win_id"), 1000)
+ assert.are.same(state:get("bufnr"), 3)
+ assert.are.same(state:get("saved_keymaps"), {})
+
+ state:reset()
+
+ assert.are.same(state:get("mode"), nil)
+ assert.are.same(state:get("win_id"), nil)
+ assert.are.same(state:get("bufnr"), nil)
+ assert.are.same(state:get("saved_keymaps"), nil)
+ end)
+end)
diff --git a/tests/swap_mode_spec.lua b/tests/swap_mode_spec.lua
new file mode 100644
index 0000000..81a2410
--- /dev/null
+++ b/tests/swap_mode_spec.lua
@@ -0,0 +1,152 @@
+local winmove = require("winmove")
+local vader = require("winmove.util.vader")
+local test_helpers = require("winmove.util.test_helpers")
+
+local given = vader.given
+local make_layout = test_helpers.make_layout
+
+describe("swap mode", function()
+ it("swaps window to the left", function()
+ given(function()
+ local windows = make_layout({
+ "row",
+ { "target", "main" },
+ })
+ local main_win_id = windows["main"]
+ local target_win_id = windows["target"]
+
+ assert.matches_winlayout(vim.fn.winlayout(), {
+ "row",
+ {
+ { "leaf", target_win_id },
+ { "leaf", main_win_id },
+ },
+ })
+
+ local bufnr1 = vim.api.nvim_win_get_buf(main_win_id)
+ local bufnr2 = vim.api.nvim_win_get_buf(target_win_id)
+
+ vim.api.nvim_set_current_win(main_win_id)
+ winmove.swap_window_in_direction(main_win_id, "h")
+
+ assert.matches_winlayout(vim.fn.winlayout(), {
+ "row",
+ {
+ { "leaf", target_win_id },
+ { "leaf", main_win_id },
+ },
+ })
+
+ assert.are.same(vim.api.nvim_win_get_buf(main_win_id), bufnr2)
+ assert.are.same(vim.api.nvim_win_get_buf(target_win_id), bufnr1)
+ end)
+ end)
+
+ it("swaps window down", function()
+ given(function()
+ local windows = make_layout({
+ "col",
+ { "main", "target" },
+ })
+ local main_win_id = windows["main"]
+ local target_win_id = windows["target"]
+
+ assert.matches_winlayout(vim.fn.winlayout(), {
+ "col",
+ {
+ { "leaf", main_win_id },
+ { "leaf", target_win_id },
+ },
+ })
+
+ local bufnr1 = vim.api.nvim_win_get_buf(main_win_id)
+ local bufnr2 = vim.api.nvim_win_get_buf(target_win_id)
+
+ vim.api.nvim_set_current_win(main_win_id)
+ winmove.swap_window_in_direction(main_win_id, "j")
+
+ assert.matches_winlayout(vim.fn.winlayout(), {
+ "col",
+ {
+ { "leaf", main_win_id },
+ { "leaf", target_win_id },
+ },
+ })
+
+ assert.are.same(vim.api.nvim_win_get_buf(main_win_id), bufnr2)
+ assert.are.same(vim.api.nvim_win_get_buf(target_win_id), bufnr1)
+ end)
+ end)
+
+ it("swaps window up", function()
+ given(function()
+ local windows = make_layout({
+ "col",
+ { "target", "main" },
+ })
+ local main_win_id = windows["main"]
+ local target_win_id = windows["target"]
+
+ assert.matches_winlayout(vim.fn.winlayout(), {
+ "col",
+ {
+ { "leaf", target_win_id },
+ { "leaf", main_win_id },
+ },
+ })
+
+ local bufnr1 = vim.api.nvim_win_get_buf(main_win_id)
+ local bufnr2 = vim.api.nvim_win_get_buf(target_win_id)
+
+ vim.api.nvim_set_current_win(main_win_id)
+ winmove.swap_window_in_direction(main_win_id, "k")
+
+ assert.matches_winlayout(vim.fn.winlayout(), {
+ "col",
+ {
+ { "leaf", target_win_id },
+ { "leaf", main_win_id },
+ },
+ })
+
+ assert.are.same(vim.api.nvim_win_get_buf(main_win_id), bufnr2)
+ assert.are.same(vim.api.nvim_win_get_buf(target_win_id), bufnr1)
+ end)
+ end)
+
+ it("swaps window to the right", function()
+ given(function()
+ local windows = make_layout({
+ "row",
+ { "main", "target" },
+ })
+ local main_win_id = windows["main"]
+ local target_win_id = windows["target"]
+
+ assert.matches_winlayout(vim.fn.winlayout(), {
+ "row",
+ {
+ { "leaf", main_win_id },
+ { "leaf", target_win_id },
+ },
+ })
+
+ local bufnr1 = vim.api.nvim_win_get_buf(main_win_id)
+ local bufnr2 = vim.api.nvim_win_get_buf(target_win_id)
+
+ vim.api.nvim_set_current_win(main_win_id)
+ winmove.swap_window_in_direction(main_win_id, "l")
+
+ assert.matches_winlayout(vim.fn.winlayout(), {
+ "row",
+ {
+ { "leaf", main_win_id },
+ { "leaf", target_win_id },
+ },
+ })
+
+ assert.are.same(vim.api.nvim_win_get_buf(main_win_id), bufnr2)
+ assert.are.same(vim.api.nvim_win_get_buf(target_win_id), bufnr1)
+ end)
+ end)
+end)
diff --git a/tests/swap_window_spec.lua b/tests/swap_window_spec.lua
new file mode 100644
index 0000000..8c14cd0
--- /dev/null
+++ b/tests/swap_window_spec.lua
@@ -0,0 +1,209 @@
+local winmove = require("winmove")
+local vader = require("winmove.util.vader")
+local test_helpers = require("winmove.util.test_helpers")
+local stub = require("luassert.stub")
+
+local given = vader.given
+local make_layout = test_helpers.make_layout
+
+describe("swap window", function()
+ assert:set_parameter("TableFormatLevel", 10)
+
+ it("swaps a window with another", function()
+ given(function()
+ local windows = make_layout({
+ "col",
+ {
+ "target",
+ {
+ "row",
+ { "leaf", "main" },
+ },
+ },
+ })
+ local main_win_id = windows["main"]
+ local target_win_id = windows["target"]
+
+ assert.matches_winlayout(vim.fn.winlayout(), {
+ "col",
+ {
+ { "leaf", target_win_id },
+ {
+ "row",
+ {
+ { "leaf" },
+ { "leaf", main_win_id },
+ },
+ },
+ },
+ })
+
+ local bufnr1 = vim.api.nvim_win_get_buf(main_win_id)
+ local bufnr2 = vim.api.nvim_win_get_buf(target_win_id)
+
+ winmove.swap_window(main_win_id)
+ winmove.swap_window(target_win_id)
+
+ assert.matches_winlayout(vim.fn.winlayout(), {
+ "col",
+ {
+ { "leaf", target_win_id },
+ {
+ "row",
+ {
+ { "leaf" },
+ { "leaf", main_win_id },
+ },
+ },
+ },
+ })
+
+ assert.are.same(vim.api.nvim_win_get_buf(main_win_id), bufnr2)
+ assert.are.same(vim.api.nvim_win_get_buf(target_win_id), bufnr1)
+ end)
+ end)
+
+ it("swaps a window with another across tabs", function()
+ given(function()
+ local main_win_id = make_layout({
+ "col",
+ {
+ "leaf",
+ {
+ "row",
+ { "leaf", "main" },
+ },
+ },
+ })["main"]
+
+ assert.matches_winlayout(vim.fn.winlayout(), {
+ "col",
+ {
+ { "leaf" },
+ {
+ "row",
+ {
+ { "leaf" },
+ { "leaf", main_win_id },
+ },
+ },
+ },
+ })
+
+ vim.cmd.tabnew()
+
+ local target_win_id = make_layout({
+ "row",
+ {
+ "target",
+ {
+ "col",
+ { "leaf", "leaf" },
+ },
+ },
+ })["target"]
+
+ assert.matches_winlayout(vim.fn.winlayout(), {
+ "row",
+ {
+ { "leaf", target_win_id },
+ {
+ "col",
+ {
+ { "leaf" },
+ { "leaf" },
+ },
+ },
+ },
+ })
+
+ local bufnr1 = vim.api.nvim_win_get_buf(main_win_id)
+ local bufnr2 = vim.api.nvim_win_get_buf(target_win_id)
+
+ winmove.swap_window(main_win_id)
+ winmove.swap_window(target_win_id)
+
+ assert.are.same(vim.api.nvim_win_get_buf(main_win_id), bufnr2)
+ assert.are.same(vim.api.nvim_win_get_buf(target_win_id), bufnr1)
+ end)
+ end)
+
+ it("fails if previously selected window is not valid anymore", function()
+ given(function()
+ stub(vim, "notify")
+
+ local windows = make_layout({
+ "col",
+ {
+ "target",
+ {
+ "row",
+ { "leaf", "main" },
+ },
+ },
+ })
+ local main_win_id = windows["main"]
+ local target_win_id = windows["target"]
+
+ assert.matches_winlayout(vim.fn.winlayout(), {
+ "col",
+ {
+ { "leaf", target_win_id },
+ {
+ "row",
+ {
+ { "leaf" },
+ { "leaf", main_win_id },
+ },
+ },
+ },
+ })
+
+ local bufnr1 = vim.api.nvim_win_get_buf(target_win_id)
+
+ winmove.swap_window(main_win_id)
+ vim.api.nvim_win_close(main_win_id, true)
+ winmove.swap_window(target_win_id)
+
+ assert.stub(vim.notify).was.called_with(
+ "[winmove.nvim]: Previously selected window is not valid anymore",
+ vim.log.levels.ERROR
+ )
+
+ assert.matches_winlayout(vim.fn.winlayout(), {
+ "col",
+ {
+ { "leaf", target_win_id },
+ { "leaf" },
+ },
+ })
+
+ assert.are.same(vim.api.nvim_win_get_buf(target_win_id), bufnr1)
+
+ ---@diagnostic disable-next-line: undefined-field
+ vim.notify:revert()
+ end)
+ end)
+
+ it("fails if swapping selected window with itself", function()
+ given(function()
+ stub(vim, "notify")
+
+ local main_win_id = make_layout("main")["main"]
+
+ assert.matches_winlayout(vim.fn.winlayout(), { "leaf", main_win_id })
+
+ winmove.swap_window(main_win_id)
+ winmove.swap_window(main_win_id)
+
+ assert
+ .stub(vim.notify).was
+ .called_with("[winmove.nvim]: Cannot swap selected window with itself", vim.log.levels.ERROR)
+
+ assert.matches_winlayout(vim.fn.winlayout(), { "leaf", main_win_id })
+
+ ---@diagnostic disable-next-line: undefined-field
+ vim.notify:revert()
+ end)
+ end)
+end)
diff --git a/tests/toggle_mode_spec.lua b/tests/toggle_mode_spec.lua
new file mode 100644
index 0000000..1829b8e
--- /dev/null
+++ b/tests/toggle_mode_spec.lua
@@ -0,0 +1,42 @@
+local winmove = require("winmove")
+local config = require("winmove.config")
+local vader = require("winmove.util.vader")
+local test_helpers = require("winmove.util.test_helpers")
+
+local given = vader.given
+local make_layout = test_helpers.make_layout
+
+describe("toggle mode", function()
+ it("toggles modes", function()
+ given(function()
+ ---@diagnostic disable-next-line: missing-fields
+ config.configure({ keymaps = { toggle_mode = "t" } })
+
+ local win_id = make_layout({
+ "row",
+ { "main", "leaf" },
+ })["main"]
+
+ assert.matches_winlayout(vim.fn.winlayout(), {
+ "row",
+ {
+ { "leaf", win_id },
+ { "leaf" },
+ },
+ })
+
+ vim.api.nvim_set_current_win(win_id)
+ winmove.start_mode(winmove.Mode.Move)
+
+ assert.are.same(winmove.current_mode(), winmove.Mode.Move)
+
+ vim.cmd([[execute "normal t"]])
+ assert.are.same(winmove.current_mode(), winmove.Mode.Swap)
+
+ vim.cmd([[execute "normal t"]])
+ assert.are.same(winmove.current_mode(), winmove.Mode.Move)
+
+ winmove.stop_mode()
+ end)
+ end)
+end)
diff --git a/tests/wrap_around_spec.lua b/tests/wrap_around_move_spec.lua
similarity index 79%
rename from tests/wrap_around_spec.lua
rename to tests/wrap_around_move_spec.lua
index 90cb78c..67e3fc5 100644
--- a/tests/wrap_around_spec.lua
+++ b/tests/wrap_around_move_spec.lua
@@ -12,16 +12,12 @@ describe("wrap-around when moving windows", function()
it("wraps around when enabled in the config", function()
config.configure({
- at_edge = {
- horizontal = at_edge.Wrap,
- vertical = at_edge.Wrap,
- },
- keymaps = {
+ modes = {
move = {
- split_left = "sh",
- split_down = "sj",
- split_up = "sk",
- split_right = "sl",
+ at_edge = {
+ horizontal = at_edge.AtEdge.Wrap,
+ vertical = at_edge.AtEdge.Wrap,
+ },
},
},
})
@@ -42,7 +38,6 @@ describe("wrap-around when moving windows", function()
},
})
- vim.api.nvim_set_current_win(main_win_id)
winmove.move_window(main_win_id, "h")
assert.matches_winlayout(vim.fn.winlayout(), {
@@ -57,16 +52,12 @@ describe("wrap-around when moving windows", function()
it("does not wrap around when disabled in the config", function()
config.configure({
- at_edge = {
- horizontal = false,
- vertical = false,
- },
- keymaps = {
+ modes = {
move = {
- split_left = "sh",
- split_down = "sj",
- split_up = "sk",
- split_right = "sl",
+ at_edge = {
+ horizontal = at_edge.AtEdge.None,
+ vertical = at_edge.AtEdge.None,
+ },
},
},
})
@@ -100,16 +91,12 @@ describe("wrap-around when moving windows", function()
it("does not wrap full width window", function()
config.configure({
- at_edge = {
- horizontal = at_edge.Wrap,
- vertical = at_edge.Wrap,
- },
- keymaps = {
+ modes = {
move = {
- split_left = "sh",
- split_down = "sj",
- split_up = "sk",
- split_right = "sl",
+ at_edge = {
+ horizontal = at_edge.AtEdge.Wrap,
+ vertical = at_edge.AtEdge.Wrap,
+ },
},
},
})
@@ -162,16 +149,12 @@ describe("wrap-around when moving windows", function()
it("does not wrap full height window", function()
config.configure({
- at_edge = {
- horizontal = at_edge.Wrap,
- vertical = at_edge.Wrap,
- },
- keymaps = {
+ modes = {
move = {
- split_left = "sh",
- split_down = "sj",
- split_up = "sk",
- split_right = "sl",
+ at_edge = {
+ horizontal = at_edge.AtEdge.Wrap,
+ vertical = at_edge.AtEdge.Wrap,
+ },
},
},
})
diff --git a/tests/wrap_around_swap_spec.lua b/tests/wrap_around_swap_spec.lua
new file mode 100644
index 0000000..4d8c293
--- /dev/null
+++ b/tests/wrap_around_swap_spec.lua
@@ -0,0 +1,224 @@
+local winmove = require("winmove")
+local at_edge = require("winmove.at_edge")
+local config = require("winmove.config")
+local vader = require("winmove.util.vader")
+local test_helpers = require("winmove.util.test_helpers")
+
+local given = vader.given
+local make_layout = test_helpers.make_layout
+
+describe("wrap-around when swapping windows", function()
+ assert:set_parameter("TableFormatLevel", 10)
+
+ it("wraps around when enabled in the config", function()
+ config.configure({
+ modes = {
+ swap = {
+ at_edge = {
+ horizontal = at_edge.AtEdge.Wrap,
+ vertical = at_edge.AtEdge.Wrap,
+ },
+ },
+ },
+ })
+
+ given(function()
+ local win_ids = make_layout({
+ "row",
+ { "main", "target" },
+ })
+
+ local main_win_id = win_ids["main"]
+ local target_win_id = win_ids["target"]
+ local bufnr1 = vim.api.nvim_win_get_buf(main_win_id)
+ local bufnr2 = vim.api.nvim_win_get_buf(target_win_id)
+
+ assert.matches_winlayout(vim.fn.winlayout(), {
+ "row",
+ {
+ { "leaf", main_win_id },
+ { "leaf", target_win_id },
+ },
+ })
+
+ winmove.swap_window_in_direction(main_win_id, "h")
+
+ assert.matches_winlayout(vim.fn.winlayout(), {
+ "row",
+ {
+ { "leaf", main_win_id },
+ { "leaf", target_win_id },
+ },
+ })
+
+ assert.are.same(vim.api.nvim_win_get_buf(main_win_id), bufnr2)
+ assert.are.same(vim.api.nvim_win_get_buf(target_win_id), bufnr1)
+ end)
+ end)
+
+ it("does not wrap around when disabled in the config", function()
+ config.configure({
+ modes = {
+ swap = {
+ at_edge = {
+ horizontal = at_edge.AtEdge.None,
+ vertical = at_edge.AtEdge.None,
+ },
+ },
+ },
+ })
+
+ given(function()
+ local win_ids = make_layout({
+ "row",
+ { "main", "target" },
+ })
+
+ local main_win_id = win_ids["main"]
+ local target_win_id = win_ids["target"]
+ local bufnr1 = vim.api.nvim_win_get_buf(main_win_id)
+ local bufnr2 = vim.api.nvim_win_get_buf(target_win_id)
+
+ assert.matches_winlayout(vim.fn.winlayout(), {
+ "row",
+ {
+ { "leaf", main_win_id },
+ { "leaf", target_win_id },
+ },
+ })
+
+ winmove.swap_window_in_direction(main_win_id, "h")
+
+ assert.matches_winlayout(vim.fn.winlayout(), {
+ "row",
+ {
+ { "leaf", main_win_id },
+ { "leaf", target_win_id },
+ },
+ })
+
+ assert.are.same(vim.api.nvim_win_get_buf(main_win_id), bufnr1)
+ assert.are.same(vim.api.nvim_win_get_buf(target_win_id), bufnr2)
+ end)
+ end)
+
+ it("does not wrap full width window", function()
+ config.configure({
+ modes = {
+ swap = {
+ at_edge = {
+ horizontal = at_edge.AtEdge.Wrap,
+ vertical = at_edge.AtEdge.Wrap,
+ },
+ },
+ },
+ })
+
+ given(function()
+ local main_win_id = make_layout({
+ "col",
+ {
+ "main",
+ {
+ "row",
+ { "leaf", "leaf" },
+ },
+ },
+ })["main"]
+
+ assert.matches_winlayout(vim.fn.winlayout(), {
+ "col",
+ {
+ { "leaf", main_win_id },
+ {
+ "row",
+ {
+ { "leaf" },
+ { "leaf" },
+ },
+ },
+ },
+ })
+
+ local bufnr = vim.api.nvim_win_get_buf(main_win_id)
+ winmove.swap_window_in_direction(main_win_id, "h")
+ winmove.swap_window_in_direction(main_win_id, "l")
+
+ assert.matches_winlayout(vim.fn.winlayout(), {
+ "col",
+ {
+ { "leaf", main_win_id },
+ {
+ "row",
+ {
+ { "leaf" },
+ { "leaf" },
+ },
+ },
+ },
+ })
+
+ assert.are.same(vim.api.nvim_win_get_buf(main_win_id), bufnr)
+ end)
+ end)
+
+ it("does not wrap full height window", function()
+ config.configure({
+ modes = {
+ swap = {
+ at_edge = {
+ horizontal = at_edge.AtEdge.Wrap,
+ vertical = at_edge.AtEdge.Wrap,
+ },
+ },
+ },
+ })
+
+ given(function()
+ local main_win_id = make_layout({
+ "row",
+ {
+ "main",
+ {
+ "col",
+ { "leaf", "leaf" },
+ },
+ },
+ })["main"]
+
+ assert.matches_winlayout(vim.fn.winlayout(), {
+ "row",
+ {
+ { "leaf", main_win_id },
+ {
+ "col",
+ {
+ { "leaf" },
+ { "leaf" },
+ },
+ },
+ },
+ })
+
+ local bufnr = vim.api.nvim_win_get_buf(main_win_id)
+ winmove.swap_window_in_direction(main_win_id, "k")
+ winmove.swap_window_in_direction(main_win_id, "j")
+
+ assert.matches_winlayout(vim.fn.winlayout(), {
+ "row",
+ {
+ { "leaf", main_win_id },
+ {
+ "col",
+ {
+ { "leaf" },
+ { "leaf" },
+ },
+ },
+ },
+ })
+
+ assert.are.same(vim.api.nvim_win_get_buf(main_win_id), bufnr)
+ end)
+ end)
+end)