diff --git a/runtime/doc/api.txt b/runtime/doc/api.txt index 3c809f9665..663d7cb4e3 100644 --- a/runtime/doc/api.txt +++ b/runtime/doc/api.txt @@ -3514,7 +3514,7 @@ nvim_get_option_info2({name}, {opts}) *nvim_get_option_info2()* • last_set_sid: Last set script id (if any) • last_set_linenr: line number where option was set • last_set_chan: Channel where option was set (0 for local) - • scope: one of "global", "win", or "buf" + • scope: one of "global", "win", "buf", or "tab" • global_local: whether win or buf option has a global value • commalist: List of comma separated values • flaglist: List of single char flags @@ -3584,6 +3584,10 @@ nvim_set_option_value({name}, {value}, {opts}) • buf: Buffer number. Used for setting buffer local option. • scope: One of "global" or "local". Analogous to |:setglobal| and |:setlocal|, respectively. + • tab: |tab-ID| for tab-local options (currently only + 'cmdheight'). Tabpage 0 means the current tabpage. If a + non-current tab is given, the value will take effect when + it is switched-to. • win: |window-ID|. Used for setting window local option. diff --git a/runtime/doc/news.txt b/runtime/doc/news.txt index 3b20154226..9193eb801a 100644 --- a/runtime/doc/news.txt +++ b/runtime/doc/news.txt @@ -117,6 +117,12 @@ API • |nvim_echo()| distinguishes zero percent from omitted percent for Progress events. • |nvim_create_user_command()| accepts `desc` for Vimscript commands. +• Support for tabpage-local options ('cmdheight'): + • |gettabvar()| + • |gettabwinvar()| + • |nvim_get_option_info2()| + • |nvim_get_option_value()| + • |nvim_set_option_value()| BUILD diff --git a/runtime/doc/vimfn.txt b/runtime/doc/vimfn.txt index e90c93dec0..977aca65a3 100644 --- a/runtime/doc/vimfn.txt +++ b/runtime/doc/vimfn.txt @@ -3246,22 +3246,25 @@ getbufvar({buf}, {varname} [, {def}]) *getbufvar()* {buf} to a bufnr; option names use |nvim_get_option_value()| or |vim.bo|. - The result is the value of option or local buffer variable - {varname} in buffer {buf}. Note that the name without "b:" - must be used. - The {varname} argument is a string. - When {varname} is empty returns a |Dictionary| with all the - buffer-local variables. - When {varname} is equal to "&" returns a |Dictionary| with all - the buffer-local options. - Otherwise, when {varname} starts with "&" returns the value of - a buffer-local option. - This also works for a global or buffer-local option, but it - doesn't work for a global variable, window-local variable or - window-local option. - For the use of {buf}, see |bufname()| above. - When the buffer or variable doesn't exist {def} or an empty - string is returned, there is no error message. + Gets the value of a buffer-local variable or option {varname} + in buffer {buf}. + + {varname} is a string: + - Name of the variable (without "b:"). + - If empty, gets a |Dictionary| of all buffer-local variables. + - If "&", gets a |Dictionary| of all buffer-local options. + - If it starts with "&", gets the value of a buffer-local + option. + + {buf} has the same form as in |bufname()|. + + Also works for a global or buffer-local option. But not for + a global variable, window-local variable or window-local + option. + + When the buffer or variable doesn't exist, {def} or an empty + string is returned; there is no error. + Examples: >vim let bufmodified = getbufvar(1, "&mod") echo "todo myvar = " .. getbufvar("todo", "myvar") @@ -4555,29 +4558,34 @@ gettabwinvar({tabnr}, {winnr}, {varname} [, {def}]) *gettabwinvar()* {tabnr} and {winnr} to a winid; option names use |nvim_get_option_value()| or |vim.wo|. - Get the value of window-local variable {varname} in window - {winnr} in tabpage {tabnr}. - The {varname} argument is a string. When {varname} is empty a - dictionary with all window-local variables is returned. - When {varname} is equal to "&" get the values of all - window-local options in a |Dictionary|. - Otherwise, when {varname} starts with "&" get the value of a - window-local option. - Note that {varname} must be the name without "w:". - Tabs are numbered starting with one. For the current tabpage - use |getwinvar()|. - {winnr} is a |window-number| or |window-ID|. + Gets the value of window-local variable {varname} in {winnr} + (|window-number| or |window-ID|) in |tabpage-number| {tabnr}. + + {varname} is a string: + - Name of the variable (without "w:"). + - If empty, gets a dictionary with all window-local variables. + - If "&", gets the values of all window-local options in + a |Dictionary|. + - If it starts with "&", gets the value of a window-local + option. + + To get window-local variables in the current tabpage use + |getwinvar()|. + When {winnr} is zero the current window is used. - This also works for a global option, buffer-local option and - window-local option, but it doesn't work for a global variable - or buffer-local variable. - When the tab, window or variable doesn't exist {def} or an - empty string is returned, there is no error message. + + Also works for a global option, buffer-local option, + window-local option, and tab-local option ('cmdheight'). + But not for a global variable or buffer-local variable. + + When the tab, window or variable doesn't exist, {def} or an + empty string is returned; there is no error. + Examples: >vim let list_is_on = gettabwinvar(1, 2, '&list') echo "myvar = " .. gettabwinvar(3, 1, 'myvar') < - To obtain all window-local variables use: >vim + To get all window-local variables: >vim gettabwinvar({tabnr}, {winnr}, '&') < diff --git a/runtime/lua/vim/_meta/api.gen.lua b/runtime/lua/vim/_meta/api.gen.lua index 1fa5776bc5..b9cc487448 100644 --- a/runtime/lua/vim/_meta/api.gen.lua +++ b/runtime/lua/vim/_meta/api.gen.lua @@ -1516,7 +1516,7 @@ function vim.api.nvim_get_option_info(name) end --- - last_set_linenr: line number where option was set --- - last_set_chan: Channel where option was set (0 for local) --- ---- - scope: one of "global", "win", or "buf" +--- - scope: one of "global", "win", "buf", or "tab" --- - global_local: whether win or buf option has a global value --- --- - commalist: List of comma separated values @@ -2336,6 +2336,9 @@ function vim.api.nvim_set_option(name, value) end --- - buf: Buffer number. Used for setting buffer local option. --- - scope: One of "global" or "local". Analogous to --- `:setglobal` and `:setlocal`, respectively. +--- - tab: `tab-ID` for tab-local options (currently only 'cmdheight'). Tabpage 0 +--- means the current tabpage. If a non-current tab is given, the value will take +--- effect when it is switched-to. --- - win: `window-ID`. Used for setting window local option. function vim.api.nvim_set_option_value(name, value, opts) end diff --git a/runtime/lua/vim/_meta/api_keysets_extra.lua b/runtime/lua/vim/_meta/api_keysets_extra.lua index 7ba00bed31..6282f84b77 100644 --- a/runtime/lua/vim/_meta/api_keysets_extra.lua +++ b/runtime/lua/vim/_meta/api_keysets_extra.lua @@ -200,7 +200,7 @@ error('Cannot require a meta file') --- @class vim.api.keyset.get_option_info --- @field name string --- @field shortname string ---- @field scope 'buf'|'win'|'global' +--- @field scope 'buf'|'win'|'global'|'tab' --- @field global_local boolean --- @field commalist boolean --- @field flaglist boolean diff --git a/runtime/lua/vim/_meta/vimfn.gen.lua b/runtime/lua/vim/_meta/vimfn.gen.lua index 32f172943a..b7184207d2 100644 --- a/runtime/lua/vim/_meta/vimfn.gen.lua +++ b/runtime/lua/vim/_meta/vimfn.gen.lua @@ -2884,22 +2884,25 @@ function vim.fn.getbufoneline(buf, lnum) end --- Lua: Prefer |nvim_buf_get_var()| or |vim.b| after resolving {buf} to a bufnr; option names use |nvim_get_option_value()| or |vim.bo|. --- ---- The result is the value of option or local buffer variable ---- {varname} in buffer {buf}. Note that the name without "b:" ---- must be used. ---- The {varname} argument is a string. ---- When {varname} is empty returns a |Dictionary| with all the ---- buffer-local variables. ---- When {varname} is equal to "&" returns a |Dictionary| with all ---- the buffer-local options. ---- Otherwise, when {varname} starts with "&" returns the value of ---- a buffer-local option. ---- This also works for a global or buffer-local option, but it ---- doesn't work for a global variable, window-local variable or ---- window-local option. ---- For the use of {buf}, see |bufname()| above. ---- When the buffer or variable doesn't exist {def} or an empty ---- string is returned, there is no error message. +--- Gets the value of a buffer-local variable or option {varname} +--- in buffer {buf}. +--- +--- {varname} is a string: +--- - Name of the variable (without "b:"). +--- - If empty, gets a |Dictionary| of all buffer-local variables. +--- - If "&", gets a |Dictionary| of all buffer-local options. +--- - If it starts with "&", gets the value of a buffer-local +--- option. +--- +--- {buf} has the same form as in |bufname()|. +--- +--- Also works for a global or buffer-local option. But not for +--- a global variable, window-local variable or window-local +--- option. +--- +--- When the buffer or variable doesn't exist, {def} or an empty +--- string is returned; there is no error. +--- --- Examples: >vim --- let bufmodified = getbufvar(1, "&mod") --- echo "todo myvar = " .. getbufvar("todo", "myvar") @@ -4090,29 +4093,34 @@ function vim.fn.gettabvar(tabnr, varname, def) end --- Lua: Prefer |nvim_win_get_var()| or |vim.w| after resolving {tabnr} and {winnr} to a winid; option names use |nvim_get_option_value()| or |vim.wo|. --- ---- Get the value of window-local variable {varname} in window ---- {winnr} in tabpage {tabnr}. ---- The {varname} argument is a string. When {varname} is empty a ---- dictionary with all window-local variables is returned. ---- When {varname} is equal to "&" get the values of all ---- window-local options in a |Dictionary|. ---- Otherwise, when {varname} starts with "&" get the value of a ---- window-local option. ---- Note that {varname} must be the name without "w:". ---- Tabs are numbered starting with one. For the current tabpage ---- use |getwinvar()|. ---- {winnr} is a |window-number| or |window-ID|. +--- Gets the value of window-local variable {varname} in {winnr} +--- (|window-number| or |window-ID|) in |tabpage-number| {tabnr}. +--- +--- {varname} is a string: +--- - Name of the variable (without "w:"). +--- - If empty, gets a dictionary with all window-local variables. +--- - If "&", gets the values of all window-local options in +--- a |Dictionary|. +--- - If it starts with "&", gets the value of a window-local +--- option. +--- +--- To get window-local variables in the current tabpage use +--- |getwinvar()|. +--- --- When {winnr} is zero the current window is used. ---- This also works for a global option, buffer-local option and ---- window-local option, but it doesn't work for a global variable ---- or buffer-local variable. ---- When the tab, window or variable doesn't exist {def} or an ---- empty string is returned, there is no error message. +--- +--- Also works for a global option, buffer-local option, +--- window-local option, and tab-local option ('cmdheight'). +--- But not for a global variable or buffer-local variable. +--- +--- When the tab, window or variable doesn't exist, {def} or an +--- empty string is returned; there is no error. +--- --- Examples: >vim --- let list_is_on = gettabwinvar(1, 2, '&list') --- echo "myvar = " .. gettabwinvar(3, 1, 'myvar') --- < ---- To obtain all window-local variables use: >vim +--- To get all window-local variables: >vim --- gettabwinvar({tabnr}, {winnr}, '&') --- < --- diff --git a/src/nvim/api/options.c b/src/nvim/api/options.c index 41aedf99ec..4443b08e7c 100644 --- a/src/nvim/api/options.c +++ b/src/nvim/api/options.c @@ -90,6 +90,14 @@ static int validate_option_value_args(Dict(option) *opts, char *name, bool allow } } + if (HAS_KEY_X(opts, tab)) { + *scope = kOptScopeTab; + *from = find_tab_by_handle(opts->tab, err); + if (ERROR_SET(err)) { + return FAIL; + } + } + *opt_idxp = find_option(name); if (*opt_idxp == kOptInvalid) { // unknown option @@ -97,11 +105,9 @@ static int validate_option_value_args(Dict(option) *opts, char *name, bool allow return FAIL; } - // The only tab-local option is 'cmdheight'. - // TODO(justinmk): introduce kOptScopeTab into option metadata, then use option_has_scope below. - VALIDATE_CON(!HAS_KEY_X(opts, tab) || (*opt_idxp == kOptCmdheight), "tab", name, { - return FAIL; - }); + // Reject keys whose scope the option doesn't support. + VALIDATE_CON(!HAS_KEY_X(opts, tab) || option_has_scope(*opt_idxp, kOptScopeTab), + "tab", name, { return FAIL; }); // If 'buf' or 'win' is passed, make sure the option supports it. if (*scope == kOptScopeBuf || *scope == kOptScopeWin) { @@ -235,15 +241,6 @@ Object nvim_get_option_value(String name, Dict(option) *opts, Error *err) return (Object)OBJECT_INIT; } - if (HAS_KEY(opts, option, tab)) { - tabpage_T *tab = find_tab_by_handle(opts->tab, err); - if (ERROR_SET(err)) { - return (Object)OBJECT_INIT; - } - const OptInt ch = (tab == curtab) ? p_ch : tab->tp_ch_used; - return optval_as_object(NUMBER_OPTVAL(ch)); - } - aco_save_T aco; bool aco_used; @@ -300,6 +297,9 @@ err: /// - buf: Buffer number. Used for setting buffer local option. /// - scope: One of "global" or "local". Analogous to /// |:setglobal| and |:setlocal|, respectively. +/// - tab: |tab-ID| for tab-local options (currently only 'cmdheight'). Tabpage 0 +/// means the current tabpage. If a non-current tab is given, the value will take +/// effect when it is switched-to. /// - win: |window-ID|. Used for setting window local option. /// @param[out] err Error details, if any void nvim_set_option_value(uint64_t channel_id, String name, Object value, Dict(option) *opts, @@ -310,8 +310,7 @@ void nvim_set_option_value(uint64_t channel_id, String name, Object value, Dict( int opt_flags = 0; OptScope scope = kOptScopeGlobal; void *to = NULL; - // TODO(justinmk): support tab-local option. - if (!validate_option_value_args(opts, name.data, false, &opt_idx, &opt_flags, &scope, &to, NULL, + if (!validate_option_value_args(opts, name.data, true, &opt_idx, &opt_flags, &scope, &to, NULL, err)) { return; } @@ -371,7 +370,7 @@ Dict nvim_get_all_options_info(Arena *arena, Error *err) /// - last_set_linenr: line number where option was set /// - last_set_chan: Channel where option was set (0 for local) /// -/// - scope: one of "global", "win", or "buf" +/// - scope: one of "global", "win", "buf", or "tab" /// - global_local: whether win or buf option has a global value /// /// - commalist: List of comma separated values diff --git a/src/nvim/eval.lua b/src/nvim/eval.lua index 0fd3a584a0..e4018cc6ea 100644 --- a/src/nvim/eval.lua +++ b/src/nvim/eval.lua @@ -3593,22 +3593,25 @@ M.funcs = { args = { 2, 3 }, base = 1, desc = [=[ - The result is the value of option or local buffer variable - {varname} in buffer {buf}. Note that the name without "b:" - must be used. - The {varname} argument is a string. - When {varname} is empty returns a |Dictionary| with all the - buffer-local variables. - When {varname} is equal to "&" returns a |Dictionary| with all - the buffer-local options. - Otherwise, when {varname} starts with "&" returns the value of - a buffer-local option. - This also works for a global or buffer-local option, but it - doesn't work for a global variable, window-local variable or - window-local option. - For the use of {buf}, see |bufname()| above. - When the buffer or variable doesn't exist {def} or an empty - string is returned, there is no error message. + Gets the value of a buffer-local variable or option {varname} + in buffer {buf}. + + {varname} is a string: + - Name of the variable (without "b:"). + - If empty, gets a |Dictionary| of all buffer-local variables. + - If "&", gets a |Dictionary| of all buffer-local options. + - If it starts with "&", gets the value of a buffer-local + option. + + {buf} has the same form as in |bufname()|. + + Also works for a global or buffer-local option. But not for + a global variable, window-local variable or window-local + option. + + When the buffer or variable doesn't exist, {def} or an empty + string is returned; there is no error. + Examples: >vim let bufmodified = getbufvar(1, "&mod") echo "todo myvar = " .. getbufvar("todo", "myvar") @@ -5006,29 +5009,34 @@ M.funcs = { args = { 3, 4 }, base = 1, desc = [=[ - Get the value of window-local variable {varname} in window - {winnr} in tabpage {tabnr}. - The {varname} argument is a string. When {varname} is empty a - dictionary with all window-local variables is returned. - When {varname} is equal to "&" get the values of all - window-local options in a |Dictionary|. - Otherwise, when {varname} starts with "&" get the value of a - window-local option. - Note that {varname} must be the name without "w:". - Tabs are numbered starting with one. For the current tabpage - use |getwinvar()|. - {winnr} is a |window-number| or |window-ID|. + Gets the value of window-local variable {varname} in {winnr} + (|window-number| or |window-ID|) in |tabpage-number| {tabnr}. + + {varname} is a string: + - Name of the variable (without "w:"). + - If empty, gets a dictionary with all window-local variables. + - If "&", gets the values of all window-local options in + a |Dictionary|. + - If it starts with "&", gets the value of a window-local + option. + + To get window-local variables in the current tabpage use + |getwinvar()|. + When {winnr} is zero the current window is used. - This also works for a global option, buffer-local option and - window-local option, but it doesn't work for a global variable - or buffer-local variable. - When the tab, window or variable doesn't exist {def} or an - empty string is returned, there is no error message. + + Also works for a global option, buffer-local option, + window-local option, and tab-local option ('cmdheight'). + But not for a global variable or buffer-local variable. + + When the tab, window or variable doesn't exist, {def} or an + empty string is returned; there is no error. + Examples: >vim let list_is_on = gettabwinvar(1, 2, '&list') echo "myvar = " .. gettabwinvar(3, 1, 'myvar') < - To obtain all window-local variables use: >vim + To get all window-local variables: >vim gettabwinvar({tabnr}, {winnr}, '&') < ]=], diff --git a/src/nvim/math.h b/src/nvim/math.h index 59be5461a1..3cd74dc946 100644 --- a/src/nvim/math.h +++ b/src/nvim/math.h @@ -1,12 +1,5 @@ #pragma once -#include #include -/// Check if number is a power of two -static inline bool is_power_of_two(uint64_t x) -{ - return x != 0 && ((x & (x - 1)) == 0); -} - #include "math.h.generated.h" diff --git a/src/nvim/option.c b/src/nvim/option.c index bd9ad6fe91..586bf0ddf9 100644 --- a/src/nvim/option.c +++ b/src/nvim/option.c @@ -74,7 +74,6 @@ #include "nvim/lua/executor.h" #include "nvim/macros_defs.h" #include "nvim/mapping.h" -#include "nvim/math.h" #include "nvim/mbyte.h" #include "nvim/memfile.h" #include "nvim/memline.h" @@ -3559,28 +3558,37 @@ bool option_has_scope(OptIndex opt_idx, OptScope scope) return get_option(opt_idx)->scope_flags & (1 << scope); } -/// Check if option is global-local. +/// Check if option is global-local (has global AND buffer/window scope). +/// Tab scope is independent and does not make an option "global-local". static inline bool option_is_global_local(OptIndex opt_idx) { - // Global-local options have at least two types, so their type flag cannot be a power of two. - return opt_idx != kOptInvalid && !is_power_of_two(options[opt_idx].scope_flags); -} - -/// Check if option only supports global scope. -static inline bool option_is_global_only(OptIndex opt_idx) -{ - // For an option to be global-only, it has to only have a single scope, which means the scope - // flags must be a power of two, and it must have the global scope. - return opt_idx != kOptInvalid && is_power_of_two(options[opt_idx].scope_flags) + if (opt_idx == kOptInvalid) { + return false; + } + const OptScopeFlags bw = (1 << kOptScopeBuf) | (1 << kOptScopeWin); + return (options[opt_idx].scope_flags & bw) != 0 && option_has_scope(opt_idx, kOptScopeGlobal); } -/// Check if option only supports window scope. +/// Check if option only supports global scope (ignoring tab scope, which is independent). +static inline bool option_is_global_only(OptIndex opt_idx) +{ + if (opt_idx == kOptInvalid) { + return false; + } + const OptScopeFlags bw = (1 << kOptScopeBuf) | (1 << kOptScopeWin); + return (options[opt_idx].scope_flags & bw) == 0 + && option_has_scope(opt_idx, kOptScopeGlobal); +} + +/// Check if option only supports window scope (ignoring tab scope, which is independent). static inline bool option_is_window_local(OptIndex opt_idx) { - // For an option to be window-local it has to only have a single scope, which means the scope - // flags must be a power of two, and it must have the window scope. - return opt_idx != kOptInvalid && is_power_of_two(options[opt_idx].scope_flags) + if (opt_idx == kOptInvalid) { + return false; + } + const OptScopeFlags exclude = (1 << kOptScopeGlobal) | (1 << kOptScopeBuf); + return (options[opt_idx].scope_flags & exclude) == 0 && option_has_scope(opt_idx, kOptScopeWin); } @@ -4073,6 +4081,10 @@ void set_option_direct_for(OptIndex opt_idx, OptVal value, int opt_flags, scid_T case kOptScopeBuf: curbuf = (buf_T *)from; break; + case kOptScopeTab: + // No context to switch; tab-scoped direct-set isn't used. Caller should route through + // set_option_value_for() instead. + abort(); } set_option_direct(opt_idx, value, opt_flags, set_sid); @@ -4156,12 +4168,18 @@ void set_option_value_give_err(const OptIndex opt_idx, OptVal value, int opt_fla } } -/// Switch current context to get/set option value for window/buffer. +/// Switch current context to get/set option value for the target window/buffer/tabpage. /// -/// @param[out] ctx Current context. switchwin_T for window and aco_save_T for buffer. -/// @param scope Option scope. See OptScope in option.h. -/// @param[in] from Target buffer/window. -/// @param[out] err Error message, if any. +/// Tab-scope switch is GET-only (SET requires side-effects, see set_option_value_for()). +/// +/// @param[out] ctx Switched-from context, restored by restore_option_context(): +/// - kOptScopeWin: switchwin_T +/// - kOptScopeBuf: aco_save_T +/// - kOptScopeTab: tabpage_T * (saved curtab) +/// - kOptScopeGlobal: unused +/// @param scope Option scope. See OptScope in option.h. +/// @param[in] from Target win_T/buf_T/tabpage_T. +/// @param[out] err Error message, if any. /// /// @return true if context was switched, false otherwise. static bool switch_option_context(void *const ctx, OptScope scope, void *const from, Error *err) @@ -4177,8 +4195,7 @@ static bool switch_option_context(void *const ctx, OptScope scope, void *const f return false; } - if (switch_win_noblock(switchwin, win, win_find_tabpage(win), true) - == FAIL) { + if (FAIL == switch_win_noblock(switchwin, win, win_find_tabpage(win), true)) { restore_win_noblock(switchwin, true); if (ERROR_SET(err)) { @@ -4199,6 +4216,20 @@ static bool switch_option_context(void *const ctx, OptScope scope, void *const f aucmd_prepbuf(aco, buf); return true; } + case kOptScopeTab: { + // GET-only: swap curtab so get_option_value() reads the target tab's stored value (p_ch). + // SET on a non-current tab cannot use this path, see comment in set_option_value_for(). + tabpage_T *const tab = (tabpage_T *)from; + tabpage_T **const saved_curtab = (tabpage_T **)ctx; + + if (tab == curtab) { + return false; + } + *saved_curtab = curtab; + unuse_tabpage(curtab); + use_tabpage(tab); + return true; + } } UNREACHABLE; } @@ -4216,6 +4247,12 @@ static void restore_option_context(void *const ctx, OptScope scope) case kOptScopeBuf: aucmd_restbuf((aco_save_T *)ctx); break; + case kOptScopeTab: { + tabpage_T *const saved_curtab = *(tabpage_T **)ctx; + unuse_tabpage(curtab); + use_tabpage(saved_curtab); + break; + } } } @@ -4235,8 +4272,11 @@ OptVal get_option_value_for(OptIndex opt_idx, int opt_flags, const OptScope scop { switchwin_T switchwin; aco_save_T aco; + tabpage_T *swtab = NULL; void *ctx = scope == kOptScopeWin ? (void *)&switchwin - : (scope == kOptScopeBuf ? (void *)&aco : NULL); + : scope == kOptScopeBuf ? (void *)&aco + : scope == kOptScopeTab ? (void *)&swtab + : NULL; bool switched = switch_option_context(ctx, scope, from, err); if (ERROR_SET(err)) { @@ -4265,10 +4305,29 @@ void set_option_value_for(const char *name, OptIndex opt_idx, OptVal value, cons const OptScope scope, void *const from, Error *err) FUNC_ATTR_NONNULL_ARG(1) { + // Special case: Tab scope (for NON-CURRENT tab) can't go through the normal "set" path: + // did_set_cmdheight() mutates globals not managed by use_tabpage()/unuse_tabpage(), which would + // leak across switch_option_context(). + // + // Workaround: set `tp_ch_used` directly; the cmdline will resize when the tab is switched-to. + if (scope == kOptScopeTab && (tabpage_T *)from != curtab) { + tabpage_T *const tab = (tabpage_T *)from; + assert(opt_idx == kOptCmdheight); + if (value.type != kOptValTypeNumber) { + api_set_error(err, kErrorTypeValidation, "'cmdheight' requires a Number"); + return; + } + tab->tp_ch_used = value.data.number; + return; + } + switchwin_T switchwin; aco_save_T aco; + tabpage_T *swtab = NULL; void *ctx = scope == kOptScopeWin ? (void *)&switchwin - : (scope == kOptScopeBuf ? (void *)&aco : NULL); + : scope == kOptScopeBuf ? (void *)&aco + : scope == kOptScopeTab ? (void *)&swtab + : NULL; bool switched = switch_option_context(ctx, scope, from, err); if (ERROR_SET(err)) { @@ -6686,6 +6745,8 @@ static Dict vimoption2dict(vimoption_T *opt, int opt_flags, buf_T *buf, win_T *w scope = "buf"; } else if (option_has_scope(opt_idx, kOptScopeWin)) { scope = "win"; + } else if (option_has_scope(opt_idx, kOptScopeTab)) { + scope = "tab"; } else { scope = "global"; } diff --git a/src/nvim/option_defs.h b/src/nvim/option_defs.h index 331b0756e2..3c5fba0e55 100644 --- a/src/nvim/option_defs.h +++ b/src/nvim/option_defs.h @@ -57,9 +57,10 @@ typedef enum { kOptScopeGlobal = 0, ///< Request global option value kOptScopeWin, ///< Request window-local option value kOptScopeBuf, ///< Request buffer-local option value + kOptScopeTab, ///< Request tabpage-local option value } OptScope; /// Always update this whenever a new option scope is added. -#define kOptScopeSize (kOptScopeBuf + 1) +#define kOptScopeSize (kOptScopeTab + 1) typedef uint8_t OptScopeFlags; typedef union { diff --git a/src/nvim/options.lua b/src/nvim/options.lua index 63eedd6855..e815f0639f 100644 --- a/src/nvim/options.lua +++ b/src/nvim/options.lua @@ -88,7 +88,7 @@ end local options = { cstr = cstr, --- @type string[] - valid_scopes = { 'global', 'buf', 'win' }, + valid_scopes = { 'global', 'buf', 'win', 'tab' }, --- @type vim.option_meta[] --- The order of the options MUST be alphabetic for ":set all". options = { @@ -1350,7 +1350,7 @@ local options = { ]=], full_name = 'cmdheight', redraw = { 'all_windows' }, - scope = { 'global' }, + scope = { 'global', 'tab' }, short_desc = N_('number of lines to use for the command-line'), type = 'number', varname = 'p_ch', diff --git a/src/nvim/window.c b/src/nvim/window.c index 1de0a021e3..a74816f3a4 100644 --- a/src/nvim/window.c +++ b/src/nvim/window.c @@ -4337,6 +4337,10 @@ void unuse_tabpage(tabpage_T *tp) tp->tp_firstwin = firstwin; tp->tp_lastwin = lastwin; tp->tp_curwin = curwin; + // Set this tab's stored cmdheight so use_tabpage() can restore it later. + // command_height() and win_new_screen_rows() also keep tp_ch_used in sync for the current tab + // between tab switches; this catches the no-display switch_win() path which bypasses them. + tp->tp_ch_used = p_ch; } // When switching tabpage, handle other side-effects in command_height(), but @@ -4352,6 +4356,9 @@ void use_tabpage(tabpage_T *tp) firstwin = curtab->tp_firstwin; lastwin = curtab->tp_lastwin; curwin = curtab->tp_curwin; + // Restore this tab's cmdheight. Layout adjustment (OptionSet, frame resize) is the caller's + // responsibility, see enter_tabpage(). + p_ch = curtab->tp_ch_used; } // Allocate the first window and put an empty buffer in it. @@ -4789,20 +4796,21 @@ static void enter_tabpage(tabpage_T *tp, buf_T *old_curbuf, bool trigger_enter_a int old_off = tp->tp_firstwin->w_winrow; win_T *next_prevwin = tp->tp_prevwin; tabpage_T *old_curtab = curtab; + OptInt prev_p_ch = p_ch; use_tabpage(tp); - if (old_curtab != curtab) { + if (old_curtab != curtab && p_ch != prev_p_ch) { + tabpage_check_windows(old_curtab); + // use_tabpage() loaded a different cmdheight for the new tab. Fire OptionSet and adjust + // the cmdline row without touching frame sizes (the new tab's frames are already correct). + OptInt new_ch = p_ch; + p_ch = prev_p_ch; + command_frame_height = false; + set_option_value(kOptCmdheight, NUMBER_OPTVAL(new_ch), 0); + command_frame_height = true; + } else if (old_curtab != curtab) { tabpage_check_windows(old_curtab); - if (p_ch != curtab->tp_ch_used) { - // Use the stored value of p_ch, so that it can be different for each tab page. - // Handle other side-effects but avoid setting frame sizes, which are still correct. - OptInt new_ch = curtab->tp_ch_used; - curtab->tp_ch_used = p_ch; - command_frame_height = false; - set_option_value(kOptCmdheight, NUMBER_OPTVAL(new_ch), 0); - command_frame_height = true; - } } // We would like doing the TabEnter event first, but we don't have a diff --git a/test/functional/api/vim_spec.lua b/test/functional/api/vim_spec.lua index d41fa8d0d0..c900c9cf61 100644 --- a/test/functional/api/vim_spec.lua +++ b/test/functional/api/vim_spec.lua @@ -1973,8 +1973,8 @@ describe('API', function() pcall_err(api.nvim_get_option_value, 'shiftwidth', { tab = tab1 }) ) eq( - "Conflict: 'tab' not allowed with this function", - pcall_err(api.nvim_set_option_value, 'cmdheight', 2, { tab = tab1 }) + "Conflict: 'tab' not allowed with 'shiftwidth'", + pcall_err(api.nvim_set_option_value, 'shiftwidth', 2, { tab = tab1 }) ) eq( "Conflict: 'tab' not allowed with this function", @@ -1986,7 +1986,7 @@ describe('API', function() ) end) - it("tabpage-local 'cmdheight' #31140", function() + it("tabpage-local option ('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 })) @@ -1998,6 +1998,14 @@ describe('API', function() 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', {})) + + -- Set non-current tab option. + api.nvim_set_option_value('cmdheight', 3, { tab = tab1 }) + eq(3, api.nvim_get_option_value('cmdheight', { tab = tab1 })) + eq(4, api.nvim_get_option_value('cmdheight', { tab = tab2 })) + eq(4, api.nvim_get_option_value('cmdheight', {})) + command('tabnext') + eq(3, api.nvim_get_option_value('cmdheight', {})) end) it('can get local values when global value is set', function() @@ -3891,10 +3899,11 @@ describe('API', function() os.remove(fname) end) - it('should return option information', function() + it('gets option info', function() eq(api.nvim_get_option_info('dictionary'), api.nvim_get_option_info2('dictionary', {})) -- buffer eq(api.nvim_get_option_info('fillchars'), api.nvim_get_option_info2('fillchars', {})) -- window eq(api.nvim_get_option_info('completeopt'), api.nvim_get_option_info2('completeopt', {})) -- global + eq('tab', api.nvim_get_option_info2('cmdheight', {}).scope) -- tab #31140 end) describe('last set', function() @@ -3933,13 +3942,13 @@ describe('API', function() end) end - it('is provided for cross-buffer requests', function() + it('cross-buffer', function() local info = api.nvim_get_option_info2('formatprg', { buf = bufs[2] }) eq(2, info.last_set_linenr) eq(1, info.last_set_sid) end) - it('is provided for cross-window requests', function() + it('cross-window', function() local info = api.nvim_get_option_info2('listchars', { win = wins[2] }) eq(6, info.last_set_linenr) eq(1, info.last_set_sid) diff --git a/test/functional/editor/tabpage_spec.lua b/test/functional/editor/tabpage_spec.lua index c8be39ea6e..da8597adfb 100644 --- a/test/functional/editor/tabpage_spec.lua +++ b/test/functional/editor/tabpage_spec.lua @@ -142,6 +142,22 @@ describe('tabpage', function() eq(42, fn.gettabvar(0, 'tabvar')) end) + it("gettabwinvar() returns tab-local 'cmdheight' #31140", function() + command('set cmdheight=5') + local tab1 = api.nvim_get_current_tabpage() + command('tabnew') + command('set cmdheight=2') + local tab2 = api.nvim_get_current_tabpage() + local tnr1 = fn.nvim_tabpage_get_number(tab1) + local tnr2 = fn.nvim_tabpage_get_number(tab2) + + -- Reading the *other* tab's cmdheight does not change the current tab. + eq(5, fn.gettabwinvar(tnr1, 1, '&cmdheight')) + eq(2, fn.gettabwinvar(tnr2, 1, '&cmdheight')) + eq(tab2, api.nvim_get_current_tabpage()) + eq(2, api.nvim_get_option_value('cmdheight', {})) + end) + it(':tabs does not overflow IObuff with long path with comma #20850', function() api.nvim_buf_set_name(0, ('x'):rep(1024) .. ',' .. ('x'):rep(1024)) command('tabs')