feat(api): add nvim_win_resize()

Ref #6645

Problem:
When a window is resized it takes space from the window right/below first,
and only falls back to the window left/above when there is no more room.
Sometimes a user wants the space to come from a specific direction.

Solution:
Add nvim_win_resize(win, width, height, {anchor}) which resizes a window
with a choosable anchor edge, letting a window grow leftwards or upwards
by taking space from the window to the left or above first. The default
anchor reproduces nvim_win_set_width()/nvim_win_set_height().
This commit is contained in:
XiaowenHu96
2026-06-27 10:41:12 +08:00
parent 08a1228f82
commit ce718e31f4
17 changed files with 527 additions and 177 deletions

View File

@@ -7,6 +7,7 @@ local Screen = require('test.functional.ui.screen')
local clear, eval, eq, ok = n.clear, n.eval, t.eq, t.ok
local api, command, fn = n.api, n.command, n.fn
local pcall_err, assert_alive = t.pcall_err, n.assert_alive
local insert, exec, feed = n.insert, n.exec, n.feed
describe('deprecated', function()
before_each(n.clear)
@@ -204,4 +205,106 @@ describe('deprecated', function()
assert_alive()
end)
end)
describe('{get,set}_height', function()
it('works', function()
command('vsplit')
eq(
api.nvim_win_get_height(api.nvim_list_wins()[2]),
api.nvim_win_get_height(api.nvim_list_wins()[1])
)
api.nvim_set_current_win(api.nvim_list_wins()[2])
command('split')
eq(
api.nvim_win_get_height(api.nvim_list_wins()[2]),
math.floor(api.nvim_win_get_height(api.nvim_list_wins()[1]) / 2)
)
api.nvim_win_set_height(api.nvim_list_wins()[2], 2)
eq(2, api.nvim_win_get_height(api.nvim_list_wins()[2]))
end)
it('failure modes', function()
command('split')
eq('Invalid window id: 999999', pcall_err(api.nvim_win_set_height, 999999, 10))
eq(
'Wrong type for argument 2 when calling nvim_win_set_height, expecting Integer',
pcall_err(api.nvim_win_set_height, 0, 0.9)
)
end)
it('correctly handles height=1', function()
command('split')
api.nvim_set_current_win(api.nvim_list_wins()[1])
api.nvim_win_set_height(api.nvim_list_wins()[2], 1)
eq(1, api.nvim_win_get_height(api.nvim_list_wins()[2]))
end)
it('correctly handles height=1 with a winbar', function()
command('set winbar=foobar')
command('set winminheight=0')
command('split')
api.nvim_set_current_win(api.nvim_list_wins()[1])
api.nvim_win_set_height(api.nvim_list_wins()[2], 1)
eq(1, api.nvim_win_get_height(api.nvim_list_wins()[2]))
end)
it('do not cause ml_get errors with foldmethod=expr #19989', function()
insert([[
aaaaa
bbbbb
ccccc]])
command('set foldmethod=expr')
exec([[
new
let w = nvim_get_current_win()
wincmd w
call nvim_win_set_height(w, 5)
]])
feed('l')
eq('', api.nvim_get_vvar('errmsg'))
end)
end)
describe('{get,set}_width', function()
it('works', function()
command('split')
eq(
api.nvim_win_get_width(api.nvim_list_wins()[2]),
api.nvim_win_get_width(api.nvim_list_wins()[1])
)
api.nvim_set_current_win(api.nvim_list_wins()[2])
command('vsplit')
eq(
api.nvim_win_get_width(api.nvim_list_wins()[2]),
math.floor(api.nvim_win_get_width(api.nvim_list_wins()[1]) / 2)
)
api.nvim_win_set_width(api.nvim_list_wins()[2], 2)
eq(2, api.nvim_win_get_width(api.nvim_list_wins()[2]))
end)
it('failure modes', function()
command('vsplit')
eq('Invalid window id: 999999', pcall_err(api.nvim_win_set_width, 999999, 10))
eq(
'Wrong type for argument 2 when calling nvim_win_set_width, expecting Integer',
pcall_err(api.nvim_win_set_width, 0, 0.9)
)
end)
it('do not cause ml_get errors with foldmethod=expr #19989', function()
insert([[
aaaaa
bbbbb
ccccc]])
command('set foldmethod=expr')
exec([[
vnew
let w = nvim_get_current_win()
wincmd w
call nvim_win_set_width(w, 5)
]])
feed('l')
eq('', api.nvim_get_vvar('errmsg'))
end)
end)
end)

View File

@@ -321,36 +321,172 @@ describe('API/win', function()
end)
end)
describe('{get,set}_height', function()
it('works', function()
command('vsplit')
eq(
api.nvim_win_get_height(api.nvim_list_wins()[2]),
api.nvim_win_get_height(api.nvim_list_wins()[1])
)
api.nvim_set_current_win(api.nvim_list_wins()[2])
command('split')
eq(
api.nvim_win_get_height(api.nvim_list_wins()[2]),
math.floor(api.nvim_win_get_height(api.nvim_list_wins()[1]) / 2)
)
api.nvim_win_set_height(api.nvim_list_wins()[2], 2)
eq(2, api.nvim_win_get_height(api.nvim_list_wins()[2]))
describe('resize', function()
local function heights(wins)
return vim.tbl_map(api.nvim_win_get_height, wins)
end
local function widths(wins)
return vim.tbl_map(api.nvim_win_get_width, wins)
end
it('height: default anchor="top"', function()
command('split | split')
local wins = api.nvim_list_wins()
local before = heights(wins)
api.nvim_win_resize(wins[2], -1, before[2] + 2, {})
-- default anchor top.
local after = heights(wins)
eq(before[1], after[1])
eq(before[2] + 2, after[2])
eq(before[3] - 2, after[3])
end)
it('failure modes', function()
command('split')
eq('Invalid window id: 999999', pcall_err(api.nvim_win_set_height, 999999, 10))
eq(
'Wrong type for argument 2 when calling nvim_win_set_height, expecting Integer',
pcall_err(api.nvim_win_set_height, 0, 0.9)
)
it('height: anchor="bottom"', function()
command('split | split')
local wins = api.nvim_list_wins()
local before = heights(wins)
api.nvim_win_resize(wins[2], -1, before[2] + 2, { anchor = 'bottom' })
local after = heights(wins)
-- bottom anchor unchanged.
eq(before[1] - 2, after[1])
eq(before[2] + 2, after[2])
eq(before[3], after[3])
end)
it('height: anchor="bottom" in a nested frame', function()
-- layout: column[ Top, row[ML|MR], Bottom ]; current window = ML
command('split | split | 2wincmd w | vsplit')
local ml = api.nvim_get_current_win()
local function row_of(w)
return fn.win_screenpos(fn.win_id2win(w))[1]
end
local top, bottom
for _, w in ipairs(api.nvim_list_wins()) do
if row_of(w) == 1 then
top = w
end
if not bottom or row_of(w) > row_of(bottom) then
bottom = w
end
end
local top_h = api.nvim_win_get_height(top)
local bottom_h = api.nvim_win_get_height(bottom)
api.nvim_win_resize(ml, -1, api.nvim_win_get_height(ml) + 3, { anchor = 'bottom' })
-- "bottom" grow the row upward, taking from Top, not Bottom
eq(top_h - 3, api.nvim_win_get_height(top))
eq(bottom_h, api.nvim_win_get_height(bottom))
end)
it('height: anchor="bottom" with borrowing', function()
-- column[ Wupper, row[ col[Ltop|Ltarget], Right ], Wlower ]; current = Ltarget.
-- Growing Ltarget more than Ltop can supply forces us to borrow from row
-- and row's growth must respect given anchor.
command('split | split | 2wincmd w | vsplit | split | wincmd j')
local target = api.nvim_get_current_win()
local function row_of(w)
return fn.win_screenpos(fn.win_id2win(w))[1]
end
local wupper, wlower
for _, w in ipairs(api.nvim_list_wins()) do
if row_of(w) == 1 then
wupper = w
end
if not wlower or row_of(w) > row_of(wlower) then
wlower = w
end
end
local wupper_h = api.nvim_win_get_height(wupper)
local wlower_h = api.nvim_win_get_height(wlower)
api.nvim_win_resize(target, -1, api.nvim_win_get_height(target) + 6, { anchor = 'bottom' })
-- must borrow from above.
ok(api.nvim_win_get_height(wupper) < wupper_h)
eq(wlower_h, api.nvim_win_get_height(wlower))
end)
it('width: default anchor="left"', function()
command('vsplit | vsplit')
local wins = api.nvim_list_wins()
local before = widths(wins)
api.nvim_win_resize(wins[2], before[2] + 4, -1, {})
local after = widths(wins)
eq(before[1], after[1])
eq(before[2] + 4, after[2])
eq(before[3] - 4, after[3])
end)
it('width: anchor="right"', function()
command('vsplit | vsplit')
local wins = api.nvim_list_wins()
local before = widths(wins)
api.nvim_win_resize(wins[2], before[2] + 4, -1, { anchor = 'right' })
local after = widths(wins)
eq(before[1] - 4, after[1])
eq(before[2] + 4, after[2])
eq(before[3], after[3])
end)
it('width: anchor="right" a nested frame', function()
-- layout: row[ Left, col[MT|MB], Right ]; current window = MT
command('vsplit | vsplit | 2wincmd w | split')
local mt = api.nvim_get_current_win()
local function col_of(w)
return fn.win_screenpos(fn.win_id2win(w))[2]
end
local left, right
for _, w in ipairs(api.nvim_list_wins()) do
if col_of(w) == 1 then
left = w
end
if not right or col_of(w) > col_of(right) then
right = w
end
end
local left_w = api.nvim_win_get_width(left)
local right_w = api.nvim_win_get_width(right)
api.nvim_win_resize(mt, api.nvim_win_get_width(mt) + 3, -1, { anchor = 'right' })
-- "right" must grow the column leftward, taking from Left, not Right
eq(left_w - 3, api.nvim_win_get_width(left))
eq(right_w, api.nvim_win_get_width(right))
end)
it('sets both height and width in a single call', function()
-- layout: column[ Top, row[ML|MR], Bottom ]; current = ML.
command('split | split | 2wincmd w | vsplit')
local ml = api.nvim_get_current_win()
local function row_of(w)
return fn.win_screenpos(fn.win_id2win(w))[1]
end
local top, bottom, mr
for _, w in ipairs(api.nvim_list_wins()) do
if row_of(w) == 1 then
top = w
end
if not bottom or row_of(w) > row_of(bottom) then
bottom = w
end
if row_of(w) == row_of(ml) and w ~= ml then
mr = w
end
end
local top_h = api.nvim_win_get_height(top)
local bottom_h = api.nvim_win_get_height(bottom)
local mr_w = api.nvim_win_get_width(mr)
local h, w = api.nvim_win_get_height(ml), api.nvim_win_get_width(ml)
-- "bottom" anchors the height axis; width has no matching anchor so it defaults to "left"
api.nvim_win_resize(ml, w - 4, h + 2, { anchor = 'bottom' })
eq(h + 2, api.nvim_win_get_height(ml))
eq(w - 4, api.nvim_win_get_width(ml))
-- height took from above
eq(top_h - 2, api.nvim_win_get_height(top))
eq(bottom_h, api.nvim_win_get_height(bottom))
-- width default to "left"
eq(mr_w + 4, api.nvim_win_get_width(mr))
end)
it('correctly handles height=1', function()
command('split')
api.nvim_set_current_win(api.nvim_list_wins()[1])
api.nvim_win_set_height(api.nvim_list_wins()[2], 1)
api.nvim_win_resize(api.nvim_list_wins()[2], -1, 1, {})
eq(1, api.nvim_win_get_height(api.nvim_list_wins()[2]))
end)
@@ -359,11 +495,11 @@ describe('API/win', function()
command('set winminheight=0')
command('split')
api.nvim_set_current_win(api.nvim_list_wins()[1])
api.nvim_win_set_height(api.nvim_list_wins()[2], 1)
api.nvim_win_resize(api.nvim_list_wins()[2], -1, 1, {})
eq(1, api.nvim_win_get_height(api.nvim_list_wins()[2]))
end)
it('do not cause ml_get errors with foldmethod=expr #19989', function()
it('does not cause ml_get errors with foldmethod=expr #19989', function()
insert([[
aaaaa
bbbbb
@@ -373,53 +509,48 @@ describe('API/win', function()
new
let w = nvim_get_current_win()
wincmd w
call nvim_win_set_height(w, 5)
call nvim_win_resize(w, -1, 5, #{})
]])
feed('l')
eq('', api.nvim_get_vvar('errmsg'))
end)
end)
describe('{get,set}_width', function()
it('works', function()
command('split')
eq(
api.nvim_win_get_width(api.nvim_list_wins()[2]),
api.nvim_win_get_width(api.nvim_list_wins()[1])
)
api.nvim_set_current_win(api.nvim_list_wins()[2])
command('vsplit')
eq(
api.nvim_win_get_width(api.nvim_list_wins()[2]),
math.floor(api.nvim_win_get_width(api.nvim_list_wins()[1]) / 2)
)
api.nvim_win_set_width(api.nvim_list_wins()[2], 2)
eq(2, api.nvim_win_get_width(api.nvim_list_wins()[2]))
end)
it(
'accepts size = 0 (clamp to minimum size). Compatible with deprecated set_height/width',
function()
command('set winminheight=1')
command('split')
api.nvim_set_current_win(api.nvim_list_wins()[1])
api.nvim_win_resize(api.nvim_list_wins()[2], -1, 0, {})
eq(1, api.nvim_win_get_height(api.nvim_list_wins()[2]))
end
)
it('failure modes', function()
command('vsplit')
eq('Invalid window id: 999999', pcall_err(api.nvim_win_set_width, 999999, 10))
command('split')
eq(
'Wrong type for argument 2 when calling nvim_win_set_width, expecting Integer',
pcall_err(api.nvim_win_set_width, 0, 0.9)
"Required: must set at least one of 'height', 'width'",
pcall_err(api.nvim_win_resize, 0, -1, -1, {})
)
end)
it('do not cause ml_get errors with foldmethod=expr #19989', function()
insert([[
aaaaa
bbbbb
ccccc]])
command('set foldmethod=expr')
exec([[
vnew
let w = nvim_get_current_win()
wincmd w
call nvim_win_set_width(w, 5)
]])
feed('l')
eq('', api.nvim_get_vvar('errmsg'))
eq(
"Invalid 'width': expected a non-negative count of columns, or -1 for no change",
pcall_err(api.nvim_win_resize, 0, -2, 5, {})
)
eq(
"Invalid 'height': expected a non-negative count of rows, or -1 for no change",
pcall_err(api.nvim_win_resize, 0, 5, -2, {})
)
-- anchor not applicable to the dimension(s) being changed
eq(
"Conflict: 'left' not allowed with this dimension",
pcall_err(api.nvim_win_resize, 0, -1, 5, { anchor = 'left' })
)
-- unrecognized anchor value
eq(
[[Invalid 'anchor': expected "top", "bottom", "left" or "right", got garbage]],
pcall_err(api.nvim_win_resize, 0, -1, 5, { anchor = 'garbage' })
)
eq('Invalid window id: 999999', pcall_err(api.nvim_win_resize, 999999, 5, -1, {}))
end)
end)