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. autocommands for the corresponding filetype.
• scope: One of "global" or "local". Analogous to |:setglobal| • scope: One of "global" or "local". Analogous to |:setglobal|
and |:setlocal|, respectively. 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. • win: |window-ID|. Used for getting window local options.
Return: ~ Return: ~

View File

@@ -1552,6 +1552,8 @@ function vim.api.nvim_get_option_info2(name, opts) end
--- autocommands for the corresponding filetype. --- autocommands for the corresponding filetype.
--- - scope: One of "global" or "local". Analogous to --- - scope: One of "global" or "local". Analogous to
--- `:setglobal` and `:setlocal`, respectively. --- `: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. --- - win: `window-ID`. Used for getting window local options.
--- @return any # Option value --- @return any # Option value
function vim.api.nvim_get_option_value(name, opts) end 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 buf? integer
--- @field filetype? string --- @field filetype? string
--- @field scope? string --- @field scope? string
--- @field tab? integer
--- @field win? integer --- @field win? integer
--- @class vim.api.keyset.redraw --- @class vim.api.keyset.redraw

View File

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

View File

@@ -17,6 +17,7 @@
#include "nvim/memory.h" #include "nvim/memory.h"
#include "nvim/memory_defs.h" #include "nvim/memory_defs.h"
#include "nvim/option.h" #include "nvim/option.h"
#include "nvim/option_vars.h"
#include "nvim/types_defs.h" #include "nvim/types_defs.h"
#include "nvim/vim_defs.h" #include "nvim/vim_defs.h"
#include "nvim/window.h" #include "nvim/window.h"
@@ -25,9 +26,33 @@
static int validate_option_value_args(Dict(option) *opts, char *name, OptIndex *opt_idxp, static int validate_option_value_args(Dict(option) *opts, char *name, OptIndex *opt_idxp,
int *opt_flags, OptScope *scope, void **from, char **filetype, 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) #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 (HAS_KEY_X(opts, scope)) {
if (!strcmp(opts->scope.data, "local")) { if (!strcmp(opts->scope.data, "local")) {
*opt_flags = OPT_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)) { if (HAS_KEY_X(opts, buf)) {
VALIDATE(!(HAS_KEY_X(opts, scope) && *opt_flags == OPT_GLOBAL), "%s", VALIDATE_CON(!(HAS_KEY_X(opts, scope) && *opt_flags == OPT_GLOBAL), "buf", "global scope", {
"cannot use both global 'scope' and 'buf'", {
return FAIL; return FAIL;
}); });
*opt_flags = OPT_LOCAL; *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) if (HAS_KEY_X(opts, tab)) {
|| !(HAS_KEY_X(opts, buf) || HAS_KEY_X(opts, scope) || HAS_KEY_X(opts, win))), *opt_tabp = find_tab_by_handle(opts->tab, err);
"%s", "cannot use 'filetype' with 'scope', 'buf' or 'win'", { if (ERROR_SET(err)) {
return FAIL; return FAIL;
}); }
}
VALIDATE((!HAS_KEY_X(opts, win) || !HAS_KEY_X(opts, buf)),
"%s", "cannot use both 'buf' and 'win'", {
return FAIL;
});
*opt_idxp = find_option(name); *opt_idxp = find_option(name);
if (*opt_idxp == kOptInvalid) { if (*opt_idxp == kOptInvalid) {
// unknown option // unknown option
api_set_error(err, kErrorTypeValidation, "Unknown option '%s'", name); api_set_error(err, kErrorTypeValidation, "Unknown option '%s'", name);
} else if (*scope == kOptScopeBuf || *scope == kOptScopeWin) { return FAIL;
// if 'buf' or 'win' is passed, make sure the option supports it }
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)) { if (!option_has_scope(*opt_idxp, *scope)) {
char *tgt = *scope == kOptScopeBuf ? "buf" : "win"; char *tgt = *scope == kOptScopeBuf ? "buf" : "win";
char *global = option_has_scope(*opt_idxp, kOptScopeGlobal) ? "global " : ""; 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'", api_set_error(err, kErrorTypeValidation, "'%s' cannot be passed for %s%soption '%s'",
tgt, global, req, name); tgt, global, req, name);
return FAIL;
} }
} }
@@ -194,6 +223,8 @@ static void wipe_ft_buf(buf_T *buf)
/// autocommands for the corresponding filetype. /// autocommands for the corresponding filetype.
/// - scope: One of "global" or "local". Analogous to /// - scope: One of "global" or "local". Analogous to
/// |:setglobal| and |:setlocal|, respectively. /// |: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. /// - win: |window-ID|. Used for getting window local options.
/// @param[out] err Error details, if any /// @param[out] err Error details, if any
/// @return Option value /// @return Option value
@@ -205,12 +236,18 @@ Object nvim_get_option_value(String name, Dict(option) *opts, Error *err)
OptScope scope = kOptScopeGlobal; OptScope scope = kOptScopeGlobal;
void *from = NULL; void *from = NULL;
char *filetype = NULL; char *filetype = NULL;
tabpage_T *opt_tab = NULL;
if (!validate_option_value_args(opts, name.data, &opt_idx, &opt_flags, &scope, &from, if (!validate_option_value_args(opts, name.data, &opt_idx, &opt_flags, &scope, &from,
&filetype, err)) { &filetype, &opt_tab, err)) {
return (Object)OBJECT_INIT; 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; aco_save_T aco;
bool aco_used; bool aco_used;
@@ -273,12 +310,19 @@ void nvim_set_option_value(uint64_t channel_id, String name, Object value, Dict(
Error *err) Error *err)
FUNC_API_SINCE(9) 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; OptIndex opt_idx = 0;
int opt_flags = 0; int opt_flags = 0;
OptScope scope = kOptScopeGlobal; OptScope scope = kOptScopeGlobal;
void *to = NULL; void *to = NULL;
tabpage_T *opt_tab = NULL;
if (!validate_option_value_args(opts, name.data, &opt_idx, &opt_flags, &scope, &to, NULL, if (!validate_option_value_args(opts, name.data, &opt_idx, &opt_flags, &scope, &to, NULL,
err)) { &opt_tab, err)) {
return; return;
} }
@@ -361,12 +405,18 @@ DictAs(get_option_info) nvim_get_option_info2(String name, Dict(option) *opts, A
Error *err) Error *err)
FUNC_API_SINCE(11) 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; OptIndex opt_idx = 0;
int opt_flags = 0; int opt_flags = 0;
OptScope scope = kOptScopeGlobal; OptScope scope = kOptScopeGlobal;
void *from = NULL; void *from = NULL;
tabpage_T *opt_tab = NULL;
if (!validate_option_value_args(opts, name.data, &opt_idx, &opt_flags, &scope, &from, 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; return (Dict)ARRAY_DICT_INIT;
} }

View File

@@ -1952,6 +1952,52 @@ describe('API', function()
'Invalid value for option \'scrolloff\': expected number, got string "wrong"', 'Invalid value for option \'scrolloff\': expected number, got string "wrong"',
pcall_err(api.nvim_set_option_value, 'scrolloff', '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) end)
it('can get local values when global value is set', function() it('can get local values when global value is set', function()