feat(float): add winborder option (#31074)

Problem:
There is currently no global option to define the default border style for floating windows. This leads to repetitive code when developers need consistent styling across multiple floating windows.

Solution:
Introduce a global option winborder to specify the default border style for floating windows. When a floating window is created without explicitly specifying a border style, the value of the winborder option will be used. This simplifies configuration and ensures consistency in floating window appearance.

Co-authored-by: Gregory Anders <greg@gpanders.com>
This commit is contained in:
glepnir
2025-03-19 05:05:35 +08:00
committed by GitHub
parent eefd72fff7
commit 62d9fab9af
12 changed files with 340 additions and 62 deletions

View File

@ -3493,28 +3493,20 @@ nvim_open_win({buffer}, {enter}, {config}) *nvim_open_win()*
`eob` flag of 'fillchars' to a space char, and clearing `eob` flag of 'fillchars' to a space char, and clearing
the |hl-EndOfBuffer| region in 'winhighlight'. the |hl-EndOfBuffer| region in 'winhighlight'.
• border: Style of (optional) window border. This can either • border: Style of (optional) window border. This can either
be a string or an array. The string values are be a string or an array. The string values are the same as
• "none": No border (default). those described in 'winborder'. If it is an array, it
• "single": A single line box. should have a length of eight or any divisor of eight. The
• "double": A double line box. array will specify the eight chars building up the border
• "rounded": Like "single", but with rounded corners in a clockwise fashion starting with the top-left corner.
("╭" etc.). As an example, the double box style could be specified as: >
• "solid": Adds padding by a single whitespace cell. [ "╔", "═" ,"╗", "║", "╝", "═", "╚", "║" ].
• "shadow": A drop shadow effect by blending with the
background.
• If it is an array, it should have a length of eight or
any divisor of eight. The array will specify the eight
chars building up the border in a clockwise fashion
starting with the top-left corner. As an example, the
double box style could be specified as: >
[ "╔", "═" ,"╗", "║", "╝", "═", "╚", "║" ].
< <
If the number of chars are less than eight, they will be If the number of chars are less than eight, they will be
repeated. Thus an ASCII border could be specified as > repeated. Thus an ASCII border could be specified as >
[ "/", "-", \"\\\\\", "|" ], [ "/", "-", \"\\\\\", "|" ],
< <
or all chars the same as > or all chars the same as >
[ "x" ]. [ "x" ].
< <
An empty string can be used to turn off a specific border, An empty string can be used to turn off a specific border,
for instance, > for instance, >

View File

@ -348,6 +348,7 @@ OPTIONS
• 'messagesopt' configures |:messages| and |hit-enter| prompt. • 'messagesopt' configures |:messages| and |hit-enter| prompt.
• 'tabclose' controls which tab page to focus when closing a tab page. • 'tabclose' controls which tab page to focus when closing a tab page.
• 'eventignorewin' to persistently ignore events in a window. • 'eventignorewin' to persistently ignore events in a window.
• 'winborder' sets the default border for |floating-windows|
PERFORMANCE PERFORMANCE

View File

@ -7150,6 +7150,18 @@ A jump table for the options with a short description can be found at |Q_op|.
UI-dependent. Works best with RGB colors. 'termguicolors' UI-dependent. Works best with RGB colors. 'termguicolors'
*'winborder'*
'winborder' string (default "")
global
Defines the default border style of floating windows. The default value
is empty, which is equivalent to "none". Valid values include:
- "none": No border.
- "single": A single line box.
- "double": A double line box.
- "rounded": Like "single", but with rounded corners ("╭" etc.).
- "solid": Adds padding by a single whitespace cell.
- "shadow": A drop shadow effect by blending with the background.
*'window'* *'wi'* *'window'* *'wi'*
'window' 'wi' number (default screen height - 1) 'window' 'wi' number (default screen height - 1)
global global

View File

@ -1797,17 +1797,11 @@ function vim.api.nvim_open_term(buffer, opts) end
--- 'fillchars' to a space char, and clearing the --- 'fillchars' to a space char, and clearing the
--- `hl-EndOfBuffer` region in 'winhighlight'. --- `hl-EndOfBuffer` region in 'winhighlight'.
--- - border: Style of (optional) window border. This can either be a string --- - border: Style of (optional) window border. This can either be a string
--- or an array. The string values are --- or an array. The string values are the same as those described in 'winborder'.
--- - "none": No border (default). --- If it is an array, it should have a length of eight or any divisor of
--- - "single": A single line box. --- eight. The array will specify the eight chars building up the border
--- - "double": A double line box. --- in a clockwise fashion starting with the top-left corner. As an
--- - "rounded": Like "single", but with rounded corners ("╭" etc.). --- example, the double box style could be specified as:
--- - "solid": Adds padding by a single whitespace cell.
--- - "shadow": A drop shadow effect by blending with the background.
--- - If it is an array, it should have a length of eight or any divisor of
--- eight. The array will specify the eight chars building up the border
--- in a clockwise fashion starting with the top-left corner. As an
--- example, the double box style could be specified as:
--- ``` --- ```
--- [ "╔", "═" ,"╗", "║", "╝", "═", "╚", "║" ]. --- [ "╔", "═" ,"╗", "║", "╝", "═", "╚", "║" ].
--- ``` --- ```

View File

@ -7832,6 +7832,19 @@ vim.o.winbl = vim.o.winblend
vim.wo.winblend = vim.o.winblend vim.wo.winblend = vim.o.winblend
vim.wo.winbl = vim.wo.winblend vim.wo.winbl = vim.wo.winblend
--- Defines the default border style of floating windows. The default value
--- is empty, which is equivalent to "none". Valid values include:
--- - "none": No border.
--- - "single": A single line box.
--- - "double": A double line box.
--- - "rounded": Like "single", but with rounded corners ("╭" etc.).
--- - "solid": Adds padding by a single whitespace cell.
--- - "shadow": A drop shadow effect by blending with the background.
---
--- @type ''|'double'|'single'|'shadow'|'rounded'|'solid'|'none'
vim.o.winborder = ""
vim.go.winborder = vim.o.winborder
--- Window height used for `CTRL-F` and `CTRL-B` when there is only one --- Window height used for `CTRL-F` and `CTRL-B` when there is only one
--- window and the value is smaller than 'lines' minus one. The screen --- window and the value is smaller than 'lines' minus one. The screen
--- will scroll 'window' minus two lines, with a minimum of one. --- will scroll 'window' minus two lines, with a minimum of one.

View File

@ -6,17 +6,6 @@ local uv = vim.uv
local M = {} local M = {}
local default_border = {
{ '', 'NormalFloat' },
{ '', 'NormalFloat' },
{ '', 'NormalFloat' },
{ ' ', 'NormalFloat' },
{ '', 'NormalFloat' },
{ '', 'NormalFloat' },
{ '', 'NormalFloat' },
{ ' ', 'NormalFloat' },
}
--- @param border string|(string|[string,string])[] --- @param border string|(string|[string,string])[]
local function border_error(border) local function border_error(border)
error( error(
@ -43,7 +32,11 @@ local border_size = {
--- @return integer height --- @return integer height
--- @return integer width --- @return integer width
local function get_border_size(opts) local function get_border_size(opts)
local border = opts and opts.border or default_border local border = opts and opts.border or vim.o.winborder
if border == '' then
border = 'none'
end
if type(border) == 'string' then if type(border) == 'string' then
if not border_size[border] then if not border_size[border] then
@ -884,7 +877,7 @@ function M.make_floating_popup_options(width, height, opts)
or 'cursor', or 'cursor',
style = 'minimal', style = 'minimal',
width = width, width = width,
border = opts.border or default_border, border = opts.border,
zindex = opts.zindex or (api.nvim_win_get_config(0).zindex or 49) + 1, zindex = opts.zindex or (api.nvim_win_get_config(0).zindex or 49) + 1,
title = title, title = title,
title_pos = title_pos, title_pos = title_pos,

View File

@ -159,17 +159,11 @@
/// 'fillchars' to a space char, and clearing the /// 'fillchars' to a space char, and clearing the
/// |hl-EndOfBuffer| region in 'winhighlight'. /// |hl-EndOfBuffer| region in 'winhighlight'.
/// - border: Style of (optional) window border. This can either be a string /// - border: Style of (optional) window border. This can either be a string
/// or an array. The string values are /// or an array. The string values are the same as those described in 'winborder'.
/// - "none": No border (default). /// If it is an array, it should have a length of eight or any divisor of
/// - "single": A single line box. /// eight. The array will specify the eight chars building up the border
/// - "double": A double line box. /// in a clockwise fashion starting with the top-left corner. As an
/// - "rounded": Like "single", but with rounded corners ("╭" etc.). /// example, the double box style could be specified as:
/// - "solid": Adds padding by a single whitespace cell.
/// - "shadow": A drop shadow effect by blending with the background.
/// - If it is an array, it should have a length of eight or any divisor of
/// eight. The array will specify the eight chars building up the border
/// in a clockwise fashion starting with the top-left corner. As an
/// example, the double box style could be specified as:
/// ``` /// ```
/// [ "╔", "═" ,"╗", "║", "╝", "═", "╚", "║" ]. /// [ "╔", "═" ,"╗", "║", "╝", "═", "╚", "║" ].
/// ``` /// ```
@ -944,11 +938,11 @@ static void parse_border_style(Object style, WinConfig *fconfig, Error *err)
char chars[8][MAX_SCHAR_SIZE]; char chars[8][MAX_SCHAR_SIZE];
bool shadow_color; bool shadow_color;
} defaults[] = { } defaults[] = {
{ "double", { "", "", "", "", "", "", "", "" }, false }, { opt_winborder_values[1], { "", "", "", "", "", "", "", "" }, false },
{ "single", { "", "", "", "", "", "", "", "" }, false }, { opt_winborder_values[2], { "", "", "", "", "", "", "", "" }, false },
{ "shadow", { "", "", " ", " ", " ", " ", " ", "" }, true }, { opt_winborder_values[3], { "", "", " ", " ", " ", " ", " ", "" }, true },
{ "rounded", { "", "", "", "", "", "", "", "" }, false }, { opt_winborder_values[4], { "", "", "", "", "", "", "", "" }, false },
{ "solid", { " ", " ", " ", " ", " ", " ", " ", " " }, false }, { opt_winborder_values[5], { " ", " ", " ", " ", " ", " ", " ", " " }, false },
{ NULL, { { NUL } }, false }, { NULL, { { NUL } }, false },
}; };
@ -1279,12 +1273,13 @@ static bool parse_win_config(win_T *wp, Dict(win_config) *config, WinConfig *fco
} }
} }
if (HAS_KEY_X(config, border)) { if (HAS_KEY_X(config, border) || *p_winbd != NUL) {
if (is_split) { if (is_split) {
api_set_error(err, kErrorTypeValidation, "non-float cannot have 'border'"); api_set_error(err, kErrorTypeValidation, "non-float cannot have 'border'");
goto fail; goto fail;
} }
parse_border_style(config->border, fconfig, err); Object style = config->border.type != kObjectTypeNil ? config->border : CSTR_AS_OBJ(p_winbd);
parse_border_style(style, fconfig, err);
if (ERROR_SET(err)) { if (ERROR_SET(err)) {
goto fail; goto fail;
} }

View File

@ -566,6 +566,7 @@ EXTERN OptInt p_wcm; ///< 'wildcharm'
EXTERN int p_wic; ///< 'wildignorecase' EXTERN int p_wic; ///< 'wildignorecase'
EXTERN char *p_wim; ///< 'wildmode' EXTERN char *p_wim; ///< 'wildmode'
EXTERN int p_wmnu; ///< 'wildmenu' EXTERN int p_wmnu; ///< 'wildmenu'
EXTERN char *p_winbd; ///< 'winborder'
EXTERN OptInt p_wh; ///< 'winheight' EXTERN OptInt p_wh; ///< 'winheight'
EXTERN OptInt p_wmh; ///< 'winminheight' EXTERN OptInt p_wmh; ///< 'winminheight'
EXTERN OptInt p_wmw; ///< 'winminwidth' EXTERN OptInt p_wmw; ///< 'winminwidth'

View File

@ -10188,6 +10188,26 @@ local options = {
short_desc = N_('Controls transparency level for floating windows'), short_desc = N_('Controls transparency level for floating windows'),
type = 'number', type = 'number',
}, },
{
defaults = { if_true = '' },
cb = 'did_set_winborder',
values = { '', 'double', 'single', 'shadow', 'rounded', 'solid', 'none' },
desc = [=[
Defines the default border style of floating windows. The default value
is empty, which is equivalent to "none". Valid values include:
- "none": No border.
- "single": A single line box.
- "double": A double line box.
- "rounded": Like "single", but with rounded corners ("╭" etc.).
- "solid": Adds padding by a single whitespace cell.
- "shadow": A drop shadow effect by blending with the background.
]=],
full_name = 'winborder',
scope = { 'global' },
short_desc = N_('border of floating window'),
type = 'string',
varname = 'p_winbd',
},
{ {
abbreviation = 'wi', abbreviation = 'wi',
cb = 'did_set_window', cb = 'did_set_window',

View File

@ -2005,6 +2005,15 @@ const char *did_set_winhighlight(optset_T *args)
return NULL; return NULL;
} }
/// The 'winborder' option is changed.
const char *did_set_winborder(optset_T *args)
{
if (opt_strings_flags(p_winbd, opt_winborder_values, NULL, true) != OK) {
return e_invarg;
}
return NULL;
}
int expand_set_winhighlight(optexpand_T *args, int *numMatches, char ***matches) int expand_set_winhighlight(optexpand_T *args, int *numMatches, char ***matches)
{ {
return expand_set_opt_generic(args, get_highlight_name, numMatches, matches); return expand_set_opt_generic(args, get_highlight_name, numMatches, matches);

View File

@ -9909,6 +9909,253 @@ describe('float window', function()
]]) ]])
eq({"%f", ""}, res) eq({"%f", ""}, res)
end) end)
it('winborder option', function()
local buf = api.nvim_create_buf(false,false)
local config = {relative='editor', width=4, height=4, row=2, col=2}
command('set winborder=single')
api.nvim_open_win(buf, true, config)
if multigrid then
screen:expect({
grid = [[
## grid 1
[2:----------------------------------------]|*6
[3:----------------------------------------]|
## grid 2
|
{0:~ }|*5
## grid 3
|
## grid 4
{5:┌────┐}|
{5:│}{1:^ }{5:│}|
{5:│}{2:~ }{5:│}|*3
{5:└────┘}|
]], float_pos={
[4] = {1001, "NW", 1, 2, 2, true, 50};
}, win_viewport={
[2] = {win = 1000, topline = 0, botline = 2, curline = 0, curcol = 0, linecount = 1, sum_scroll_delta = 0};
[4] = {win = 1001, topline = 0, botline = 2, curline = 0, curcol = 0, linecount = 1, sum_scroll_delta = 0};
}, win_viewport_margins={
[2] = {
bottom = 0,
left = 0,
right = 0,
top = 0,
win = 1000
},
[4] = {
bottom = 1,
left = 1,
right = 1,
top = 1,
win = 1001
}
}
})
else
screen:expect({
grid = [[
{5:┌────┐} |
{0:~ }{5:│}{1:^ }{5:│}{0: }|
{0:~ }{5:│}{2:~ }{5:│}{0: }|*3
{0:~ }{5:└────┘}{0: }|
|
]]
})
end
command('fclose')
command('set winborder=double')
api.nvim_open_win(buf, true, config)
if multigrid then
screen:expect({
grid = [[
## grid 1
[2:----------------------------------------]|*6
[3:----------------------------------------]|
## grid 2
|
{0:~ }|*5
## grid 3
|
## grid 5
{5:╔════╗}|
{5:║}{1:^ }{5:║}|
{5:║}{2:~ }{5:║}|*3
{5:╚════╝}|
]], float_pos={
[5] = {1002, "NW", 1, 2, 2, true, 50};
}, win_viewport={
[2] = {win = 1000, topline = 0, botline = 2, curline = 0, curcol = 0, linecount = 1, sum_scroll_delta = 0};
[5] = {win = 1002, topline = 0, botline = 2, curline = 0, curcol = 0, linecount = 1, sum_scroll_delta = 0};
}, win_viewport_margins={
[2] = {
bottom = 0,
left = 0,
right = 0,
top = 0,
win = 1000
},
[5] = {
bottom = 1,
left = 1,
right = 1,
top = 1,
win = 1002
}
}
})
else
screen:expect({
grid = [[
{5:╔════╗} |
{0:~ }{5:║}{1:^ }{5:║}{0: }|
{0:~ }{5:║}{2:~ }{5:║}{0: }|*3
{0:~ }{5:╚════╝}{0: }|
|
]]
})
end
command('fclose!')
command('set winborder=none')
api.nvim_buf_set_lines(buf, 0, -1, false, {'none border'})
api.nvim_open_win(buf, true, config)
if multigrid then
screen:expect({
grid = [[
## grid 1
[2:----------------------------------------]|*6
[3:----------------------------------------]|
## grid 2
|
{0:~ }|*5
## grid 3
|
## grid 6
{1:^none}|
{1: bor}|
{1:der }|
{2:~ }|
]],
win_pos = {
[2] = {
height = 6,
startcol = 0,
startrow = 0,
width = 40,
win = 1000
}
},
float_pos = {
[6] = {1003, "NW", 1, 2, 2, true, 50};
},
win_viewport = {
[2] = {win = 1000, topline = 0, botline = 2, curline = 0, curcol = 0, linecount = 1, sum_scroll_delta = 0};
[6] = {win = 1003, topline = 0, botline = 2, curline = 0, curcol = 0, linecount = 1, sum_scroll_delta = 0};
},
win_viewport_margins = {
[2] = {
bottom = 0,
left = 0,
right = 0,
top = 0,
win = 1000
},
[6] = {
bottom = 0,
left = 0,
right = 0,
top = 0,
win = 1003
}
},
})
else
screen:expect([[
|
{0:~ }|
{0:~ }{1:^none}{0: }|
{0:~ }{1: bor}{0: }|
{0:~ }{1:der }{0: }|
{0:~ }{2:~ }{0: }|
|
]])
end
command('fclose!')
-- respect config.border
command('set winborder=rounded')
config.border = 'single'
api.nvim_open_win(buf, false, config)
if multigrid then
screen:expect({
grid = [[
## grid 1
[2:----------------------------------------]|*6
[3:----------------------------------------]|
## grid 2
^ |
{0:~ }|*5
## grid 3
|
## grid 7
{5:┌────┐}|
{5:│}{1:none}{5:│}|
{5:│}{1: bor}{5:│}|
{5:│}{1:der }{5:│}|
{5:│}{2:~ }{5:│}|
{5:└────┘}|
]],
win_pos = {
[2] = {
height = 6,
startcol = 0,
startrow = 0,
width = 40,
win = 1000
}
},
float_pos = {
[7] = {1004, "NW", 1, 2, 2, true, 50};
},
win_viewport = {
[2] = {win = 1000, topline = 0, botline = 2, curline = 0, curcol = 0, linecount = 1, sum_scroll_delta = 0};
[7] = {win = 1004, topline = 0, botline = 2, curline = 0, curcol = 0, linecount = 1, sum_scroll_delta = 0};
},
win_viewport_margins = {
[2] = {
bottom = 0,
left = 0,
right = 0,
top = 0,
win = 1000
},
[7] = {
bottom = 1,
left = 1,
right = 1,
top = 1,
win = 1004
}
},
})
else
screen:expect([[
^ {5:┌────┐} |
{0:~ }{5:│}{1:none}{5:│}{0: }|
{0:~ }{5:│}{1: bor}{5:│}{0: }|
{0:~ }{5:│}{1:der }{5:│}{0: }|
{0:~ }{5:│}{2:~ }{5:│}{0: }|
{0:~ }{5:└────┘}{0: }|
|
]])
end
-- it is currently not supported.
eq('Vim(set):E474: Invalid argument: winborder=custom', pcall_err(command, 'set winborder=custom'))
end)
end end
describe('with ext_multigrid', function() describe('with ext_multigrid', function()

View File

@ -75,6 +75,7 @@ let test_values = {
\ 'shada': [['', '''50', '"30'], ['xxx']], \ 'shada': [['', '''50', '"30'], ['xxx']],
\ 'termpastefilter': [['BS', 'HT', 'FF', 'ESC', 'DEL', 'C0', 'C1', 'C0,C1'], \ 'termpastefilter': [['BS', 'HT', 'FF', 'ESC', 'DEL', 'C0', 'C1', 'C0,C1'],
\ ['xxx', 'C0,C1,xxx']], \ ['xxx', 'C0,C1,xxx']],
\ 'winborder': [['rounded', 'none', 'single', 'solid'], ['xxx']],
\ 'winhighlight': [['', 'a:b', 'a:', 'a:b,c:d'], \ 'winhighlight': [['', 'a:b', 'a:', 'a:b,c:d'],
\ ['a', ':', ':b', 'a:b:c', 'a:/', '/:b', ',', 'a:b,,', 'a:b,c']], \ ['a', ':', ':b', 'a:b:c', 'a:/', '/:b', ',', 'a:b,,', 'a:b,c']],
\ \