From 2d795face6e488c17ad4532d0703716b8acaf999 Mon Sep 17 00:00:00 2001 From: EliWiegman Date: Sat, 16 May 2026 09:31:05 -0400 Subject: [PATCH] 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 = } for 'cmdheight'. --- runtime/doc/api.txt | 2 + runtime/lua/vim/_meta/api.gen.lua | 2 + runtime/lua/vim/_meta/api_keysets.gen.lua | 1 + src/nvim/api/keysets_defs.h | 1 + src/nvim/api/options.c | 86 ++++++++++++++++++----- test/functional/api/vim_spec.lua | 46 ++++++++++++ 6 files changed, 120 insertions(+), 18 deletions(-) diff --git a/runtime/doc/api.txt b/runtime/doc/api.txt index 6ca05b2503..3c809f9665 100644 --- a/runtime/doc/api.txt +++ b/runtime/doc/api.txt @@ -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: ~ diff --git a/runtime/lua/vim/_meta/api.gen.lua b/runtime/lua/vim/_meta/api.gen.lua index 016d68d872..1fa5776bc5 100644 --- a/runtime/lua/vim/_meta/api.gen.lua +++ b/runtime/lua/vim/_meta/api.gen.lua @@ -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 diff --git a/runtime/lua/vim/_meta/api_keysets.gen.lua b/runtime/lua/vim/_meta/api_keysets.gen.lua index 5aa5a3e63f..dea2a70507 100644 --- a/runtime/lua/vim/_meta/api_keysets.gen.lua +++ b/runtime/lua/vim/_meta/api_keysets.gen.lua @@ -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 diff --git a/src/nvim/api/keysets_defs.h b/src/nvim/api/keysets_defs.h index eb2f809ab2..399e5aa6bb 100644 --- a/src/nvim/api/keysets_defs.h +++ b/src/nvim/api/keysets_defs.h @@ -168,6 +168,7 @@ typedef struct { String scope; Window win; Buffer buf; + Tabpage tab; String filetype; } Dict(option); diff --git a/src/nvim/api/options.c b/src/nvim/api/options.c index bf4813d9ee..894baf9f39 100644 --- a/src/nvim/api/options.c +++ b/src/nvim/api/options.c @@ -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; } diff --git a/test/functional/api/vim_spec.lua b/test/functional/api/vim_spec.lua index 581aaaa301..5461af197b 100644 --- a/test/functional/api/vim_spec.lua +++ b/test/functional/api/vim_spec.lua @@ -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()