fix(api): nvim_get_option_value tab-local 'cmdheight' #39259

Problem:
API clients cannot query the tab-local value of 'cmdheight'.

Solution:
Allow nvim_get_option_value() to accept { tab = <tab-ID> } for 'cmdheight'.
This commit is contained in:
EliWiegman
2026-05-16 09:31:05 -04:00
committed by GitHub
parent b92ca58d69
commit 2d795face6
6 changed files with 120 additions and 18 deletions

View File

@@ -3559,6 +3559,8 @@ nvim_get_option_value({name}, {opts}) *nvim_get_option_value()*
autocommands for the corresponding filetype.
• scope: One of "global" or "local". Analogous to |:setglobal|
and |:setlocal|, respectively.
• tab: |tab-ID| for tab-local options. Currently only supports
"cmdheight". Tabpage `0` means the current tabpage.
• win: |window-ID|. Used for getting window local options.
Return: ~

View File

@@ -1552,6 +1552,8 @@ function vim.api.nvim_get_option_info2(name, opts) end
--- autocommands for the corresponding filetype.
--- - scope: One of "global" or "local". Analogous to
--- `:setglobal` and `:setlocal`, respectively.
--- - tab: `tab-ID` for tab-local options. Currently only
--- supports "cmdheight". Tabpage `0` means the current tabpage.
--- - win: `window-ID`. Used for getting window local options.
--- @return any # Option value
function vim.api.nvim_get_option_value(name, opts) end

View File

@@ -381,6 +381,7 @@ error('Cannot require a meta file')
--- @field buf? integer
--- @field filetype? string
--- @field scope? string
--- @field tab? integer
--- @field win? integer
--- @class vim.api.keyset.redraw

View File

@@ -168,6 +168,7 @@ typedef struct {
String scope;
Window win;
Buffer buf;
Tabpage tab;
String filetype;
} Dict(option);

View File

@@ -17,6 +17,7 @@
#include "nvim/memory.h"
#include "nvim/memory_defs.h"
#include "nvim/option.h"
#include "nvim/option_vars.h"
#include "nvim/types_defs.h"
#include "nvim/vim_defs.h"
#include "nvim/window.h"
@@ -25,9 +26,33 @@
static int validate_option_value_args(Dict(option) *opts, char *name, OptIndex *opt_idxp,
int *opt_flags, OptScope *scope, void **from, char **filetype,
Error *err)
tabpage_T **opt_tabp, Error *err)
{
#define HAS_KEY_X(d, v) HAS_KEY(d, option, v)
assert(opt_tabp != NULL);
*opt_tabp = NULL;
// Validate incompatible argument combinations first, then resolve handles and scope.
if (HAS_KEY_X(opts, filetype)) {
VALIDATE_CON(!HAS_KEY_X(opts, scope) && !HAS_KEY_X(opts, buf)
&& !HAS_KEY_X(opts, win) && !HAS_KEY_X(opts, tab),
"filetype", "'scope', 'buf', 'win' or 'tab'", {
return FAIL;
});
}
if (HAS_KEY_X(opts, tab)) {
VALIDATE_CON(!HAS_KEY_X(opts, win) && !HAS_KEY_X(opts, buf)
&& !HAS_KEY_X(opts, filetype) && !HAS_KEY_X(opts, scope),
"tab", "'win', 'buf', 'filetype' or 'scope'", {
return FAIL;
});
}
VALIDATE_CON(!(HAS_KEY_X(opts, win) && HAS_KEY_X(opts, buf)), "buf", "win", {
return FAIL;
});
if (HAS_KEY_X(opts, scope)) {
if (!strcmp(opts->scope.data, "local")) {
*opt_flags = OPT_LOCAL;
@@ -55,8 +80,7 @@ static int validate_option_value_args(Dict(option) *opts, char *name, OptIndex *
}
if (HAS_KEY_X(opts, buf)) {
VALIDATE(!(HAS_KEY_X(opts, scope) && *opt_flags == OPT_GLOBAL), "%s",
"cannot use both global 'scope' and 'buf'", {
VALIDATE_CON(!(HAS_KEY_X(opts, scope) && *opt_flags == OPT_GLOBAL), "buf", "global scope", {
return FAIL;
});
*opt_flags = OPT_LOCAL;
@@ -67,23 +91,27 @@ static int validate_option_value_args(Dict(option) *opts, char *name, OptIndex *
}
}
VALIDATE((!HAS_KEY_X(opts, filetype)
|| !(HAS_KEY_X(opts, buf) || HAS_KEY_X(opts, scope) || HAS_KEY_X(opts, win))),
"%s", "cannot use 'filetype' with 'scope', 'buf' or 'win'", {
return FAIL;
});
VALIDATE((!HAS_KEY_X(opts, win) || !HAS_KEY_X(opts, buf)),
"%s", "cannot use both 'buf' and 'win'", {
return FAIL;
});
if (HAS_KEY_X(opts, tab)) {
*opt_tabp = find_tab_by_handle(opts->tab, err);
if (ERROR_SET(err)) {
return FAIL;
}
}
*opt_idxp = find_option(name);
if (*opt_idxp == kOptInvalid) {
// unknown option
api_set_error(err, kErrorTypeValidation, "Unknown option '%s'", name);
} else if (*scope == kOptScopeBuf || *scope == kOptScopeWin) {
// if 'buf' or 'win' is passed, make sure the option supports it
return FAIL;
}
if (*opt_tabp != NULL && *opt_idxp != kOptCmdheight) {
api_set_error(err, kErrorTypeValidation, "'tab' can only be used with option 'cmdheight'");
return FAIL;
}
// If 'buf' or 'win' is passed, make sure the option supports it.
if (*scope == kOptScopeBuf || *scope == kOptScopeWin) {
if (!option_has_scope(*opt_idxp, *scope)) {
char *tgt = *scope == kOptScopeBuf ? "buf" : "win";
char *global = option_has_scope(*opt_idxp, kOptScopeGlobal) ? "global " : "";
@@ -93,6 +121,7 @@ static int validate_option_value_args(Dict(option) *opts, char *name, OptIndex *
api_set_error(err, kErrorTypeValidation, "'%s' cannot be passed for %s%soption '%s'",
tgt, global, req, name);
return FAIL;
}
}
@@ -194,6 +223,8 @@ static void wipe_ft_buf(buf_T *buf)
/// autocommands for the corresponding filetype.
/// - scope: One of "global" or "local". Analogous to
/// |:setglobal| and |:setlocal|, respectively.
/// - tab: |tab-ID| for tab-local options. Currently only
/// supports "cmdheight". Tabpage `0` means the current tabpage.
/// - win: |window-ID|. Used for getting window local options.
/// @param[out] err Error details, if any
/// @return Option value
@@ -205,12 +236,18 @@ Object nvim_get_option_value(String name, Dict(option) *opts, Error *err)
OptScope scope = kOptScopeGlobal;
void *from = NULL;
char *filetype = NULL;
tabpage_T *opt_tab = NULL;
if (!validate_option_value_args(opts, name.data, &opt_idx, &opt_flags, &scope, &from,
&filetype, err)) {
&filetype, &opt_tab, err)) {
return (Object)OBJECT_INIT;
}
if (opt_tab != NULL) {
const OptInt ch = opt_tab == curtab ? p_ch : opt_tab->tp_ch_used;
return optval_as_object(NUMBER_OPTVAL(ch));
}
aco_save_T aco;
bool aco_used;
@@ -273,12 +310,19 @@ void nvim_set_option_value(uint64_t channel_id, String name, Object value, Dict(
Error *err)
FUNC_API_SINCE(9)
{
// TODO(EliWiegman): support tab-local option setting (only nvim_get_option_value supports `tab`).
VALIDATE_CON(!(opts && HAS_KEY(opts, option, tab)),
"tab", "nvim_set_option_value", {
return;
});
OptIndex opt_idx = 0;
int opt_flags = 0;
OptScope scope = kOptScopeGlobal;
void *to = NULL;
tabpage_T *opt_tab = NULL;
if (!validate_option_value_args(opts, name.data, &opt_idx, &opt_flags, &scope, &to, NULL,
err)) {
&opt_tab, err)) {
return;
}
@@ -361,12 +405,18 @@ DictAs(get_option_info) nvim_get_option_info2(String name, Dict(option) *opts, A
Error *err)
FUNC_API_SINCE(11)
{
VALIDATE_CON(!(opts && HAS_KEY(opts, option, tab)),
"tab", "nvim_get_option_info2", {
return (Dict)ARRAY_DICT_INIT;
});
OptIndex opt_idx = 0;
int opt_flags = 0;
OptScope scope = kOptScopeGlobal;
void *from = NULL;
tabpage_T *opt_tab = NULL;
if (!validate_option_value_args(opts, name.data, &opt_idx, &opt_flags, &scope, &from, NULL,
err)) {
&opt_tab, err)) {
return (Dict)ARRAY_DICT_INIT;
}

View File

@@ -1952,6 +1952,52 @@ describe('API', function()
'Invalid value for option \'scrolloff\': expected number, got string "wrong"',
pcall_err(api.nvim_set_option_value, 'scrolloff', 'wrong', {})
)
local tab1 = api.nvim_get_current_tabpage()
eq(
"Conflict: 'tab' not allowed with 'win', 'buf', 'filetype' or 'scope'",
pcall_err(
api.nvim_get_option_value,
'cmdheight',
{ tab = tab1, win = api.nvim_get_current_win() }
)
)
eq(
"Conflict: 'tab' not allowed with 'win', 'buf', 'filetype' or 'scope'",
pcall_err(api.nvim_get_option_value, 'cmdheight', {
tab = tab1,
scope = 'local',
})
)
eq(
"'tab' can only be used with option 'cmdheight'",
pcall_err(api.nvim_get_option_value, 'shiftwidth', { tab = tab1 })
)
eq(
"Conflict: 'tab' not allowed with 'nvim_set_option_value'",
pcall_err(api.nvim_set_option_value, 'cmdheight', 2, { tab = tab1 })
)
eq(
"Conflict: 'tab' not allowed with 'nvim_get_option_info2'",
pcall_err(api.nvim_get_option_info2, 'cmdheight', { tab = tab1 })
)
eq(
"Conflict: 'filetype' not allowed with 'scope', 'buf', 'win' or 'tab'",
pcall_err(api.nvim_get_option_value, 'cmdheight', { filetype = 'c', tab = 0 })
)
end)
it("tabpage-local 'cmdheight' #31140", function()
api.nvim_set_option_value('cmdheight', 1, {})
local tab1 = api.nvim_get_current_tabpage()
eq(1, api.nvim_get_option_value('cmdheight', { tab = 0 }))
eq(1, api.nvim_get_option_value('cmdheight', { tab = tab1 }))
eq(1, api.nvim_get_option_value('cmdheight', {}))
command('tabnew')
local tab2 = api.nvim_get_current_tabpage()
api.nvim_set_option_value('cmdheight', 4, {})
eq(4, api.nvim_get_option_value('cmdheight', { tab = tab2 }))
eq(1, api.nvim_get_option_value('cmdheight', { tab = tab1 }))
eq(4, api.nvim_get_option_value('cmdheight', {}))
end)
it('can get local values when global value is set', function()