fix(options): better handling of empty values

Problem:

Whether an option is allowed to be empty isn't well defined and
isn't properly checked.

Solution:

- For non-list string options, explicitly check the option value
  if it is empty.
- Annotate non-list string options that can accept an empty value.
  - Adjust command completion to ignore the empty value.
- Render values in Lua meta files
This commit is contained in:
Lewis Russell 2025-01-10 10:20:43 +00:00 committed by Lewis Russell
parent cb7b4e2962
commit 34e2185022
9 changed files with 72 additions and 42 deletions

View File

@ -347,11 +347,15 @@ Options:
- `:set {option}<` removes local value for all |global-local| options.
- `:setlocal {option}<` copies global value to local value for all options.
- 'ambiwidth' cannot be set to empty.
- 'autoread' works in the terminal (if it supports "focus" events)
- 'background' cannot be set to empty.
- 'cpoptions' flags: |cpo-_|
- 'diffopt' "linematch" feature
- 'eadirection' cannot be set to empty.
- 'exrc' searches for ".nvim.lua", ".nvimrc", or ".exrc" files. The
user is prompted whether to trust the file.
- 'fileformat' cannot be set to empty.
- 'fillchars' flags: "msgsep", "horiz", "horizup", "horizdown",
"vertleft", "vertright", "verthoriz"
- 'foldcolumn' supports up to 9 dynamic/fixed columns
@ -363,14 +367,17 @@ Options:
- "clean" removes unloaded buffers from the jumplist.
- the |jumplist|, |changelist|, |alternate-file| or using |mark-motions|.
- 'laststatus' global statusline support
- 'mousemodel' cannot be set to empty.
- 'mousescroll' amount to scroll by when scrolling with a mouse
- 'pumblend' pseudo-transparent popupmenu
- 'scrollback'
- 'shortmess'
- "F" flag does not affect output from autocommands.
- "q" flag fully hides macro recording message.
- 'showcmdloc' cannot be set to empty.
- 'signcolumn' can show multiple signs (dynamic or fixed columns)
- 'statuscolumn' full control of columns using 'statusline' format
- 'splitkeep' cannot be set to empty.
- 'tabline' middle-click on tabpage label closes tabpage,
and %@Func@foo%X can call any function on mouse-click
- 'termpastefilter'

View File

@ -52,7 +52,7 @@ vim.go.ari = vim.go.allowrevins
--- set to one of CJK locales. See Unicode Standard Annex #11
--- (https://www.unicode.org/reports/tr11).
---
--- @type string
--- @type 'single'|'double'
vim.o.ambiwidth = "single"
vim.o.ambw = vim.o.ambiwidth
vim.go.ambiwidth = vim.o.ambiwidth
@ -208,7 +208,7 @@ vim.go.awa = vim.go.autowriteall
--- will change. To use other settings, place ":highlight" commands AFTER
--- the setting of the 'background' option.
---
--- @type string
--- @type 'light'|'dark'
vim.o.background = "dark"
vim.o.bg = vim.o.background
vim.go.background = vim.o.background
@ -595,7 +595,7 @@ vim.wo.briopt = vim.wo.breakindentopt
--- This option is used together with 'buftype' and 'swapfile' to specify
--- special kinds of buffers. See `special-buffers`.
---
--- @type string
--- @type ''|'hide'|'unload'|'delete'|'wipe'
vim.o.bufhidden = ""
vim.o.bh = vim.o.bufhidden
vim.bo.bufhidden = vim.o.bufhidden
@ -658,7 +658,7 @@ vim.bo.bl = vim.bo.buflisted
--- without saving. For writing there must be matching `BufWriteCmd|,
--- |FileWriteCmd` or `FileAppendCmd` autocommands.
---
--- @type string
--- @type ''|'acwrite'|'help'|'nofile'|'nowrite'|'quickfix'|'terminal'|'prompt'
vim.o.buftype = ""
vim.o.bt = vim.o.buftype
vim.bo.buftype = vim.o.buftype
@ -1118,7 +1118,7 @@ vim.go.cot = vim.go.completeopt
--- For Insert mode completion the buffer-local value is used. For
--- command line completion the global value is used.
---
--- @type string
--- @type ''|'slash'|'backslash'
vim.o.completeslash = ""
vim.o.csl = vim.o.completeslash
vim.bo.completeslash = vim.o.completeslash
@ -1824,7 +1824,7 @@ vim.go.dy = vim.go.display
--- hor horizontally, height of windows is not affected
--- both width and height of windows is affected
---
--- @type string
--- @type 'both'|'ver'|'hor'
vim.o.eadirection = "both"
vim.o.ead = vim.o.eadirection
vim.go.eadirection = vim.o.eadirection
@ -2126,7 +2126,7 @@ vim.go.fencs = vim.go.fileencodings
--- option is set, because the file would be different when written.
--- This option cannot be changed when 'modifiable' is off.
---
--- @type string
--- @type 'unix'|'dos'|'mac'
vim.o.fileformat = "unix"
vim.o.ff = vim.o.fileformat
vim.bo.fileformat = vim.o.fileformat
@ -2382,7 +2382,7 @@ vim.go.fcl = vim.go.foldclose
--- "[1-9]": to display a fixed number of columns
--- See `folding`.
---
--- @type string
--- @type 'auto'|'auto:1'|'auto:2'|'auto:3'|'auto:4'|'auto:5'|'auto:6'|'auto:7'|'auto:8'|'auto:9'|'0'|'1'|'2'|'3'|'4'|'5'|'6'|'7'|'8'|'9'
vim.o.foldcolumn = "0"
vim.o.fdc = vim.o.foldcolumn
vim.wo.foldcolumn = vim.o.foldcolumn
@ -2479,7 +2479,7 @@ vim.wo.fmr = vim.wo.foldmarker
--- `fold-syntax` syntax Syntax highlighting items specify folds.
--- `fold-diff` diff Fold text that is not changed.
---
--- @type string
--- @type 'manual'|'expr'|'marker'|'indent'|'syntax'|'diff'
vim.o.foldmethod = "manual"
vim.o.fdm = vim.o.foldmethod
vim.wo.foldmethod = vim.o.foldmethod
@ -3144,7 +3144,7 @@ vim.bo.ims = vim.bo.imsearch
--- 'redrawtime') then 'inccommand' is automatically disabled until
--- `Command-line-mode` is done.
---
--- @type string
--- @type 'nosplit'|'split'|''
vim.o.inccommand = "nosplit"
vim.o.icm = vim.o.inccommand
vim.go.inccommand = vim.o.inccommand
@ -4354,7 +4354,7 @@ vim.go.mh = vim.go.mousehide
--- "g<LeftMouse>" is "<C-LeftMouse> (jump to tag under mouse click)
--- "g<RightMouse>" is "<C-RightMouse> ("CTRL-T")
---
--- @type string
--- @type 'extend'|'popup'|'popup_setpos'
vim.o.mousemodel = "popup_setpos"
vim.o.mousem = vim.o.mousemodel
vim.go.mousemodel = vim.o.mousemodel
@ -4947,7 +4947,7 @@ vim.wo.rl = vim.wo.rightleft
--- This is useful for languages such as Hebrew, Arabic and Farsi.
--- The 'rightleft' option must be set for 'rightleftcmd' to take effect.
---
--- @type string
--- @type 'search'
vim.o.rightleftcmd = "search"
vim.o.rlc = vim.o.rightleftcmd
vim.wo.rightleftcmd = vim.o.rightleftcmd
@ -5222,7 +5222,7 @@ vim.go.sect = vim.go.sections
--- backwards, you cannot include the last character of a line, when
--- starting in Normal mode and 'virtualedit' empty.
---
--- @type string
--- @type 'inclusive'|'exclusive'|'old'
vim.o.selection = "inclusive"
vim.o.sel = vim.o.selection
vim.go.selection = vim.o.selection
@ -5788,7 +5788,7 @@ vim.go.sc = vim.go.showcmd
--- place the text. Without a custom 'statusline' or 'tabline' it will be
--- displayed in a convenient location.
---
--- @type string
--- @type 'last'|'statusline'|'tabline'
vim.o.showcmdloc = "last"
vim.o.sloc = vim.o.showcmdloc
vim.go.showcmdloc = vim.o.showcmdloc
@ -5920,7 +5920,7 @@ vim.go.siso = vim.go.sidescrolloff
--- "number" display signs in the 'number' column. If the number
--- column is not present, then behaves like "auto".
---
--- @type string
--- @type 'yes'|'no'|'auto'|'auto:1'|'auto:2'|'auto:3'|'auto:4'|'auto:5'|'auto:6'|'auto:7'|'auto:8'|'auto:9'|'yes:1'|'yes:2'|'yes:3'|'yes:4'|'yes:5'|'yes:6'|'yes:7'|'yes:8'|'yes:9'|'number'
vim.o.signcolumn = "auto"
vim.o.scl = vim.o.signcolumn
vim.wo.signcolumn = vim.o.signcolumn
@ -6228,7 +6228,7 @@ vim.go.sb = vim.go.splitbelow
--- with the previous cursor position. For "screen", the text cannot always
--- be kept on the same screen line when 'wrap' is enabled.
---
--- @type string
--- @type 'cursor'|'screen'|'topline'
vim.o.splitkeep = "cursor"
vim.o.spk = vim.o.splitkeep
vim.go.splitkeep = vim.o.splitkeep
@ -6876,7 +6876,7 @@ vim.go.tbs = vim.go.tagbsearch
--- match Match case
--- smart Ignore case unless an upper case letter is used
---
--- @type string
--- @type 'followic'|'ignore'|'match'|'followscs'|'smart'
vim.o.tagcase = "followic"
vim.o.tc = vim.o.tagcase
vim.bo.tagcase = vim.o.tagcase
@ -7758,7 +7758,7 @@ vim.go.wop = vim.go.wildoptions
--- key is never used for the menu.
--- This option is not used for <F10>; on Win32.
---
--- @type string
--- @type 'yes'|'menu'|'no'
vim.o.winaltkeys = "menu"
vim.o.wak = vim.o.winaltkeys
vim.go.winaltkeys = vim.o.winaltkeys

View File

@ -666,7 +666,16 @@ local function render_option_meta(_f, opt, write)
write('--- ' .. l)
end
write('--- @type ' .. OPTION_TYPES[opt.type])
if opt.type == 'string' and not opt.list and opt.values then
local values = {} --- @type string[]
for _, e in ipairs(opt.values) do
values[#values + 1] = fmt("'%s'", e)
end
write('--- @type ' .. table.concat(values, '|'))
else
write('--- @type ' .. OPTION_TYPES[opt.type])
end
write('vim.o.' .. opt.full_name .. ' = ' .. render_option_default(opt.defaults))
if opt.abbreviation then
write('vim.o.' .. opt.abbreviation .. ' = vim.o.' .. opt.full_name)

View File

@ -7,7 +7,7 @@
--- @field alias? string|string[]
--- @field short_desc? string|fun(): string
--- @field varname? string
--- @field type vim.option_type|vim.option_type[]
--- @field type vim.option_type
--- @field immutable? boolean
--- @field list? 'comma'|'onecomma'|'commacolon'|'onecommacolon'|'flags'|'flagscomma'
--- @field scope vim.option_scope[]
@ -834,7 +834,7 @@ return {
abbreviation = 'bh',
cb = 'did_set_bufhidden',
defaults = { if_true = '' },
values = { 'hide', 'unload', 'delete', 'wipe' },
values = { '', 'hide', 'unload', 'delete', 'wipe' },
desc = [=[
This option specifies what happens when a buffer is no longer
displayed in a window:
@ -888,11 +888,12 @@ return {
cb = 'did_set_buftype',
defaults = { if_true = '' },
values = {
'',
'acwrite',
'help',
'nofile',
'nowrite',
'quickfix',
'help',
'acwrite',
'terminal',
'prompt',
},
@ -1554,7 +1555,7 @@ return {
abbreviation = 'csl',
cb = 'did_set_completeslash',
defaults = { if_true = '' },
values = { 'slash', 'backslash' },
values = { '', 'slash', 'backslash' },
desc = [=[
only modifiable in MS-Windows
When this option is set it overrules 'shellslash' for completion:
@ -2017,8 +2018,10 @@ return {
"msg" and "throw" are useful for debugging 'foldexpr', 'formatexpr' or
'indentexpr'.
]=],
-- TODO(lewis6991): bug, values currently cannot be combined
expand_cb = 'expand_set_debug',
full_name = 'debug',
list = 'comma',
scope = { 'global' },
short_desc = N_('to "msg" to see all error messages'),
type = 'string',
@ -4299,7 +4302,7 @@ return {
abbreviation = 'icm',
cb = 'did_set_inccommand',
defaults = { if_true = 'nosplit' },
values = { 'nosplit', 'split' },
values = { 'nosplit', 'split', '' },
desc = [=[
When nonempty, shows the effects of |:substitute|, |:smagic|,
|:snomagic| and user commands with the |:command-preview| flag as you
@ -5735,7 +5738,7 @@ return {
abbreviation = 'mousem',
cb = 'did_set_mousemodel',
defaults = { if_true = 'popup_setpos' },
values = { 'extend', 'popup', 'popup_setpos', 'mac' },
values = { 'extend', 'popup', 'popup_setpos' },
desc = [=[
Sets the model to use for the mouse. The name mostly specifies what
the right mouse button is used for:

View File

@ -395,7 +395,9 @@ static int expand_set_opt_string(optexpand_T *args, const char **values, size_t
}
for (const char **val = values; *val != NULL; val++) {
if (include_orig_val && *option_val != NUL) {
if (**val == NUL) {
continue; // Ignore empty
} else if (include_orig_val && *option_val != NUL) {
if (strcmp(*val, option_val) == 0) {
continue;
}
@ -1091,7 +1093,7 @@ int expand_set_cursorlineopt(optexpand_T *args, int *numMatches, char ***matches
/// The 'debug' option is changed.
const char *did_set_debug(optset_T *args FUNC_ATTR_UNUSED)
{
return did_set_opt_strings(p_debug, opt_debug_values, false);
return did_set_opt_strings(p_debug, opt_debug_values, true);
}
int expand_set_debug(optexpand_T *args, int *numMatches, char ***matches)
@ -2545,7 +2547,7 @@ int expand_set_winhighlight(optexpand_T *args, int *numMatches, char ***matches)
/// @param list when true: accept a list of values
///
/// @return OK for correct value, FAIL otherwise. Empty is always OK.
static int check_opt_strings(char *val, const char **values, int list)
static int check_opt_strings(char *val, const char **values, bool list)
{
return opt_strings_flags(val, values, NULL, list);
}
@ -2562,7 +2564,10 @@ static int opt_strings_flags(const char *val, const char **values, unsigned *fla
{
unsigned new_flags = 0;
while (*val) {
// If not list and val is empty, then force one iteration of the while loop
bool iter_one = (*val == NUL) && !list;
while (*val || iter_one) {
for (unsigned i = 0;; i++) {
if (values[i] == NULL) { // val not found in values[]
return FAIL;
@ -2577,6 +2582,9 @@ static int opt_strings_flags(const char *val, const char **values, unsigned *fla
break; // check next item in val list
}
}
if (iter_one) {
break;
}
}
if (flagp != NULL) {
*flagp = new_flags;

View File

@ -145,8 +145,8 @@ let test_values = {
\ 'winwidth': [[1, 10, 999], [-1, 0]],
\
"\ string options
\ 'ambiwidth': [['', 'single', 'double'], ['xxx']],
\ 'background': [['', 'light', 'dark'], ['xxx']],
\ 'ambiwidth': [['single', 'double'], ['xxx']],
\ 'background': [['light', 'dark'], ['xxx']],
"\ 'backspace': [[0, 1, 2, 3, '', 'indent', 'eol', 'start', 'nostop',
"\ " 'eol,start', 'indent,eol,nostop'],
"\ " [-1, 4, 'xxx']],
@ -214,12 +214,12 @@ let test_values = {
\ ['xxx', 'foldcolumn:xxx', 'algorithm:xxx', 'algorithm:']],
\ 'display': [['', 'lastline', 'truncate', 'uhex', 'lastline,uhex'],
\ ['xxx']],
\ 'eadirection': [['', 'both', 'ver', 'hor'], ['xxx', 'ver,hor']],
\ 'eadirection': [['both', 'ver', 'hor'], ['xxx', 'ver,hor']],
"\ 'encoding': [['latin1'], ['xxx', '']],
\ 'eventignore': [['', 'WinEnter', 'WinLeave,winenter', 'all,WinEnter'],
\ ['xxx']],
\ 'fileencoding': [['', 'latin1', 'xxx'], []],
\ 'fileformat': [['', 'dos', 'unix', 'mac'], ['xxx']],
\ 'fileformat': [['dos', 'unix', 'mac'], ['xxx']],
\ 'fileformats': [['', 'dos', 'dos,unix'], ['xxx']],
\ 'fillchars': [['', 'stl:x', 'stlnc:x', 'vert:x', 'fold:x', 'foldopen:x',
\ 'foldclose:x', 'foldsep:x', 'diff:x', 'eob:x', 'lastline:x',
@ -274,7 +274,7 @@ let test_values = {
\ 'mkspellmem': [['10000,100,12'], ['', 'xxx', '10000,100']],
\ 'mouse': [['', 'n', 'v', 'i', 'c', 'h', 'a', 'r', 'nvi'],
\ ['xxx', 'n,v,i']],
\ 'mousemodel': [['', 'extend', 'popup', 'popup_setpos'], ['xxx']],
\ 'mousemodel': [['extend', 'popup', 'popup_setpos'], ['xxx']],
\ 'mouseshape': [['', 'n:arrow'], ['xxx']],
\ 'nrformats': [['', 'alpha', 'octal', 'hex', 'bin', 'unsigned', 'blank',
\ 'alpha,hex,bin'],
@ -299,7 +299,7 @@ let test_values = {
\ 'sessionoptions': [['', 'blank', 'curdir', 'sesdir',
\ 'help,options,slash'],
\ ['xxx', 'curdir,sesdir']],
\ 'showcmdloc': [['', 'last', 'statusline', 'tabline'], ['xxx']],
\ 'showcmdloc': [['last', 'statusline', 'tabline'], ['xxx']],
"\ 'signcolumn': [['', 'auto', 'no', 'yes', 'number'], ['xxx', 'no,yes']],
\ 'spellfile': [['', 'file.en.add', 'xxx.en.add,yyy.gb.add,zzz.ja.add',
\ '/tmp/dir\ with\ space/en.utf-8.add',
@ -311,7 +311,7 @@ let test_values = {
\ 'spellsuggest': [['', 'best', 'double', 'fast', '100', 'timeout:100',
\ 'timeout:-1', 'file:/tmp/file', 'expr:Func()', 'double,33'],
\ ['xxx', '-1', 'timeout:', 'best,double', 'double,fast']],
\ 'splitkeep': [['', 'cursor', 'screen', 'topline'], ['xxx']],
\ 'splitkeep': [['cursor', 'screen', 'topline'], ['xxx']],
\ 'statusline': [['', 'xxx'], ['%$', '%{', '%{%', '%{%}', '%(', '%)']],
"\ 'swapsync': [['', 'sync', 'fsync'], ['xxx']],
\ 'switchbuf': [['', 'useopen', 'usetab', 'split', 'vsplit', 'newtab',

View File

@ -504,7 +504,8 @@ func Test_set_completion_string_values()
call assert_equal('current', getcompletion('set browsedir=', 'cmdline')[1])
endif
call assert_equal('unload', getcompletion('set bufhidden=', 'cmdline')[1])
call assert_equal('nowrite', getcompletion('set buftype=', 'cmdline')[1])
"call assert_equal('nowrite', getcompletion('set buftype=', 'cmdline')[1])
call assert_equal('help', getcompletion('set buftype=', 'cmdline')[1])
call assert_equal('internal', getcompletion('set casemap=', 'cmdline')[1])
if exists('+clipboard')
" call assert_match('unnamed', getcompletion('set clipboard=', 'cmdline')[1])

View File

@ -391,7 +391,8 @@ endfunc
function Test_termdebug_save_restore_variables()
" saved mousemodel
let &mousemodel=''
"let &mousemodel=''
let &mousemodel='extend'
" saved keys
nnoremap K :echo "hello world!"<cr>
@ -414,7 +415,8 @@ function Test_termdebug_save_restore_variables()
quit!
call WaitForAssert({-> assert_equal(1, winnr('$'))})
call assert_true(empty(&mousemodel))
"call assert_true(empty(&mousemodel))
call assert_equal(&mousemodel, 'extend')
call assert_true(empty(expected_map_minus))
call assert_equal(expected_map_K.rhs, maparg('K', 'n', 0, 1).rhs)

View File

@ -11,8 +11,8 @@ local check_ff_value = function(ff)
end
describe('check_ff_value', function()
itp('views empty string as valid', function()
eq(1, check_ff_value(''))
itp('views empty string as invalid', function()
eq(0, check_ff_value(''))
end)
itp('views "unix", "dos" and "mac" as valid', function()