Skip to content

Commit

Permalink
Finish the win-resizer plugin
Browse files Browse the repository at this point in the history
  • Loading branch information
Kaiser-Yang committed Jan 17, 2025
1 parent 33c59f7 commit dbade0c
Show file tree
Hide file tree
Showing 6 changed files with 336 additions and 0 deletions.
107 changes: 107 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,109 @@
# win-resizer.nvim

Humane commands and APIs for resizing nvim windows.

## Installation

With `lazy.nvim`:

```lua
return {
'Kaiser-Yang/win-resizer.nvim',
lazy = false,
config = function()
require('win.resizer').setup({
ignore_filetypes = {
-- put the files you don't want them be influenced by win-resizer here
}
})
end
```

## Quick Start

You may want to set up those key mappings:

```lua
local resize = require('win.resizer').resize
local map_set = vim.keymap.set
local delta = 3
map_set({ 'n' }, '<up>', function()
local _ = resize(0, 'up', delta, true) or
resize(0, 'down', -delta, true) or
resize(0, 'up', delta, false) or
resize(0, 'down', -delta, false)
end, { desc = 'Smart resize up' })
map_set({ 'n' }, '<down>', function()
local _ = resize(0, 'up', -delta, true) or
resize(0, 'down', delta, true) or
resize(0, 'up', -delta, false) or
resize(0, 'down', delta, false)
end, { desc = 'Smart resize down' })
map_set({ 'n' }, '<left>', function()
local _ = resize(0, 'right', -delta, true) or
resize(0, 'left', delta, true) or
resize(0, 'right', -delta, false) or
resize(0, 'left', delta, false)
end, { desc = 'Smart resize left' })
map_set({ 'n' }, '<right>', function()
local _ = resize(0, 'right', delta, true) or
resize(0, 'left', -delta, true) or
resize(0, 'right', delta, false) or
resize(0, 'left', -delta, false)
end, { desc = 'Smart resize right' })
```

Let me explain what happens for the `<right>` key. First the `<right>` try to make the right border
of the window increase by `delta`. If it fails (which means there is no window on the right or
there is a window whose file type is in `ignore_filetypes`), it tries to make the left border of the
window decrease by `delta`. If it fails again (which means there is no window on the left or there
is a window whose file type is in `ignore_filetypes`), it tries to make the right border of the
window increase by `delta` without considering `ignore_filetypes`. If it fails again (which
means there is no window on the right), it tries to make the left border of the window decrease by
`delta` without considering `ignore_filetypes`.

Or you may not like these smart resizing, you can use the simple ones:

```lua
local delta = 3
local map_set = vim.keymap.set
map_set({ 'n' }, '<up>', function()
resize(0, 'top', delta, true)
end, { desc = 'Increase top border' })
map_set({ 'n' }, '<s-up>', function()
resize(0, 'top', -delta, true)
end, { desc = 'Decrease top border' })
map_set({ 'n' }, '<right>', function()
resize(0, 'right', delta, true)
end, { desc = 'Increase right border' })
map_set({ 'n' }, '<s-right>', function()
resize(0, 'right', -delta, true)
end, { desc = 'Decrease right border' })
map_set({ 'n' }, '<down>', function()
resize(0, 'bottom', delta, true)
end, { desc = 'Increase bottom border' })
map_set({ 'n' }, '<s-down>', function()
resize(0, 'bottom', -delta, true)
end, { desc = 'Decrease bottom border' })
map_set({ 'n' }, '<left>', function()
resize(0, 'left', delta, true)
end, { desc = 'Increase left border' })
map_set({ 'n' }, '<s-left>', function()
resize(0, 'left', -delta, true)
end, { desc = 'Decrease left border' })
```

## Command

There is a command `WinResizer` for resizing current window.

Examples:

```vim
" Increase the top border of the current window by 1
:WinResizer top
" Decrease the top border of the current window by 1
:WinResizer top -1
" Increase the right border of the current window by 1 without considering ignore_filetypes
:WinResizer right 1 false
```
1 change: 1 addition & 0 deletions lua/win-resizer.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
return require('win.resizer')
1 change: 1 addition & 0 deletions lua/win/resizer/config/default.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
return {}
185 changes: 185 additions & 0 deletions lua/win/resizer/init.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
local win_resizer = {}

local utils = require('win.resizer.utils')
local default = require('win.resizer.config.default')
local config = {}

local function get_visible_windows()
local windows = vim.api.nvim_tabpage_list_wins(0)
local visible_windows = {}
for _, win in ipairs(windows) do
local win_config = vim.api.nvim_win_get_config(win)
-- ignore invisible windows and floating windows
if not utils.truthy(win_config.hide) and not utils.truthy(win_config.relative) then
visible_windows[#visible_windows + 1] = win
end
end
return visible_windows
end

--- Get the nearest neighbor window of the given window,
--- win is the window id
---
--- @param win number the window id
--- @param direction win.resizer.Border
--- @return number|nil|number[]
local function get_nearest_neighbor(win, direction)
local current_pos = vim.api.nvim_win_get_position(win)
local current_row, current_col = current_pos[1], current_pos[2]
local curretn_height = vim.api.nvim_win_get_height(win)
local current_width = vim.api.nvim_win_get_width(win)

local windows = {}
for _, win_in_tab in ipairs(get_visible_windows()) do
if win_in_tab == win then
goto continue
end
local pos = vim.api.nvim_win_get_position(win_in_tab)
local row, col = pos[1], pos[2]
local height = vim.api.nvim_win_get_height(win_in_tab)
local width = vim.api.nvim_win_get_width(win_in_tab)
if direction == 'left' and col + width < current_col or
direction == 'right' and col > current_col + current_width or
direction == 'top' and row + height < current_row or
direction == 'bottom' and row > current_row + curretn_height then
table.insert(windows, win_in_tab)
end
::continue::
end
local neighbors = {}
for _, win_in_direction in ipairs(windows) do
local pos = vim.api.nvim_win_get_position(win_in_direction)
local row, col = pos[1], pos[2]
local height = vim.api.nvim_win_get_height(win_in_direction)
local width = vim.api.nvim_win_get_width(win_in_direction)
if direction == 'left' and (#neighbors == 0 or col + width > neighbors[1].col + neighbors[1].width) or
direction == 'right' and (#neighbors == 0 or col < neighbors[1].col) or
direction == 'top' and (#neighbors == 0 or row + height > neighbors[1].row + neighbors[1].height) or
direction == 'bottom' and (#neighbors == 0 or row < neighbors[1].row) then
neighbors = {
{
id = win_in_direction,
row = row,
col = col,
width = width,
height = height,
}
}
elseif direction == 'left' and #neighbors > 0 and col + width == neighbors[1].col + neighbors[1].width or
direction == 'right' and #neighbors > 0 and col == neighbors[1].col or
direction == 'top' and #neighbors > 0 and row + height == neighbors[1].row + neighbors[1].height or
direction == 'bottom' and #neighbors > 0 and row == neighbors[1].row then
table.insert(neighbors, {
id = win_in_direction,
row = row,
col = col,
})
end
end
if #neighbors == 0 then
return nil
elseif #neighbors == 1 then
return neighbors[1].id
else
local neighbor = nil
for _, win_in_direction in ipairs(neighbors) do
if (direction == 'left' or direction == 'right') and win_in_direction.row == current_row or
(direction == 'top' or direction == 'bottom') and win_in_direction.col == current_col then
neighbor = win_in_direction
break
end
end
if not neighbor then
local neighbor_ids = {}
for _, win_in_direction in ipairs(neighbors) do
table.insert(neighbor_ids, win_in_direction.id)
end
return neighbor_ids
end
return neighbor.id
end
end

function win_resizer.setup(opts)
config = vim.tbl_deep_extend('force', default, opts or {})
vim.api.nvim_create_user_command('WinResize', function(args)
local border = args.fargs[1]
local delta = args.fargs[2] and tonumber(args.fargs[2]) or 1
local respect_ignore_firetypes = args.fargs[3] == nil or args.fargs[3] == 'true'
return win_resizer.resize(0, border, delta, respect_ignore_firetypes)
end, {
nargs = '*',
complete = function(_, cmd_line, _)
local arg_num = #vim.split(cmd_line, ' ', {trimempty = true})
if arg_num == 1 then
return { 'top', 'bottom', 'left', 'right' }
elseif arg_num == 2 then
return { '1', '-1' }
elseif arg_num == 3 then
return { 'true', 'false' }
end
end,
desc = 'Resize window with border'
})
end

--- Resize window with border.
---
--- For example, resize(0, 'right', 5) will let the right border of current window
--- move 5 columns to the right if there is a visible window on the right.
--- resize(0, 'right', -5) will let the right border of current window move 5 columns
--- to the left if there is a visible window on the right. And win must be in the current tabpage.
---
--- When respect_ignore_firetypes is true, the window whose filetype is
--- in the ignore_filetypes will be treated as unvisible.
---
--- Return false if the window is not be resized.
---
--- @param win number the window id, 0 means the current window
--- @param border win.resizer.Border
--- @param delta? number default 1
--- @param respect_ignore_firetypes? boolean default true
--- @return boolean
function win_resizer.resize(win, border, delta, respect_ignore_firetypes)
respect_ignore_firetypes = respect_ignore_firetypes == nil or respect_ignore_firetypes
win = win == 0 and vim.api.nvim_get_current_win() or win
local neighbor = get_nearest_neighbor(win, border)
if not neighbor then
return false
end
local ignore_filetypes = utils.get_option(config.ignore_filetypes)
if type(neighbor) == 'table' then
if not respect_ignore_firetypes or not utils.truthy(ignore_filetypes) then
neighbor = neighbor[1]
else
for _, neighbor_id in pairs(neighbor) do
local filetype = vim.bo[vim.api.nvim_win_get_buf(neighbor_id)].filetype
if vim.tbl_contains(ignore_filetypes, filetype) then
return false
end
end
end
else
if respect_ignore_firetypes and utils.truthy(ignore_filetypes) then
local filetype = vim.bo[vim.api.nvim_win_get_buf(neighbor)].filetype
if vim.tbl_contains(ignore_filetypes, filetype) then
return false
end
end
end
delta = delta or 1
win = vim.fn.win_id2win(win)
neighbor = vim.fn.win_id2win(neighbor)
if border == 'right' then
vim.fn.win_move_separator(win, delta)
elseif border == 'bottom' then
vim.fn.win_move_statusline(win, delta)
elseif border == 'left' then
vim.fn.win_move_separator(neighbor, -delta)
elseif border == 'top' then
vim.fn.win_move_statusline(neighbor, -delta)
end
return true
end

return win_resizer
4 changes: 4 additions & 0 deletions lua/win/resizer/types.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
--- @alias win.resizer.Border string | 'top' | 'bottom' | 'left' | 'right'
---
--- @class win.resizer.Config
--- @field ignore_filetypes string[]|function():string[]
38 changes: 38 additions & 0 deletions lua/win/resizer/utils.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
local M = {}

function M.get_option(opt, ...)
if type(opt) == 'function' then
return opt(...)
else
return opt
end
end

function M.truthy(value)
if type(value) == 'boolean' then
return value
elseif type(value) == 'function' then
return M.truthy(value())
elseif type(value) == 'table' then
return not vim.tbl_isempty(value)
elseif type(value) == 'string' then
return value ~= ''
elseif type(value) == 'number' then
return value ~= 0
elseif type(value) == 'nil' then
return false
else
return true
end
end

--- Transform arguments to string, and concatenate them with a space.
function M.str(...)
local args = { ... }
for i, v in ipairs(args) do
args[i] = vim.inspect(v)
end
return table.concat(args, ' ')
end

return M

0 comments on commit dbade0c

Please sign in to comment.