feat(api): tab-local option scope #39811

Problem:
2d795face6 added support for tab-local options ('cmdheight')
to `nvim_get_option_value`, but not to:

    nvim_get_option_info2()
    nvim_set_option_value(…, { tab = … })
    gettabwinvar()

Solution:
- Update `options.lua` to model tab-local options. Introduce `kOptScopeTab`.
- Handle tab scope in the options layer so it works for all options APIs.

Note:
- No change to `gettabvar()`. Not sure if needed/wanted.

fix https://github.com/neovim/neovim/issues/31140
This commit is contained in:
Justin M. Keyes
2026-05-17 10:24:46 -04:00
committed by GitHub
parent c55b6128f8
commit e572c9c80a
15 changed files with 297 additions and 173 deletions

View File

@@ -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.

View File

@@ -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

View File

@@ -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}, '&')
<

View File

@@ -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

View File

@@ -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

View File

@@ -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}, '&')
--- <
---

View File

@@ -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

View File

@@ -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}, '&')
<
]=],

View File

@@ -1,12 +1,5 @@
#pragma once
#include <stdbool.h>
#include <stdint.h>
/// 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"

View File

@@ -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";
}

View File

@@ -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 {

View File

@@ -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',

View File

@@ -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

View File

@@ -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)

View File

@@ -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')