From f8cfd7f06ae87d47aac04ddce860b302f7820858 Mon Sep 17 00:00:00 2001 From: "Justin M. Keyes" Date: Tue, 21 Jul 2026 18:43:55 +0200 Subject: [PATCH] feat(options): schema, "dict" options, messages MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Problem: Options parsing is still painful for dict-style options. Solution: schema-maxxing => better `opt:get()` (will be the basis for `vim.o()`), unified (and more-detailed) err msgs. - Drop bespoke structure-builder in `_core/options.lua`. - Define `schema` for all non-primitive options (except 'guicursor' and statusline-style options); generate reified keysets `OptKeyDict`). - Generate 'fillchars' => `fcs_tab`, 'listchars' => `lcs_tab`. - `nvim_set_option_value`: - Return the improved structures. Also from `vim.opt.x:get()`. - Eliminate api <=> lua roundtrip, centralize option structure handling. - Improve/unify errors. - Bump ERR_BUFLEN 80 → 256 so the "one of" list isn't truncated. - Eliminate old 'diffopt' order-dependence (`iwhiteall` before `iwhite`) Error samples: Typed-key path (opt_strings_check → diffopt/mousescroll/breakindentopt): E474: Unknown item 'foo' E474: 'context' requires a number E474: 'ver' number is out of range E474: 'algorithm' must be one of: myers, minimal, patience, histogram E474: 'filler' does not take a value Related: - #31084 - #34661 - #31820 - #14739 - #20107 - fix #18875 - :get() returns `{ sbr = true, shift = '3' }` (reified-keyset) instead of `{'sbr', 'shift:3'}` - Setting via table now works too. `object_as_optval_for` `is_map` now recognizes struct options. - fix #30296 - instead of `E474: Invalid argument`, errors now look like: ``` E474: Invalid value 'x', expected one of: single, double: ambiwidth=x E474: Unknown item 'foo': diffopt=foo E474: 'context' requires a number: diffopt=context:x ``` simplify `win_float_parse_option` from #26799. --- runtime/doc/news.txt | 6 + runtime/lua/vim/_core/options.lua | 154 +--- scripts/linterrcodes.lua | 2 +- src/gen/gen_api_dispatch.lua | 83 +- src/gen/gen_eval_files.lua | 12 +- src/gen/gen_options.lua | 395 +++++++-- src/gen/gen_steps.zig | 2 + src/gen/keyset.lua | 55 ++ src/nlua0.zig | 3 + src/nvim/CMakeLists.txt | 8 +- src/nvim/api/deprecated.c | 7 +- src/nvim/api/options.c | 37 +- src/nvim/buffer.c | 16 +- src/nvim/buffer_defs.h | 2 +- src/nvim/bufwrite.c | 28 +- src/nvim/change.c | 6 +- src/nvim/diff.c | 181 ++--- src/nvim/drawline.c | 4 +- src/nvim/drawscreen.c | 6 +- src/nvim/eval.c | 4 + src/nvim/eval/vars.c | 31 +- src/nvim/ex_cmds.c | 16 +- src/nvim/ex_cmds2.c | 2 +- src/nvim/ex_docmd.c | 8 +- src/nvim/ex_getln.c | 2 +- src/nvim/file_search.c | 2 +- src/nvim/fileio.c | 12 +- src/nvim/indent.c | 64 +- src/nvim/insert.c | 22 +- src/nvim/insexpand.c | 20 +- src/nvim/keycodes.c | 6 +- src/nvim/mapping.c | 4 +- src/nvim/match.c | 2 +- src/nvim/memline.c | 2 +- src/nvim/message.c | 4 +- src/nvim/mouse.c | 2 +- src/nvim/move.c | 2 +- src/nvim/normal.c | 12 +- src/nvim/ops.c | 22 +- src/nvim/option.c | 395 ++++++++- src/nvim/option.h | 2 + src/nvim/option_defs.h | 40 + src/nvim/option_vars.h | 119 +-- src/nvim/options.lua | 750 ++++++++++++------ src/nvim/optionstr.c | 477 +++++++---- src/nvim/quickfix.c | 2 +- src/nvim/regexp.c | 2 +- src/nvim/register.c | 2 +- src/nvim/runtime.c | 2 +- src/nvim/search.c | 32 +- src/nvim/spell.c | 4 +- src/nvim/spellsuggest.c | 53 +- src/nvim/tag.c | 2 +- src/nvim/textformat.c | 44 +- src/nvim/textobject.c | 4 +- src/nvim/ui.c | 14 +- src/nvim/undo.c | 12 +- src/nvim/version.c | 2 +- test/functional/legacy/tagcase_spec.lua | 8 +- test/functional/lua/option_and_var_spec.lua | 39 +- test/functional/options/mousescroll_spec.lua | 29 +- .../{legacy => options}/options_spec.lua | 50 +- test/old/testdir/test_options.vim | 42 +- 63 files changed, 2142 insertions(+), 1230 deletions(-) create mode 100644 src/gen/keyset.lua rename test/functional/{legacy => options}/options_spec.lua (52%) diff --git a/runtime/doc/news.txt b/runtime/doc/news.txt index d1294268bb..c380b32d05 100644 --- a/runtime/doc/news.txt +++ b/runtime/doc/news.txt @@ -38,6 +38,12 @@ LUA • pos.to_cursor() returns a (`row,` `col)` tuple instead of returning them as separate values. • Renamed current_level param of vim.log.new(). +• `vim.opt.{option}:get()` has improved structure for dict-like options. + Some shapes changed: + - "key:value" list options (e.g. 'diffopt', 'mousescroll') return a map + `{ key = value }` instead of an array of `"key:value"` strings. + - The `,,` literal-comma convention (e.g. 'isfname') is not reconstructed in + the structured view; the raw string value is unchanged. DIAGNOSTICS diff --git a/runtime/lua/vim/_core/options.lua b/runtime/lua/vim/_core/options.lua index e814841344..3bcf54ffd5 100644 --- a/runtime/lua/vim/_core/options.lua +++ b/runtime/lua/vim/_core/options.lua @@ -95,46 +95,6 @@ local M = {} local api = vim.api --- TODO(tjdevries): Improve option metadata so that this doesn't have to be hardcoded. -local key_value_options = { - fillchars = true, - fcs = true, - listchars = true, - lcs = true, - winhighlight = true, - winhl = true, -} - ---- @nodoc ---- @class vim._option.Info : vim.api.keyset.get_option_info ---- @field metatype 'boolean'|'string'|'number'|'map'|'array'|'set' - ---- Convert a vimoption_T style dictionary to the correct OptionType associated with it. ----@return string -local function get_option_metatype(name, info) - if info.type == 'string' then - if info.flaglist then - return 'set' - elseif info.commalist then - if key_value_options[name] then - return 'map' - end - return 'array' - end - return 'string' - end - return info.type -end - ---- @param name string ---- @return vim._option.Info -local function get_options_info(name) - local info = api.nvim_get_option_info2(name) - --- @cast info vim._option.Info - info.metatype = get_option_metatype(name, info) - return info -end - --- Gets or sets environment variables in the current editor process. See |expand-env| and --- |:let-environment| for the Vimscript behavior. Invalid or unset key returns `nil`. --- @@ -305,113 +265,6 @@ vim.bo = new_buf_opt_accessor() --- ``` vim.wo = new_win_opt_accessor() -local function passthrough(_, x) - return x -end - --- Map of OptionType to functions that take vimoption_T values and convert to Lua values. --- Each function takes (info, vim_value) -> lua_value -local to_lua_value = { - boolean = passthrough, - number = passthrough, - string = passthrough, - - array = function(_, value) - -- Empty strings mean that there is nothing there, - -- so empty table should be returned. - if value == '' then - return {} - end - - -- Handles unescaped commas in a list. - if value:find(',,,') then - --- @type string, string - local left, right = unpack(vim.split(value, ',,,')) - - local result = {} - vim.list_extend(result, vim.split(left, ',')) - table.insert(result, ',') - vim.list_extend(result, vim.split(right, ',')) - - table.sort(result) - - return result - end - - if value:find(',^,,', 1, true) then - --- @type string, string - local left, right = unpack(vim.split(value, ',^,,', { plain = true })) - - local result = {} - vim.list_extend(result, vim.split(left, ',')) - table.insert(result, '^,') - vim.list_extend(result, vim.split(right, ',')) - - table.sort(result) - - return result - end - - return vim.split(value, ',') - end, - - set = function(info, value) - if type(value) == 'table' then - return value - end - - -- Empty strings mean that there is nothing there, - -- so empty table should be returned. - if value == '' then - return {} - end - - assert(info.flaglist, 'That is the only one I know how to handle') - - local result = {} --- @type table - - if info.flaglist and info.commalist then - local split_value = vim.split(value, ',') - for _, v in ipairs(split_value) do - result[v] = true - end - else - for i = 1, #value do - result[value:sub(i, i)] = true - end - end - - return result - end, - - map = function(info, raw_value) - if type(raw_value) == 'table' then - return raw_value - end - - assert(info.commalist, 'Only commas are supported currently') - - local result = {} --- @type table - - local comma_split = vim.split(raw_value, ',') - for _, key_value_str in ipairs(comma_split) do - --- @type string, string - local key, value = unpack(vim.split(key_value_str, ':')) - key = vim.trim(key) - - result[key] = value - end - - return result - end, -} - ---- Converts a vimoption_T style value to a Lua value -function M.convert_value_to_lua(name, option_value) - local info = get_options_info(name) or error('Not a valid option name: ' .. name) - return to_lua_value[info.metatype](info, option_value) -end - local function create_option_accessor(scope) --- @diagnostic disable-next-line: no-unknown local option_mt @@ -433,7 +286,12 @@ local function create_option_accessor(scope) option_mt = { get = function(self) - return M.convert_value_to_lua(self._name, self._value) + -- `nvim_set_option_value(dry_run)` returns the value in its structured form. + return api.nvim_set_option_value( + self._name, + self._value, + { operation = 'set', scope = scope, dry_run = true } + ) end, append = function(self, right) diff --git a/scripts/linterrcodes.lua b/scripts/linterrcodes.lua index 3242d3ea23..06286db28d 100644 --- a/scripts/linterrcodes.lua +++ b/scripts/linterrcodes.lua @@ -33,7 +33,7 @@ local dup_allowed = { E317 = 4, E319 = 2, E423 = 3, - E474 = 52, + E474 = 59, E475 = 6, E482 = 3, E484 = 2, diff --git a/src/gen/gen_api_dispatch.lua b/src/gen/gen_api_dispatch.lua index d243106750..b15afd2b08 100644 --- a/src/gen/gen_api_dispatch.lua +++ b/src/gen/gen_api_dispatch.lua @@ -4,6 +4,7 @@ -- to obtain how the script is invoked, look in build/build.ninja and grep for -- "gen_api_dispatch.lua" local hashy = require 'gen.hashy' +local keyset = require('gen.keyset') local c_grammar = require('gen.c_grammar') -- output h file with generated dispatch functions (dispatch_wrappers.generated.h) @@ -356,63 +357,49 @@ local keysets_defs = assert(io.open(keysets_outputf, 'wb')) keysets_defs:write('// IWYU pragma: private, include "nvim/api/private/dispatch.h"\n\n') -for _, k in ipairs(keysets) do - local neworder, hashfun = hashy.hashy_hash(k.name, k.keys, function(idx) - return k.name .. '_table[' .. idx .. '].str' - end) - - keysets_defs:write('extern KeySetLink ' .. k.name .. '_table[' .. (1 + #neworder) .. '];\n') - - local function typename(type) - if type == 'HLGroupID' then - return 'kObjectTypeInteger' - elseif not type or startswith(type, 'Union') then - return 'kObjectTypeNil' - elseif type == 'StringArray' then - return 'kUnpackTypeStringArray' - end - return 'kObjectType' .. real_type(type) +local function typename(type) + if type == 'HLGroupID' then + return 'kObjectTypeInteger' + elseif not type or startswith(type, 'Union') then + return 'kObjectTypeNil' + elseif type == 'StringArray' then + return 'kUnpackTypeStringArray' end + return 'kObjectType' .. real_type(type) +end - output:write('KeySetLink ' .. k.name .. '_table[] = {\n') - for i, key in ipairs(neworder) do +for _, k in ipairs(keysets) do + local order, hashfun = keyset.hash(k.name, k.keys) + keysets_defs:write('extern KeySetLink ' .. k.name .. '_table[' .. (1 + #order) .. '];\n') + + local entry = {} --- @type table + for i, key in ipairs(order) do + -- Only keysets with optional keys carry a per-key HAS_KEY index (and its `KEYSET_OPTIDX` define). local ind = -1 if k.has_optional then ind = i keysets_defs:write('#define KEYSET_OPTIDX_' .. k.name .. '__' .. key .. ' ' .. ind .. '\n') end - output:write( - ' {"' - .. key - .. '", offsetof(KeyDict_' - .. k.name - .. ', ' - .. (k.c_names[key] or key) - .. '), ' - .. typename(k.types[key]) - .. ', ' - .. ind - .. ', ' - .. (k.types[key] == 'HLGroupID' and 'true' or 'false') - .. '},\n' - ) + entry[key] = { + field = k.c_names[key] or key, + type = typename(k.types[key]), + opt_index = ind, + is_hlgroup = k.types[key] == 'HLGroupID', + } end - output:write(' {NULL, 0, kObjectTypeNil, -1, false},\n') - output:write('};\n\n') - output:write(hashfun) - - output:write([[ -KeySetLink *KeyDict_]] .. k.name .. [[_get_field(const char *str, size_t len) -{ - int hash = ]] .. k.name .. [[_hash(str, len); - if (hash == -1) { - return NULL; - } - return &]] .. k.name .. [[_table[hash]; -} - -]]) + keyset.emit(function(s) + output:write(s .. '\n') + end, { + name = k.name, + get_field = 'KeyDict_' .. k.name .. '_get_field', + struct = 'KeyDict_' .. k.name, + order = order, + hashfun = hashfun, + entry = entry, + static = false, + }) + output:write('\n') end keysets_defs:close() diff --git a/src/gen/gen_eval_files.lua b/src/gen/gen_eval_files.lua index 76f450b8c6..f3e6da0d86 100755 --- a/src/gen/gen_eval_files.lua +++ b/src/gen/gen_eval_files.lua @@ -657,11 +657,15 @@ local function render_option_meta(_f, opt, write) write('--- ' .. l) end - 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) + -- A non-list string option with a fixed value set (e.g. 'ambiwidth', 'tagcase') documents its + -- exact value union; everything else uses its Lua type. + local values = {} --- @type string[] + if opt.type == 'string' and not opt.list and opt.schema then + for _, v in ipairs(require('nvim.options').schema_values(opt.schema)) do + values[#values + 1] = fmt("'%s'", v) end + end + if #values > 0 then write('--- @type ' .. table.concat(values, '|')) else write('--- @type ' .. OPTION_TYPES[opt.type]) diff --git a/src/gen/gen_options.lua b/src/gen/gen_options.lua index fa0d46cc26..c1d61a609e 100644 --- a/src/gen/gen_options.lua +++ b/src/gen/gen_options.lua @@ -1,11 +1,27 @@ ---@diagnostic disable: no-unknown -local options_input_file = arg[5] +local options_input_file = arg[7] --- @module 'nvim.options' local options = loadfile(options_input_file)() local options_meta = options.options local cstr = options.cstr local valid_scopes = options.valid_scopes +local schema_values = options.schema_values + +-- Object type of a `dict` key's value, for the generated KeySetLink table. +local keyset_ctype = { + num = 'kObjectTypeInteger', + snum = 'kObjectTypeInteger', + enum = 'kObjectTypeString', + str = 'kObjectTypeString', +} +-- C field type (an API type) for each `dict` key kind. +local keyset_ftype = { + num = 'Integer', + snum = 'Integer', + enum = 'String', + str = 'String', +} --- @param o vim.option_meta --- @return string @@ -13,6 +29,14 @@ local function get_values_var(o) return ('opt_%s_values'):format(o.abbreviation or o.full_name) end +--- True if an option is stored as a reified keyset (kOptValTypeDict): a `dict` schema (typed +--- key:value map, e.g. 'diffopt'), as opposed to a flag/enum/char category. +--- @param o vim.option_meta +--- @return boolean +local function is_dict_option(o) + return o.schema ~= nil and o.schema.dict ~= nil +end + --- @param s string --- @return string local function lowercase_to_titlecase(s) @@ -191,12 +215,20 @@ local function dump_option(i, o, write) if o.abbreviation then write(' .shortname=', cstr(o.abbreviation)) end - write(' .type=', opt_type_enum(o.type)) + write(' .type=', is_dict_option(o) and 'kOptValTypeDict' or opt_type_enum(o.type)) write(' .flags=', get_flags(o)) write(' .scope_flags=', get_scope_flags(o)) write(' .scope_idx=', get_scope_idx(o)) - write(' .values=', (o.values and get_values_var(o) or 'NULL')) - write(' .values_len=', (o.values and #o.values or '0')) + -- `chars` (dispatch tables) and `flagchars` don't generate completion values (they self-expand). + local values = o.schema and schema_values(o.schema) or {} + write(' .values=', (#values > 0 and get_values_var(o) or 'NULL')) + write(' .values_len=', (#values > 0 and #values or '0')) + -- A `dict` schema also emits an OptSchemaItem[]; expose it so the structured value conversion can + -- read typed sub-values. + write( + ' .schema=', + (is_dict_option(o) and ('opt_%s_schema'):format(o.abbreviation or o.full_name) or 'NULL') + ) write(' .flags_var=', (o.flags_varname and ('&%s'):format(o.flags_varname) or 'NULL')) if o.enable_if then write(('#if defined(%s)'):format(o.enable_if)) @@ -245,87 +277,66 @@ local function dump_option(i, o, write) write(' },') end ---- @param prefix string ---- @param values vim.option_valid_values -local function preorder_traversal(prefix, values) - local out = {} --- @type string[] - - local function add(s) - table.insert(out, s) - end - - add('') - add(('EXTERN const char *(%s_values[%s]) INIT( = {'):format(prefix, #vim.tbl_keys(values) + 1)) - - --- @type [string,vim.option_valid_values][] - local children = {} - - for _, value in ipairs(values) do - if type(value) == 'string' then - add((' "%s",'):format(value)) - else - assert(type(value) == 'table' and type(value[1]) == 'string' and type(value[2]) == 'table') - add((' "%s",'):format(value[1])) - table.insert(children, value) - end - end - - add(' NULL') - add('});') - - for _, value in pairs(children) do - -- Remove trailing colon from the added prefix to prevent syntax errors. - add(preorder_traversal(prefix .. '_' .. value[1]:gsub(':$', ''), value[2])) - end - - return table.concat(out, '\n') -end - +--- Emit an option's flag-set enum from its `flagchars` or `flags` schema: +--- * `flagchars` (e.g. 'formatoptions'): a name->char map -> `enum { kFoWrap = 't', … }` +--- * `flags` (e.g. 'foldopen', 'virtualedit'): an ordered list of names (bit = 2^i) or of +--- {name, bit} pairs -> `typedef enum { kOptFdoFlagHor = 0x…, … } OptFdoFlags;`. A pair's +--- optional 3rd element overrides the C constant's token (e.g. 'NONE' -> `NoneU`). --- @param o vim.option_meta --- @return string -local function gen_opt_enum(o) +local function gen_token_enum(o) local out = {} --- @type string[] - local function add(s) table.insert(out, s) end + local opt_name = lowercase_to_titlecase(o.abbreviation or o.full_name) + + if o.schema.flagchars then + -- Char flags complete as raw chars, so order is irrelevant; sort by name for a stable enum. + local names = vim.tbl_keys(o.schema.flagchars) --[[ @as string[] ]] + table.sort(names) + add('') + add('enum {') + for _, name in ipairs(names) do + add( + (" k%s%s = '%s',"):format( + opt_name, + lowercase_to_titlecase(name), + (o.schema.flagchars[name]:gsub("[\\']", '\\%0')) + ) + ) + end + add('};') + return table.concat(out, '\n') + end + + -- Bitmask flags: collect (C-token, bit) and emit in bit order. + local flags = {} --- @type {name:string,bit:integer}[] + for i, entry in ipairs(o.schema.flags) do + if type(entry) == 'string' then + flags[#flags + 1] = { name = entry, bit = math.pow(2, i - 1) } + else + local e = entry --[[ @as {[1]:string, [2]:integer, [3]:string?} ]] + flags[#flags + 1] = { name = e[3] or e[1], bit = e[2] } + end + end + table.sort(flags, function(a, b) + return a.bit < b.bit + end) add('') add('typedef enum {') - - local opt_name = lowercase_to_titlecase(o.abbreviation or o.full_name) - --- @type table - local enum_values - - if type(o.flags) == 'table' then - enum_values = o.flags --[[ @as table ]] - else - enum_values = {} - for i, flag_name in ipairs(o.values) do - assert(type(flag_name) == 'string') - enum_values[flag_name] = math.pow(2, i - 1) - end - end - - -- Sort the keys by the flag value so that the enum can be generated in order. - --- @type string[] - local flag_names = vim.tbl_keys(enum_values) - table.sort(flag_names, function(a, b) - return enum_values[a] < enum_values[b] - end) - - for _, flag_name in pairs(flag_names) do + for _, f in ipairs(flags) do + -- A "key:" token (e.g. 'messagesopt' "wait:") names its flag without the trailing colon. add( (' kOpt%sFlag%s = 0x%02x,'):format( opt_name, - lowercase_to_titlecase(flag_name:gsub(':$', '')), - enum_values[flag_name] + lowercase_to_titlecase((f.name:gsub(':$', ''))), + f.bit ) ) end - add(('} Opt%sFlags;'):format(opt_name)) - return table.concat(out, '\n') end @@ -452,6 +463,87 @@ local function gen_map(output_file, option_index) fd:close() end +-- Maps a `dict` key's value-kind to its OptSchemaKind enum. Enum keys carry a values array instead. +local schema_kinds = { + num = 'kOptSchemaNum', + snum = 'kOptSchemaSNum', + str = 'kOptSchemaStr', +} + +--- Emit the OptSchemaItem[] that opt_strings_check() validates against, from a `dict` schema. +--- @param prefix string e.g. "opt_dip" +--- @param schema vim.option_schema +--- @return string +local function gen_opt_schema(prefix, schema) + local out = {} --- @type string[] + local function add(s) + table.insert(out, s) + end + + add('') + add(('EXTERN const OptSchemaItem %s_schema[] INIT( = {'):format(prefix)) + for _, item in ipairs(schema.dict) do + if type(item) == 'string' then + add((' { "%s", kOptSchemaFlag, NULL },'):format(item)) + elseif item[2] == 'enum' then + add((' { "%s", kOptSchemaEnum, %s_%s_values },'):format(item[1], prefix, item[1])) + else + local kind = schema_kinds[item[2]] or error(('%s: bad kind %q'):format(prefix, item[2])) + add((' { "%s", %s, NULL },'):format(item[1], kind)) + end + end + add(' { NULL, kOptSchemaFlag, NULL },') + add('});') + + return table.concat(out, '\n') +end + +--- Emit the completion values arrays (`opt__values`, plus `opt___values` for each +--- `dict` enum key). Enum/set/flag tokens complete as-is; `dict` keys complete as "key:". +--- @param prefix string e.g. "opt_dip" +--- @param schema vim.option_schema +--- @return string +local function gen_schema_values(prefix, schema) + local out = {} --- @type string[] + local function add(s) + table.insert(out, s) + end + + local values = schema_values(schema) + add('') + add(('EXTERN const char *(%s_values[%s]) INIT( = {'):format(prefix, #values + 1)) + for _, v in ipairs(values) do + add((' "%s",'):format(v)) + end + add(' NULL') + add('});') + + for _, item in ipairs(schema.dict or {}) do + if type(item) == 'table' and item[2] == 'enum' then + local vals = item[3].values --[[ @as string[] ]] + add('') + add(('EXTERN const char *(%s_%s_values[%s]) INIT( = {'):format(prefix, item[1], #vals + 1)) + for _, v in ipairs(vals) do + add((' "%s",'):format(v)) + end + add(' NULL') + add('});') + end + end + + return table.concat(out, '\n') +end + +local c_keywords = { inline = true, default = true, auto = true, register = true } + +--- Sanitize a schema key into a C struct field name. C keywords get a trailing '_'. +--- @param name string +--- @return string +local function c_field_name(name) + local f = (name:gsub('%-', '_')) + return c_keywords[f] and (f .. '_') or f +end + --- @param output_file string local function gen_vars(output_file) local fd = assert(io.open(output_file, 'w')) @@ -466,23 +558,170 @@ local function gen_vars(output_file) -- Generate enums for option flags. for _, o in ipairs(options_meta) do - if o.flags and (type(o.flags) == 'table' or o.values) then - write(gen_opt_enum(o)) + if o.schema and (o.schema.flags or o.schema.flagchars) then + write(gen_token_enum(o)) end end - -- Generate valid values for each option. + -- Generate valid values (for completion) and, for `dict` schemas, the validation schema. for _, option in ipairs(options_meta) do - -- Since option values can be nested, we need to do preorder traversal to generate the values. - if option.values then - local values_var = ('opt_%s'):format(option.abbreviation or option.full_name) - write(preorder_traversal(values_var, option.values)) + local values_var = ('opt_%s'):format(option.abbreviation or option.full_name) + -- `chars` dispatch tables (gen_chartab) and `flagchars` (gen_token_enum) have no completion + -- values; every other schema does. + if option.schema and #schema_values(option.schema) > 0 then + write(gen_schema_values(values_var, option.schema)) + if is_dict_option(option) then + write(gen_opt_schema(values_var, option.schema)) + end end end fd:close() end +--- Emit the chars_tab[] dispatch tables (fcs_tab/lcs_tab) from options with a `char`/`chars` +--- schema. +--- +--- #include "options_chartab.generated.h" +--- +--- @param output_file string +local function gen_chartab(output_file) + local fd = assert(io.open(output_file, 'w')) + --- @param s string + local function write(s) + fd:write(s) + fd:write('\n') + end + + write('// IWYU pragma: private, include "nvim/optionstr.c"') + for _, o in ipairs(options_meta) do + if o.schema and o.schema.chars then + local abbr = o.abbreviation or o.full_name + write('') + write(('static const struct chars_tab %s_tab[] = {'):format(abbr)) + for _, item in ipairs(o.schema.chars) do + --- @cast item vim.option_schema.char + local name = item[1] + local opts = item[3] or {} + -- opts.field == false means no storage (NULL cp), e.g. 'multispace'; nil defaults to name. + local cp = 'NULL' + if opts.field ~= false then + cp = ('&%s_chars.%s'):format(abbr, opts.field or name) + end + local def = opts.def and ('"%s"'):format(opts.def) or 'NULL' + local fallback = opts.fallback and ('"%s"'):format(opts.fallback) or 'NULL' + write((' CHARSTAB_ENTRY(%s, "%s", %s, %s),'):format(cp, name, def, fallback)) + end + write('};') + end + end + + fd:close() +end + +--- Emit an option keyset: a `KeyDict_…` struct + `KeySetLink` + perfect-hash get_field, generated +--- from a `dict` schema. Consumed by `api_dict_to_keydict()`. +--- @param output_file string +local function gen_keysets(output_file) + local keyset = require('gen.keyset') + local fd = assert(io.open(output_file, 'w')) + local function write(s) + fd:write(s) + fd:write('\n') + end + + write('#pragma once') + write('') + write('#include ') + write('#include "nvim/api/private/defs.h"') + write('#include "nvim/option_defs.h"') + + local struct_opts = {} --- @type {enum:string,abbr:string,kd:string}[] + + for _, o in ipairs(options_meta) do + -- A `dict` schema reifies to a keyset; generate it here. + if is_dict_option(o) then + -- Mirrors the API's KeyDict codegen with an "Opt" prefix: struct OptKeyDict_, and + -- as the keyset name for the table/hash/HAS_KEY (e.g. HAS_KEY(v, dip, filler)). + local abbr = o.abbreviation or o.full_name + local kd = 'OptKeyDict_' .. abbr + struct_opts[#struct_opts + 1] = + { enum = 'kOpt' .. lowercase_to_titlecase(o.full_name), abbr = abbr, kd = kd } + local keys = {} --- @type string[] + local info = {} --- @type table + for _, item in ipairs(o.schema.dict) do + local key = type(item) == 'string' and item or item[1] + local kind = type(item) == 'string' and 'flag' or item[2] + keys[#keys + 1] = key + info[key] = { + field = c_field_name(key), + ctype = keyset_ctype[kind] or 'kObjectTypeBoolean', + } + end + + -- The schema array (validation grammar) lives in option_vars.generated.h; forward-declare it + -- so this header stays self-contained. + write('') + write(('extern const OptSchemaItem opt_%s_schema[];'):format(abbr)) + write('') + write(('/// %s'):format(o.full_name)) + write(('typedef struct %s {'):format(kd)) + write((' OptionalKeys is_set__%s_;'):format(abbr)) + for _, item in ipairs(o.schema.dict) do + local key = type(item) == 'string' and item or item[1] + local kind = type(item) == 'string' and 'flag' or item[2] + write((' %s %s;'):format(keyset_ftype[kind] or 'Boolean', c_field_name(key))) + end + write(('} %s;'):format(kd)) + write('') + + local order, hashfun = keyset.hash(abbr, keys) + local entry = {} --- @type table + for i, key in ipairs(order) do + write(('#define KEYSET_OPTIDX_%s__%s %d'):format(abbr, c_field_name(key), i)) + entry[key] = + { field = info[key].field, type = info[key].ctype, opt_index = i, is_hlgroup = false } + end + -- `static` (inline): diff.c includes this header for the struct type alone, so a plain `static` + -- table/hash/get_field would warn as unused. + keyset.emit(write, { + name = abbr, + get_field = kd .. '_get_field', + struct = kd, + order = order, + hashfun = hashfun, + entry = entry, + static = true, + }) + end + end + + -- Dispatch from option index to its keyset handle (opt_dict_info); NULL for non-dict options. + write('') + write('static inline const OptDictInfo *opt_dict_info(OptIndex opt_idx)') + write('{') + write(' switch (opt_idx) {') + for _, s in ipairs(struct_opts) do + write((' case %s: {'):format(s.enum)) + write( + (' static const OptDictInfo info = { %s_get_field, %s_table, opt_%s_schema,'):format( + s.kd, + s.abbr, + s.abbr + ) + ) + write((' sizeof(%s) };'):format(s.kd)) + write(' return &info;') + write(' }') + end + write(' default:') + write(' return NULL;') + write(' }') + write('}') + + fd:close() +end + --- @param output_file string local function gen_options(output_file) local fd = assert(io.open(output_file, 'w')) @@ -527,10 +766,14 @@ local function main() local options_enum_file = arg[2] local options_map_file = arg[3] local option_vars_file = arg[4] + local options_chartab_file = arg[5] + local options_keysets_file = arg[6] local option_index = gen_enums(options_enum_file) gen_map(options_map_file, option_index) gen_vars(option_vars_file) + gen_chartab(options_chartab_file) + gen_keysets(options_keysets_file) gen_options(options_file) end diff --git a/src/gen/gen_steps.zig b/src/gen/gen_steps.zig index 4b002742f1..f1a7758156 100644 --- a/src/gen/gen_steps.zig +++ b/src/gen/gen_steps.zig @@ -41,6 +41,8 @@ pub fn nvim_gen_sources( _ = gen_header(b, gen_step, "options_enum.generated.h", gen_headers); _ = gen_header(b, gen_step, "options_map.generated.h", gen_headers); _ = gen_header(b, gen_step, "option_vars.generated.h", gen_headers); + _ = gen_header(b, gen_step, "options_chartab.generated.h", gen_headers); + _ = gen_header(b, gen_step, "options_keysets.generated.h", gen_headers); gen_step.addFileArg(b.path("src/nvim/options.lua")); const test_gen_step = b.step("wipopt", "debug one nlua0 (options)"); diff --git a/src/gen/keyset.lua b/src/gen/keyset.lua new file mode 100644 index 0000000000..0e5eb0553c --- /dev/null +++ b/src/gen/keyset.lua @@ -0,0 +1,55 @@ +-- Shared codegen for a `KeySetLink` perfect-hash table and its `get_field()` lookup. +local hashy = require('gen.hashy') + +local M = {} + +--- Perfect-hash `keys`, using the shared `_table[idx].str` probe convention. +--- @param name string +--- @param keys string[] +--- @return string[] order keys in perfect-hash order +--- @return string hashfun source of the `_hash()` function +function M.hash(name, keys) + return hashy.hashy_hash(name, keys, function(idx) + return name .. '_table[' .. idx .. '].str' + end) +end + +--- @class gen.keyset.entry +--- @field field string C struct field name, for offsetof() +--- @field type string ObjectType/UnpackType enum, e.g. 'kObjectTypeInteger' +--- @field opt_index integer index used by HAS_KEY (or -1 when the keyset has no optional keys) +--- @field is_hlgroup boolean + +--- Emit `_table[]` (a `KeySetLink[]` in perfect-hash `order`) followed by `()`. +--- @param write fun(s: string) writer that appends a newline +--- @param p { name: string, get_field: string, struct: string, order: string[], hashfun: string, entry: table, static: boolean } +--- `static` emits a `static const` table + `static inline` funcs (a self-contained header, e.g. the +--- option keysets); otherwise external linkage (the API keydicts, declared in a separate header). +function M.emit(write, p) + write(('%sKeySetLink %s_table[] = {'):format(p.static and 'static const ' or '', p.name)) + for _, key in ipairs(p.order) do + local e = p.entry[key] + write( + (' { "%s", offsetof(%s, %s), %s, %d, %s },'):format( + key, + p.struct, + e.field, + e.type, + e.opt_index, + e.is_hlgroup and 'true' or 'false' + ) + ) + end + write(' { NULL, 0, kObjectTypeNil, -1, false },') + write('};') + write('') + local inl = p.static and 'static inline ' or '' + write(inl .. p.hashfun) + write(inl .. ('KeySetLink *%s(const char *str, size_t len)'):format(p.get_field)) + write('{') + write((' int hash = %s_hash(str, len);'):format(p.name)) + write((' return hash == -1 ? NULL : (KeySetLink *)&%s_table[hash];'):format(p.name)) + write('}') +end + +return M diff --git a/src/nlua0.zig b/src/nlua0.zig index bfea786a7d..3d6d2db554 100644 --- a/src/nlua0.zig +++ b/src/nlua0.zig @@ -13,6 +13,7 @@ const embedded_data = @import("embedded_data"); // these are common dependencies used by many generators const hashy = @embedFile("gen/hashy.lua"); +const keyset = @embedFile("gen/keyset.lua"); const c_grammar = @embedFile("gen/c_grammar.lua"); const Lua = ziglua.Lua; @@ -43,6 +44,8 @@ fn init_lua() !*Lua { _ = lua.getField(-1, "preload"); try lua.loadBuffer(hashy, "hashy.lua"); // [package, preload, hashy] lua.setField(-2, "gen.hashy"); + try lua.loadBuffer(keyset, "keyset.lua"); // [package, preload, keyset] + lua.setField(-2, "gen.keyset"); try lua.loadBuffer(c_grammar, "c_grammar.lua"); // [package, preload, c_grammar] lua.setField(-2, "gen.c_grammar"); lua.pop(2); diff --git a/src/nvim/CMakeLists.txt b/src/nvim/CMakeLists.txt index 34d690e764..96872ea846 100644 --- a/src/nvim/CMakeLists.txt +++ b/src/nvim/CMakeLists.txt @@ -336,6 +336,8 @@ set(GENERATED_OPTIONS ${GENERATED_DIR}/options.generated.h) set(GENERATED_OPTIONS_ENUM ${GENERATED_DIR}/options_enum.generated.h) set(GENERATED_OPTIONS_MAP ${GENERATED_DIR}/options_map.generated.h) set(GENERATED_OPTION_VARS ${GENERATED_DIR}/option_vars.generated.h) +set(GENERATED_OPTIONS_CHARTAB ${GENERATED_DIR}/options_chartab.generated.h) +set(GENERATED_OPTIONS_KEYSETS ${GENERATED_DIR}/options_keysets.generated.h) set(GENERATED_UI_EVENTS_CALL ${GENERATED_DIR}/ui_events_call.generated.h) set(GENERATED_UI_EVENTS_CLIENT ${GENERATED_DIR}/ui_events_client.generated.h) set(GENERATED_UI_EVENTS_REMOTE ${GENERATED_DIR}/ui_events_remote.generated.h) @@ -711,8 +713,8 @@ add_custom_command(OUTPUT ${GENERATED_KEYCODE_NAMES} DEPENDS ${LUA_GEN_DEPS} ${KEYCODES_GENERATOR} ${GENERATOR_HASHY} ${CMAKE_CURRENT_LIST_DIR}/keycodes.lua ) -add_custom_command(OUTPUT ${GENERATED_OPTIONS} ${GENERATED_OPTIONS_ENUM} ${GENERATED_OPTIONS_MAP} ${GENERATED_OPTION_VARS} - COMMAND ${LUA_GEN} ${OPTIONS_GENERATOR} ${GENERATED_OPTIONS} ${GENERATED_OPTIONS_ENUM} ${GENERATED_OPTIONS_MAP} ${GENERATED_OPTION_VARS} ${CMAKE_CURRENT_LIST_DIR}/options.lua +add_custom_command(OUTPUT ${GENERATED_OPTIONS} ${GENERATED_OPTIONS_ENUM} ${GENERATED_OPTIONS_MAP} ${GENERATED_OPTION_VARS} ${GENERATED_OPTIONS_CHARTAB} ${GENERATED_OPTIONS_KEYSETS} + COMMAND ${LUA_GEN} ${OPTIONS_GENERATOR} ${GENERATED_OPTIONS} ${GENERATED_OPTIONS_ENUM} ${GENERATED_OPTIONS_MAP} ${GENERATED_OPTION_VARS} ${GENERATED_OPTIONS_CHARTAB} ${GENERATED_OPTIONS_KEYSETS} ${CMAKE_CURRENT_LIST_DIR}/options.lua DEPENDS ${LUA_GEN_DEPS} ${OPTIONS_GENERATOR} ${GENERATOR_HASHY} ${CMAKE_CURRENT_LIST_DIR}/options.lua ) @@ -732,6 +734,8 @@ list(APPEND NVIM_GENERATED_FOR_SOURCES "${GENERATED_KEYCODE_NAMES}" "${GENERATED_OPTIONS}" "${GENERATED_OPTIONS_MAP}" + "${GENERATED_OPTIONS_CHARTAB}" + "${GENERATED_OPTIONS_KEYSETS}" "${VIM_MODULE_FILE}" "${GENERATED_API_METADATA}" "${PROJECT_BINARY_DIR}/cmake.config/auto/pathdef.h" diff --git a/src/nvim/api/deprecated.c b/src/nvim/api/deprecated.c index d3958aaa8c..5b3ab74b9e 100644 --- a/src/nvim/api/deprecated.c +++ b/src/nvim/api/deprecated.c @@ -757,7 +757,12 @@ static Object get_option_from(void *from, OptScope scope, String name, Error *er return (Object)OBJECT_INIT; }); - return optval_as_object(value); + Object rv = optval_as_object(value); + // A struct value serializes into a fresh Object string, so its keyset must be freed here. + if (value.type == kOptValTypeDict) { + optval_free(value); + } + return rv; } /// Sets the value of a global or local (buffer, window) option. diff --git a/src/nvim/api/options.c b/src/nvim/api/options.c index cc35d2a4af..4d8d8fc11e 100644 --- a/src/nvim/api/options.c +++ b/src/nvim/api/options.c @@ -305,7 +305,13 @@ Object nvim_get_option_value(String name, Dict(option) *opts, Error *err) goto err; }); - return optval_as_object(value); + Object rv = optval_as_object(value); + // kOptValTypeDict serializes into a fresh Object string (not aliased like a plain string), so + // its keyset must be freed here. + if (value.type == kOptValTypeDict) { + optval_free(value); + } + return rv; err: optval_free(value); return (Object)OBJECT_INIT; @@ -397,6 +403,10 @@ Object nvim_set_option_value(uint64_t channel_id, String name, Object value, Dic case kOptValTypeBoolean: merged_val = optval_right; break; + case kOptValTypeDict: + // Unreachable: object_as_optval_for() yields the string form for dict options, which is + // merged as a string (below) and reified by set_option(). + break; } optval_free(optval_right); @@ -416,27 +426,10 @@ Object nvim_set_option_value(uint64_t channel_id, String name, Object value, Dic }); } - if (merged_val.type == kOptValTypeString) { - // Convert string/list/map style option to a (Lua) structure. - Error lua_err = ERROR_INIT; - MAXSIZE_TEMP_ARRAY(lua_args, 2); - ADD_C(lua_args, STRING_OBJ(name)); - ADD_C(lua_args, STRING_OBJ(merged_val.data.string)); - Object lua_val = - NLUA_EXEC_STATIC("return require('vim._core.options').convert_value_to_lua(...)", - lua_args, kRetObject, arena, &lua_err); - - optval_free(merged_val); - - VALIDATE(!ERROR_SET(&lua_err), "%s", lua_err.msg, { - api_clear_error(&lua_err); - return NIL; - }); - - return lua_val; - } - - return optval_as_object(merged_val); + // Return the value in its structured (list/map/set) form. + Object rv = optval_to_struct(opt_idx, merged_val, arena); + optval_free(merged_val); + return rv; } /// Gets the option information for all options. diff --git a/src/nvim/buffer.c b/src/nvim/buffer.c index fbb2e3c1f2..2607969884 100644 --- a/src/nvim/buffer.c +++ b/src/nvim/buffer.c @@ -177,7 +177,7 @@ int get_highest_fnum(void) static int read_buffer(bool read_stdin, exarg_T *eap, int flags) { int retval = OK; - bool silent = shortmess(SHM_FILEINFO); + bool silent = shortmess(kShmFileinfo); // Read from the buffer which the text is already filled in and append at // the end. This makes it possible to retry when 'fileformat' or @@ -250,7 +250,7 @@ int open_buffer(bool read_stdin, exarg_T *eap, int flags_arg) bufref_T old_curbuf; OptInt old_tw = curbuf->b_p_tw; bool read_fifo = false; - bool silent = shortmess(SHM_FILEINFO); + bool silent = shortmess(kShmFileinfo); // The 'readonly' flag is only set when BF_NEVERLOADED is being reset. // When re-entering the same buffer, it should not change, because the @@ -384,9 +384,9 @@ int open_buffer(bool read_stdin, exarg_T *eap, int flags_arg) // When reading stdin, the buffer contents always needs writing, so set // the changed flag. Unless in readonly mode: "ls | nvim -R -". // When interrupted and 'cpoptions' contains 'i' set changed flag. - if ((got_int && vim_strchr(p_cpo, CPO_INTMOD) != NULL) + if ((got_int && vim_strchr(p_cpo, kCpoIntmod) != NULL) || curbuf->b_modified_was_set // autocmd did ":set modified" - || (aborting() && vim_strchr(p_cpo, CPO_INTMOD) != NULL)) { + || (aborting() && vim_strchr(p_cpo, kCpoIntmod) != NULL)) { changed(curbuf); } else if (retval != FAIL && !read_stdin && !read_fifo) { unchanged(curbuf, false, true); @@ -1287,7 +1287,7 @@ static int empty_curbuf(bool close_others, int forceit, int action) if (!close_others) { need_fileinfo = false; - } else if (retval == OK && !shortmess(SHM_FILEINFO)) { + } else if (retval == OK && !shortmess(kShmFileinfo)) { // do_ecmd() does not display file info for a new empty buffer. need_fileinfo = true; } @@ -1834,7 +1834,7 @@ static void enter_buffer(buf_T *buf) open_buffer(false, NULL, 0); } else { - if (!msg_silent && !shortmess(SHM_FILEINFO)) { + if (!msg_silent && !shortmess(kShmFileinfo)) { need_fileinfo = true; // display file info after redraw } // check if file changed @@ -3349,7 +3349,7 @@ void fileinfo(int fullname, int shorthelp, bool dont_truncate) IOSIZE - bufferlen, "\"%s%s%s%s%s%s", curbufIsChanged() - ? (shortmess(SHM_MOD) ? " [+]" : _(" [Modified]")) + ? (shortmess(kShmMod) ? " [+]" : _(" [Modified]")) : " ", (curbuf->b_flags & BF_NOTEDITED) && !dontwrite ? _("[Not edited]") : "", @@ -3358,7 +3358,7 @@ void fileinfo(int fullname, int shorthelp, bool dont_truncate) (curbuf->b_flags & BF_READERR) ? _("[Read errors]") : "", curbuf->b_p_ro - ? (shortmess(SHM_RO) ? _("[RO]") : _("[readonly]")) + ? (shortmess(kShmRo) ? _("[RO]") : _("[readonly]")) : "", (curbufIsChanged() || (curbuf->b_flags & BF_WRITE_MASK) diff --git a/src/nvim/buffer_defs.h b/src/nvim/buffer_defs.h index 38646bd0e2..5ff5c3b0d5 100644 --- a/src/nvim/buffer_defs.h +++ b/src/nvim/buffer_defs.h @@ -92,7 +92,7 @@ typedef struct { #define w_p_arab w_onebuf_opt.wo_arab // 'arabic' int wo_bri; #define w_p_bri w_onebuf_opt.wo_bri // 'breakindent' - char *wo_briopt; + struct OptKeyDict_briopt *wo_briopt; // 'breakindentopt' #define w_p_briopt w_onebuf_opt.wo_briopt // 'breakindentopt' int wo_diff; #define w_p_diff w_onebuf_opt.wo_diff // 'diff' diff --git a/src/nvim/bufwrite.c b/src/nvim/bufwrite.c index 426a492407..f646850416 100644 --- a/src/nvim/bufwrite.c +++ b/src/nvim/bufwrite.c @@ -467,7 +467,7 @@ static int buf_write_do_autocmds(buf_T *buf, char **fnamep, char **sfnamep, char } } if (reset_changed && buf->b_changed && !append - && (overwriting || vim_strchr(p_cpo, CPO_PLUS) != NULL)) { + && (overwriting || vim_strchr(p_cpo, kCpoPlus) != NULL)) { // Buffer still changed, the autocommands didn't work properly. return FAIL; } @@ -664,7 +664,7 @@ static int get_fileinfo(buf_T *buf, char *fname, bool overwriting, bool forceit, *readonly = !os_file_is_writable(fname); if (!forceit && *readonly) { - if (vim_strchr(p_cpo, CPO_FWRITE) != NULL) { + if (vim_strchr(p_cpo, kCpoFwrite) != NULL) { *err = set_err_num("E504", _(err_readonly)); } else { *err = set_err_num("E505", _("is read-only (add ! to override)")); @@ -903,7 +903,7 @@ nobackup: // If 'cpoptions' includes the "W" flag, we don't want to // overwrite a read-only file. But rename may be possible // anyway, thus we need an extra check here. - if (file_readonly && vim_strchr(p_cpo, CPO_FWRITE) != NULL) { + if (file_readonly && vim_strchr(p_cpo, kCpoFwrite) != NULL) { *err = set_err_num("E504", _(err_readonly)); return FAIL; } @@ -1028,8 +1028,8 @@ int buf_write(buf_T *buf, char *fname, char *sfname, linenr_T start, linenr_T en && buf == curbuf && !bt_nofilename(buf) && !filtering - && (!append || vim_strchr(p_cpo, CPO_FNAMEAPP) != NULL) - && vim_strchr(p_cpo, CPO_FNAMEW) != NULL) { + && (!append || vim_strchr(p_cpo, kCpoFnameapp) != NULL) + && vim_strchr(p_cpo, kCpoFnamew) != NULL) { if (set_rw_fname(fname, sfname) == FAIL) { return FAIL; } @@ -1076,7 +1076,7 @@ int buf_write(buf_T *buf, char *fname, char *sfname, linenr_T start, linenr_T en buf->b_op_end = orig_end; } - if (shortmess(SHM_OVER) && !exiting) { + if (shortmess(kShmOver) && !exiting) { msg_scroll = false; // overwrite previous file message } else { msg_scroll = true; // don't overwrite previous file message @@ -1164,7 +1164,7 @@ int buf_write(buf_T *buf, char *fname, char *sfname, linenr_T start, linenr_T en // When using ":w!" and the file was read-only: make it writable if (forceit && perm >= 0 && !(perm & 0200) && file_info_old.stat.st_uid == getuid() - && vim_strchr(p_cpo, CPO_FWRITE) == NULL) { + && vim_strchr(p_cpo, kCpoFwrite) == NULL) { perm |= 0200; os_setperm(fname, perm); made_writable = true; @@ -1173,7 +1173,7 @@ int buf_write(buf_T *buf, char *fname, char *sfname, linenr_T start, linenr_T en // When using ":w!" and writing to the current file, 'readonly' makes no // sense, reset it, unless 'Z' appears in 'cpoptions'. - if (forceit && overwriting && vim_strchr(p_cpo, CPO_KEEPRO) == NULL) { + if (forceit && overwriting && vim_strchr(p_cpo, kCpoKeepro) == NULL) { buf->b_p_ro = false; need_maketitle = true; // set window title later status_redraw_all(); // redraw status lines later @@ -1328,7 +1328,7 @@ int buf_write(buf_T *buf, char *fname, char *sfname, linenr_T start, linenr_T en err = set_err(_("E166: Can't open linked file for writing")); } else { err = set_err_arg(_("E212: Can't open file for writing: %s"), fd); - if (forceit && vim_strchr(p_cpo, CPO_FWRITE) == NULL && perm >= 0) { + if (forceit && vim_strchr(p_cpo, kCpoFwrite) == NULL && perm >= 0) { // we write to the file, thus it should be marked // writable after all if (!(perm & 0200)) { @@ -1347,7 +1347,7 @@ int buf_write(buf_T *buf, char *fname, char *sfname, linenr_T start, linenr_T en } #else err = set_err_arg(_("E212: Can't open file for writing: %s"), fd); - if (forceit && vim_strchr(p_cpo, CPO_FWRITE) == NULL && perm >= 0) { + if (forceit && vim_strchr(p_cpo, kCpoFwrite) == NULL && perm >= 0) { if (!append) { // don't remove when appending os_remove(wfname); } @@ -1704,11 +1704,11 @@ restore_backup: insert_space = true; } msg_add_lines(insert_space, lnum, nchars); // add line/char count - if (!shortmess(SHM_WRITE)) { + if (!shortmess(kShmWrite)) { if (append) { - xstrlcat(IObuff, shortmess(SHM_WRI) ? _(" [a]") : _(" appended"), IOSIZE); + xstrlcat(IObuff, shortmess(kShmWri) ? _(" [a]") : _(" appended"), IOSIZE); } else { - xstrlcat(IObuff, shortmess(SHM_WRI) ? _(" [w]") : _(" written"), IOSIZE); + xstrlcat(IObuff, shortmess(kShmWri) ? _(" [w]") : _(" written"), IOSIZE); } } // Hide cursor while emitting "written" message, so cursor doesn't flicker in cmdline. #25974 @@ -1721,7 +1721,7 @@ restore_backup: // writing to the original file and '+' is not in 'cpoptions'. if (reset_changed && whole && !append && !write_info.bw_conv_error - && (overwriting || vim_strchr(p_cpo, CPO_PLUS) != NULL)) { + && (overwriting || vim_strchr(p_cpo, kCpoPlus) != NULL)) { unchanged(buf, true, false); const varnumber_T changedtick = buf_get_changedtick(buf); if (buf->b_last_changedtick + 1 == changedtick) { diff --git a/src/nvim/change.c b/src/nvim/change.c index ff6a25eaa8..6d2d2e7a1d 100644 --- a/src/nvim/change.c +++ b/src/nvim/change.c @@ -440,7 +440,7 @@ void changed_bytes(linenr_T lnum, colnr_T col) // Don't do this when displaying '$' at the end of changed text. if (spell_check_window(curwin) && lnum < curbuf->b_ml.ml_line_count - && vim_strchr(p_cpo, CPO_DOLLAR) == NULL) { + && vim_strchr(p_cpo, kCpoDollar) == NULL) { redrawWinline(curwin, lnum + 1); } // notify any channels that are watching @@ -747,7 +747,7 @@ void ins_char_bytes(char *buf, size_t charlen) // Returns the old value of list, so when finished, // curwin->w_p_list should be set back to this. int old_list = curwin->w_p_list; - if (old_list && vim_strchr(p_cpo, CPO_LISTWM) == NULL) { + if (old_list && vim_strchr(p_cpo, kCpoListwm) == NULL) { curwin->w_p_list = false; } // In virtual replace mode each character may replace one or more @@ -1237,7 +1237,7 @@ bool open_line(int dir, int flags, int second_line_indent, bool *did_do_comment) if (flags & OPENLINE_DO_COM) { lead_len = get_leader_len(saved_line, &lead_flags, dir == BACKWARD, true); if (lead_len == 0 && curbuf->b_p_cin && do_cindent && dir == FORWARD - && (!has_format_option(FO_NO_OPEN_COMS) || (flags & OPENLINE_FORMAT))) { + && (!has_format_option(kFoNoOpenComs) || (flags & OPENLINE_FORMAT))) { // Check for a line comment after code. comment_start = check_linecomment(saved_line); if (comment_start != MAXCOL) { diff --git a/src/nvim/diff.c b/src/nvim/diff.c index a34eadf8be..26afac9063 100644 --- a/src/nvim/diff.c +++ b/src/nvim/diff.c @@ -16,6 +16,7 @@ #include #include "auto/config.h" +#include "nvim/api/private/helpers.h" #include "nvim/ascii_defs.h" #include "nvim/autocmd.h" #include "nvim/autocmd_defs.h" @@ -71,6 +72,9 @@ #include "nvim/window.h" #include "xdiff/xdiff.h" +// KeyDict_dip + KeyDict_dip_get_field, generated from the 'diffopt' schema (reusing hashy.lua). +#include "options_keysets.generated.h" + static bool diff_busy = false; // using diff structs, don't change them static bool diff_need_update = false; // ex_diffupdate needs to be called @@ -2650,147 +2654,63 @@ int diffanchors_changed(bool buflocal) return result; } -/// This is called when 'diffopt' is changed. +/// Map the 'diffopt' keyset onto diff.c's globals. `v` is the stored value (`p_dip`), reified from +/// the ":set" string by `opt_fill()` when the option is set. /// -/// @return -int diffopt_changed(void) +/// @return FAIL only for the cross-part "horizontal" + "vertical" conflict. +static int diffopt_apply(OptKeyDict_dip *v) { - int diff_context_new = 6; - int linematch_lines_new = 0; - int diff_flags_new = 0; - int diff_foldcolumn_new = 2; - int diff_algorithm_new = 0; - int diff_indent_heuristic = 0; - - char *p = p_dip; - while (*p != NUL) { - // Note: Keep this in sync with opt_dip_values. - if (strncmp(p, "filler", 6) == 0) { - p += 6; - diff_flags_new |= DIFF_FILLER; - } else if (strncmp(p, "anchor", 6) == 0) { - p += 6; - diff_flags_new |= DIFF_ANCHOR; - } else if ((strncmp(p, "context:", 8) == 0) && ascii_isdigit(p[8])) { - p += 8; - diff_context_new = getdigits_int(&p, false, diff_context_new); - } else if (strncmp(p, "iblank", 6) == 0) { - p += 6; - diff_flags_new |= DIFF_IBLANK; - } else if (strncmp(p, "icase", 5) == 0) { - p += 5; - diff_flags_new |= DIFF_ICASE; - } else if (strncmp(p, "iwhiteall", 9) == 0) { - p += 9; - diff_flags_new |= DIFF_IWHITEALL; - } else if (strncmp(p, "iwhiteeol", 9) == 0) { - p += 9; - diff_flags_new |= DIFF_IWHITEEOL; - } else if (strncmp(p, "iwhite", 6) == 0) { - p += 6; - diff_flags_new |= DIFF_IWHITE; - } else if (strncmp(p, "horizontal", 10) == 0) { - p += 10; - diff_flags_new |= DIFF_HORIZONTAL; - } else if (strncmp(p, "vertical", 8) == 0) { - p += 8; - diff_flags_new |= DIFF_VERTICAL; - } else if ((strncmp(p, "foldcolumn:", 11) == 0) && ascii_isdigit(p[11])) { - p += 11; - diff_foldcolumn_new = getdigits_int(&p, false, diff_foldcolumn_new); - } else if (strncmp(p, "hiddenoff", 9) == 0) { - p += 9; - diff_flags_new |= DIFF_HIDDEN_OFF; - } else if (strncmp(p, "closeoff", 8) == 0) { - p += 8; - diff_flags_new |= DIFF_CLOSE_OFF; - } else if (strncmp(p, "followwrap", 10) == 0) { - p += 10; - diff_flags_new |= DIFF_FOLLOWWRAP; - } else if (strncmp(p, "indent-heuristic", 16) == 0) { - p += 16; - diff_indent_heuristic = XDF_INDENT_HEURISTIC; - } else if (strncmp(p, "internal", 8) == 0) { - p += 8; - diff_flags_new |= DIFF_INTERNAL; - } else if (strncmp(p, "algorithm:", 10) == 0) { - // Note: Keep this in sync with opt_dip_algorithm_values. - p += 10; - if (strncmp(p, "myers", 5) == 0) { - p += 5; - diff_algorithm_new = 0; - } else if (strncmp(p, "minimal", 7) == 0) { - p += 7; - diff_algorithm_new = XDF_NEED_MINIMAL; - } else if (strncmp(p, "patience", 8) == 0) { - p += 8; - diff_algorithm_new = XDF_PATIENCE_DIFF; - } else if (strncmp(p, "histogram", 9) == 0) { - p += 9; - diff_algorithm_new = XDF_HISTOGRAM_DIFF; - } else { - return FAIL; - } - } else if (strncmp(p, "inline:", 7) == 0) { - // Note: Keep this in sync with opt_dip_inline_values. - p += 7; - if (strncmp(p, "none", 4) == 0) { - p += 4; - diff_flags_new &= ~(ALL_INLINE); - diff_flags_new |= DIFF_INLINE_NONE; - } else if (strncmp(p, "simple", 6) == 0) { - p += 6; - diff_flags_new &= ~(ALL_INLINE); - diff_flags_new |= DIFF_INLINE_SIMPLE; - } else if (strncmp(p, "char", 4) == 0) { - p += 4; - diff_flags_new &= ~(ALL_INLINE); - diff_flags_new |= DIFF_INLINE_CHAR; - } else if (strncmp(p, "word", 4) == 0) { - p += 4; - diff_flags_new &= ~(ALL_INLINE); - diff_flags_new |= DIFF_INLINE_WORD; - } else { - return FAIL; - } - } else if ((strncmp(p, "linematch:", 10) == 0) && ascii_isdigit(p[10])) { - p += 10; - linematch_lines_new = getdigits_int(&p, false, linematch_lines_new); - diff_flags_new |= DIFF_LINEMATCH; - - // linematch does not make sense without filler set - diff_flags_new |= DIFF_FILLER; - } - - if ((*p != ',') && (*p != NUL)) { - return FAIL; - } - - if (*p == ',') { - p++; + int flags = (v->filler ? DIFF_FILLER : 0) | (v->anchor ? DIFF_ANCHOR : 0) + | (v->iblank ? DIFF_IBLANK : 0) | (v->icase ? DIFF_ICASE : 0) + | (v->iwhiteall ? DIFF_IWHITEALL : 0) | (v->iwhiteeol ? DIFF_IWHITEEOL : 0) + | (v->iwhite ? DIFF_IWHITE : 0) | (v->horizontal ? DIFF_HORIZONTAL : 0) + | (v->vertical ? DIFF_VERTICAL : 0) | (v->closeoff ? DIFF_CLOSE_OFF : 0) + | (v->hiddenoff ? DIFF_HIDDEN_OFF : 0) | (v->followwrap ? DIFF_FOLLOWWRAP : 0) + | (v->internal ? DIFF_INTERNAL : 0); + if (HAS_KEY(v, dip, linematch)) { + flags |= DIFF_LINEMATCH | DIFF_FILLER; // linematch needs filler + } + if (HAS_KEY(v, dip, inline_)) { + flags &= ~ALL_INLINE; + if (option_slice_eq(v->inline_.data, v->inline_.size, "simple")) { + flags |= DIFF_INLINE_SIMPLE; + } else if (option_slice_eq(v->inline_.data, v->inline_.size, "char")) { + flags |= DIFF_INLINE_CHAR; + } else if (option_slice_eq(v->inline_.data, v->inline_.size, "word")) { + flags |= DIFF_INLINE_WORD; + } else { + flags |= DIFF_INLINE_NONE; } } - - diff_algorithm_new |= diff_indent_heuristic; + int algorithm = v->indent_heuristic ? XDF_INDENT_HEURISTIC : 0; + if (HAS_KEY(v, dip, algorithm)) { + if (option_slice_eq(v->algorithm.data, v->algorithm.size, "minimal")) { + algorithm |= XDF_NEED_MINIMAL; + } else if (option_slice_eq(v->algorithm.data, v->algorithm.size, "patience")) { + algorithm |= XDF_PATIENCE_DIFF; + } else if (option_slice_eq(v->algorithm.data, v->algorithm.size, "histogram")) { + algorithm |= XDF_HISTOGRAM_DIFF; + } // else "myers" -> 0 + } // Can't have both "horizontal" and "vertical". - if ((diff_flags_new & DIFF_HORIZONTAL) && (diff_flags_new & DIFF_VERTICAL)) { + if ((flags & DIFF_HORIZONTAL) && (flags & DIFF_VERTICAL)) { return FAIL; } - // If flags were added or removed, or the algorithm was changed, need to - // update the diff. - if (diff_flags != diff_flags_new || diff_algorithm != diff_algorithm_new) { + // If flags were added or removed, or the algorithm was changed, update the diff. + if (diff_flags != flags || diff_algorithm != algorithm) { FOR_ALL_TABS(tp) { tp->tp_diff_invalid = true; } } - diff_flags = diff_flags_new; - diff_context = diff_context_new == 0 ? 1 : diff_context_new; - linematch_lines = linematch_lines_new; - diff_foldcolumn = diff_foldcolumn_new; - diff_algorithm = diff_algorithm_new; + int context = HAS_KEY(v, dip, context) ? (int)v->context : 6; + diff_flags = flags; + diff_context = context == 0 ? 1 : context; + linematch_lines = HAS_KEY(v, dip, linematch) ? (int)v->linematch : 0; + diff_foldcolumn = HAS_KEY(v, dip, foldcolumn) ? (int)v->foldcolumn : 2; + diff_algorithm = algorithm; diff_redraw(true); @@ -2800,6 +2720,13 @@ int diffopt_changed(void) return OK; } +/// Apply the current 'diffopt'. Its value is stored as the reified keyset `p_dip` (see +/// `opt_dict_info()`), so this just hands the stored struct to `diffopt_apply()`. +int diffopt_changed(void) +{ + return p_dip == NULL ? OK : diffopt_apply(p_dip); +} + /// Check that "diffopt" contains "horizontal". bool diffopt_horizontal(void) FUNC_ATTR_PURE FUNC_ATTR_WARN_UNUSED_RESULT diff --git a/src/nvim/drawline.c b/src/nvim/drawline.c index 025047f9a6..e97e9c1679 100644 --- a/src/nvim/drawline.c +++ b/src/nvim/drawline.c @@ -668,7 +668,7 @@ static int get_line_number_attr(win_T *wp, winlinevars_T *wlv) /// blanks when the 'n' flag isn't in 'cpo'. static void draw_lnum_col(win_T *wp, winlinevars_T *wlv) { - bool has_cpo_n = vim_strchr(p_cpo, CPO_NUMCOL) != NULL; + bool has_cpo_n = vim_strchr(p_cpo, kCpoNumcol) != NULL; if ((wp->w_p_nu || wp->w_p_rnu) && (wlv->row == wlv->startrow + wlv->filler_lines || !has_cpo_n) @@ -3268,7 +3268,7 @@ end_check: if (wlv.filler_todo <= 0) { wlv.need_showbreak = true; } - if (statuscol.draw && vim_strchr(p_cpo, CPO_NUMCOL) + if (statuscol.draw && vim_strchr(p_cpo, kCpoNumcol) && wlv.row > startrow + wlv.filler_lines) { statuscol.draw = false; // don't draw status column if "n" is in 'cpo' } diff --git a/src/nvim/drawscreen.c b/src/nvim/drawscreen.c index f0a5bcaebf..3e4fe575cf 100644 --- a/src/nvim/drawscreen.c +++ b/src/nvim/drawscreen.c @@ -944,7 +944,7 @@ int showmode(void) if (do_mode) { msg_puts_hl("--", hl_id, false); // CTRL-X in Insert mode - if (edit_submode != NULL && !shortmess(SHM_COMPLETIONMENU)) { + if (edit_submode != NULL && !shortmess(kShmCompletionmenu)) { // These messages can get long, avoid a wrap in a narrow window. // Prefer showing edit_submode_extra. With external messages there // is no imposed limit. @@ -1116,7 +1116,7 @@ void clearmode(void) static void recording_mode(int hl_id) { - if (shortmess(SHM_RECORDING)) { + if (shortmess(kShmRecording)) { return; } @@ -2553,7 +2553,7 @@ void win_draw_end(win_T *wp, schar_T c1, bool draw_margin, int startrow, int end } // draw the number column - if ((wp->w_p_nu || wp->w_p_rnu) && vim_strchr(p_cpo, CPO_NUMCOL) == NULL) { + if ((wp->w_p_nu || wp->w_p_rnu) && vim_strchr(p_cpo, kCpoNumcol) == NULL) { int width = number_width(wp) + 1; n = grid_line_fill(n, MIN(view_width, n + width), schar_from_ascii(' '), win_hl_attr(wp, HLF_N)); diff --git a/src/nvim/eval.c b/src/nvim/eval.c index c8df429c72..0a9ba2bdef 100644 --- a/src/nvim/eval.c +++ b/src/nvim/eval.c @@ -3409,6 +3409,10 @@ int eval_option(const char **const arg, typval_T *const rettv, const bool evalua assert(value.type != kOptValTypeNil); *rettv = optval_as_tv(value, true); + // A dict option serializes into a fresh typval string that rettv now owns, so free its keyset. + if (value.type == kOptValTypeDict) { + optval_free(value); + } } else if (working && !is_tty_opt && is_option_hidden(opt_idx)) { ret = FAIL; } diff --git a/src/nvim/eval/vars.c b/src/nvim/eval/vars.c index 345975b5ba..9de5bc35b3 100644 --- a/src/nvim/eval/vars.c +++ b/src/nvim/eval/vars.c @@ -41,6 +41,7 @@ #include "nvim/message.h" #include "nvim/option.h" #include "nvim/option_defs.h" +#include "nvim/optionstr.h" #include "nvim/os/os.h" #include "nvim/register.h" #include "nvim/runtime.h" @@ -1377,6 +1378,14 @@ static char *ex_let_option(char *arg, typval_T *const tv, const bool is_const, semsg(_(e_unknown_option2), arg); goto theend; } + // `:let &opt` operates on the string form; a dict option is handled as its serialization + // (set_option() reifies it back). This also lets `.=` concatenate onto it like any string option. + if (curval.type == kOptValTypeDict) { + OptVal strval = CSTR_AS_OPTVAL(opt_serialize(curval.data.dictval.ptr, + curval.data.dictval.table)); + optval_free(curval); + curval = strval; + } if (op != NULL && *op != '=' && ((curval.type != kOptValTypeString && *op == '.') || (curval.type == kOptValTypeString && *op != '.'))) { @@ -3202,7 +3211,9 @@ static OptVal tv_to_optval(typval_T *tv, OptIndex opt_idx, const char *option, b const bool is_tty_opt = is_tty_option(option); const bool option_has_bool = !is_tty_opt && option_has_type(opt_idx, kOptValTypeBoolean); const bool option_has_num = !is_tty_opt && option_has_type(opt_idx, kOptValTypeNumber); - const bool option_has_str = is_tty_opt || option_has_type(opt_idx, kOptValTypeString); + // Struct-stored options (e.g. 'diffopt') take their ":set" string here; set_option() reifies it. + const bool option_has_str = is_tty_opt || option_has_type(opt_idx, kOptValTypeString) + || option_has_type(opt_idx, kOptValTypeDict); if (!is_tty_opt && (get_option(opt_idx)->flags & kOptFlagFunc) && tv_is_func(*tv)) { // If the option can be set to a function reference or a lambda @@ -3249,6 +3260,10 @@ static OptVal tv_to_optval(typval_T *tv, OptIndex opt_idx, const char *option, b /// Convert an option value to typval. /// +/// A "schema.dict" option has no stored string to alias, so it serializes to an allocated string +/// owned by the returned; every other type borrows from `value`. Either transfer that string to +/// a longer-lived owner or release it with `optval_as_tv_free()`. +/// /// @param[in] value Option value to convert. /// @param numbool Whether to convert boolean values to number. /// Used for backwards compatibility. @@ -3278,11 +3293,25 @@ typval_T optval_as_tv(OptVal value, bool numbool) rettv.v_type = VAR_STRING; rettv.vval.v_string = value.data.string.data; break; + case kOptValTypeDict: + // Surfaced to Vimscript as its ":set" string, allocated. + rettv.v_type = VAR_STRING; + rettv.vval.v_string = opt_serialize(value.data.dictval.ptr, value.data.dictval.table); + break; } return rettv; } +/// Release `optval_as_tv()` result. Only for "schema.dict" options; no-op for other types (they +/// alias the source value). +void optval_as_tv_free(OptVal value, typval_T tv) +{ + if (value.type == kOptValTypeDict) { + xfree(tv.vval.v_string); + } +} + /// Set option "varname" to the value of "varp" for the current buffer/window. static void set_option_from_tv(const char *varname, typval_T *varp) { diff --git a/src/nvim/ex_cmds.c b/src/nvim/ex_cmds.c index 7f494f3c2d..cef02bb0a2 100644 --- a/src/nvim/ex_cmds.c +++ b/src/nvim/ex_cmds.c @@ -1395,7 +1395,7 @@ static void do_filter(linenr_T line1, linenr_T line2, exarg_T *eap, char *cmd, b if (do_in) { if ((cmdmod.cmod_flags & CMOD_KEEPMARKS) - || vim_strchr(p_cpo, CPO_REMMARK) == NULL) { + || vim_strchr(p_cpo, kCpoRemmark) == NULL) { // TODO(bfredl): Currently not active for extmarks. What would we // do if columns don't match, assume added/deleted bytes at the // end of each line? @@ -1786,7 +1786,7 @@ void ex_file(exarg_T *eap) } // print file name if no argument or 'F' is not in 'shortmess' - if (*eap->arg == NUL || !shortmess(SHM_FILEINFO)) { + if (*eap->arg == NUL || !shortmess(kShmFileinfo)) { fileinfo(false, false, eap->forceit); } } @@ -1875,7 +1875,7 @@ int do_write(exarg_T *eap) // If we have a new file, put its name in the list of alternate file names. if (other) { - if (vim_strchr(p_cpo, CPO_ALTWRITE) != NULL + if (vim_strchr(p_cpo, kCpoAltwrite) != NULL || eap->cmdidx == CMD_saveas) { alt_buf = setaltfname(ffname, fname, 1); } else { @@ -2024,7 +2024,7 @@ int check_overwrite(exarg_T *eap, buf_T *buf, char *fname, char *ffname, bool ot || (!bt_nofilename(buf) && ((buf->b_flags & BF_NOTEDITED) || ((buf->b_flags & BF_NEW) - && vim_strchr(p_cpo, CPO_OVERNEW) == NULL) + && vim_strchr(p_cpo, kCpoOvernew) == NULL) || (buf->b_flags & BF_READERR)))) && !p_wa && os_path_exists(ffname)) { @@ -2900,7 +2900,7 @@ int do_ecmd(int fnum, char *ffname, char *sfname, exarg_T *eap, linenr_T newlnum // Obey the 'O' flag in 'cpoptions': overwrite any previous file // message. - if (shortmess(SHM_OVERALL) && !msg_listdo_overwrite && !exiting && p_verbose == 0) { + if (shortmess(kShmOverall) && !msg_listdo_overwrite && !exiting && p_verbose == 0) { msg_scroll = false; } if (!msg_scroll) { // wait a bit when overwriting an error msg @@ -2910,7 +2910,7 @@ int do_ecmd(int fnum, char *ffname, char *sfname, exarg_T *eap, linenr_T newlnum msg_scroll = msg_scroll_save; msg_scrolled_ign = true; - if (!shortmess(SHM_FILEINFO)) { + if (!shortmess(kShmFileinfo)) { fileinfo(false, true, false); } @@ -3940,7 +3940,7 @@ static int do_sub(exarg_T *eap, proftime_T tm, const int cmdpreview_ns, // When 'cpoptions' contains "u" don't sync undo when // asking for confirmation. - if (vim_strchr(p_cpo, CPO_UNDO) != NULL) { + if (vim_strchr(p_cpo, kCpoUndo) != NULL) { no_u_sync++; } @@ -4082,7 +4082,7 @@ static int do_sub(exarg_T *eap, proftime_T tm, const int cmdpreview_ns, } State = save_State; setmouse(); - if (vim_strchr(p_cpo, CPO_UNDO) != NULL) { + if (vim_strchr(p_cpo, kCpoUndo) != NULL) { no_u_sync--; } diff --git a/src/nvim/ex_cmds2.c b/src/nvim/ex_cmds2.c index de8138e55e..8bb02605b0 100644 --- a/src/nvim/ex_cmds2.c +++ b/src/nvim/ex_cmds2.c @@ -490,7 +490,7 @@ void ex_listdo(exarg_T *eap) char *save_ei = NULL; - // Temporarily override SHM_OVER and SHM_OVERALL to avoid that file + // Temporarily override kShmOver and kShmOverall to avoid that file // message overwrites output from the command. msg_listdo_overwrite++; diff --git a/src/nvim/ex_docmd.c b/src/nvim/ex_docmd.c index 0ffcc43e69..7b6d875738 100644 --- a/src/nvim/ex_docmd.c +++ b/src/nvim/ex_docmd.c @@ -4251,7 +4251,7 @@ void separate_nextcmd(exarg_T *eap) || *p == '\n') { // We remove the '\' before the '|', unless EX_CTRLV is used // AND 'b' is present in 'cpoptions'. - if ((vim_strchr(p_cpo, CPO_BAR) == NULL + if ((vim_strchr(p_cpo, kCpoBar) == NULL || !(eap->argt & EX_CTRLV)) && *(p - 1) == '\\') { STRMOVE(p - 1, p); // remove the '\' p--; @@ -6353,7 +6353,7 @@ static void ex_read(exarg_T *eap) i = readfile(curbuf->b_ffname, curbuf->b_fname, eap->line2, 0, (linenr_T)MAXLNUM, eap, 0, false); } else { - if (vim_strchr(p_cpo, CPO_ALTREAD) != NULL) { + if (vim_strchr(p_cpo, kCpoAltread) != NULL) { setaltfname(eap->arg, eap->arg, 1); } i = readfile(eap->arg, NULL, @@ -6453,7 +6453,7 @@ static void post_chdir(CdScope scope, bool trigger_dirchanged) } last_chdir_reason = NULL; - shorten_fnames(vim_strchr(p_cpo, CPO_NOSYMLINKS) == NULL); + shorten_fnames(vim_strchr(p_cpo, kCpoNosymlinks) == NULL); if (trigger_dirchanged) { do_autocmd_dirchanged(cwd, scope, kCdCauseManual, false); @@ -6863,7 +6863,7 @@ static void ex_at(exarg_T *eap) } // Put the register in the typeahead buffer with the "silent" flag. - if (do_execreg(c, true, vim_strchr(p_cpo, CPO_EXECBUF) != NULL, true) == FAIL) { + if (do_execreg(c, true, vim_strchr(p_cpo, kCpoExecbuf) != NULL, true) == FAIL) { beep_flush(); return; } diff --git a/src/nvim/ex_getln.c b/src/nvim/ex_getln.c index bbb3ef09d5..6c443cdda5 100644 --- a/src/nvim/ex_getln.c +++ b/src/nvim/ex_getln.c @@ -1480,7 +1480,7 @@ static int command_line_execute(VimState *state, int key) || s->c == '\r' || s->c == K_KENTER || (s->c == ESC - && (!KeyTyped || vim_strchr(p_cpo, CPO_ESC) != NULL))) { + && (!KeyTyped || vim_strchr(p_cpo, kCpoEsc) != NULL))) { // In Ex mode a backslash escapes a newline. if (exmode_active && s->c != ESC diff --git a/src/nvim/file_search.c b/src/nvim/file_search.c index d36242393c..23b3d74358 100644 --- a/src/nvim/file_search.c +++ b/src/nvim/file_search.c @@ -295,7 +295,7 @@ void *vim_findfile_init(char *path, char *filename, size_t filenamelen, char *st // If path is absolute, we do that later. if (path[0] == '.' && (vim_ispathsep(path[1]) || path[1] == NUL) - && (!tagfile || vim_strchr(p_cpo, CPO_DOTTAG) == NULL) + && (!tagfile || vim_strchr(p_cpo, kCpoDottag) == NULL) && rel_fname != NULL) { size_t len = (size_t)(path_tail(rel_fname) - rel_fname); diff --git a/src/nvim/fileio.c b/src/nvim/fileio.c index dc534a8c2e..465d9e2668 100644 --- a/src/nvim/fileio.c +++ b/src/nvim/fileio.c @@ -124,7 +124,7 @@ void filemess(buf_T *buf, char *name, char *s) // For further ones overwrite the previous one, reset msg_scroll before // calling filemess(). int msg_scroll_save = msg_scroll; - if (shortmess(SHM_OVERALL) && !msg_listdo_overwrite && !exiting && p_verbose == 0) { + if (shortmess(kShmOverall) && !msg_listdo_overwrite && !exiting && p_verbose == 0) { msg_scroll = false; } if (!msg_scroll) { // wait a bit when overwriting an error msg @@ -260,7 +260,7 @@ int readfile(char *fname, char *sfname, linenr_T from, linenr_T lines_to_skip, if (curbuf->b_ffname == NULL && !filtering && fname != NULL - && vim_strchr(p_cpo, CPO_FNAMER) != NULL + && vim_strchr(p_cpo, kCpoFnamer) != NULL && !(flags & READ_DUMMY)) { if (set_rw_fname(fname, sfname) == FAIL) { goto theend; @@ -344,7 +344,7 @@ int readfile(char *fname, char *sfname, linenr_T from, linenr_T lines_to_skip, } } - if (((shortmess(SHM_OVER) && !msg_listdo_overwrite) || curbuf->b_help) && p_verbose == 0) { + if (((shortmess(kShmOver) && !msg_listdo_overwrite) || curbuf->b_help) && p_verbose == 0) { msg_scroll = false; // overwrite previous file message } else { msg_scroll = true; // don't overwrite previous file message @@ -1783,7 +1783,7 @@ failed: #endif if (curbuf->b_p_ro) { buflen += snprintf(IObuff + buflen, (size_t)(IOSIZE - buflen), "%s", - shortmess(SHM_RO) ? _("[RO]") : _("[readonly]")); + shortmess(kShmRo) ? _("[RO]") : _("[readonly]")); c = true; } if (read_no_eol_lnum) { @@ -2210,7 +2210,7 @@ void msg_add_lines(int insert_space, linenr_T lnum, off_T nchars) { size_t len = strlen(IObuff); - if (shortmess(SHM_LINES)) { + if (shortmess(kShmLines)) { snprintf(IObuff + len, IOSIZE - len, _("%s%" PRId64 "L, %" PRId64 "B"), // l10n: L as in line, B as in byte insert_space ? " " : "", (int64_t)lnum, (int64_t)nchars); @@ -3205,7 +3205,7 @@ void buf_reload(buf_T *buf, int orig_mode, bool reload_options) curbuf->b_flags |= BF_CHECK_RO; // check for RO again curbuf->b_keep_filetype = true; // don't detect 'filetype' if (readfile(buf->b_ffname, buf->b_fname, 0, 0, - (linenr_T)MAXLNUM, &ea, flags, shortmess(SHM_FILEINFO)) != OK) { + (linenr_T)MAXLNUM, &ea, flags, shortmess(kShmFileinfo)) != OK) { if (!aborting()) { semsg(_("E321: Could not reload \"%s\""), buf->b_fname); } diff --git a/src/nvim/indent.c b/src/nvim/indent.c index 98d150680f..6fbe0fc132 100644 --- a/src/nvim/indent.c +++ b/src/nvim/indent.c @@ -52,6 +52,8 @@ #include "indent.c.generated.h" +#include "options_keysets.generated.h" + /// Set the integer values corresponding to the string setting of 'vartabstop'. /// "array" will be set, caller must free it if needed. /// @@ -747,7 +749,7 @@ int get_number_indent(linenr_T lnum) pos.lnum = 0; // In format_lines() (i.e. not insert mode), fo+=q is needed too... - if ((State & MODE_INSERT) || has_format_option(FO_Q_COMS)) { + if ((State & MODE_INSERT) || has_format_option(kFoQComs)) { lead_len = get_leader_len(ml_get(lnum), NULL, false, true); } regmatch.regprog = vim_regcomp(curbuf->b_p_flp, RE_MAGIC); @@ -780,59 +782,19 @@ int get_number_indent(linenr_T lnum) /// @param wp when NULL: only check "briopt" /// /// @return FAIL for failure, OK otherwise. -bool briopt_check(char *briopt, win_T *wp) +void briopt_check(win_T *wp) { - int bri_shift = 0; - int bri_min = 20; - bool bri_sbr = false; - int bri_list = 0; - int bri_vcol = 0; - - char *p = empty_string_option; - if (briopt != NULL) { - p = briopt; - } else if (wp != NULL) { - p = wp->w_p_briopt; - } - - while (*p != NUL) { - // Note: Keep this in sync with opt_briopt_values. - if (strncmp(p, "shift:", 6) == 0 - && ((p[6] == '-' && ascii_isdigit(p[7])) || ascii_isdigit(p[6]))) { - p += 6; - bri_shift = getdigits_int(&p, true, 0); - } else if (strncmp(p, "min:", 4) == 0 && ascii_isdigit(p[4])) { - p += 4; - bri_min = getdigits_int(&p, true, 0); - } else if (strncmp(p, "sbr", 3) == 0) { - p += 3; - bri_sbr = true; - } else if (strncmp(p, "list:", 5) == 0) { - p += 5; - bri_list = (int)getdigits(&p, false, 0); - } else if (strncmp(p, "column:", 7) == 0) { - p += 7; - bri_vcol = (int)getdigits(&p, false, 0); - } - if (*p != ',' && *p != NUL) { - return false; - } - if (*p == ',') { - p++; - } - } - if (wp == NULL) { - return OK; + return; // Setting the global value: nothing to apply to a window. } - - wp->w_briopt_shift = bri_shift; - wp->w_briopt_min = bri_min; - wp->w_briopt_sbr = bri_sbr; - wp->w_briopt_list = bri_list; - wp->w_briopt_vcol = bri_vcol; - - return true; + // 'breakindentopt' is stored as its reified keyset (validated when set); just map it onto the + // applied per-window fields. A NULL keyset (before the option is set) means all-default. + OptKeyDict_briopt *v = wp->w_p_briopt; + wp->w_briopt_shift = (v != NULL && HAS_KEY(v, briopt, shift)) ? (int)v->shift : 0; + wp->w_briopt_min = (v != NULL && HAS_KEY(v, briopt, min)) ? (int)v->min : 20; + wp->w_briopt_sbr = (v != NULL && HAS_KEY(v, briopt, sbr)); + wp->w_briopt_list = (v != NULL && HAS_KEY(v, briopt, list)) ? (int)v->list : 0; + wp->w_briopt_vcol = (v != NULL && HAS_KEY(v, briopt, column)) ? (int)v->column : 0; } // Return appropriate space number for breakindent, taking influencing diff --git a/src/nvim/insert.c b/src/nvim/insert.c index 9f69c2f59e..4e349bbd0a 100644 --- a/src/nvim/insert.c +++ b/src/nvim/insert.c @@ -1949,7 +1949,7 @@ void insertchar(int c, int flags, int second_indent) int force_format = flags & INSCHAR_FORMAT; const int textwidth = comp_textwidth(force_format); - const bool fo_ins_blank = has_format_option(FO_INS_BLANK); + const bool fo_ins_blank = has_format_option(kFoInsBlank); // Try to break the line in two or more pieces when: // - Always do this if we have been called to do formatting only. @@ -1971,7 +1971,7 @@ void insertchar(int c, int flags, int second_indent) && !(State & VREPLACE_FLAG) && *get_cursor_pos_ptr() != NUL) && (curwin->w_cursor.lnum != Ins.start.lnum - || ((!has_format_option(FO_INS_LONG) + || ((!has_format_option(kFoInsLong) || Ins.start_textlen <= (colnr_T)textwidth) && (!fo_ins_blank || Ins.start_blank_vcol <= (colnr_T)textwidth)))))) { @@ -2265,7 +2265,7 @@ static void stop_insert(pos_T *end_insert_pos, int esc, int nomove) // insertion (or moving the cursor), but it's required when appending // a line and having it end in a space. But only do it when something // was actually inserted, otherwise undo won't work. - if (!Ins.need_undo && has_format_option(FO_AUTO)) { + if (!Ins.need_undo && has_format_option(kFoAuto)) { pos_T tpos = curwin->w_cursor; // When the cursor is at the end of the line after a space the @@ -2304,7 +2304,7 @@ static void stop_insert(pos_T *end_insert_pos, int esc, int nomove) // Do this when ESC was used or moving the cursor up/down. // Check for the old position still being valid, just in case the text // got changed unexpectedly. - if (!nomove && Ins.did_ai && (esc || (vim_strchr(p_cpo, CPO_INDENT) == NULL + if (!nomove && Ins.did_ai && (esc || (vim_strchr(p_cpo, kCpoIndent) == NULL && curwin->w_cursor.lnum != end_insert_pos->lnum)) && end_insert_pos->lnum <= curbuf->b_ml.ml_line_count) { @@ -3097,7 +3097,7 @@ static bool ins_esc(int *count, int cmdchar, bool nomove) if (--*count > 0) { // repeat what was typed // Vi repeats the insert without replacing characters. - if (vim_strchr(p_cpo, CPO_REPLCNT) != NULL) { + if (vim_strchr(p_cpo, kCpoReplcnt) != NULL) { State &= ~REPLACE_FLAG; } @@ -3435,8 +3435,8 @@ static bool ins_bs(int c, int mode, int *inserted_space_p) // When "aw" is in 'formatoptions' we must delete the space at // the end of the line, otherwise the line will be broken // again when auto-formatting. - if (has_format_option(FO_AUTO) - && has_format_option(FO_WHITE_PAR)) { + if (has_format_option(kFoAuto) + && has_format_option(kFoWhitePar)) { const char *ptr = ml_get_buf(curbuf, curwin->w_cursor.lnum); int len = get_cursor_line_len(); if (len > 0 && ptr[len - 1] == ' ') { @@ -3675,7 +3675,7 @@ static bool ins_bs(int c, int mode, int *inserted_space_p) // We can emulate the vi behaviour by pretending there is a dollar // displayed even when there isn't. // --pkv Sun Jan 19 01:56:40 EST 2003 - if (vim_strchr(p_cpo, CPO_BACKSPACE) != NULL && dollar_vcol == -1) { + if (vim_strchr(p_cpo, kCpoBackspace) != NULL && dollar_vcol == -1) { dollar_vcol = curwin->w_virtcol; } @@ -4023,7 +4023,7 @@ static bool ins_tab(void) } // When 'L' is not in 'cpoptions' a tab always takes up 'ts' spaces. - if (vim_strchr(p_cpo, CPO_LISTWM) == NULL) { + if (vim_strchr(p_cpo, kCpoListwm) == NULL) { curwin->w_p_list = false; } @@ -4182,7 +4182,7 @@ bool ins_eol(int c) AppendToRedobuff(NL_STR); bool i = open_line(FORWARD, - has_format_option(FO_RET_COMS) ? OPENLINE_DO_COM : 0, + has_format_option(kFoRetComs) ? OPENLINE_DO_COM : 0, old_indent, NULL); old_indent = 0; Ins.can_cindent = true; @@ -4338,7 +4338,7 @@ colnr_T get_nolist_virtcol(void) || curwin->w_cursor.lnum > curwin->w_buffer->b_ml.ml_line_count) { return 0; } - if (curwin->w_p_list && vim_strchr(p_cpo, CPO_LISTWM) == NULL) { + if (curwin->w_p_list && vim_strchr(p_cpo, kCpoListwm) == NULL) { return getvcol_nolist(&curwin->w_cursor); } validate_virtcol(curwin); diff --git a/src/nvim/insexpand.c b/src/nvim/insexpand.c index 3067fa5d5a..c3ec2c6472 100644 --- a/src/nvim/insexpand.c +++ b/src/nvim/insexpand.c @@ -2005,7 +2005,7 @@ static void ins_compl_files(int count, char **files, bool thesaurus, int flags, for (int i = 0; i < count && !got_int && !ins_compl_interrupted(); i++) { FILE *fp = os_fopen(files[i], "r"); // open dictionary file - if (flags != DICT_EXACT && !shortmess(SHM_COMPLETIONSCAN) && !compl_autocomplete) { + if (flags != DICT_EXACT && !shortmess(kShmCompletionscan) && !compl_autocomplete) { vim_snprintf(IObuff, IOSIZE, _("Scanning dictionary: %s"), files[i]); msg_progress(IObuff, "nvim.completion", "running", HLF_R, false, true); } @@ -2768,7 +2768,7 @@ static bool ins_compl_stop(const int c, const int prev_mode, bool retval) ins_compl_free(); compl_started = false; compl_matches = 0; - if (!shortmess(SHM_COMPLETIONMENU)) { + if (!shortmess(kShmCompletionmenu)) { msg_clr_cmdline(); // necessary for "noshowmode" } ctrl_x_mode = CTRL_X_NORMAL; @@ -3898,7 +3898,7 @@ static int process_next_cpt_value(ins_compl_next_state_T *st, int *compl_type_ar st->dict = st->ins_buf->b_fname; st->dict_f = DICT_EXACT; } - if (!shortmess(SHM_COMPLETIONSCAN) && !compl_autocomplete) { + if (!shortmess(kShmCompletionscan) && !compl_autocomplete) { vim_snprintf(IObuff, IOSIZE, _("Scanning: %s"), st->ins_buf->b_fname == NULL ? buf_spname(st->ins_buf) @@ -3937,7 +3937,7 @@ static int process_next_cpt_value(ins_compl_next_state_T *st, int *compl_type_ar compl_type = CTRL_X_BUFNAMES; } else if (*st->e_cpt == ']' || *st->e_cpt == 't') { compl_type = CTRL_X_TAGS; - if (!shortmess(SHM_COMPLETIONSCAN) && !compl_autocomplete) { + if (!shortmess(kShmCompletionscan) && !compl_autocomplete) { vim_snprintf(IObuff, IOSIZE, "%s", _("Scanning tags.")); msg_progress(IObuff, "nvim.completion", "running", HLF_R, false, true); } @@ -5961,7 +5961,7 @@ static int get_userdefined_compl_info(colnr_T curs_col, Callback *cb, int *start } ctrl_x_mode = CTRL_X_NORMAL; edit_submode = NULL; - if (!shortmess(SHM_COMPLETIONMENU)) { + if (!shortmess(kShmCompletionmenu)) { msg_clr_cmdline(); } return FAIL; @@ -6158,7 +6158,7 @@ static int ins_compl_start(void) } if (compl_status_adding()) { - if (!shortmess(SHM_COMPLETIONMENU)) { + if (!shortmess(kShmCompletionmenu)) { edit_submode_pre = _(" Adding"); } if (ctrl_x_mode_line_or_eval()) { @@ -6179,7 +6179,7 @@ static int ins_compl_start(void) compl_startpos.col = compl_col; } - if (!shortmess(SHM_COMPLETIONMENU) && !compl_autocomplete) { + if (!shortmess(kShmCompletionmenu) && !compl_autocomplete) { if (compl_cont_status & CONT_LOCAL) { edit_submode = _(ctrl_x_msgs[CTRL_X_LOCAL_MSG]); } else { @@ -6213,7 +6213,7 @@ static int ins_compl_start(void) // showmode might reset the internal line pointers, so it must // be called before line = ml_get(), or when this address is no // longer needed. -- Acevedo. - if (!shortmess(SHM_COMPLETIONMENU) && !compl_autocomplete) { + if (!shortmess(kShmCompletionmenu) && !compl_autocomplete) { edit_submode_extra = _("-- Searching..."); edit_submode_highl = HLF_COUNT; showmode(); @@ -6278,7 +6278,7 @@ static void ins_compl_show_statusmsg(void) // Show a message about what (completion) mode we're in. redraw_mode = true; - if (!shortmess(SHM_COMPLETIONMENU)) { + if (!shortmess(kShmCompletionmenu)) { if (edit_submode_extra != NULL) { if (!p_smd) { msg_hist_off = true; @@ -6360,7 +6360,7 @@ int ins_complete(int c, bool enable_pum) compl_cont_status &= ~CONT_S_IPOS; } - if (!shortmess(SHM_COMPLETIONMENU) && !compl_autocomplete) { + if (!shortmess(kShmCompletionmenu) && !compl_autocomplete) { ins_compl_show_statusmsg(); } diff --git a/src/nvim/keycodes.c b/src/nvim/keycodes.c index ec91b078ae..fadba8e0bc 100644 --- a/src/nvim/keycodes.c +++ b/src/nvim/keycodes.c @@ -694,7 +694,7 @@ int get_mouse_button(int code, bool *is_click, bool *is_drag) /// `` => 0x08. /// /// When "flags" has REPTERM_FROM_PART, trailing is included, otherwise it is removed (to make -/// ":map xx ^V" map xx to nothing). When cpo_val contains CPO_BSLASH, a backslash can be used in +/// ":map xx ^V" map xx to nothing). When cpo_val contains kCpoBslash, a backslash can be used in /// place of . All other characters are removed. /// /// @param[in] from What characters to replace. @@ -709,7 +709,7 @@ int get_mouse_button(int code, bool *is_click, bool *is_drag) /// REPTERM_NO_SPECIAL do not accept notation /// REPTERM_NO_SIMPLIFY do not simplify into 0x08, etc. /// @param[out] did_simplify set when some code was simplified, unless it is NULL. -/// @param[in] cpo_val The value of 'cpoptions' to use. Only CPO_BSLASH matters. +/// @param[in] cpo_val The value of 'cpoptions' to use. Only kCpoBslash matters. /// /// @return The same as what `*bufp` is set to. char *replace_termcodes(const char *const from, const size_t from_len, char **const bufp, @@ -721,7 +721,7 @@ char *replace_termcodes(const char *const from, const size_t from_len, char **co const char *const end = from + from_len - 1; // backslash is a special character - const bool do_backslash = (vim_strchr(cpo_val, CPO_BSLASH) == NULL); + const bool do_backslash = (vim_strchr(cpo_val, kCpoBslash) == NULL); const bool do_special = !(flags & REPTERM_NO_SPECIAL); bool allocated = (*bufp == NULL); diff --git a/src/nvim/mapping.c b/src/nvim/mapping.c index c878bc339e..74d35a4206 100644 --- a/src/nvim/mapping.c +++ b/src/nvim/mapping.c @@ -461,7 +461,7 @@ static int str_to_mapargs(const char *strargs, bool is_unmap, MapArguments *mapa // With :unmap, literal white space is included in the {lhs}; there is no // separate {rhs}. const char *lhs_end = to_parse; - bool do_backslash = (vim_strchr(p_cpo, CPO_BSLASH) == NULL); + bool do_backslash = (vim_strchr(p_cpo, kCpoBslash) == NULL); while (*lhs_end && (is_unmap || !ascii_iswhite(*lhs_end))) { if ((lhs_end[0] == Ctrl_V || (do_backslash && lhs_end[0] == '\\')) && lhs_end[1] != NUL) { @@ -1202,7 +1202,7 @@ static char *translate_mapping(const char *const str_in, const char *const cpo_v garray_T ga; ga_init(&ga, 1, 40); - const bool cpo_bslash = (vim_strchr(cpo_val, CPO_BSLASH) != NULL); + const bool cpo_bslash = (vim_strchr(cpo_val, kCpoBslash) != NULL); for (; *str; str++) { int c = *str; diff --git a/src/nvim/match.c b/src/nvim/match.c index 40e9c3282a..8f92a82185 100644 --- a/src/nvim/match.c +++ b/src/nvim/match.c @@ -423,7 +423,7 @@ static void next_search_hl(win_T *win, match_T *search_hl, match_T *shl, linenr_ // 3. Vi compatible searching: continue at end of previous match. if (shl->lnum == 0) { matchcol = 0; - } else if (vim_strchr(p_cpo, CPO_SEARCH) == NULL + } else if (vim_strchr(p_cpo, kCpoSearch) == NULL || (shl->rm.endpos[0].lnum == 0 && shl->rm.endpos[0].col <= shl->rm.startpos[0].col)) { matchcol = shl->rm.startpos[0].col; diff --git a/src/nvim/memline.c b/src/nvim/memline.c index 7bc4a3f410..6c915473b5 100644 --- a/src/nvim/memline.c +++ b/src/nvim/memline.c @@ -3533,7 +3533,7 @@ static char *findswapname(buf_T *buf, char **dirp, char *old_fname, bool *found_ // - there is an old swapfile for the current file // - the buffer was not recovered if (!differ && !(curbuf->b_flags & BF_RECOVERED) - && vim_strchr(p_shm, SHM_ATTENTION) == NULL) { + && vim_strchr(p_shm, kShmAttention) == NULL) { sea_choice_T choice = SEA_CHOICE_NONE; // It's safe to delete the swapfile if all these are true: diff --git a/src/nvim/message.c b/src/nvim/message.c index f1fdb4b546..eb725b03eb 100644 --- a/src/nvim/message.c +++ b/src/nvim/message.c @@ -501,7 +501,7 @@ char *msg_strtrunc(const char *s, int force) char *buf = NULL; // May truncate message to avoid a hit-return prompt - if ((!msg_scroll && !need_wait_return && shortmess(SHM_TRUNCALL) + if ((!msg_scroll && !need_wait_return && shortmess(kShmTruncall) && !exmode_active && msg_silent == 0 && !ui_has(kUIMessages)) || force) { int room; @@ -1072,7 +1072,7 @@ char *msg_may_trunc(bool force, char *s) // just in case. int room = (Rows - cmdline_row - 1) * Columns + sc_col - 1; if (room > 0 - && (force || (shortmess(SHM_TRUNC) && !exmode_active)) + && (force || (shortmess(kShmTrunc) && !exmode_active)) && (int)strlen(s) - room > 0) { int size = vim_strsize(s); diff --git a/src/nvim/mouse.c b/src/nvim/mouse.c index 333c84d155..22059067a3 100644 --- a/src/nvim/mouse.c +++ b/src/nvim/mouse.c @@ -612,7 +612,7 @@ bool do_mouse(oparg_T *oap, int c, int dir, int count, bool fixindent) pos_T end_visual = { 0 }; pos_T start_visual = { 0 }; - bool mouse_can_visual = ui_mouse_has(MOUSE_VISUAL); + bool mouse_can_visual = ui_mouse_has(kMouseVisual); if ((State & (MODE_NORMAL | MODE_INSERT)) && !(mod_mask & (MOD_MASK_SHIFT | MOD_MASK_CTRL))) { if (which_button == MOUSE_LEFT && mouse_can_visual) { diff --git a/src/nvim/move.c b/src/nvim/move.c index 25bcf91e1e..576a19c7f7 100644 --- a/src/nvim/move.c +++ b/src/nvim/move.c @@ -823,7 +823,7 @@ int win_col_off(win_T *wp) int win_col_off2(win_T *wp) { if ((wp->w_p_nu || wp->w_p_rnu || *wp->w_p_stc != NUL) - && vim_strchr(p_cpo, CPO_NUMCOL) != NULL) { + && vim_strchr(p_cpo, kCpoNumcol) != NULL) { return number_width(wp) + (*wp->w_p_stc == NUL); } return 0; diff --git a/src/nvim/normal.c b/src/nvim/normal.c index 3a89a7307c..7497cbdcaa 100644 --- a/src/nvim/normal.c +++ b/src/nvim/normal.c @@ -781,7 +781,7 @@ static void normal_get_additional_char(NormalState *s) // Typing CTRL-K gets a digraph. if (*cp == Ctrl_K && ((nv_cmds[s->idx].cmd_flags & NV_LANG) || cp == &s->ca.extra_char) - && vim_strchr(p_cpo, CPO_DIGRAPH) == NULL) { + && vim_strchr(p_cpo, kCpoDigraph) == NULL) { s->c = get_digraph(false); if (s->c > 0) { *cp = s->c; @@ -1379,7 +1379,7 @@ static void normal_redraw(NormalState *s) } // show fileinfo after redraw - if (need_fileinfo && !shortmess(SHM_FILEINFO)) { + if (need_fileinfo && !shortmess(kShmFileinfo)) { fileinfo(false, true, false); need_fileinfo = false; } @@ -2302,7 +2302,7 @@ static void nv_gd(oparg_T *oap, int nchar, int thisblock) foldOpenCursor(); } // clear any search statistics - if (messaging() && !msg_silent && !shortmess(SHM_SEARCHCOUNT)) { + if (messaging() && !msg_silent && !shortmess(kShmSearchcount)) { clear_cmdline = true; } } @@ -4394,7 +4394,7 @@ static void nv_percent(cmdarg_T *cap) // Skip matching parens inside C-style comments, like the "=" operator // does, but not when "%" is in 'cpoptions' (Vi-compatible) or the // cursor sits in a line comment (so a match there can still be found). - if (vim_strchr(p_cpo, CPO_MATCH) == NULL && buf_has_cstyle_comments()) { + if (vim_strchr(p_cpo, kCpoMatch) == NULL && buf_has_cstyle_comments()) { int comment_col = check_linecomment(get_cursor_line_ptr()); if (comment_col == MAXCOL || curwin->w_cursor.col < (colnr_T)comment_col) { flags = FM_SKIPCOMM; @@ -5765,7 +5765,7 @@ static void n_opencmd(cmdarg_T *cap) if (u_save(curwin->w_cursor.lnum - (cap->cmdchar == 'O' ? 1 : 0), curwin->w_cursor.lnum + (cap->cmdchar == 'o' ? 1 : 0)) && open_line(cap->cmdchar == 'O' ? BACKWARD : FORWARD, - has_format_option(FO_OPEN_COMS) ? OPENLINE_DO_COM : 0, + has_format_option(kFoOpenComs) ? OPENLINE_DO_COM : 0, 0, NULL)) { if (win_cursorline_standout(curwin)) { // force redraw of cursorline @@ -5991,7 +5991,7 @@ static void nv_wordcmd(cmdarg_T *cap) // Another strangeness: When standing on the end of a word "ce" will // change until the end of the next word, but "cw" will change only one // character! This is done by setting "flag". - if (vim_strchr(p_cpo, CPO_CHANGEW) != NULL) { + if (vim_strchr(p_cpo, kCpoChangew) != NULL) { cap->oap->inclusive = true; word_end = true; } diff --git a/src/nvim/ops.c b/src/nvim/ops.c index 7243f53e89..c577abb838 100644 --- a/src/nvim/ops.c +++ b/src/nvim/ops.c @@ -769,7 +769,7 @@ int op_delete(oparg_T *oap) // marks as if it happened. goto setmarks; } - if (vim_strchr(p_cpo, CPO_EMPTYREGION) != NULL) { + if (vim_strchr(p_cpo, kCpoEmptyregion) != NULL) { beep_flush(); } return OK; @@ -942,7 +942,7 @@ int op_delete(oparg_T *oap) } // if 'cpoptions' contains '$', display '$' at end of change - if (vim_strchr(p_cpo, CPO_DOLLAR) != NULL + if (vim_strchr(p_cpo, kCpoDollar) != NULL && oap->op_type == OP_CHANGE && oap->end.lnum == curwin->w_cursor.lnum && !oap->is_VIsual) { @@ -1894,7 +1894,7 @@ int do_join(size_t count, bool insert_space, bool save_undo, bool use_formatopti int sumsize = 0; // size of the long new line int ret = OK; int *comments = NULL; - bool remove_comments = use_formatoptions && has_format_option(FO_REMOVE_COMS); + bool remove_comments = use_formatoptions && has_format_option(kFoRemoveComs); bool prev_was_comment = false; assert(count >= 1); @@ -1939,9 +1939,9 @@ int do_join(size_t count, bool insert_space, bool save_undo, bool use_formatopti && *curr != ')' && sumsize != 0 && endcurr1 != TAB - && (!has_format_option(FO_MBYTE_JOIN) + && (!has_format_option(kFoMbyteJoin) || (utf_ptr2char(curr) < 0x100 && endcurr1 < 0x100)) - && (!has_format_option(FO_MBYTE_JOIN2) + && (!has_format_option(kFoMbyteJoin2) || (utf_ptr2char(curr) < 0x100 && !utf_eat_space(endcurr1)) || (endcurr1 < 0x100 && !utf_eat_space(utf_ptr2char(curr))))) { @@ -2062,7 +2062,7 @@ int do_join(size_t count, bool insert_space, bool save_undo, bool use_formatopti // Vi compatible: use the column of the first join // vim: use the column of the last join curwin->w_cursor.col = - (vim_strchr(p_cpo, CPO_JOINCOL) != NULL ? currsize : col); + (vim_strchr(p_cpo, kCpoJoincol) != NULL ? currsize : col); check_cursor_col(curwin); curwin->w_cursor.coladd = 0; @@ -3285,7 +3285,7 @@ void do_pending_operator(cmdarg_T *cap, int old_col, bool gui_yank) bool include_line_break = false; // Yank can be redone when 'y' is in 'cpoptions', but not when yanking // for the clipboard. - const bool redo_yank = vim_strchr(p_cpo, CPO_YANK) != NULL && !gui_yank; + const bool redo_yank = vim_strchr(p_cpo, kCpoYank) != NULL && !gui_yank; // Avoid a problem with unwanted linebreaks in block mode reset_lbr(); @@ -3334,7 +3334,7 @@ void do_pending_operator(cmdarg_T *cap, int old_col, bool gui_yank) if (cap->cmdchar == '/' || cap->cmdchar == '?') { // was a search // If 'cpoptions' does not contain 'r', insert the search // pattern to really repeat the same command. - if (vim_strchr(p_cpo, CPO_REDO) == NULL) { + if (vim_strchr(p_cpo, kCpoRedo) == NULL) { AppendToRedobuffLit(cap->searchbuf, -1); } AppendToRedobuff(NL_STR); @@ -3602,7 +3602,7 @@ void do_pending_operator(cmdarg_T *cap, int old_col, bool gui_yank) // For delete, change and yank, it's an error to operate on an // empty region, when 'E' included in 'cpoptions' (Vi compatible). empty_region_error = (oap->empty - && vim_strchr(p_cpo, CPO_EMPTYREGION) != NULL); + && vim_strchr(p_cpo, kCpoEmptyregion) != NULL); // Force a redraw when operating on an empty Visual region, when // 'modifiable is off or creating a fold. @@ -3668,7 +3668,7 @@ void do_pending_operator(cmdarg_T *cap, int old_col, bool gui_yank) op_delete(oap); // save cursor line for undo if it wasn't saved yet if (oap->motion_type == kMTLineWise - && has_format_option(FO_AUTO) + && has_format_option(kFoAuto) && u_save_cursor() == OK) { auto_format(false, true); } @@ -3724,7 +3724,7 @@ void do_pending_operator(cmdarg_T *cap, int old_col, bool gui_yank) break; case OP_FILTER: - if (vim_strchr(p_cpo, CPO_FILTER) != NULL) { + if (vim_strchr(p_cpo, kCpoFilter) != NULL) { AppendToRedobuff("!\r"); // Use any last used !cmd. } else { bangredo = true; // do_bang() will put cmd in redo buffer. diff --git a/src/nvim/option.c b/src/nvim/option.c index b39136a0d8..bc6cc4d229 100644 --- a/src/nvim/option.c +++ b/src/nvim/option.c @@ -159,6 +159,7 @@ static char *p_vsts_nopaste; // ignored and they are not printed. #include "options.generated.h" +#include "options_keysets.generated.h" #include "options_map.generated.h" static int p_bin_dep_opts[] = { @@ -471,6 +472,19 @@ static void alloc_options_default(void) { for (OptIndex opt_idx = 0; opt_idx < kOptCount; opt_idx++) { options[opt_idx].def_val = optval_copy(options[opt_idx].def_val); + + // A dict option declares its default as a ":set" string (options.lua). Reify it into a + // keyset once here, so the default is a keyset, for reset and ":set opt&". + if (option_has_type(opt_idx, kOptValTypeDict) + && options[opt_idx].def_val.type == kOptValTypeString) { + const char *emsg = NULL; + OptVal reified = opt_dict_from_string(opt_idx, options[opt_idx].def_val.data.string.data, + NULL, 0, &emsg); + assert(emsg == NULL); // A built-in default must be valid. + (void)emsg; + optval_free(options[opt_idx].def_val); + options[opt_idx].def_val = reified; + } } } @@ -1446,6 +1460,15 @@ OptVal get_option_newval(OptIndex opt_idx, int opt_flags, set_prefix_T prefix, c newval = CSTR_AS_OPTVAL(newval_str); break; } + case kOptValTypeDict: { + // Merge at the string level (serialize the old keyset, apply =/+=/-=); set_option() reifies and + // validates the result. stropt_get_newval() doesn't dereference varp except for 'keywordprg'. + char *oldval_str = opt_serialize(oldval.data.dictval.ptr, oldval.data.dictval.table); + char *newval_str = stropt_get_newval(opt_idx, argp, varp, oldval_str, &op); + xfree(oldval_str); + newval = CSTR_AS_OPTVAL(newval_str); + break; + } } return newval; @@ -2009,20 +2032,17 @@ bool parse_winhl_opt(const char *winhl, win_T *wp) } } - while (*p) { - const char *colon = strchr(p, ':'); - if (!colon) { - return false; + const char *key, *val; + size_t keylen, vallen; + while (option_next_keyval(&p, &key, &keylen, &val, &vallen)) { + if (val == NULL) { + return false; // every part must be "group:hl" } - size_t nlen = (size_t)(colon - p); - const char *hi = colon + 1; - const char *commap = xstrchrnul(hi, ','); - size_t len = (size_t)(commap - hi); - int hl_id = len ? syn_check_group(hi, len) : -1; + int hl_id = vallen ? syn_check_group(val, vallen) : -1; if (hl_id == 0) { return false; } - int hl_id_link = nlen ? syn_check_group(p, nlen) : 0; + int hl_id_link = keylen ? syn_check_group(key, keylen) : 0; if (hl_id_link == 0) { return false; } @@ -2032,8 +2052,6 @@ bool parse_winhl_opt(const char *winhl, win_T *wp) attrs.rgb_ae_attr |= HL_GLOBAL; ns_hl_def(ns_hl, hl_id_link, attrs, hl_id, NULL); } - - p = *commap ? commap + 1 : ""; } if (wp != NULL) { @@ -2118,6 +2136,12 @@ void apply_optionset_autocmd_now(OptIndex opt_idx, int opt_flags, OptVal oldval, } apply_autocmds(EVENT_OPTIONSET, options[opt_idx].fullname, NULL, false, NULL); reset_v_option_vars(); + + // set_vim_var_tv() copied each typval, so release the strings optval_as_tv() freshly allocated. + optval_as_tv_free(oldval, oldval_tv); + optval_as_tv_free(oldval_g, oldval_g_tv); + optval_as_tv_free(oldval_l, oldval_l_tv); + optval_as_tv_free(newval, newval_tv); } /// For 'modified', the event is deferred. @@ -3338,6 +3362,86 @@ OptIndex find_option(const char *const name) return find_option_len(name, strlen(name)); } +/// Free the owned resources of a dict option value: its `String` fields and the keyset. +static void opt_dict_free(OptDict s) +{ + if (s.ptr == NULL) { + return; + } + for (const KeySetLink *f = s.table; f->str != NULL; f++) { + if (f->type == kObjectTypeString) { + api_free_string(*(String *)((char *)s.ptr + f->ptr_off)); + } + } + xfree(s.ptr); +} + +/// Deep-copy a dict option value (keyset + its `String` fields). +static OptDict opt_dict_dup(OptDict s) +{ + if (s.ptr == NULL) { + return s; + } + void *ptr = xmemdup(s.ptr, s.size); + for (const KeySetLink *f = s.table; f->str != NULL; f++) { + if (f->type == kObjectTypeString) { + String *field = (String *)((char *)ptr + f->ptr_off); + *field = copy_string(*field, NULL); + } + } + return (OptDict){ ptr, s.table, s.size }; +} + +/// Compare two dict option values: same keys present (`is_set_`) and same field values. +static bool opt_dict_equal(OptDict a, OptDict b) +{ + if (a.ptr == NULL || b.ptr == NULL) { + return a.ptr == b.ptr; + } + for (const KeySetLink *f = a.table; f->str != NULL; f++) { + const void *fa = (const char *)a.ptr + f->ptr_off; + const void *fb = (const char *)b.ptr + f->ptr_off; + switch (f->type) { + case kObjectTypeInteger: + if (*(const Integer *)fa != *(const Integer *)fb) { + return false; + } + break; + case kObjectTypeString: { + const String *sa = fa; + const String *sb = fb; + if (sa->size != sb->size || (sa->size != 0 && memcmp(sa->data, sb->data, sa->size) != 0)) { + return false; + } + break; + } + default: // Boolean and other scalars + if (*(const Boolean *)fa != *(const Boolean *)fb) { + return false; + } + break; + } + } + // is_set___ (OptionalKeys) is the first member of every keyset. + return *(const OptionalKeys *)a.ptr == *(const OptionalKeys *)b.ptr; +} + +/// Validate a ":set" string against a dict option's grammar and reify it into a fresh +/// heap keyset. On success returns an owned struct OptVal; on failure sets `errmsg` and returns Nil. +static OptVal opt_dict_from_string(OptIndex opt_idx, const char *str, char *errbuf, + size_t errbuflen, const char **errmsg) +{ + const OptDictInfo *si = opt_dict_info(opt_idx); + const char *err = opt_strings_check(str, si->schema, errbuf, errbuflen); + if (err != NULL) { + *errmsg = err; + return NIL_OPTVAL; + } + void *ptr = xcalloc(1, si->size); + opt_fill(str, si->get_field, ptr); + return (OptVal){ .type = kOptValTypeDict, .data.dictval = { ptr, si->table, si->size } }; +} + /// Free an allocated OptVal. void optval_free(OptVal o) { @@ -3352,6 +3456,9 @@ void optval_free(OptVal o) api_free_string(o.data.string); } break; + case kOptValTypeDict: + opt_dict_free(o.data.dictval); + break; } } @@ -3365,6 +3472,9 @@ OptVal optval_copy(OptVal o) return o; case kOptValTypeString: return STRING_OPTVAL(copy_string(o.data.string, NULL)); + case kOptValTypeDict: + return (OptVal){ .type = kOptValTypeDict, + .data.dictval = opt_dict_dup(o.data.dictval) }; } UNREACHABLE; } @@ -3387,6 +3497,8 @@ bool optval_equal(OptVal o1, OptVal o2) return o1.data.string.size == o2.data.string.size && (o1.data.string.data == o2.data.string.data || strnequal(o1.data.string.data, o2.data.string.data, o1.data.string.size)); + case kOptValTypeDict: + return opt_dict_equal(o1.data.dictval, o2.data.dictval); } UNREACHABLE; } @@ -3423,6 +3535,13 @@ OptVal optval_from_varp(OptIndex opt_idx, void *varp) return NUMBER_OPTVAL(*(OptInt *)varp); case kOptValTypeString: return STRING_OPTVAL(cstr_as_string(*(char **)varp)); + case kOptValTypeDict: { + // Alias the stored keyset (like the string case aliases the stored char*): free frees it, copy + // dupes it. The table/size travel with the value so free/copy need no option index. + const OptDictInfo *si = opt_dict_info(opt_idx); + return (OptVal){ .type = kOptValTypeDict, + .data.dictval = { *(void **)varp, si->table, si->size } }; + } } UNREACHABLE; } @@ -3454,6 +3573,11 @@ static void set_option_varp(OptIndex opt_idx, void *varp, OptVal value, bool fre case kOptValTypeString: *(char **)varp = value.data.string.data; return; + case kOptValTypeDict: + // Move the keyset pointer in (ownership transfers, like the string case). The old value was + // already freed above when free_oldval is set. + *(void **)varp = value.data.dictval.ptr; + return; } UNREACHABLE; } @@ -3476,6 +3600,14 @@ static char *optval_to_cstr(OptVal o) snprintf(buf, o.data.string.size + 3, "\"%s\"", o.data.string.data); return buf; } + case kOptValTypeDict: { + char *s = opt_serialize(o.data.dictval.ptr, o.data.dictval.table); + size_t len = strlen(s); + char *buf = xmalloc(len + 3); + snprintf(buf, len + 3, "\"%s\"", s); + xfree(s); + return buf; + } } UNREACHABLE; } @@ -3499,10 +3631,96 @@ Object optval_as_object(OptVal o) return INTEGER_OBJ(o.data.number); case kOptValTypeString: return STRING_OBJ(o.data.string); + case kOptValTypeDict: + // The API surfaces the string form; the returned Object owns a fresh serialization. + return STRING_OBJ(cstr_as_string(opt_serialize(o.data.dictval.ptr, o.data.dictval.table))); } UNREACHABLE; } +/// Converts an option value to its structured form. +/// +/// @return Object allocated in `arena`. +Object optval_to_struct(OptIndex opt_idx, OptVal value, Arena *arena) +{ + if (value.type != kOptValTypeString && value.type != kOptValTypeDict) { + return optval_as_object(value); // boolean/number/nil scalar + } + + const uint32_t flags = options[opt_idx].flags; + const OptSchemaItem *schema = options[opt_idx].schema; + + bool owned = false; + char *str; + if (value.type == kOptValTypeDict) { + str = opt_serialize(value.data.dictval.ptr, value.data.dictval.table); + owned = true; + } else { + str = value.data.string.data != NULL ? value.data.string.data : ""; + } + + Object rv; + // Plain string option: no list/map structure. + if (!(flags & (kOptFlagComma | kOptFlagFlagList))) { + rv = STRING_OBJ(CSTR_TO_ARENA_STR(arena, str)); + } else if ((flags & kOptFlagFlagList) && !(flags & kOptFlagComma)) { + // Single-char flag list, e.g. 'shortmess': each character is a flag. + size_t n = strlen(str); + Dict d = arena_dict(arena, n); + for (size_t i = 0; i < n; i++) { + PUT_C(d, arena_memdupz(arena, &str[i], 1), BOOLEAN_OBJ(true)); + } + rv = DICT_OBJ(d); + } else { + // Comma-separated list/map/set. Upper-bound the item count for arena sizing. + size_t nparts = 1; + for (char *p = str; *p != NUL; p++) { + nparts += (*p == ','); + } + const bool as_map = (flags & kOptFlagColon) || (flags & kOptFlagFlagList) || schema != NULL; + Dict d = as_map ? arena_dict(arena, nparts) : (Dict)ARRAY_DICT_INIT; + Array a = as_map ? (Array)ARRAY_DICT_INIT : arena_array(arena, nparts); + char *item = xmalloc(strlen(str) + 1); + for (char *p = str; *p != NUL;) { + copy_option_part(&p, item, strlen(str) + 1, ","); + if (*item == NUL) { + continue; // skip empty parts, e.g. "a,,b" + } + if (!as_map) { + // Note: the raw string preserves the ",," literal-comma convention (e.g. 'isfname'); the + // structured view splits on every comma and does not reconstruct literal commas. + ADD_C(a, STRING_OBJ(CSTR_TO_ARENA_STR(arena, item))); + continue; + } + char *colon = strchr(item, ':'); + char *val = NULL; + if (colon != NULL) { + *colon = NUL; + val = colon + 1; + } + String key = CSTR_TO_ARENA_STR(arena, item); + Object v; + if (flags & kOptFlagFlagList) { + v = BOOLEAN_OBJ(true); // comma flag list ('whichwrap'): each item is a flag + } else if (val == NULL) { + v = BOOLEAN_OBJ(true); // bare flag in a typed/colon map + } else { + // A key's value stays a string, even for a num/enum key: option sub-value types are not a + // stable contract (Vim options are "stringly typed"), so we expose structure, not types. + v = STRING_OBJ(CSTR_TO_ARENA_STR(arena, val)); + } + PUT_C(d, key.data, v); + } + xfree(item); + rv = as_map ? DICT_OBJ(d) : ARRAY_OBJ(a); + } + + if (owned) { + xfree(str); + } + return rv; +} + /// Convert an API Object to an OptVal. OptVal object_as_optval(Object o, bool *error) { @@ -3537,7 +3755,9 @@ OptVal object_as_optval_for(OptIndex opt_idx, Object o, set_op_T op, bool *error const uint32_t flags = options[opt_idx].flags; const bool is_list = flags & (kOptFlagComma | kOptFlagFlagList); - const bool is_map = flags & kOptFlagColon; // "key:value" list, e.g. 'listchars'. + // "key:value" list, e.g. 'listchars', or a dict option, e.g. 'breakindentopt' (which + // accepts a Dict even without kOptFlagColon, mirroring optval_to_struct()'s `as_map`). + const bool is_map = (flags & kOptFlagColon) || option_has_type(opt_idx, kOptValTypeDict); const bool is_flaglist = flags & kOptFlagFlagList; // single-char flag list, e.g. 'shortmess'. const bool is_comma = flags & kOptFlagComma; const bool allow_dup = !(flags & kOptFlagNoDup); @@ -3552,7 +3772,9 @@ OptVal object_as_optval_for(OptIndex opt_idx, Object o, set_op_T op, bool *error type_ok = option_has_type(opt_idx, kOptValTypeNumber); break; case kObjectTypeString: + // Struct-stored options take their ":set" string here; set_option() reifies it. type_ok = option_has_type(opt_idx, kOptValTypeString) + || option_has_type(opt_idx, kOptValTypeDict) || opt_idx == kOptWildchar || opt_idx == kOptWildcharm; break; case kObjectTypeArray: @@ -3620,6 +3842,17 @@ OptVal object_as_optval_for(OptIndex opt_idx, Object o, set_op_T op, bool *error char *kv = concat_str(k.data, ":"); GA_APPEND(char *, &ga, concat_str(kv, v.data.string.data)); xfree(kv); + } else if (v.type == kObjectTypeInteger) { + // Typed map value, e.g. 'diffopt' `{ context = 4 }` -> "context:4". + char buf[NUMBUFLEN]; + snprintf(buf, sizeof(buf), ":%" PRId64, (int64_t)v.data.integer); + char *kv = concat_str(k.data, buf); + GA_APPEND(char *, &ga, kv); + } else if (v.type == kObjectTypeBoolean || v.type == kObjectTypeNil) { + // Bare flag in a typed map, e.g. 'diffopt' `{ internal = true }` -> "internal". + if (v.type == kObjectTypeBoolean && v.data.boolean) { + GA_APPEND(char *, &ga, xstrdup(k.data)); + } } else { *error = true; GA_DEEP_CLEAR_PTR(&ga); @@ -4048,6 +4281,19 @@ static const char *set_option(const OptIndex opt_idx, OptVal value, int opt_flag const char *errmsg = NULL; + // Struct-stored options keep a reified keyset as the stored value. Every set path (":set", the + // API, Vimscript, a merge) funnels through here as a ":set" string; validate and reify it once, + // the single choke point. Reset-to-default/global (":set opt&/<") already produces a keyset. + if (value.type == kOptValTypeString && option_has_type(opt_idx, kOptValTypeDict)) { + OptVal reified = opt_dict_from_string(opt_idx, value.data.string.data, errbuf, errbuflen, + &errmsg); + optval_free(value); + if (errmsg != NULL) { + return errmsg; + } + value = reified; + } + if (!direct) { errmsg = validate_option_value(opt_idx, &value, opt_flags, errbuf, errbuflen); @@ -4862,6 +5108,16 @@ static int put_set(FILE *fd, char *cmd, OptIndex opt_idx, void *varp) xfree(part); return FAIL; } + case kOptValTypeDict: { + // Written back as its ":set" string, e.g. `set diffopt=internal,filler,...`. + char *value_str = opt_serialize(value.data.dictval.ptr, value.data.dictval.table); + bool ok = fprintf(fd, "%s %s=", cmd, name) >= 0 && put_escstr(fd, value_str, 2) == OK; + xfree(value_str); + if (!ok) { + return FAIL; + } + break; + } } if (put_eol(fd) < 0) { @@ -5335,6 +5591,21 @@ static char *copy_option_val(const char *val) return xstrdup(val); } +/// Deep-copy a dict (keyset) window/buffer option value, or NULL. +static void *copy_opt_dict(void *keyset, OptIndex opt_idx) +{ + const OptDictInfo *si = opt_dict_info(opt_idx); + return opt_dict_dup((OptDict){ keyset, si->table, si->size }).ptr; +} + +/// Free a dict option value and clear the pointer. +static void clear_opt_dict(void **keyset, OptIndex opt_idx) +{ + const OptDictInfo *si = opt_dict_info(opt_idx); + opt_dict_free((OptDict){ *keyset, si->table, si->size }); + *keyset = NULL; +} + /// Copy the options from one winopt_T to another. /// Doesn't free the old option values in "to", use clear_winopt() for that. /// The 'scroll' option is not copied, because it depends on the window height. @@ -5359,7 +5630,7 @@ void copy_winopt(winopt_T *from, winopt_T *to) to->wo_wrap_save = from->wo_wrap_save; to->wo_lbr = from->wo_lbr; to->wo_bri = from->wo_bri; - to->wo_briopt = copy_option_val(from->wo_briopt); + to->wo_briopt = copy_opt_dict(from->wo_briopt, kOptBreakindentopt); to->wo_scb = from->wo_scb; to->wo_scb_save = from->wo_scb_save; to->wo_sms = from->wo_sms; @@ -5435,7 +5706,7 @@ static void check_winopt(winopt_T *wop) check_string_option(&wop->wo_culopt); check_string_option(&wop->wo_cc); check_string_option(&wop->wo_cocu); - check_string_option(&wop->wo_briopt); + // wo_briopt: keyset (may be NULL until set); briopt_check() handles NULL. check_string_option(&wop->wo_winhl); check_string_option(&wop->wo_lcs); check_string_option(&wop->wo_fcs); @@ -5463,7 +5734,7 @@ void clear_winopt(winopt_T *wop) clear_string_option(&wop->wo_culopt); clear_string_option(&wop->wo_cc); clear_string_option(&wop->wo_cocu); - clear_string_option(&wop->wo_briopt); + clear_opt_dict((void **)&wop->wo_briopt, kOptBreakindentopt); clear_string_option(&wop->wo_winhl); clear_string_option(&wop->wo_lcs); clear_string_option(&wop->wo_fcs); @@ -5481,7 +5752,7 @@ void didset_window_options(win_T *wp, bool valid_cursor) wp->w_skipcol = 0; } check_colorcolumn(NULL, wp); - briopt_check(NULL, wp); + briopt_check(wp); fill_culopt_flags(NULL, wp); set_chars_option(wp, wp->w_p_fcs, kFillchars, true, NULL, 0); set_chars_option(wp, wp->w_p_lcs, kListchars, true, NULL, 0); @@ -5522,10 +5793,10 @@ void buf_copy_options(buf_T *buf, int flags) // X no no no true // no yes no X true /// - if ((vim_strchr(p_cpo, CPO_BUFOPTGLOB) == NULL || !(flags & BCO_ENTER)) + if ((vim_strchr(p_cpo, kCpoBufoptglob) == NULL || !(flags & BCO_ENTER)) && (buf->b_p_initialized || (!(flags & BCO_ENTER) - && vim_strchr(p_cpo, CPO_BUFOPT) != NULL))) { + && vim_strchr(p_cpo, kCpoBufopt) != NULL))) { should_copy = false; } @@ -6264,6 +6535,14 @@ int ExpandSettingSubtract(expand_T *xp, regmatch_T *regmatch, int *numMatches, c expand_option_flags, curbuf, curwin); + // A dict option's varp holds a keyset; expand on its serialized string form instead. + char *option_serialized = NULL; + if (option_has_type(expand_option_idx, kOptValTypeDict)) { + option_serialized = opt_serialize((void *)option_val, + opt_dict_info(expand_option_idx)->table); + option_val = option_serialized; + } + uint32_t option_flags = options[expand_option_idx].flags; if (option_has_type(expand_option_idx, kOptValTypeNumber)) { @@ -6275,11 +6554,13 @@ int ExpandSettingSubtract(expand_T *xp, regmatch_T *regmatch, int *numMatches, c // kOptFlagComma and kOptFlagFlagList. if (*option_val == NUL) { + xfree(option_serialized); return FAIL; } // Make a copy as we need to inject null characters destructively. char *option_copy = xstrdup(option_val); + xfree(option_serialized); // struct form copied into option_copy; no longer needed char *next_val = option_copy; garray_T ga; @@ -6381,6 +6662,10 @@ static void option_value2string(vimoption_T *opt, int opt_flags) "%" PRId64, (int64_t)(*(OptInt *)varp)); } + } else if (option_has_type(get_opt_idx(opt), kOptValTypeDict)) { + char *str = opt_serialize(*(void **)varp, opt_dict_info(get_opt_idx(opt))->table); + xstrlcpy(NameBuff, str, MAXPATHL); + xfree(str); } else { // string varp = *(char **)varp; @@ -6760,6 +7045,73 @@ size_t copy_option_part(char **option, char *buf, size_t maxlen, char *sep_chars return len; } +/// Iterates comma-separated parts of option, splitting each part at its first ':' into key/value. +/// The returned pointers are slices into the original string: for simple "key:value" list options +/// like 'winhighlight' and 'previewpopup', but not 'listchars'/'fillchars' whose values need +/// decoding. +/// +/// Slices are paired lengths; copy with xmemcpyz() if you need a NUL-terminated string. +/// +/// A part with no ':' yields `*val == NULL`, i.e. a bare flag such as 'diffopt' "filler". An empty +/// part (from "a,,b" or a leading ',') yields "*keylen == 0" with "*val == NULL"; the caller +/// decides if that is an error (strict key:value options, but not 'diffopt'). +/// +/// Drive it with a loop, advancing `*p` each step: +/// +/// const char *key, *val; +/// size_t keylen, vallen; +/// for (const char *p = value; option_next_keyval(&p, &key, &keylen, &val, &vallen);) { +/// ... +/// } +/// +/// @param[in,out] p Cursor into the option string; advanced past the part. +/// @param[out] key Start of the key slice. +/// @param[out] keylen Length of the key slice. +/// @param[out] val Start of the value slice, or NULL if the part had no ':'. +/// @param[out] vallen Length of the value slice (0 when "*val" is NULL). +/// +/// @return true if a part was returned, false at the end of the string. +bool option_next_keyval(const char **p, const char **key, size_t *keylen, const char **val, + size_t *vallen) + FUNC_ATTR_NONNULL_ALL +{ + const char *s = *p; + if (*s == NUL) { + return false; + } + + const char *comma = vim_strchr(s, ','); + const char *end = comma != NULL ? comma : s + strlen(s); + const char *colon = vim_strchr(s, ':'); + + *key = s; + if (colon != NULL && colon < end) { + *keylen = (size_t)(colon - s); + *val = colon + 1; + *vallen = (size_t)(end - (colon + 1)); + } else { + *keylen = (size_t)(end - s); + *val = NULL; + *vallen = 0; + } + + *p = comma != NULL ? comma + 1 : end; + return true; +} + +/// Compare a (non-NUL-terminated) slice from option_next_keyval() against a name. +/// +/// @param slice Start of the slice; may be NULL (e.g. a bare flag's value). +/// @param slicelen Length of the slice. +/// @param name NUL-terminated name to match. +/// +/// @return true if "slice[0..slicelen)" equals "name". +bool option_slice_eq(const char *slice, size_t slicelen, const char *name) + FUNC_ATTR_PURE FUNC_ATTR_WARN_UNUSED_RESULT +{ + return slice != NULL && strlen(name) == slicelen && strncmp(slice, name, slicelen) == 0; +} + /// Return true when 'shell' has "csh" in the tail. int csh_like_shell(void) { @@ -6786,8 +7138,11 @@ dict_T *get_winbuf_options(const int bufopt) void *varp = get_varp(opt); if (varp != NULL) { - typval_T opt_tv = optval_as_tv(optval_from_varp(opt_idx, varp), true); + OptVal value = optval_from_varp(opt_idx, varp); + typval_T opt_tv = optval_as_tv(value, true); tv_dict_add_tv(d, opt->fullname, strlen(opt->fullname), &opt_tv); + // tv_dict_add_tv() copied the typval, so release any string optval_as_tv() allocated. + optval_as_tv_free(value, opt_tv); } } } diff --git a/src/nvim/option.h b/src/nvim/option.h index 93d7998fd7..3c4ff1c0ee 100644 --- a/src/nvim/option.h +++ b/src/nvim/option.h @@ -52,6 +52,8 @@ static inline const char *optval_type_get_name(const OptValType type) return "number"; case kOptValTypeString: return "string"; + case kOptValTypeDict: + return "string"; // dict options present as strings at the API/error surface. } UNREACHABLE; } diff --git a/src/nvim/option_defs.h b/src/nvim/option_defs.h index 3c5fba0e55..e022aeff65 100644 --- a/src/nvim/option_defs.h +++ b/src/nvim/option_defs.h @@ -50,8 +50,18 @@ typedef enum { kOptValTypeBoolean, kOptValTypeNumber, kOptValTypeString, + kOptValTypeDict, ///< Option stored as a reified keyset (`schema.dict` in options.lua). } OptValType; +/// Storage for a dict option ("schema" in options.lua): a heap-allocated keyset +/// (`OptKeyDict_`) plus the field table needed to free, copy and serialize it without an +/// option index. The string form is derived on-demand (`opt_serialize()`), never stored. +typedef struct { + void *ptr; ///< Heap keyset (owned), or NULL. + const KeySetLink *table; ///< Field layout (borrowed; points at generated static data). + size_t size; ///< sizeof the keyset, for (re)allocation. +} OptDict; + /// Scopes that an option can support. typedef enum { kOptScopeGlobal = 0, ///< Request global option value @@ -68,6 +78,7 @@ typedef union { TriState boolean; OptInt number; String string; + OptDict dictval; } OptValData; /// Option value @@ -76,6 +87,32 @@ typedef struct { OptValData data; } OptVal; +/// Value kind of one key in a dict option (see "schema" in options.lua). +typedef enum { + kOptSchemaFlag, ///< bare flag, e.g. 'diffopt' "filler" + kOptSchemaNum, ///< "key:N" non-negative number, e.g. 'diffopt' "context:4" + kOptSchemaSNum, ///< "key:N" signed number, e.g. 'breakindentopt' "shift:-2" + kOptSchemaEnum, ///< "key:val" where val is one of "enum_values", e.g. "algorithm:patience" + kOptSchemaStr, ///< "key:val" where val is a free string, e.g. 'previewpopup' "highlight:Foo" +} OptSchemaKind; + +/// One key of a dict option, generated from its `dict` schema in options.lua. +/// Consumed by opt_strings_check(). A NULL "name" terminates the array. +typedef struct { + const char *name; ///< key name, without a trailing ':' + OptSchemaKind kind; + const char **enum_values; ///< kOptSchemaEnum: NULL-terminated valid values; else NULL +} OptSchemaItem; + +/// Per-option handle for a dict option ("schema" in options.lua), generated by +/// `opt_dict_info()`. NULL from `opt_dict_info()` means the option is not a dict option. +typedef struct { + FieldHashfn get_field; ///< Keyset perfect-hash lookup, for opt_fill(). + const KeySetLink *table; ///< Field layout, for opt_serialize()/free/copy. + const OptSchemaItem *schema; ///< Grammar, for opt_strings_check() validation. + size_t size; ///< sizeof the keyset, for allocation. +} OptDictInfo; + /// :set operator types typedef enum { OP_NONE = 0, @@ -180,6 +217,9 @@ typedef struct { const char **values; ///< possible values for string options const size_t values_len; ///< length of values array + /// Grammar of a dict option ("schema.dict" in options.lua); NULL otherwise. + const OptSchemaItem *schema; + /// callback function to invoke after an option is modified to validate and /// apply the new value. opt_did_set_cb_T opt_did_set_cb; diff --git a/src/nvim/option_vars.h b/src/nvim/option_vars.h index a389622ca1..09c074bbac 100644 --- a/src/nvim/option_vars.h +++ b/src/nvim/option_vars.h @@ -1,6 +1,7 @@ #pragma once #include "nvim/macros_defs.h" +#include "nvim/option_defs.h" #include "nvim/os/os_defs.h" #include "nvim/sign_defs.h" #include "nvim/statusline_defs.h" @@ -57,81 +58,16 @@ #define EOL_DOS 1 // CR NL #define EOL_MAC 2 // CR -// Formatting options for p_fo 'formatoptions' -#define FO_WRAP 't' -#define FO_WRAP_COMS 'c' -#define FO_RET_COMS 'r' -#define FO_OPEN_COMS 'o' -#define FO_NO_OPEN_COMS '/' -#define FO_Q_COMS 'q' -#define FO_Q_NUMBER 'n' -#define FO_Q_SECOND '2' -#define FO_INS_VI 'v' -#define FO_INS_LONG 'l' -#define FO_INS_BLANK 'b' -#define FO_MBYTE_BREAK 'm' // break before/after multi-byte char -#define FO_MBYTE_JOIN 'M' // no space before/after multi-byte char -#define FO_MBYTE_JOIN2 'B' // no space between multi-byte chars -#define FO_ONE_LETTER '1' -#define FO_WHITE_PAR 'w' // trailing white space continues paragr. -#define FO_AUTO 'a' // automatic formatting -#define FO_RIGOROUS_TW ']' // respect textwidth rigorously -#define FO_REMOVE_COMS 'j' // remove comment leaders when joining lines -#define FO_PERIOD_ABBR 'p' // don't break a single space after a period - +// Formatting options for 'formatoptions': the per-flag kFo* constants are generated from the +// option's `flagchars` schema (options.lua); FO_ALL is the concatenated set for do_set(). #define DFLT_FO_VI "vt" #define DFLT_FO_VIM "tcqj" #define FO_ALL "tcro/q2vlb1mMBn,aw]jp" // for do_set() #define MAX_MCO 6 // fixed value for 'maxcombine' -// characters for the p_cpo option: -#define CPO_ALTREAD 'a' // ":read" sets alternate file name -#define CPO_ALTWRITE 'A' // ":write" sets alternate file name -#define CPO_BAR 'b' // "\|" ends a mapping -#define CPO_BSLASH 'B' // backslash in mapping is not special -#define CPO_SEARCH 'c' -#define CPO_CONCAT 'C' // Don't concatenate sourced lines -#define CPO_DOTTAG 'd' // "./tags" in 'tags' is in current dir -#define CPO_DIGRAPH 'D' // No digraph after "r", "f", etc. -#define CPO_EXECBUF 'e' -#define CPO_EMPTYREGION 'E' // operating on empty region is an error -#define CPO_FNAMER 'f' // set file name for ":r file" -#define CPO_FNAMEW 'F' // set file name for ":w file" -#define CPO_INTMOD 'i' // interrupt a read makes buffer modified -#define CPO_INDENT 'I' // remove auto-indent more often -#define CPO_ENDOFSENT 'J' // need two spaces to detect end of sentence -#define CPO_KOFFSET 'K' // don't wait for key code in mappings -#define CPO_LITERAL 'l' // take char after backslash in [] literal -#define CPO_LISTWM 'L' // 'list' changes wrapmargin -#define CPO_SHOWMATCH 'm' -#define CPO_MATCHBSL 'M' // "%" ignores use of backslashes -#define CPO_NUMCOL 'n' // 'number' column also used for text -#define CPO_LINEOFF 'o' -#define CPO_OVERNEW 'O' // silently overwrite new file -#define CPO_FNAMEAPP 'P' // set file name for ":w >>file" -#define CPO_JOINCOL 'q' // with "3J" use column after first join -#define CPO_REDO 'r' -#define CPO_REMMARK 'R' // remove marks when filtering -#define CPO_BUFOPT 's' -#define CPO_BUFOPTGLOB 'S' -#define CPO_TAGPAT 't' // tag pattern is used for "n" -#define CPO_UNDO 'u' // "u" undoes itself -#define CPO_BACKSPACE 'v' // "v" keep deleted text -#define CPO_FWRITE 'W' // "w!" doesn't overwrite readonly files -#define CPO_ESC 'x' -#define CPO_REPLCNT 'X' // "R" with a count only deletes chars once -#define CPO_YANK 'y' -#define CPO_KEEPRO 'Z' // don't reset 'readonly' on ":w!" -#define CPO_DOLLAR '$' -#define CPO_FILTER '!' -#define CPO_MATCH '%' -#define CPO_PLUS '+' // ":write file" resets 'modified' -#define CPO_REGAPPEND '>' // insert NL when appending to a register -#define CPO_SCOLON ';' // using "," and ";" will skip over char if - // cursor would not move -#define CPO_NOSYMLINKS '~' // don't resolve symlinks when changing directory -#define CPO_CHANGEW '_' // "cw" special-case +// characters for the p_cpo option: the per-flag kCpo* constants are generated from the +// option's `flagchars` schema (options.lua). CPO_VI/CPO_VIM are the Vi/Vim default sets. // default values for Vim and Vi #define CPO_VIM "aABceFs_" #define CPO_VI "aAbBcCdDeEfFiIJKlLmMnoOpPqrRsStuvWxXyZ$!%+>;~_" @@ -139,17 +75,10 @@ // characters for p_ww option: #define WW_ALL "bshl<>[]~" -// characters for p_mouse option: -#define MOUSE_NORMAL 'n' // use mouse in Normal mode -#define MOUSE_VISUAL 'v' // use mouse in Visual/Select mode -#define MOUSE_INSERT 'i' // use mouse in Insert mode -#define MOUSE_COMMAND 'c' // use mouse in Command-line mode -#define MOUSE_HELP 'h' // use mouse in help buffers -#define MOUSE_RETURN 'r' // use mouse for hit-return message +// characters for p_mouse option: the per-flag kMouse* constants are generated from the option's +// `flagchars` schema (options.lua). #define MOUSE_A "nvich" // used for 'a' flag #define MOUSE_ALL "anvichr" // all possible characters -#define MOUSE_NONE ' ' // don't use Visual selection -#define MOUSE_NONEF 'x' // forced modeless selection // default vertical and horizontal mouse scroll values. // Note: This should be in sync with the default mousescroll option. @@ -158,31 +87,11 @@ #define COCU_ALL "nvic" // flags for 'concealcursor' -/// characters for p_shm option: -enum { - SHM_RO = 'r', ///< Readonly. - SHM_MOD = 'm', ///< Modified. - SHM_LINES = 'l', ///< "L" instead of "lines". - SHM_WRI = 'w', ///< "[w]" instead of "written". - SHM_ABBREVIATIONS = 'a', ///< Use abbreviations from #SHM_ALL_ABBREVIATIONS. - SHM_WRITE = 'W', ///< Don't use "written" at all. - SHM_TRUNC = 't', ///< Truncate file messages. - SHM_TRUNCALL = 'T', ///< Truncate all messages. - SHM_OVER = 'o', ///< Overwrite file messages. - SHM_OVERALL = 'O', ///< Overwrite more messages. - SHM_SEARCH = 's', ///< No search hit bottom messages. - SHM_ATTENTION = 'A', ///< No ATTENTION messages. - SHM_INTRO = 'I', ///< Intro messages. - SHM_COMPLETIONMENU = 'c', ///< Completion menu messages. - SHM_COMPLETIONSCAN = 'C', ///< Completion scanning messages. - SHM_RECORDING = 'q', ///< No recording message. - SHM_FILEINFO = 'F', ///< No file info messages. - SHM_SEARCHCOUNT = 'S', ///< No search stats: '[1/10]'. - SHM_UNDO = 'u', ///< No undo messages. -}; +// characters for the p_shm option: the per-flag kShm* constants are generated from the option's +// `flagchars` schema (options.lua). /// Represented by 'a' flag. #define SHM_ALL_ABBREVIATIONS ((char[]) { \ - SHM_RO, SHM_MOD, SHM_LINES, SHM_WRI, \ + kShmRo, kShmMod, kShmLines, kShmWri, \ 0 }) // characters for p_go: @@ -329,7 +238,7 @@ EXTERN char *p_debug; ///< 'debug' EXTERN char *p_def; ///< 'define' EXTERN char *p_inc; EXTERN char *p_dia; ///< 'diffanchors' -EXTERN char *p_dip; ///< 'diffopt' +EXTERN struct OptKeyDict_dip *p_dip; ///< 'diffopt' EXTERN char *p_dex; ///< 'diffexpr' EXTERN char *p_dict; ///< 'dictionary' EXTERN int p_dg; ///< 'digraph' @@ -439,7 +348,7 @@ EXTERN char *p_mousem; ///< 'mousemodel' EXTERN int p_mousemev; ///< 'mousemoveevent' EXTERN int p_mousef; ///< 'mousefocus' EXTERN int p_mh; ///< 'mousehide' -EXTERN char *p_mousescroll; ///< 'mousescroll' +EXTERN struct OptKeyDict_mousescroll *p_mousescroll; ///< 'mousescroll' EXTERN OptInt p_mousescroll_vert INIT( = MOUSESCROLL_VERT_DFLT); EXTERN OptInt p_mousescroll_hor INIT( = MOUSESCROLL_HOR_DFLT); EXTERN OptInt p_mouset; ///< 'mousetime' @@ -608,7 +517,9 @@ EXTERN int p_cdh; ///< 'cdhome' // Value for b_p_ul indicating the global value must be used. #define NO_LOCAL_UNDOLEVEL (-123456) -#define ERR_BUFLEN 80 +// Buffer for an option-set error message. Large enough to list an option's valid values (see +// opt_invalid_value_err()); the value is appended separately into IObuff. +#define ERR_BUFLEN 256 #define SB_MAX 1000000 // Maximum 'scrollback' value. diff --git a/src/nvim/options.lua b/src/nvim/options.lua index 0facf0ecb7..19812a0a00 100644 --- a/src/nvim/options.lua +++ b/src/nvim/options.lua @@ -15,8 +15,7 @@ --- @field deny_duplicates? boolean --- @field enable_if? string --- @field defaults? vim.option_defaults|vim.option_value|fun(): string ---- @field values? vim.option_valid_values ---- @field flags? true|table +--- @field schema? vim.option_schema --- @field secure? true --- @field noglob? true --- @field normal_fname_chars? true @@ -49,7 +48,31 @@ --- @alias vim.option_scope 'global'|'buf'|'win' --- @alias vim.option_type 'boolean'|'number'|'string' --- @alias vim.option_value boolean|integer|string ---- @alias vim.option_valid_values (string|[string,vim.option_valid_values])[] + +--- Options for a `char`/`chars` schema key. +--- @class vim.option_schema.char.opts +--- @field field? string|false fcs_chars/lcs_chars field (default: key name; false = no storage) +--- @field def? string default char +--- @field fallback? string default char when "def" isn't single-width + +--- A `char`/`chars` schema key (e.g. 'listchars' "eol"/"tab"), generating a chars_tab[] entry. +--- @alias vim.option_schema.char [string, 'char'|'chars', vim.option_schema.char.opts?] + +--- A key of a `dict` schema: a bare flag (boolean), a typed value, or an enum value. +--- @alias vim.option_schema.dictkey +--- | string +--- | [string, 'num'|'snum'|'str'] +--- | [string, 'enum', {values: string[]}] + +--- Declarative grammar of a structured "string" option, as a category record. Usually exactly one +--- field is set; `flags`+`enum` combine for 'cursorlineopt' (its "both" alias has no bit). +--- @class vim.option_schema +--- @field chars? vim.option_schema.char[] chars_tab[] dispatch, e.g. 'listchars' +--- @field dict? vim.option_schema.dictkey[] key:value map, reified to a keyset, e.g. 'diffopt' +--- @field enum? string[] single-choice values, e.g. 'ambiwidth' {'single','double'} +--- @field flagchars? table char flags: name -> char, e.g. 'formatoptions' +--- @field flags? (string|[string,integer]|[string,integer,string])[] bitmask flags (a `set` + C constants), e.g. 'foldopen'. A 3rd tuple element overrides the C token. +--- @field set? string[] multi-choice (comma list) values, e.g. 'backspace' --- @alias vim.option_redraw --- |'statuslines' @@ -119,7 +142,9 @@ local options = { abbreviation = 'ambw', cb = 'did_set_ambiwidth', defaults = 'single', - values = { 'single', 'double' }, + schema = { + enum = { 'single', 'double' }, + }, desc = [=[ Tells Vim what to do with characters with East Asian Width Class Ambiguous (such as Euro, Registered Sign, Copyright Sign, Greek @@ -366,7 +391,9 @@ local options = { abbreviation = 'bg', cb = 'did_set_background', defaults = 'dark', - values = { 'light', 'dark' }, + schema = { + enum = { 'light', 'dark' }, + }, desc = [=[ When set to "dark" or "light", adjusts the default color groups for that background type. The |TUI| or other UI sets this on startup @@ -404,7 +431,9 @@ local options = { abbreviation = 'bs', cb = 'did_set_backspace', defaults = 'indent,eol,start', - values = { 'indent', 'eol', 'start', 'nostop' }, + schema = { + set = { 'indent', 'eol', 'start', 'nostop' }, + }, deny_duplicates = true, desc = [=[ Influences the working of , , CTRL-W and CTRL-U in Insert @@ -453,8 +482,9 @@ local options = { abbreviation = 'bkc', cb = 'did_set_backupcopy', defaults = { condition = 'UNIX', if_false = 'auto', if_true = 'auto' }, - values = { 'yes', 'auto', 'no', 'breaksymlink', 'breakhardlink' }, - flags = true, + schema = { + flags = { 'yes', 'auto', 'no', 'breaksymlink', 'breakhardlink' }, + }, deny_duplicates = true, desc = [=[ When writing a file and a backup is made, this option tells how it's @@ -649,29 +679,30 @@ local options = { { abbreviation = 'bo', defaults = 'all', - values = { - 'all', - 'backspace', - 'cursor', - 'complete', - 'copy', - 'ctrlg', - 'error', - 'esc', - 'ex', - 'hangul', - 'insertmode', - 'lang', - 'mess', - 'showmatch', - 'operator', - 'register', - 'shell', - 'spell', - 'term', - 'wildmode', + schema = { + flags = { + 'all', + 'backspace', + 'cursor', + 'complete', + 'copy', + 'ctrlg', + 'error', + 'esc', + 'ex', + 'hangul', + 'insertmode', + 'lang', + 'mess', + 'showmatch', + 'operator', + 'register', + 'shell', + 'spell', + 'term', + 'wildmode', + }, }, - flags = true, deny_duplicates = true, desc = [=[ Specifies for which events the bell will not be rung. It is a comma- @@ -793,7 +824,6 @@ local options = { if_true = ' \t!@*-+;:,./?', doc = '" ^I!@*-+;:,./?"', }, - flags = true, desc = [=[ This option lets you choose which characters might cause a line break if 'linebreak' is on. Only works for ASCII characters. @@ -824,8 +854,15 @@ local options = { abbreviation = 'briopt', cb = 'did_set_breakindentopt', defaults = '', - -- Keep this in sync with briopt_check(). - values = { 'shift:', 'min:', 'sbr', 'list:', 'column:' }, + schema = { + dict = { + { 'shift', 'snum' }, + { 'min', 'num' }, + 'sbr', -- unsigned number. + { 'list', 'snum' }, + { 'column', 'snum' }, + }, + }, deny_duplicates = true, desc = [=[ Settings for 'breakindent'. It can consist of the following optional @@ -861,7 +898,7 @@ local options = { redraw = { 'current_buffer' }, scope = { 'win' }, short_desc = N_("settings for 'breakindent'"), - type = 'string', + type = 'string', -- The `schema` reifies to OptKeyDict_briopt. }, { abbreviation = 'bsdir', @@ -887,7 +924,9 @@ local options = { abbreviation = 'bh', cb = 'did_set_bufhidden', defaults = '', - values = { '', 'hide', 'unload', 'delete', 'wipe' }, + schema = { + enum = { '', 'hide', 'unload', 'delete', 'wipe' }, + }, desc = [=[ This option specifies what happens when a buffer is no longer displayed in a window: @@ -939,15 +978,17 @@ local options = { abbreviation = 'bt', cb = 'did_set_buftype', defaults = '', - values = { - '', - 'acwrite', - 'help', - 'nofile', - 'nowrite', - 'quickfix', - 'terminal', - 'prompt', + schema = { + enum = { + '', + 'acwrite', + 'help', + 'nofile', + 'nowrite', + 'quickfix', + 'terminal', + 'prompt', + }, }, desc = [=[ The value of this option specifies the type of a buffer: @@ -1022,8 +1063,9 @@ local options = { { abbreviation = 'cmp', defaults = 'internal,keepascii', - values = { 'internal', 'keepascii' }, - flags = true, + schema = { + flags = { 'internal', 'keepascii' }, + }, deny_duplicates = true, desc = [=[ Specifies details about changing the case of letters. It may contain @@ -1305,8 +1347,9 @@ local options = { { abbreviation = 'cb', defaults = '', - values = { 'unnamed', 'unnamedplus' }, - flags = true, + schema = { + flags = { 'unnamed', 'unnamedplus' }, + }, desc = [=[ This option is a list of comma-separated names. These names are recognized: @@ -1477,7 +1520,9 @@ local options = { abbreviation = 'cpt', cb = 'did_set_complete', defaults = '.,w,b,u,t', - values = { '.', 'w', 'b', 'u', 'k', 'kspell', 's', 'i', 'd', ']', 't', 'U', 'f', 'F', 'o' }, + schema = { + set = { '.', 'w', 'b', 'u', 'k', 'kspell', 's', 'i', 'd', ']', 't', 'U', 'f', 'F', 'o' }, + }, deny_duplicates = true, desc = [=[ This option controls how completion |ins-completion| behaves when @@ -1577,7 +1622,6 @@ local options = { abbreviation = 'cia', cb = 'did_set_completeitemalign', defaults = 'abbr,kind,menu', - flags = true, deny_duplicates = true, desc = [=[ A comma-separated list of strings that controls the alignment and @@ -1598,21 +1642,22 @@ local options = { abbreviation = 'cot', cb = 'did_set_completeopt', defaults = 'menu,popup', - values = { - 'fuzzy', - 'longest', - 'menu', - 'menuone', - 'nearest', - 'noinsert', - 'noselect', - 'nosort', - 'popup', - 'preinsert', - 'preselect', - 'preview', + schema = { + flags = { + 'fuzzy', + 'longest', + 'menu', + 'menuone', + 'nearest', + 'noinsert', + 'noselect', + 'nosort', + 'popup', + 'preinsert', + 'preselect', + 'preview', + }, }, - flags = true, deny_duplicates = true, desc = [=[ A comma-separated list of options for Insert mode completion @@ -1715,7 +1760,9 @@ local options = { abbreviation = 'csl', cb = 'did_set_completeslash', defaults = '', - values = { '', 'slash', 'backslash' }, + schema = { + enum = { '', 'slash', 'backslash' }, + }, desc = [=[ only modifiable in MS-Windows When this option is set it overrules 'shellslash' for completion: @@ -2088,6 +2135,56 @@ local options = { whitespace following the word in the motion. ]=], expand_cb = 'expand_set_cpoptions', + -- Generates kCpo* flag constants. CPO_VI/CPO_VIM stay hand-defined in option_vars.h. + schema = { + flagchars = { + altread = 'a', -- ":read" sets alternate file name + altwrite = 'A', -- ":write" sets alternate file name + bar = 'b', -- "\|" ends a mapping + bslash = 'B', -- backslash in mapping is not special + search = 'c', + concat = 'C', -- Don't concatenate sourced lines + dottag = 'd', -- "./tags" in 'tags' is in current dir + digraph = 'D', -- No digraph after "r", "f", etc. + execbuf = 'e', + emptyregion = 'E', -- operating on empty region is an error + fnamer = 'f', -- set file name for ":r file" + fnamew = 'F', -- set file name for ":w file" + intmod = 'i', -- interrupt a read makes buffer modified + indent = 'I', -- remove auto-indent more often + endofsent = 'J', -- need two spaces to detect end of sentence + koffset = 'K', -- don't wait for key code in mappings + literal = 'l', -- take char after backslash in [] literal + listwm = 'L', -- 'list' changes wrapmargin + showmatch = 'm', + matchbsl = 'M', -- "%" ignores use of backslashes + numcol = 'n', -- 'number' column also used for text + lineoff = 'o', + overnew = 'O', -- silently overwrite new file + fnameapp = 'P', -- set file name for ":w >>file" + joincol = 'q', -- with "3J" use column after first join + redo = 'r', + remmark = 'R', -- remove marks when filtering + bufopt = 's', + bufoptglob = 'S', + tagpat = 't', -- tag pattern is used for "n" + undo = 'u', -- "u" undoes itself + backspace = 'v', -- "v" keep deleted text + fwrite = 'W', -- "w!" doesn't overwrite readonly files + esc = 'x', + replcnt = 'X', -- "R" with a count only deletes chars once + yank = 'y', + keepro = 'Z', -- don't reset 'readonly' on ":w!" + dollar = '$', + filter = '!', + match = '%', + plus = '+', -- ":write file" resets 'modified' + regappend = '>', -- insert NL when appending to a register + scolon = ';', -- using "," and ";" will skip over char if cursor would not move + nosymlinks = '~', -- don't resolve symlinks when changing directory + changew = '_', -- "cw" special-case + }, + }, full_name = 'cpoptions', list = 'flags', redraw = { 'all_windows' }, @@ -2152,12 +2249,14 @@ local options = { abbreviation = 'culopt', cb = 'did_set_cursorlineopt', defaults = 'both', - -- Keep this in sync with fill_culopt_flags(). - values = { 'line', 'screenline', 'number', 'both' }, - flags = { - Line = 0x01, - Screenline = 0x02, - Number = 0x04, + -- Keep this in sync with fill_culopt_flags(). "both" is an alias (line+number), not its own bit. + schema = { + flags = { + { 'line', 0x01 }, + { 'screenline', 0x02 }, + { 'number', 0x04 }, + }, + enum = { 'both' }, }, deny_duplicates = true, desc = [=[ @@ -2184,7 +2283,9 @@ local options = { }, { defaults = '', - values = { 'msg', 'throw', 'beep' }, + schema = { + set = { 'msg', 'throw', 'beep' }, + }, desc = [=[ These values can be used: msg Error messages that would otherwise be omitted will be given @@ -2359,27 +2460,28 @@ local options = { abbreviation = 'dip', cb = 'did_set_diffopt', defaults = 'internal,filler,closeoff,indent-heuristic,inline:char,linematch:40', - -- Keep this in sync with diffopt_changed(). - values = { - 'filler', - 'anchor', - 'context:', - 'iblank', - 'icase', - 'iwhite', - 'iwhiteall', - 'iwhiteeol', - 'horizontal', - 'vertical', - 'closeoff', - 'hiddenoff', - 'foldcolumn:', - 'followwrap', - 'internal', - 'indent-heuristic', - { 'algorithm:', { 'myers', 'minimal', 'patience', 'histogram' } }, - { 'inline:', { 'none', 'simple', 'char', 'word' } }, - 'linematch:', + schema = { + dict = { + 'filler', + 'anchor', + { 'context', 'num' }, + 'iblank', + 'icase', + 'iwhite', + 'iwhiteall', + 'iwhiteeol', + 'horizontal', + 'vertical', + 'closeoff', + 'hiddenoff', + { 'foldcolumn', 'num' }, + 'followwrap', + 'internal', + 'indent-heuristic', + { 'algorithm', 'enum', { values = { 'myers', 'minimal', 'patience', 'histogram' } } }, + { 'inline', 'enum', { values = { 'none', 'simple', 'char', 'word' } } }, + { 'linematch', 'num' }, + }, }, deny_duplicates = true, desc = [=[ @@ -2523,7 +2625,7 @@ local options = { redraw = { 'current_window' }, scope = { 'global' }, short_desc = N_('options for using diff mode'), - type = 'string', + type = 'string', -- The `schema` reifies to OptKeyDict_dip. varname = 'p_dip', }, { @@ -2597,8 +2699,9 @@ local options = { abbreviation = 'dy', cb = 'did_set_display', defaults = 'lastline', - values = { 'lastline', 'truncate', 'uhex', 'msgsep' }, - flags = true, + schema = { + flags = { 'lastline', 'truncate', 'uhex', 'msgsep' }, + }, deny_duplicates = true, desc = [=[ Change the way text is displayed. This is a comma-separated list of @@ -2631,7 +2734,9 @@ local options = { { abbreviation = 'ead', defaults = 'both', - values = { 'both', 'ver', 'hor' }, + schema = { + enum = { 'both', 'ver', 'hor' }, + }, desc = [=[ Tells when the 'equalalways' option applies: ver vertically, width of windows is not affected @@ -3050,7 +3155,9 @@ local options = { if_false = 'unix', doc = 'Windows: "dos", Unix: "unix"', }, - values = { 'unix', 'dos', 'mac' }, + schema = { + enum = { 'unix', 'dos', 'mac' }, + }, desc = [=[ This gives the of the current buffer, which is used for reading/writing the buffer from/to a file: @@ -3207,6 +3314,32 @@ local options = { cb = 'did_set_chars_option', defaults = '', deny_duplicates = true, + -- 'fillchars' schema: generates `fcs_tab` (the `fcs_chars` dispatch table). + schema = { + chars = { + { 'stl', 'char', { def = ' ' } }, + { 'stlnc', 'char', { def = ' ' } }, + { 'wbr', 'char', { def = ' ' } }, + { 'horiz', 'char', { def = '─', fallback = '-' } }, + { 'horizup', 'char', { def = '┴', fallback = '-' } }, + { 'horizdown', 'char', { def = '┬', fallback = '-' } }, + { 'vert', 'char', { def = '│', fallback = '|' } }, + { 'vertleft', 'char', { def = '┤', fallback = '|' } }, + { 'vertright', 'char', { def = '├', fallback = '|' } }, + { 'verthoriz', 'char', { def = '┼', fallback = '+' } }, + { 'fold', 'char', { def = '·', fallback = '-' } }, + { 'foldopen', 'char', { def = '-' } }, + { 'foldclose', 'char', { field = 'foldclosed', def = '+' } }, + { 'foldsep', 'char', { def = '│', fallback = '|' } }, + { 'foldinner', 'char' }, + { 'diff', 'char', { def = '-' } }, + { 'msgsep', 'char', { def = ' ' } }, + { 'eob', 'char', { def = '~' } }, + { 'lastline', 'char', { def = '@' } }, + { 'trunc', 'char', { def = '>' } }, + { 'truncrl', 'char', { def = '<' } }, + }, + }, desc = [=[ Characters to fill the statuslines, vertical separators, special lines in the window and truncated text in the |ins-completion-menu|. @@ -3371,7 +3504,9 @@ local options = { { abbreviation = 'fcl', defaults = '', - values = { 'all' }, + schema = { + set = { 'all' }, + }, deny_duplicates = true, desc = [=[ When set to "all", a fold is closed when the cursor isn't in it and @@ -3389,27 +3524,29 @@ local options = { { abbreviation = 'fdc', defaults = '0', - values = { - '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', + schema = { + enum = { + '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', + }, }, desc = [=[ When and how to draw the foldcolumn. Valid values are: @@ -3546,7 +3683,9 @@ local options = { abbreviation = 'fdm', cb = 'did_set_foldmethod', defaults = 'manual', - values = { 'manual', 'expr', 'marker', 'indent', 'syntax', 'diff' }, + schema = { + enum = { 'manual', 'expr', 'marker', 'indent', 'syntax', 'diff' }, + }, desc = [=[ The kind of folding used for the current window. Possible values: |fold-manual| manual Folds are created manually. @@ -3599,20 +3738,21 @@ local options = { { abbreviation = 'fdo', defaults = 'block,hor,mark,percent,quickfix,search,tag,undo', - values = { - 'all', - 'block', - 'hor', - 'mark', - 'percent', - 'quickfix', - 'search', - 'tag', - 'insert', - 'undo', - 'jump', + schema = { + flags = { + 'all', + 'block', + 'hor', + 'mark', + 'percent', + 'quickfix', + 'search', + 'tag', + 'insert', + 'undo', + 'jump', + }, }, - flags = true, deny_duplicates = true, desc = [=[ Specifies for which type of commands folds will be opened, if the @@ -3767,6 +3907,32 @@ local options = { "+=" and "-=" feature of ":set" |add-option-flags|. ]=], expand_cb = 'expand_set_formatoptions', + -- Generates kFo* flag constants (used by has_format_option()). Keep the concatenated set in + -- sync with FO_ALL in option_vars.h. + schema = { + flagchars = { + wrap = 't', + wrap_coms = 'c', + ret_coms = 'r', + open_coms = 'o', + no_open_coms = '/', + q_coms = 'q', + q_number = 'n', + q_second = '2', + ins_vi = 'v', + ins_long = 'l', + ins_blank = 'b', + mbyte_break = 'm', -- break before/after multi-byte char + mbyte_join = 'M', -- no space before/after multi-byte char + mbyte_join2 = 'B', -- no space between multi-byte chars + one_letter = '1', + white_par = 'w', -- trailing white space continues paragr. + auto = 'a', -- automatic formatting + rigorous_tw = ']', -- respect textwidth rigorously + remove_coms = 'j', -- remove comment leaders when joining lines + period_abbr = 'p', -- don't break a single space after a period + }, + }, full_name = 'formatoptions', list = 'flags', scope = { 'buf' }, @@ -4569,7 +4735,9 @@ local options = { abbreviation = 'icm', cb = 'did_set_inccommand', defaults = 'nosplit', - values = { 'nosplit', 'split', '' }, + schema = { + enum = { 'nosplit', 'split', '' }, + }, desc = [=[ When nonempty, shows the effects of |:substitute|, |:smagic|, |:snomagic| and user commands with the |:command-preview| flag as you @@ -4980,8 +5148,9 @@ local options = { { abbreviation = 'jop', defaults = 'clean', - values = { 'stack', 'view', 'clean' }, - flags = true, + schema = { + flags = { 'stack', 'view', 'clean' }, + }, deny_duplicates = true, desc = [=[ List of words that change the behavior of the |jumplist|. @@ -5031,7 +5200,9 @@ local options = { abbreviation = 'km', cb = 'did_set_keymodel', defaults = '', - values = { 'startsel', 'stopsel' }, + schema = { + set = { 'startsel', 'stopsel' }, + }, deny_duplicates = true, desc = [=[ List of comma-separated words, which enable special things that keys @@ -5339,7 +5510,9 @@ local options = { abbreviation = 'lop', cb = 'did_set_lispoptions', defaults = '', - values = { 'expr:0', 'expr:1' }, + schema = { + set = { 'expr:0', 'expr:1' }, + }, deny_duplicates = true, desc = [=[ Comma-separated list of items that influence the Lisp indenting when @@ -5405,6 +5578,25 @@ local options = { cb = 'did_set_chars_option', defaults = 'tab:> ,trail:-,nbsp:+', deny_duplicates = true, + -- 'listchars' schema: generates `lcs_tab` (the `lcs_chars` dispatch table). + -- "tab"/"leadtab" fill a multi-char field; "multispace"/"leadmultispace" have no single + -- storage (field=false) and are handled specially in set_chars_option(). + schema = { + chars = { + { 'eol', 'char' }, + { 'extends', 'char', { field = 'ext' } }, + { 'nbsp', 'char' }, + { 'precedes', 'char', { field = 'prec' } }, + { 'space', 'char' }, + { 'tab', 'chars', { field = 'tab2' } }, + { 'leadtab', 'chars', { field = 'leadtab2' } }, + { 'lead', 'char' }, + { 'trail', 'char' }, + { 'conceal', 'char' }, + { 'multispace', 'chars', { field = false } }, + { 'leadmultispace', 'chars', { field = false } }, + }, + }, desc = [=[ Strings to use in 'list' mode and for the |:list| command. It is a comma-separated list of string settings. *E1511* @@ -5771,8 +5963,9 @@ local options = { abbreviation = 'mopt', cb = 'did_set_messagesopt', defaults = 'hit-enter,history:500,progress:c', - values = { 'hit-enter', 'wait:', 'history:', 'progress:' }, - flags = true, + schema = { + flags = { 'hit-enter', 'wait:', 'history:', 'progress:' }, + }, deny_duplicates = true, desc = [=[ Option settings for outputting messages. It can consist of the @@ -6011,6 +6204,19 @@ local options = { 'selectmode' whether to start Select mode or Visual mode ]=], expand_cb = 'expand_set_mouse', + -- Generates kMouse* flag constants. MOUSE_A/MOUSE_ALL stay hand-defined in option_vars.h. + schema = { + flagchars = { + normal = 'n', -- use mouse in Normal mode + visual = 'v', -- use mouse in Visual/Select mode + insert = 'i', -- use mouse in Insert mode + command = 'c', -- use mouse in Command-line mode + help = 'h', -- use mouse in help buffers + ['return'] = 'r', -- use mouse for hit-return message + none = ' ', -- don't use Visual selection + nonef = 'x', -- forced modeless selection + }, + }, full_name = 'mouse', list = 'flags', scope = { 'global' }, @@ -6053,7 +6259,9 @@ local options = { { abbreviation = 'mousem', defaults = 'popup_setpos', - values = { 'extend', 'popup', 'popup_setpos' }, + schema = { + enum = { '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: @@ -6133,7 +6341,9 @@ local options = { { cb = 'did_set_mousescroll', defaults = 'ver:3,hor:6', - values = { 'hor:', 'ver:' }, + schema = { + dict = { { 'hor', 'num' }, { 'ver', 'num' } }, + }, desc = [=[ This option controls the number of lines / columns to scroll by when scrolling with a mouse wheel (|scroll-mouse-wheel|). The option is @@ -6158,7 +6368,7 @@ local options = { scope = { 'global' }, short_desc = N_('amount to scroll by when scrolling with a mouse'), tags = { 'E5080' }, - type = 'string', + type = 'string', -- The `schema` reifies to OptKeyDict_mousescroll. varname = 'p_mousescroll', vi_def = true, }, @@ -6254,7 +6464,9 @@ local options = { { abbreviation = 'nf', defaults = 'bin,hex', - values = { 'bin', 'octal', 'hex', 'alpha', 'unsigned', 'blank' }, + schema = { + set = { 'bin', 'octal', 'hex', 'alpha', 'unsigned', 'blank' }, + }, deny_duplicates = true, desc = [=[ This defines what bases Vim will consider for numbers when using the @@ -6723,7 +6935,9 @@ local options = { scope = { 'global' }, cb = 'did_set_pumborder', defaults = { if_true = '' }, - values = { '', 'double', 'single', 'shadow', 'rounded', 'solid', 'bold', 'none' }, + schema = { + set = { '', 'double', 'single', 'shadow', 'rounded', 'solid', 'bold', 'none' }, + }, desc = [=[ Defines the default border style of popupmenu windows. See 'winborder' for valid values. |hl-PmenuBorder| is used for highlighting the border, and when @@ -6862,15 +7076,16 @@ local options = { { abbreviation = 'rdb', defaults = '', - values = { - 'compositor', - 'nothrottle', - 'invalid', - 'nodelta', - 'line', - 'flush', + schema = { + flags = { + 'compositor', + 'nothrottle', + 'invalid', + 'nodelta', + 'line', + 'flush', + }, }, - flags = true, desc = [=[ Flags to change the way redrawing works, for debugging purposes. Most useful with 'writedelay' set to some reasonable value. @@ -7043,7 +7258,9 @@ local options = { { abbreviation = 'rlc', defaults = 'search', - values = { 'search' }, + schema = { + set = { 'search' }, + }, desc = [=[ Each word in this option enables the command line editing to work in right-to-left mode for a group of commands: @@ -7362,7 +7579,9 @@ local options = { { abbreviation = 'sbo', defaults = 'ver,jump', - values = { 'ver', 'hor', 'jump' }, + schema = { + set = { 'ver', 'hor', 'jump' }, + }, deny_duplicates = true, desc = [=[ This is a comma-separated list of words that specifies how @@ -7427,7 +7646,9 @@ local options = { abbreviation = 'sel', cb = 'did_set_selection', defaults = 'inclusive', - values = { 'inclusive', 'exclusive', 'old' }, + schema = { + enum = { 'inclusive', 'exclusive', 'old' }, + }, desc = [=[ This option defines the behavior of the selection. It is only used in Visual and Select mode. @@ -7463,7 +7684,9 @@ local options = { { abbreviation = 'slm', defaults = '', - values = { 'mouse', 'key', 'cmd' }, + schema = { + set = { 'mouse', 'key', 'cmd' }, + }, deny_duplicates = true, desc = [=[ This is a comma-separated list of words, which specifies when to start @@ -7486,27 +7709,28 @@ local options = { cb = 'did_set_sessionoptions', defaults = 'blank,buffers,curdir,folds,help,tabpages,winsize,terminal', -- Also used for 'viewoptions'. - values = { - 'buffers', - 'winpos', - 'resize', - 'winsize', - 'localoptions', - 'options', - 'help', - 'blank', - 'globals', - 'slash', - 'unix', - 'sesdir', - 'curdir', - 'folds', - 'cursor', - 'tabpages', - 'terminal', - 'skiprtp', + schema = { + flags = { + 'buffers', + 'winpos', + 'resize', + 'winsize', + 'localoptions', + 'options', + 'help', + 'blank', + 'globals', + 'slash', + 'unix', + 'sesdir', + 'curdir', + 'folds', + 'cursor', + 'tabpages', + 'terminal', + 'skiprtp', + }, }, - flags = true, deny_duplicates = true, desc = [=[ Changes the effect of the |:mksession| command. It is a comma- @@ -8096,6 +8320,30 @@ local options = { shm=at Abbreviation, and truncate message when necessary. ]=], expand_cb = 'expand_set_shortmess', + -- Generates kShm* flag constants; SHM_ALL_ABBREVIATIONS stays hand-defined in option_vars.h. + schema = { + flagchars = { + ro = 'r', -- Readonly. + mod = 'm', -- Modified. + lines = 'l', -- "L" instead of "lines". + wri = 'w', -- "[w]" instead of "written". + abbreviations = 'a', -- Use abbreviations from SHM_ALL_ABBREVIATIONS. + write = 'W', -- Don't use "written" at all. + trunc = 't', -- Truncate file messages. + truncall = 'T', -- Truncate all messages. + over = 'o', -- Overwrite file messages. + overall = 'O', -- Overwrite more messages. + search = 's', -- No search hit bottom messages. + attention = 'A', -- No ATTENTION messages. + intro = 'I', -- Intro messages. + completionmenu = 'c', -- Completion menu messages. + completionscan = 'C', -- Completion scanning messages. + recording = 'q', -- No recording message. + fileinfo = 'F', -- No file info messages. + searchcount = 'S', -- No search stats: '[1/10]'. + undo = 'u', -- No undo messages. + }, + }, full_name = 'shortmess', list = 'flags', scope = { 'global' }, @@ -8159,7 +8407,9 @@ local options = { abbreviation = 'sloc', cb = 'did_set_showcmdloc', defaults = 'last', - values = { 'last', 'statusline', 'tabline' }, + schema = { + enum = { 'last', 'statusline', 'tabline' }, + }, desc = [=[ This option can be used to display the (partially) entered command in another location. Possible values are: @@ -8306,29 +8556,31 @@ local options = { abbreviation = 'scl', cb = 'did_set_signcolumn', defaults = 'auto', - values = { - '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', + schema = { + enum = { + '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', + }, }, desc = [=[ When and how to draw the signcolumn. Valid values are: @@ -8595,8 +8847,9 @@ local options = { abbreviation = 'spo', cb = 'did_set_spelloptions', defaults = '', - values = { 'camel', 'noplainbuffer' }, - flags = true, + schema = { + flags = { 'camel', 'noplainbuffer' }, + }, deny_duplicates = true, desc = [=[ A comma-separated list of options for spell checking: @@ -8622,7 +8875,9 @@ local options = { cb = 'did_set_spellsuggest', defaults = 'best', -- Keep this in sync with spell_check_sps(). - values = { 'best', 'fast', 'double', 'expr:', 'file:', 'timeout:' }, + schema = { + set = { 'best', 'fast', 'double', 'expr:', 'file:', 'timeout:' }, + }, deny_duplicates = true, desc = [=[ Methods used for spelling suggestions. Both for the |z=| command and @@ -8716,7 +8971,9 @@ local options = { abbreviation = 'spk', cb = 'did_set_splitkeep', defaults = 'cursor', - values = { 'cursor', 'screen', 'topline' }, + schema = { + enum = { 'cursor', 'screen', 'topline' }, + }, desc = [=[ The value of this option determines the scroll behavior when opening, closing or resizing horizontal splits. @@ -9154,8 +9411,9 @@ local options = { { abbreviation = 'swb', defaults = 'uselast', - values = { 'useopen', 'usetab', 'split', 'newtab', 'vsplit', 'uselast' }, - flags = true, + schema = { + flags = { 'useopen', 'usetab', 'split', 'newtab', 'vsplit', 'uselast' }, + }, deny_duplicates = true, desc = [=[ This option controls the behavior when switching between buffers. @@ -9252,8 +9510,9 @@ local options = { { abbreviation = 'tcl', defaults = '', - values = { 'left', 'uselast' }, - flags = true, + schema = { + flags = { 'left', 'uselast' }, + }, deny_duplicates = true, desc = [=[ This option controls the behavior when closing tabpages (e.g., using @@ -9403,8 +9662,9 @@ local options = { abbreviation = 'tc', cb = 'did_set_tagcase', defaults = 'followic', - values = { 'followic', 'ignore', 'match', 'followscs', 'smart' }, - flags = true, + schema = { + flags = { 'followic', 'ignore', 'match', 'followscs', 'smart' }, + }, desc = [=[ This option specifies how case is handled when searching the tags file: @@ -9567,8 +9827,9 @@ local options = { { abbreviation = 'tpf', defaults = 'BS,HT,ESC,DEL', - values = { 'BS', 'HT', 'FF', 'ESC', 'DEL', 'C0', 'C1' }, - flags = true, + schema = { + flags = { 'BS', 'HT', 'FF', 'ESC', 'DEL', 'C0', 'C1' }, + }, deny_duplicates = true, desc = [=[ A comma-separated list of options for specifying control characters @@ -10160,7 +10421,6 @@ local options = { abbreviation = 'vop', cb = 'did_set_str_generic', defaults = 'folds,cursor,curdir', - flags = true, deny_duplicates = true, desc = [=[ Changes the effect of the |:mkview| command. It is a comma-separated @@ -10189,14 +10449,15 @@ local options = { abbreviation = 've', cb = 'did_set_virtualedit', defaults = '', - values = { 'block', 'insert', 'all', 'onemore', 'none', 'NONE' }, - flags = { - Block = 5, - Insert = 6, - All = 4, - Onemore = 8, - None = 16, - NoneU = 32, + schema = { + flags = { + { 'block', 0x05 }, + { 'insert', 0x06 }, + { 'all', 0x04 }, + { 'onemore', 0x08 }, + { 'none', 0x10 }, + { 'NONE', 0x20, 'NoneU' }, -- alternative spelling of "none" (C token override) + }, }, deny_duplicates = true, desc = [=[ @@ -10447,8 +10708,9 @@ local options = { cb = 'did_set_wildmode', defaults = 'full', -- Keep this in sync with check_opt_wim(). - values = { 'full', 'longest', 'list', 'lastused', 'noselect', 'noinsert' }, - flags = true, + schema = { + flags = { 'full', 'longest', 'list', 'lastused', 'noselect', 'noinsert' }, + }, deny_duplicates = false, desc = [=[ Completion mode used for the character specified with 'wildchar'. @@ -10526,8 +10788,9 @@ local options = { { abbreviation = 'wop', defaults = 'pum,tagfile', - values = { 'fuzzy', 'tagfile', 'pum', 'exacttext' }, - flags = true, + schema = { + flags = { 'fuzzy', 'tagfile', 'pum', 'exacttext' }, + }, deny_duplicates = true, desc = [=[ A list of words that change how |cmdline-completion| is done. @@ -10579,7 +10842,9 @@ local options = { { abbreviation = 'wak', defaults = 'menu', - values = { 'yes', 'menu', 'no' }, + schema = { + enum = { 'yes', 'menu', 'no' }, + }, desc = [=[ only used in Win32 Some GUI versions allow the access to menu entries by using the ALT @@ -10652,7 +10917,9 @@ local options = { scope = { 'global' }, cb = 'did_set_winborder', defaults = { if_true = '' }, - values = { '', 'double', 'single', 'shadow', 'rounded', 'solid', 'bold', 'none' }, + schema = { + set = { '', 'double', 'single', 'shadow', 'rounded', 'solid', 'bold', 'none' }, + }, desc = [=[ Defines the default border style of floating windows. The default value is empty, which is equivalent to "none". Valid values include: @@ -11012,9 +11279,32 @@ local options = { }, } +--- Ordered completion values of a schema: `flags` and `enum` tokens as-is, `dict` keys as "key:" +--- (or bare for a flag key). Empty for `chars` (e.g. 'listchars') and `flagchars` (e.g. +--- 'formatoptions'), which self-expand. Shared with gen_options.lua. +--- @param schema vim.option_schema +--- @return string[] +local function schema_values(schema) + local values = {} --- @type string[] + for _, f in ipairs(schema.flags or {}) do + values[#values + 1] = type(f) == 'string' and f or f[1] + end + for _, e in ipairs(schema.enum or schema.set or {}) do + values[#values + 1] = e + end + for _, k in ipairs(schema.dict or {}) do + values[#values + 1] = type(k) == 'string' and k or (k[1] .. ':') -- bare flag / typed "key:" + end + -- flagchars and chars self-expand: no completion values. + return values +end +options.schema_values = schema_values + --- @param o vim.option_meta local function preprocess(o) - if o.values then + -- Options with a fixed set of string values get generic completion, and a generic did_set + -- (opt_strings_flags) unless they define their own cb. char/chars and char flags expand themselves. + if o.schema and #schema_values(o.schema) > 0 then o.cb = o.cb or 'did_set_str_generic' o.expand_cb = o.expand_cb or 'expand_set_str_generic' end diff --git a/src/nvim/optionstr.c b/src/nvim/optionstr.c index 241ac7751e..198f57a128 100644 --- a/src/nvim/optionstr.c +++ b/src/nvim/optionstr.c @@ -22,6 +22,7 @@ #include "nvim/eval/vars.h" #include "nvim/ex_getln.h" #include "nvim/fold.h" +#include "nvim/garray.h" #include "nvim/gettext_defs.h" #include "nvim/globals.h" #include "nvim/grid.h" @@ -57,6 +58,7 @@ #include "nvim/window.h" #include "nvim/winfloat.h" +#include "options_keysets.generated.h" #include "optionstr.c.generated.h" static const char e_illegal_character_after_chr[] @@ -78,33 +80,33 @@ static const char e_wrong_character_width_for_field_str[] /// All possible flags for 'shm'. /// the literal chars before 0 are removed flags. these are safely ignored -static char SHM_ALL[] = { SHM_RO, SHM_MOD, SHM_LINES, - SHM_WRI, SHM_ABBREVIATIONS, SHM_WRITE, SHM_TRUNC, SHM_TRUNCALL, - SHM_OVER, SHM_OVERALL, SHM_SEARCH, SHM_ATTENTION, SHM_INTRO, - SHM_COMPLETIONMENU, SHM_COMPLETIONSCAN, SHM_RECORDING, SHM_FILEINFO, - SHM_SEARCHCOUNT, SHM_UNDO, 'n', 'f', 'x', 'i', 0, }; +static char SHM_ALL[] = { kShmRo, kShmMod, kShmLines, + kShmWri, kShmAbbreviations, kShmWrite, kShmTrunc, kShmTruncall, + kShmOver, kShmOverall, kShmSearch, kShmAttention, kShmIntro, + kShmCompletionmenu, kShmCompletionscan, kShmRecording, kShmFileinfo, + kShmSearchcount, kShmUndo, 'n', 'f', 'x', 'i', 0, }; /// After setting various option values: recompute variables that depend on /// option values. void didset_string_options(void) { - check_str_opt(kOptCasemap, NULL); - check_str_opt(kOptBackupcopy, NULL); - check_str_opt(kOptBelloff, NULL); - check_str_opt(kOptCompleteopt, NULL); - check_str_opt(kOptSessionoptions, NULL); - check_str_opt(kOptViewoptions, NULL); - check_str_opt(kOptFoldopen, NULL); - check_str_opt(kOptDisplay, NULL); - check_str_opt(kOptJumpoptions, NULL); - check_str_opt(kOptRedrawdebug, NULL); - check_str_opt(kOptTagcase, NULL); - check_str_opt(kOptTermpastefilter, NULL); - check_str_opt(kOptVirtualedit, NULL); - check_str_opt(kOptSwitchbuf, NULL); - check_str_opt(kOptTabclose, NULL); - check_str_opt(kOptWildoptions, NULL); - check_str_opt(kOptClipboard, NULL); + check_str_opt(kOptCasemap, NULL, NULL, 0); + check_str_opt(kOptBackupcopy, NULL, NULL, 0); + check_str_opt(kOptBelloff, NULL, NULL, 0); + check_str_opt(kOptCompleteopt, NULL, NULL, 0); + check_str_opt(kOptSessionoptions, NULL, NULL, 0); + check_str_opt(kOptViewoptions, NULL, NULL, 0); + check_str_opt(kOptFoldopen, NULL, NULL, 0); + check_str_opt(kOptDisplay, NULL, NULL, 0); + check_str_opt(kOptJumpoptions, NULL, NULL, 0); + check_str_opt(kOptRedrawdebug, NULL, NULL, 0); + check_str_opt(kOptTagcase, NULL, NULL, 0); + check_str_opt(kOptTermpastefilter, NULL, NULL, 0); + check_str_opt(kOptVirtualedit, NULL, NULL, 0); + check_str_opt(kOptSwitchbuf, NULL, NULL, 0); + check_str_opt(kOptTabclose, NULL, NULL, 0); + check_str_opt(kOptWildoptions, NULL, NULL, 0); + check_str_opt(kOptClipboard, NULL, NULL, 0); } char *illegal_char(char *errbuf, size_t errbuflen, int c) @@ -241,7 +243,7 @@ int check_signcolumn(char *scl, win_T *wp) return FAIL; } - if (opt_strings_flags(val, opt_scl_values, NULL, false) == OK) { + if (opt_strings_flags(val, opt_scl_values, NULL, false, NULL, 0) == NULL) { if (wp == NULL) { return OK; } @@ -369,12 +371,10 @@ bool check_illegal_path_names(char *val, uint32_t flags) /// An option that accepts a list of flags is changed. /// e.g. 'viewoptions', 'switchbuf', 'casemap', etc. -static const char *did_set_opt_flags(char *val, const char **values, unsigned *flagp, bool list) +static const char *did_set_opt_flags(char *val, const char **values, unsigned *flagp, bool list, + char *errbuf, size_t errbuflen) { - if (opt_strings_flags(val, values, flagp, list) != OK) { - return e_invarg; - } - return NULL; + return opt_strings_flags(val, values, flagp, list, errbuf, errbuflen); } static const char **opt_values(OptIndex idx, size_t *values_len) @@ -390,7 +390,7 @@ static const char **opt_values(OptIndex idx, size_t *values_len) return opt->values; } -static int check_str_opt(OptIndex idx, char **varp) +static const char *check_str_opt(OptIndex idx, char **varp, char *errbuf, size_t errbuflen) { vimoption_T *opt = get_option(idx); if (varp == NULL) { @@ -398,7 +398,7 @@ static int check_str_opt(OptIndex idx, char **varp) } bool list = opt->flags & (kOptFlagComma | kOptFlagOneComma); const char **values = opt_values(idx, NULL); - return opt_strings_flags(*varp, values, opt->flags_var, list); + return opt_strings_flags(*varp, values, opt->flags_var, list, errbuf, errbuflen); } int expand_set_str_generic(optexpand_T *args, int *numMatches, char ***matches) @@ -410,7 +410,7 @@ int expand_set_str_generic(optexpand_T *args, int *numMatches, char ***matches) const char *did_set_str_generic(optset_T *args) { - return check_str_opt(args->os_idx, args->os_varp) != OK ? e_invarg : NULL; + return check_str_opt(args->os_idx, args->os_varp, args->os_errbuf, args->os_errbuflen); } /// An option which is a list of flags is set. Valid values are in "flags". @@ -559,8 +559,9 @@ const char *did_set_ambiwidth(optset_T *args) /// The 'emoji' option is changed. const char *did_set_emoji(optset_T *args) { - if (check_str_opt(kOptAmbiwidth, NULL) != OK) { - return e_invarg; + const char *errmsg = check_str_opt(kOptAmbiwidth, NULL, args->os_errbuf, args->os_errbuflen); + if (errmsg != NULL) { + return errmsg; } return check_chars_options(); } @@ -637,15 +638,17 @@ const char *did_set_backupcopy(optset_T *args) // make the local value empty: use the global value *flags = 0; } else { - if (opt_strings_flags(bkc, opt_bkc_values, flags, true) != OK) { - return e_invarg; + const char *errmsg = opt_strings_flags(bkc, opt_bkc_values, flags, true, args->os_errbuf, + args->os_errbuflen); + if (errmsg != NULL) { + return errmsg; } if (((*flags & kOptBkcFlagAuto) != 0) + ((*flags & kOptBkcFlagYes) != 0) + ((*flags & kOptBkcFlagNo) != 0) != 1) { // Must have exactly one of "auto", "yes" and "no". - opt_strings_flags(oldval, opt_bkc_values, flags, true); + opt_strings_flags(oldval, opt_bkc_values, flags, true, NULL, 0); return e_invarg; } } @@ -684,14 +687,12 @@ const char *did_set_breakat(optset_T *args FUNC_ATTR_UNUSED) const char *did_set_breakindentopt(optset_T *args) { win_T *win = (win_T *)args->os_win; - char **varp = (char **)args->os_varp; - - if (briopt_check(*varp, varp == &win->w_p_briopt ? win : NULL) == FAIL) { - return e_invarg; - } + // Apply it to the window, or to nothing if setting the global value. + bool is_local = args->os_varp == (void *)&win->w_p_briopt; + briopt_check(is_local ? win : NULL); // list setting requires a redraw - if (varp == &win->w_p_briopt && win->w_briopt_list) { + if (is_local && win->w_briopt_list) { redraw_all_later(UPD_NOT_VALID); } @@ -702,7 +703,8 @@ const char *did_set_breakindentopt(optset_T *args) const char *did_set_bufhidden(optset_T *args) { buf_T *buf = (buf_T *)args->os_buf; - return did_set_opt_flags(buf->b_p_bh, opt_bh_values, NULL, false); + return did_set_opt_flags(buf->b_p_bh, opt_bh_values, NULL, false, args->os_errbuf, + args->os_errbuflen); } /// The 'buftype' option is changed. @@ -711,9 +713,13 @@ const char *did_set_buftype(optset_T *args) buf_T *buf = (buf_T *)args->os_buf; win_T *win = (win_T *)args->os_win; // When 'buftype' is set, check for valid value. + const char *errmsg = opt_strings_flags(buf->b_p_bt, opt_bt_values, NULL, false, args->os_errbuf, + args->os_errbuflen); + if (errmsg != NULL) { + return errmsg; + } if ((buf->terminal && buf->b_p_bt[0] != 't') - || (!buf->terminal && buf->b_p_bt[0] == 't') - || opt_strings_flags(buf->b_p_bt, opt_bt_values, NULL, false) != OK) { + || (!buf->terminal && buf->b_p_bt[0] == 't')) { return e_invarg; } // buftype=prompt: @@ -997,11 +1003,7 @@ const char *did_set_completeopt(optset_T *args FUNC_ATTR_UNUSED) buf->b_cot_flags = 0; } - if (opt_strings_flags(cot, opt_cot_values, flags, true) != OK) { - return e_invarg; - } - - return NULL; + return opt_strings_flags(cot, opt_cot_values, flags, true, args->os_errbuf, args->os_errbuflen); } #ifdef BACKSLASH_IN_FILENAME @@ -1009,11 +1011,13 @@ const char *did_set_completeopt(optset_T *args FUNC_ATTR_UNUSED) const char *did_set_completeslash(optset_T *args) { buf_T *buf = (buf_T *)args->os_buf; - if (opt_strings_flags(p_csl, opt_csl_values, NULL, false) != OK - || opt_strings_flags(buf->b_p_csl, opt_csl_values, NULL, false) != OK) { - return e_invarg; + const char *errmsg = opt_strings_flags(p_csl, opt_csl_values, NULL, false, args->os_errbuf, + args->os_errbuflen); + if (errmsg != NULL) { + return errmsg; } - return NULL; + return opt_strings_flags(buf->b_p_csl, opt_csl_values, NULL, false, args->os_errbuf, + args->os_errbuflen); } #endif @@ -1556,64 +1560,13 @@ int expand_set_mouse(optexpand_T *args, int *numMatches, char ***matches) /// @return error message, NULL if it's OK. const char *did_set_mousescroll(optset_T *args FUNC_ATTR_UNUSED) { - OptInt vertical = -1; - OptInt horizontal = -1; - - char *string = p_mousescroll; - - while (true) { - char *end = vim_strchr(string, ','); - size_t length = end ? (size_t)(end - string) : strlen(string); - - // Both "ver:" and "hor:" are 4 bytes long. - // They should be followed by at least one digit. - if (length <= 4) { - return e_invarg; - } - - OptInt *direction; - - if (memcmp(string, "ver:", 4) == 0) { - direction = &vertical; - } else if (memcmp(string, "hor:", 4) == 0) { - direction = &horizontal; - } else { - return e_invarg; - } - - // If the direction has already been set, this is a duplicate. - if (*direction != -1) { - return e_invarg; - } - - // Verify that only digits follow the colon. - for (size_t i = 4; i < length; i++) { - if (!ascii_isdigit(string[i])) { - return N_("E5080: Digit expected"); - } - } - - string += 4; - *direction = getdigits_int(&string, false, -1); - - // Num options are generally kept within the signed int range. - // We know this number won't be negative because we've already checked for - // a minus sign. We'll allow 0 as a means of disabling mouse scrolling. - if (*direction == -1) { - return e_invarg; - } - - if (!end) { - break; - } - - string = end + 1; + // An empty value sets no direction; reject it (mousescroll always needs at least one). + OptKeyDict_mousescroll *v = p_mousescroll; + if (!HAS_KEY(v, mousescroll, hor) && !HAS_KEY(v, mousescroll, ver)) { + return e_invarg; } - - // If a direction wasn't set, fallback to the default value. - p_mousescroll_vert = (vertical == -1) ? MOUSESCROLL_VERT_DFLT : vertical; - p_mousescroll_hor = (horizontal == -1) ? MOUSESCROLL_HOR_DFLT : horizontal; - + p_mousescroll_hor = HAS_KEY(v, mousescroll, hor) ? (int)v->hor : MOUSESCROLL_HOR_DFLT; + p_mousescroll_vert = HAS_KEY(v, mousescroll, ver) ? (int)v->ver : MOUSESCROLL_VERT_DFLT; return NULL; } @@ -1663,7 +1616,7 @@ const char *did_set_sessionoptions(optset_T *args) if ((ssop_flags & kOptSsopFlagCurdir) && (ssop_flags & kOptSsopFlagSesdir)) { // Don't allow both "sesdir" and "curdir". const char *oldval = args->os_oldval.string.data; - opt_strings_flags(oldval, opt_ssop_values, &ssop_flags, true); + opt_strings_flags(oldval, opt_ssop_values, &ssop_flags, true, NULL, 0); return e_invarg; } return NULL; @@ -1845,13 +1798,19 @@ const char *did_set_spelloptions(optset_T *args) int opt_flags = args->os_flags; const char *val = args->os_newval.string.data; - if (!(opt_flags & OPT_LOCAL) - && opt_strings_flags(val, opt_spo_values, &spo_flags, true) != OK) { - return e_invarg; + if (!(opt_flags & OPT_LOCAL)) { + const char *errmsg = opt_strings_flags(val, opt_spo_values, &spo_flags, true, args->os_errbuf, + args->os_errbuflen); + if (errmsg != NULL) { + return errmsg; + } } - if (!(opt_flags & OPT_GLOBAL) - && opt_strings_flags(val, opt_spo_values, &win->w_s->b_p_spo_flags, true) != OK) { - return e_invarg; + if (!(opt_flags & OPT_GLOBAL)) { + const char *errmsg = opt_strings_flags(val, opt_spo_values, &win->w_s->b_p_spo_flags, true, + args->os_errbuf, args->os_errbuflen); + if (errmsg != NULL) { + return errmsg; + } } return NULL; } @@ -1970,8 +1929,12 @@ const char *did_set_tagcase(optset_T *args) if ((opt_flags & OPT_LOCAL) && *p == NUL) { // make the local value empty: use the global value *flags = 0; - } else if (opt_strings_flags(p, opt_tc_values, flags, false) != OK) { - return e_invarg; + } else { + const char *errmsg = opt_strings_flags(p, opt_tc_values, flags, false, args->os_errbuf, + args->os_errbuflen); + if (errmsg != NULL) { + return errmsg; + } } return NULL; } @@ -2089,8 +2052,10 @@ const char *did_set_virtualedit(optset_T *args) // make the local value empty: use the global value *flags = 0; } else { - if (opt_strings_flags(ve, opt_ve_values, flags, true) != OK) { - return e_invarg; + const char *errmsg = opt_strings_flags(ve, opt_ve_values, flags, true, args->os_errbuf, + args->os_errbuflen); + if (errmsg != NULL) { + return errmsg; } else if (strcmp(ve, args->os_oldval.string.data) != 0) { // Recompute cursor position in case the new 've' setting // changes something. @@ -2176,15 +2141,38 @@ int expand_set_winhighlight(optexpand_T *args, int *numMatches, char ***matches) return expand_set_opt_generic(args, get_highlight_name, numMatches, matches); } +/// Format an "invalid value" error naming the offending item (up to the next comma) and listing the +/// valid values. Returns `errbuf`, or the generic `e_invarg` when no buffer is available. +static const char *opt_invalid_value_err(const char *val, const char **values, char *errbuf, + size_t errbuflen) +{ + if (errbuf == NULL) { + return e_invarg; + } + size_t bad_len = 0; + while (val[bad_len] != NUL && val[bad_len] != ',') { + bad_len++; + } + char badbuf[64]; + xmemcpyz(badbuf, val, MIN(bad_len, sizeof(badbuf) - 1)); + int n = vim_snprintf(errbuf, errbuflen, _("E474: Invalid value '%s', expected one of:"), badbuf); + for (int j = 0; values[j] != NULL && n > 0 && (size_t)n < errbuflen; j++) { + n += vim_snprintf(errbuf + n, errbuflen - (size_t)n, "%s%s", j == 0 ? " " : ", ", values[j]); + } + return errbuf; +} + /// Handle an option that can be a range of string values. /// Set a flag in "*flagp" for each string present. /// -/// @param val new value -/// @param values array of valid string values -/// @param list when true: accept a list of values +/// @param val new value +/// @param values array of valid string values +/// @param list when true: accept a list of values +/// @param errbuf buffer for the error message (may be NULL, then a generic error is returned) /// -/// @return OK for correct value, FAIL otherwise. Empty is always OK. -static int opt_strings_flags(const char *val, const char **values, unsigned *flagp, bool list) +/// @return NULL for a correct value, otherwise an error message. Empty is always OK. +static const char *opt_strings_flags(const char *val, const char **values, unsigned *flagp, + bool list, char *errbuf, size_t errbuflen) { unsigned new_flags = 0; @@ -2194,7 +2182,7 @@ static int opt_strings_flags(const char *val, const char **values, unsigned *fla while (*val || iter_one) { for (unsigned i = 0;; i++) { if (values[i] == NULL) { // val not found in values[] - return FAIL; + return opt_invalid_value_err(val, values, errbuf, errbuflen); } size_t len = strlen(values[i]); @@ -2214,13 +2202,199 @@ static int opt_strings_flags(const char *val, const char **values, unsigned *fla *flagp = new_flags; } - return OK; + return NULL; +} + +/// Format a schema-validation error naming the offending key. Returns `errbuf`, or the generic +/// `e_invarg` when no buffer is available. +static const char *opt_schema_err(char *errbuf, size_t errbuflen, const char *fmt, const char *key, + size_t keylen) +{ + if (errbuf == NULL) { + return e_invarg; + } + char keybuf[64]; + xmemcpyz(keybuf, key, MIN(keylen, sizeof(keybuf) - 1)); + vim_snprintf(errbuf, errbuflen, _(fmt), keybuf); + return errbuf; +} + +/// Validates a dict option against its schema (options.lua). On failure, writes a msg to `errbuf`. +/// +/// @param val The option value. +/// @param schema NULL-terminated schema array (opt__schema). +/// @param errbuf Error message (may be NULL, then a generic error is returned). +/// +/// @return NULL when valid, otherwise an (untranslated) error message. +const char *opt_strings_check(const char *val, const OptSchemaItem *schema, char *errbuf, + size_t errbuflen) + FUNC_ATTR_NONNULL_ARG(1, 2) +{ + const char *key, *v; + size_t keylen, vlen; + for (const char *p = val; option_next_keyval(&p, &key, &keylen, &v, &vlen);) { + if (keylen == 0 && v == NULL) { + continue; // tolerate empty parts, e.g. "a,,b" + } + const OptSchemaItem *it = schema; + while (it->name != NULL && !option_slice_eq(key, keylen, it->name)) { + it++; + } + if (it->name == NULL) { + return opt_schema_err(errbuf, errbuflen, N_("E474: Unknown item '%s'"), key, keylen); + } + switch (it->kind) { + case kOptSchemaFlag: + if (v != NULL) { + return opt_schema_err(errbuf, errbuflen, N_("E474: '%s' does not take a value"), key, + keylen); + } + break; + case kOptSchemaNum: + case kOptSchemaSNum: { + // snum allows a leading '-'; both require at least one digit and nothing else. + size_t i = (v != NULL && it->kind == kOptSchemaSNum && *v == '-') ? 1 : 0; + bool ok = v != NULL && vlen > i; + for (; ok && i < vlen; i++) { + ok = ascii_isdigit((uint8_t)v[i]); + } + if (!ok) { + return opt_schema_err(errbuf, errbuflen, N_("E474: '%s' requires a number"), key, keylen); + } + // The value must fit the `int` it is parsed into (opt_fill() uses getdigits_int()). + char *end = (char *)v; + intmax_t n; + if (!try_getdigits(&end, &n) || n < INT_MIN || n > INT_MAX) { + return opt_schema_err(errbuf, errbuflen, N_("E474: '%s' number is out of range"), key, + keylen); + } + break; + } + case kOptSchemaEnum: { + const char **ev = it->enum_values; + while (v != NULL && *ev != NULL && !option_slice_eq(v, vlen, *ev)) { + ev++; + } + if (v != NULL && *ev != NULL) { + break; // matched an enum value + } + if (errbuf == NULL) { + return e_invarg; + } + char keybuf[64]; + xmemcpyz(keybuf, key, MIN(keylen, sizeof(keybuf) - 1)); + int n = vim_snprintf(errbuf, errbuflen, _("E474: '%s' must be one of:"), keybuf); + for (ev = it->enum_values; *ev != NULL && n > 0 && (size_t)n < errbuflen; ev++) { + n += vim_snprintf(errbuf + n, errbuflen - (size_t)n, "%s%s", + ev == it->enum_values ? " " : ", ", *ev); + } + return errbuf; + } + case kOptSchemaStr: + if (v == NULL) { + return opt_schema_err(errbuf, errbuflen, N_("E474: '%s' requires a value"), key, keylen); + } + break; + } + } + return NULL; +} + +/// Parses a validated dict option ":set" string into its keyset (`OptKeyDict_…`), see also +/// `api_dict_to_keydict()`. The keyset owns its `String` fields, so free it with `opt_dict_free()` +/// (via `optval_free()`/`clear_opt_dict()`), not `xfree()`. +void opt_fill(const char *value, FieldHashfn get_field, void *out) + FUNC_ATTR_NONNULL_ALL +{ + const char *key, *v; + size_t keylen, vlen; + for (const char *p = value; option_next_keyval(&p, &key, &keylen, &v, &vlen);) { + KeySetLink *f = get_field(key, keylen); + if (f == NULL) { + continue; // unknown key can't occur after validation + } + void *field = (char *)out + f->ptr_off; + switch (f->type) { + case kObjectTypeBoolean: + *(Boolean *)field = true; // a flag: present means true + break; + case kObjectTypeInteger: { + char *end = (char *)v; + *(Integer *)field = getdigits_int(&end, false, 0); + break; + } + case kObjectTypeString: + *(String *)field = (String){ .data = xmemdupz(v, vlen), .size = vlen }; // owned; keyset is stored + break; + default: + break; + } + ((OptKeySet *)out)->is_set_ |= (1ULL << (unsigned)f->opt_index); + } +} + +/// Serializes a keyset to ":set" string form (inverse of `opt_fill()`), for storage and `:set opt?`. +/// Emits set keys only: "key" for a true flag, "key:value" for a set num/enum. `table` is the +/// keyset's `KeySetLink` table. +/// +/// XXX: Keys are emitted in a canonical (alphanum) order, so the string is deterministic regardless +/// of the order they were set in. The reified value is an unordered map; unlike Vim, the serialized +/// order is not insertion-order. +/// +/// @return an owned string (caller frees). +char *opt_serialize(const void *keyset, const KeySetLink *table) + FUNC_ATTR_NONNULL_ALL +{ + // Collect the set keys, then sort by name. `set[]` is bounded by the number of sub-options. + const KeySetLink *set[64]; + int n = 0; + for (const KeySetLink *f = table; f->str != NULL; f++) { + if (!(((const OptKeySet *)keyset)->is_set_ & (1ULL << (unsigned)f->opt_index))) { + continue; + } + if (f->type == kObjectTypeBoolean && !*(const Boolean *)((const char *)keyset + f->ptr_off)) { + continue; // a false flag is simply absent + } + assert(n < (int)ARRAY_SIZE(set)); + set[n++] = f; + } + for (int i = 1; i < n; i++) { // insertion sort (n is small) + const KeySetLink *cur = set[i]; + int j = i - 1; + while (j >= 0 && strcmp(set[j]->str, cur->str) > 0) { + set[j + 1] = set[j]; + j--; + } + set[j + 1] = cur; + } + + garray_T ga; + ga_init(&ga, 1, 64); + for (int i = 0; i < n; i++) { + const KeySetLink *f = set[i]; + const void *field = (const char *)keyset + f->ptr_off; + if (ga.ga_len > 0) { + ga_append(&ga, ','); + } + ga_concat(&ga, f->str); + if (f->type == kObjectTypeInteger) { + char buf[32]; + snprintf(buf, sizeof(buf), ":%" PRId64, *(const Integer *)field); + ga_concat(&ga, buf); + } else if (f->type == kObjectTypeString) { + const String *s = field; + ga_append(&ga, ':'); + ga_concat_len(&ga, s->data, s->size); + } + } + ga_append(&ga, NUL); + return ga.ga_data; } /// @return OK if "p" is a valid fileformat name, FAIL otherwise. int check_ff_value(char *p) { - return opt_strings_flags(p, opt_ff_values, NULL, false); + return opt_strings_flags(p, opt_ff_values, NULL, false, NULL, 0) == NULL ? OK : FAIL; } static const char e_conflicts_with_value_of_listchars[] @@ -2268,45 +2442,12 @@ struct chars_tab { { (cp), STATIC_CSTR_STRING_INIT(name), def, fallback } static fcs_chars_T fcs_chars; -static const struct chars_tab fcs_tab[] = { - CHARSTAB_ENTRY(&fcs_chars.stl, "stl", " ", NULL), - CHARSTAB_ENTRY(&fcs_chars.stlnc, "stlnc", " ", NULL), - CHARSTAB_ENTRY(&fcs_chars.wbr, "wbr", " ", NULL), - CHARSTAB_ENTRY(&fcs_chars.horiz, "horiz", "─", "-"), - CHARSTAB_ENTRY(&fcs_chars.horizup, "horizup", "┴", "-"), - CHARSTAB_ENTRY(&fcs_chars.horizdown, "horizdown", "┬", "-"), - CHARSTAB_ENTRY(&fcs_chars.vert, "vert", "│", "|"), - CHARSTAB_ENTRY(&fcs_chars.vertleft, "vertleft", "┤", "|"), - CHARSTAB_ENTRY(&fcs_chars.vertright, "vertright", "├", "|"), - CHARSTAB_ENTRY(&fcs_chars.verthoriz, "verthoriz", "┼", "+"), - CHARSTAB_ENTRY(&fcs_chars.fold, "fold", "·", "-"), - CHARSTAB_ENTRY(&fcs_chars.foldopen, "foldopen", "-", NULL), - CHARSTAB_ENTRY(&fcs_chars.foldclosed, "foldclose", "+", NULL), - CHARSTAB_ENTRY(&fcs_chars.foldsep, "foldsep", "│", "|"), - CHARSTAB_ENTRY(&fcs_chars.foldinner, "foldinner", NULL, NULL), - CHARSTAB_ENTRY(&fcs_chars.diff, "diff", "-", NULL), - CHARSTAB_ENTRY(&fcs_chars.msgsep, "msgsep", " ", NULL), - CHARSTAB_ENTRY(&fcs_chars.eob, "eob", "~", NULL), - CHARSTAB_ENTRY(&fcs_chars.lastline, "lastline", "@", NULL), - CHARSTAB_ENTRY(&fcs_chars.trunc, "trunc", ">", NULL), - CHARSTAB_ENTRY(&fcs_chars.truncrl, "truncrl", "<", NULL), -}; - static lcs_chars_T lcs_chars; -static const struct chars_tab lcs_tab[] = { - CHARSTAB_ENTRY(&lcs_chars.eol, "eol", NULL, NULL), - CHARSTAB_ENTRY(&lcs_chars.ext, "extends", NULL, NULL), - CHARSTAB_ENTRY(&lcs_chars.nbsp, "nbsp", NULL, NULL), - CHARSTAB_ENTRY(&lcs_chars.prec, "precedes", NULL, NULL), - CHARSTAB_ENTRY(&lcs_chars.space, "space", NULL, NULL), - CHARSTAB_ENTRY(&lcs_chars.tab2, "tab", NULL, NULL), - CHARSTAB_ENTRY(&lcs_chars.leadtab2, "leadtab", NULL, NULL), - CHARSTAB_ENTRY(&lcs_chars.lead, "lead", NULL, NULL), - CHARSTAB_ENTRY(&lcs_chars.trail, "trail", NULL, NULL), - CHARSTAB_ENTRY(&lcs_chars.conceal, "conceal", NULL, NULL), - CHARSTAB_ENTRY(NULL, "multispace", NULL, NULL), - CHARSTAB_ENTRY(NULL, "leadmultispace", NULL, NULL), -}; + +// Generated from options.lua: +// - 'fillchars' => fcs_tab[] +// - 'listchars' => lcs_tab[] +#include "options_chartab.generated.h" #undef CHARSTAB_ENTRY diff --git a/src/nvim/quickfix.c b/src/nvim/quickfix.c index 8d843b87bf..7f2b464b73 100644 --- a/src/nvim/quickfix.c +++ b/src/nvim/quickfix.c @@ -3129,7 +3129,7 @@ static void qf_jump_print_msg(qf_info_T *qi, int qf_index, qfline_T *qf_ptr, buf if (curbuf == old_curbuf && curwin->w_cursor.lnum == old_lnum) { msg_scroll = true; } else if ((msg_scrolled == 0 || (p_ch == 0 && msg_scrolled == 1)) - && shortmess(SHM_OVERALL)) { + && shortmess(kShmOverall)) { msg_scroll = false; } msg_ext_set_kind("quickfix"); diff --git a/src/nvim/regexp.c b/src/nvim/regexp.c index 4abde83bba..4331670add 100644 --- a/src/nvim/regexp.c +++ b/src/nvim/regexp.c @@ -707,7 +707,7 @@ static int reg_cpo_lit; // 'cpoptions' contains 'l' flag static void get_cpo_flags(void) { - reg_cpo_lit = vim_strchr(p_cpo, CPO_LITERAL) != NULL; + reg_cpo_lit = vim_strchr(p_cpo, kCpoLiteral) != NULL; } /// Skip over a "[]" range. diff --git a/src/nvim/register.c b/src/nvim/register.c index 172d0b0d8f..fb5fe5de49 100644 --- a/src/nvim/register.c +++ b/src/nvim/register.c @@ -1094,7 +1094,7 @@ void op_yank_reg(oparg_T *oap, bool message, yankreg_T *reg, bool append) // Concatenate the last line of the old block with the first line of // the new block, unless being Vi compatible. if (curr->y_type == kMTCharWise - && vim_strchr(p_cpo, CPO_REGAPPEND) == NULL) { + && vim_strchr(p_cpo, kCpoRegappend) == NULL) { char *pnew = xmalloc(curr->y_array[curr->y_size - 1].size + reg->y_array[0].size + 1); j--; diff --git a/src/nvim/runtime.c b/src/nvim/runtime.c index 28e79a922f..db216043f1 100644 --- a/src/nvim/runtime.c +++ b/src/nvim/runtime.c @@ -2774,7 +2774,7 @@ char *getsourceline(int c, void *cookie, int indent, bool do_concat) // Only concatenate lines starting with a \ when 'cpoptions' doesn't // contain the 'C' flag. - if (line != NULL && do_concat && (vim_strchr(p_cpo, CPO_CONCAT) == NULL)) { + if (line != NULL && do_concat && (vim_strchr(p_cpo, kCpoConcat) == NULL)) { char *p; // compensate for the one line read-ahead sp->sourcing_lnum--; diff --git a/src/nvim/search.c b/src/nvim/search.c index b51a119c4a..132a34da5d 100644 --- a/src/nvim/search.c +++ b/src/nvim/search.c @@ -610,7 +610,7 @@ int searchit(win_T *win, buf_T *buf, pos_T *pos, pos_T *end_pos, Direction dir, return FAIL; } - const bool search_from_match_end = vim_strchr(p_cpo, CPO_SEARCH) != NULL; + const bool search_from_match_end = vim_strchr(p_cpo, kCpoSearch) != NULL; // find the string do { // loop for count @@ -951,8 +951,8 @@ int searchit(win_T *win, buf_T *buf, pos_T *pos, pos_T *end_pos, Direction dir, lnum = dir == BACKWARD // start second loop at the other end ? buf->b_ml.ml_line_count : 1; - if (!shortmess(SHM_SEARCH) - && shortmess(SHM_SEARCHCOUNT) + if (!shortmess(kShmSearch) + && shortmess(kShmSearchcount) && (options & SEARCH_MSG)) { give_warning(_(dir == BACKWARD ? top_bot_msg : bot_top_msg), true, false); } @@ -1151,7 +1151,7 @@ int do_search(oparg_T *oap, int dirc, int search_delim, char *pat, size_t patlen Search.cmdlen = 0; // A line offset is not remembered, this is vi compatible. - if (spats[0].off.line && vim_strchr(p_cpo, CPO_LINEOFF) != NULL) { + if (spats[0].off.line && vim_strchr(p_cpo, kCpoLineoff) != NULL) { spats[0].off.line = false; spats[0].off.off = 0; } @@ -1226,7 +1226,7 @@ int do_search(oparg_T *oap, int dirc, int search_delim, char *pat, size_t patlen bool show_search_stats = false; if ((options & SEARCH_ECHO) && messaging() && !msg_silent - && (!cmd_silent || !shortmess(SHM_SEARCHCOUNT))) { + && (!cmd_silent || !shortmess(kShmSearchcount))) { char off_buf[40]; size_t off_len = 0; @@ -1260,7 +1260,7 @@ int do_search(oparg_T *oap, int dirc, int search_delim, char *pat, size_t patlen } size_t msgbufsize; - if (!shortmess(SHM_SEARCHCOUNT) || cmd_silent) { + if (!shortmess(kShmSearchcount) || cmd_silent) { // Reserve enough space for the search pattern + offset + // search stat. Use all the space available, so that the // search state is right aligned. If there is not enough space @@ -1343,7 +1343,7 @@ int do_search(oparg_T *oap, int dirc, int search_delim, char *pat, size_t patlen msg_nowait = true; // don't wait for this message } - if (!shortmess(SHM_SEARCHCOUNT)) { + if (!shortmess(kShmSearchcount)) { show_search_stats = true; } } @@ -1390,7 +1390,7 @@ int do_search(oparg_T *oap, int dirc, int search_delim, char *pat, size_t patlen *dircp = (char)search_delim; // restore second '/' or '?' for normal_cmd() } - if (!shortmess(SHM_SEARCH) && sia && sia->sa_wrapped) { + if (!shortmess(kShmSearch) && sia && sia->sa_wrapped) { show_top_bot_msg = true; } @@ -1514,7 +1514,7 @@ int search_for_exact_line(buf_T *buf, pos_T *pos, Direction dir, char *pat) if (pos->lnum < 1) { if (p_ws) { pos->lnum = buf->b_ml.ml_line_count; - if (!shortmess(SHM_SEARCH)) { + if (!shortmess(kShmSearch)) { give_warning(_(top_bot_msg), true, false); } } else { @@ -1524,7 +1524,7 @@ int search_for_exact_line(buf_T *buf, pos_T *pos, Direction dir, char *pat) } else if (pos->lnum > buf->b_ml.ml_line_count) { if (p_ws) { pos->lnum = 1; - if (!shortmess(SHM_SEARCH)) { + if (!shortmess(kShmSearch)) { give_warning(_(bot_top_msg), true, false); } } else { @@ -1600,7 +1600,7 @@ int searchc(cmdarg_T *cap, bool t_cmd) // Force a move of at least one char, so ";" and "," will move the // cursor, even if the cursor is right in front of char we are looking // at. - if (vim_strchr(p_cpo, CPO_SCOLON) == NULL && count == 1 && t_cmd) { + if (vim_strchr(p_cpo, kCpoScolon) == NULL && count == 1 && t_cmd) { stop = false; } } @@ -1798,9 +1798,9 @@ pos_T *findmatchlimit(oparg_T *oap, int initc, int flags, int64_t maxtravel) char *linep = ml_get(pos.lnum); // pointer to current line // vi compatible matching - bool cpo_match = (vim_strchr(p_cpo, CPO_MATCH) != NULL); + bool cpo_match = (vim_strchr(p_cpo, kCpoMatch) != NULL); // don't recognize backslashes - bool cpo_bsl = (vim_strchr(p_cpo, CPO_MATCHBSL) != NULL); + bool cpo_bsl = (vim_strchr(p_cpo, kCpoMatchbsl) != NULL); // Direction to search when initc is '/', '*' or '#' if (flags & FM_BACKWARD) { @@ -2425,7 +2425,7 @@ void showmatch(int c) // brief pause, unless 'm' is present in 'cpo' and a character is // available. - if (vim_strchr(p_cpo, CPO_SHOWMATCH) != NULL) { + if (vim_strchr(p_cpo, kCpoShowmatch) != NULL) { os_delay((uint64_t)p_mat * 100 + 8, true); } else if (!char_avail()) { os_delay((uint64_t)p_mat * 100 + 9, false); @@ -2831,7 +2831,7 @@ void f_searchcount(typval_T *argvars, typval_T *rettv, EvalFuncData fptr) tv_dict_alloc_ret(rettv); - if (shortmess(SHM_SEARCHCOUNT)) { // 'shortmess' contains 'S' flag + if (shortmess(kShmSearchcount)) { // 'shortmess' contains 'S' flag recompute = true; } @@ -3187,7 +3187,7 @@ void find_pattern_in_path(char *ptr, Direction dir, size_t len, bool whole, bool files[depth].name = curr_fname = new_fname; files[depth].lnum = 0; files[depth].matched = false; - if (action == ACTION_EXPAND && !shortmess(SHM_COMPLETIONSCAN) && !silent) { + if (action == ACTION_EXPAND && !shortmess(kShmCompletionscan) && !silent) { msg_hist_off = true; // reset in msg_trunc() vim_snprintf(IObuff, IOSIZE, _("Scanning included file: %s"), diff --git a/src/nvim/spell.c b/src/nvim/spell.c index 06ac6fa845..47bff07aae 100644 --- a/src/nvim/spell.c +++ b/src/nvim/spell.c @@ -1480,7 +1480,7 @@ size_t spell_move_to(win_T *wp, int dir, smt_T behaviour, bool curline, hlf_T *a // starting line again and accept the last match. lnum = wp->w_buffer->b_ml.ml_line_count; wrapped = true; - if (!shortmess(SHM_SEARCH)) { + if (!shortmess(kShmSearch)) { give_warning(_(top_bot_msg), true, false); } } @@ -1495,7 +1495,7 @@ size_t spell_move_to(win_T *wp, int dir, smt_T behaviour, bool curline, hlf_T *a // starting line again and accept the first match. lnum = 1; wrapped = true; - if (!shortmess(SHM_SEARCH)) { + if (!shortmess(kShmSearch)) { give_warning(_(bot_top_msg), true, false); } } diff --git a/src/nvim/spellsuggest.c b/src/nvim/spellsuggest.c index e0a82e19f0..47ded7a86e 100644 --- a/src/nvim/spellsuggest.c +++ b/src/nvim/spellsuggest.c @@ -386,33 +386,44 @@ static int sps_limit = 9999; ///< max nr of suggestions given /// Sets "sps_flags" and "sps_limit". int spell_check_sps(void) { - char buf[MAXPATHL]; - sps_flags = 0; sps_limit = 9999; - for (char *p = p_sps; *p != NUL;) { - copy_option_part(&p, buf, MAXPATHL, ","); - + const char *key, *val; + size_t keylen, vallen; + for (const char *p = p_sps; option_next_keyval(&p, &key, &keylen, &val, &vallen);) { int f = 0; - if (ascii_isdigit(*buf)) { - char *s = buf; - sps_limit = getdigits_int(&s, true, 0); - if (*s != NUL && !ascii_isdigit(*s)) { + if (val == NULL) { + // A bare number is the suggestion limit. + if (keylen > 0 && ascii_isdigit((uint8_t)(*key))) { + char *s = (char *)key; + sps_limit = getdigits_int(&s, true, 0); + if (s != key + keylen) { // trailing non-digits + f = -1; + } + } else if (option_slice_eq(key, keylen, "best")) { + f = SPS_BEST; + } else if (option_slice_eq(key, keylen, "fast")) { + f = SPS_FAST; + } else if (option_slice_eq(key, keylen, "double")) { + f = SPS_DOUBLE; + } else { f = -1; } - // Note: Keep this in sync with opt_sps_values. - } else if (strcmp(buf, "best") == 0) { - f = SPS_BEST; - } else if (strcmp(buf, "fast") == 0) { - f = SPS_FAST; - } else if (strcmp(buf, "double") == 0) { - f = SPS_DOUBLE; - } else if (strncmp(buf, "expr:", 5) != 0 - && strncmp(buf, "file:", 5) != 0 - && (strncmp(buf, "timeout:", 8) != 0 - || (!ascii_isdigit(buf[8]) - && !(buf[8] == '-' && ascii_isdigit(buf[9]))))) { + } else if (option_slice_eq(key, keylen, "expr") || option_slice_eq(key, keylen, "file")) { + // Value is an expression/filename, consumed later in spell_find_suggest(). + } else if (option_slice_eq(key, keylen, "timeout")) { + // Optional leading '-', then at least one digit. + const char *v = val; + size_t vl = vallen; + if (vl > 0 && *v == '-') { + v++; + vl--; + } + if (vl == 0 || !ascii_isdigit((uint8_t)(*v))) { + f = -1; + } + } else { f = -1; } diff --git a/src/nvim/tag.c b/src/nvim/tag.c index bb9f3aa552..8728c6fdc0 100644 --- a/src/nvim/tag.c +++ b/src/nvim/tag.c @@ -2770,7 +2770,7 @@ static int jumpto_tag(const char *lbuf_arg, int forceit, bool keep_help) // If 'cpoptions' contains 't', store the search pattern for the "n" // command. If 'cpoptions' does not contain 't', the search pattern // is not stored. - if (vim_strchr(p_cpo, CPO_TAGPAT) != NULL) { + if (vim_strchr(p_cpo, kCpoTagpat) != NULL) { search_options = 0; } else { search_options = SEARCH_KEEP; diff --git a/src/nvim/textformat.c b/src/nvim/textformat.c index 8bc9fcfb9c..7bfe5367ca 100644 --- a/src/nvim/textformat.c +++ b/src/nvim/textformat.c @@ -72,10 +72,10 @@ void internal_format(int textwidth, int second_indent, int flags, bool format_on int cc; char save_char = NUL; bool haveto_redraw = false; - const bool fo_ins_blank = has_format_option(FO_INS_BLANK); - const bool fo_multibyte = has_format_option(FO_MBYTE_BREAK); - const bool fo_rigor_tw = has_format_option(FO_RIGOROUS_TW); - const bool fo_white_par = has_format_option(FO_WHITE_PAR); + const bool fo_ins_blank = has_format_option(kFoInsBlank); + const bool fo_multibyte = has_format_option(kFoMbyteBreak); + const bool fo_rigor_tw = has_format_option(kFoRigorousTw); + const bool fo_white_par = has_format_option(kFoWhitePar); bool first_line = true; colnr_T leader_len; bool no_leader = false; @@ -114,7 +114,7 @@ void internal_format(int textwidth, int second_indent, int flags, bool format_on if (no_leader) { do_comments = false; } else if (!(flags & INSCHAR_FORMAT) - && has_format_option(FO_WRAP_COMS)) { + && has_format_option(kFoWrapComs)) { do_comments = true; } @@ -145,7 +145,7 @@ void internal_format(int textwidth, int second_indent, int flags, bool format_on } if (!(flags & INSCHAR_FORMAT) && leader_len == 0 - && !has_format_option(FO_WRAP)) { + && !has_format_option(kFoWrap)) { break; } if ((startcol = curwin->w_cursor.col) == 0) { @@ -162,7 +162,7 @@ void internal_format(int textwidth, int second_indent, int flags, bool format_on // Find position to break at. // Stop at first entered white when 'formatoptions' has 'v' - while ((!fo_ins_blank && !has_format_option(FO_INS_VI)) + while ((!fo_ins_blank && !has_format_option(kFoInsVi)) || (flags & INSCHAR_FORMAT) || curwin->w_cursor.lnum != Ins.start.lnum || curwin->w_cursor.col >= Ins.start.col) { @@ -193,7 +193,7 @@ void internal_format(int textwidth, int second_indent, int flags, bool format_on // Don't break after a period when 'formatoptions' has 'p' and // there are less than two spaces. - if (has_format_option(FO_PERIOD_ABBR) && cc == '.' && wcc < 2) { + if (has_format_option(kFoPeriodAbbr) && cc == '.' && wcc < 2) { continue; } @@ -202,7 +202,7 @@ void internal_format(int textwidth, int second_indent, int flags, bool format_on break; } - if (has_format_option(FO_ONE_LETTER)) { + if (has_format_option(kFoOneLetter)) { // do not break after one-letter words if (curwin->w_cursor.col == 0) { break; // one-letter word at begin @@ -396,7 +396,7 @@ void internal_format(int textwidth, int second_indent, int flags, bool format_on // flag will be set and open_line() will handle it (as seen // above). The code here (and in get_number_indent()) will // recognize comments if needed... - if (second_indent < 0 && has_format_option(FO_Q_NUMBER)) { + if (second_indent < 0 && has_format_option(kFoQNumber)) { second_indent = get_number_indent(curwin->w_cursor.lnum - 1); } if (second_indent >= 0) { @@ -583,7 +583,7 @@ static bool paragraph_start(linenr_T lnum) if (*p == NUL) { return true; // after empty line } - const bool do_comments = has_format_option(FO_Q_COMS); // format comments + const bool do_comments = has_format_option(kFoQComs); // format comments if (fmt_check_par(lnum - 1, &leader_len, &leader_flags, do_comments)) { return true; // after non-paragraph line } @@ -592,10 +592,10 @@ static bool paragraph_start(linenr_T lnum) return true; // "lnum" is not a paragraph line } - if (has_format_option(FO_WHITE_PAR) && !ends_in_white(lnum - 1)) { + if (has_format_option(kFoWhitePar) && !ends_in_white(lnum - 1)) { return true; // missing trailing space in previous line. } - if (has_format_option(FO_Q_NUMBER) && (get_number_indent(lnum) > 0)) { + if (has_format_option(kFoQNumber) && (get_number_indent(lnum) > 0)) { return true; // numbered item starts in "lnum". } if (!same_leader(lnum - 1, leader_len, leader_flags, @@ -615,7 +615,7 @@ static bool paragraph_start(linenr_T lnum) /// @param prev_line may start in previous line void auto_format(bool trailblank, bool prev_line) { - if (!has_format_option(FO_AUTO)) { + if (!has_format_option(kFoAuto)) { return; } @@ -635,7 +635,7 @@ void auto_format(bool trailblank, bool prev_line) dec_cursor(); int cc = gchar_cursor(); if (!WHITECHAR(cc) && curwin->w_cursor.col > 0 - && has_format_option(FO_ONE_LETTER)) { + && has_format_option(kFoOneLetter)) { dec_cursor(); } cc = gchar_cursor(); @@ -664,7 +664,7 @@ void auto_format(bool trailblank, bool prev_line) // With the 'c' flag in 'formatoptions' and 't' missing: only format // comments. - if (has_format_option(FO_WRAP_COMS) && !has_format_option(FO_WRAP) + if (has_format_option(kFoWrapComs) && !has_format_option(kFoWrap) && get_leader_len(old, NULL, false, true) == 0) { return; } @@ -698,7 +698,7 @@ void auto_format(bool trailblank, bool prev_line) // previously wasn't, the line was broken. Because of the rule above we // need to add a space when 'w' is in 'formatoptions' to keep a paragraph // formatted. - if (!wasatend && has_format_option(FO_WHITE_PAR)) { + if (!wasatend && has_format_option(kFoWhitePar)) { char *linep = get_cursor_line_ptr(); colnr_T len = get_cursor_line_len(); if (curwin->w_cursor.col == len) { @@ -927,11 +927,11 @@ void format_lines(linenr_T line_count, bool avoid_fex) const int max_len = comp_textwidth(true) * 3; // check for 'q', '2', 'n' and 'w' in 'formatoptions' - const bool do_comments = has_format_option(FO_Q_COMS); // format comments + const bool do_comments = has_format_option(kFoQComs); // format comments int do_comments_list = 0; // format comments with 'n' or '2' - const bool do_second_indent = has_format_option(FO_Q_SECOND); - const bool do_number_indent = has_format_option(FO_Q_NUMBER); - const bool do_trail_white = has_format_option(FO_WHITE_PAR); + const bool do_second_indent = has_format_option(kFoQSecond); + const bool do_number_indent = has_format_option(kFoQNumber); + const bool do_trail_white = has_format_option(kFoWhitePar); // Get info about the previous and current line. if (curwin->w_cursor.lnum > 1) { @@ -1101,7 +1101,7 @@ void format_lines(linenr_T line_count, bool avoid_fex) if (next_leader_len > 0) { del_bytes(next_leader_len, false, false); mark_col_adjust(curwin->w_cursor.lnum, 0, 0, -next_leader_len, 0); - } else if (second_indent > 0) { // the "leader" for FO_Q_SECOND + } else if (second_indent > 0) { // the "leader" for kFoQSecond int indent = (int)getwhitecols_curline(); if (indent > 0) { diff --git a/src/nvim/textobject.c b/src/nvim/textobject.c index 9c17ab580d..da243a0d9d 100644 --- a/src/nvim/textobject.c +++ b/src/nvim/textobject.c @@ -98,7 +98,7 @@ int findsent(Direction dir, int count) // remember the line where the search started const int startlnum = pos.lnum; - const bool cpo_J = vim_strchr(p_cpo, CPO_ENDOFSENT) != NULL; + const bool cpo_J = vim_strchr(p_cpo, kCpoEndofsent) != NULL; while (true) { // find end of sentence c = gchar_pos(&pos); @@ -989,7 +989,7 @@ int current_block(oparg_T *oap, int count, bool include, int what, int other) // Ignore quotes here. Keep the "M" flag in 'cpo', as that is what the // user wants. char *save_cpo = p_cpo; - p_cpo = vim_strchr(p_cpo, CPO_MATCHBSL) != NULL ? "%M" : "%"; + p_cpo = vim_strchr(p_cpo, kCpoMatchbsl) != NULL ? "%M" : "%"; if ((pos = findmatch(NULL, what)) != NULL) { while (count-- > 0) { if ((pos = findmatch(NULL, what)) == NULL) { diff --git a/src/nvim/ui.c b/src/nvim/ui.c index 96e0c383e8..c68a104d81 100644 --- a/src/nvim/ui.c +++ b/src/nvim/ui.c @@ -617,15 +617,15 @@ void ui_check_mouse(void) return; } - int checkfor = MOUSE_NORMAL; // assume normal mode + int checkfor = kMouseNormal; // assume normal mode if (Visual.active) { - checkfor = MOUSE_VISUAL; + checkfor = kMouseVisual; } else if (State == MODE_HITRETURN || State == MODE_ASKMORE || State == MODE_SETWSIZE) { - checkfor = MOUSE_RETURN; + checkfor = kMouseReturn; } else if (State & MODE_INSERT) { - checkfor = MOUSE_INSERT; + checkfor = kMouseInsert; } else if (State & MODE_CMDLINE) { - checkfor = MOUSE_COMMAND; + checkfor = kMouseCommand; } else if (State == MODE_EXTERNCMD) { checkfor = ' '; // don't use mouse for ":!cmd" } @@ -652,8 +652,8 @@ bool ui_mouse_has(int mode) } break; - case MOUSE_HELP: - if (mode != MOUSE_RETURN && curbuf->b_help) { + case kMouseHelp: + if (mode != kMouseReturn && curbuf->b_help) { return true; } diff --git a/src/nvim/undo.c b/src/nvim/undo.c index e132033bc4..0ca53e3229 100644 --- a/src/nvim/undo.c +++ b/src/nvim/undo.c @@ -1790,7 +1790,7 @@ void u_undo(int count) count = 1; } - if (vim_strchr(p_cpo, CPO_UNDO) == NULL) { + if (vim_strchr(p_cpo, kCpoUndo) == NULL) { undo_undoes = true; } else { undo_undoes = !undo_undoes; @@ -1802,7 +1802,7 @@ void u_undo(int count) /// If 'cpoptions' does not contain 'u': Always redo. void u_redo(int count) { - if (vim_strchr(p_cpo, CPO_UNDO) == NULL) { + if (vim_strchr(p_cpo, kCpoUndo) == NULL) { undo_undoes = false; } @@ -1893,7 +1893,7 @@ static void u_doit(int startcount, bool quiet, bool do_buf_event) curbuf->b_u_curhead = curbuf->b_u_oldhead; beep_flush(); if (count == startcount - 1) { - if (!shortmess(SHM_UNDO)) { + if (!shortmess(kShmUndo)) { msg(_("Already at oldest change"), 0); } return; @@ -1906,7 +1906,7 @@ static void u_doit(int startcount, bool quiet, bool do_buf_event) if (curbuf->b_u_curhead == NULL || get_undolevel(curbuf) <= 0) { beep_flush(); // nothing to redo if (count == startcount - 1) { - if (!shortmess(SHM_UNDO)) { + if (!shortmess(kShmUndo)) { msg(_("Already at newest change"), 0); } return; @@ -2129,7 +2129,7 @@ void undo_time(int step, bool sec, bool file, bool absolute) } if (closest == closest_start) { - if (!shortmess(SHM_UNDO)) { + if (!shortmess(kShmUndo)) { if (step < 0) { msg(_("Already at oldest change"), 0); } else { @@ -2565,7 +2565,7 @@ static void u_undo_end(bool did_undo, bool absolute, bool quiet) if (quiet || global_busy // no messages until global is finished || !messaging() // 'lazyredraw' set, don't do messages now - || shortmess(SHM_UNDO)) { + || shortmess(kShmUndo)) { return; } diff --git a/src/nvim/version.c b/src/nvim/version.c index f3fb031c3f..cf7300f73a 100644 --- a/src/nvim/version.c +++ b/src/nvim/version.c @@ -4281,7 +4281,7 @@ bool may_show_intro(void) && (curbuf->handle == 1) && (curwin->handle == LOWEST_WIN_ID) && one_window(curwin, NULL) - && (vim_strchr(p_shm, SHM_INTRO) == NULL)); + && (vim_strchr(p_shm, kShmIntro) == NULL)); } /// Give an introductory message about Vim. diff --git a/test/functional/legacy/tagcase_spec.lua b/test/functional/legacy/tagcase_spec.lua index 08cfc9ce8a..e5a2a72a98 100644 --- a/test/functional/legacy/tagcase_spec.lua +++ b/test/functional/legacy/tagcase_spec.lua @@ -52,8 +52,12 @@ describe("'tagcase' option", function() -- does not. The first of these (setting the local value to ) should -- succeed; the other two should fail. n.command('setl tc=') - eq('Vim(setglobal):E474: Invalid argument: tc=', pcall_err(n.command, 'setg tc=')) - eq('Vim(set):E474: Invalid argument: tc=', pcall_err(n.command, 'set tc=')) + local one_of = 'expected one of: followic, ignore, match, followscs, smart' + eq( + "Vim(setglobal):E474: Invalid value '', " .. one_of .. ': tc=', + pcall_err(n.command, 'setg tc=') + ) + eq("Vim(set):E474: Invalid value '', " .. one_of .. ': tc=', pcall_err(n.command, 'set tc=')) end) it("should work with 'ignorecase' correctly in all combinations", function() diff --git a/test/functional/lua/option_and_var_spec.lua b/test/functional/lua/option_and_var_spec.lua index c3c60f6643..48dde21d56 100644 --- a/test/functional/lua/option_and_var_spec.lua +++ b/test/functional/lua/option_and_var_spec.lua @@ -897,6 +897,37 @@ describe('lua stdlib', function() end) end) + it("returns structured values for a dict option ('diffopt')", function() + -- Bare flags -> true; sub-values stay strings (option sub-value types are not exposed). + eq_exec_lua({ internal = true, filler = true, context = '4' }, function() + vim.opt.diffopt = 'internal,filler,context:4' + return vim.opt.diffopt:get() + end) + end) + + it('roundtrips a dict option through :set', function() + eq_exec_lua('context:4,filler,internal', function() + vim.opt.diffopt = 'internal,filler,context:4' + vim.opt.diffopt = vim.opt.diffopt:get() + return vim.go.diffopt + end) + end) + + -- Dict option accepts a table and :get() returns a map. #18875 + it("roundtrips a window-local dict option ('breakindentopt')", function() + eq_exec_lua({ sbr = true, shift = '3' }, function() + vim.opt.breakindentopt = { sbr = true, shift = 3 } + return vim.opt.breakindentopt:get() + end) + end) + + it("roundtrips a table for 'mousescroll'", function() + eq_exec_lua({ hor = '6', ver = '3' }, function() + vim.opt.mousescroll = { hor = 6, ver = 3 } + return vim.opt.mousescroll:get() + end) + end) + it('works for key-value pair options', function() eq_exec_lua({ tab = '> ', space = '_' }, function() vim.opt.listchars = 'tab:> ,space:_' @@ -1249,17 +1280,17 @@ describe('lua stdlib', function() end) end) - -- isfname=a,b,c,,,d,e,f + -- The raw string keeps the ",," literal-comma convention; the structured view splits on every + -- comma and does not reconstruct literal commas. it('can handle isfname ,,,', function() - eq_exec_lua({ { ',', 'a', 'b', 'c' }, 'a,b,,,c' }, function() + eq_exec_lua({ { 'a', 'b', 'c' }, 'a,b,,,c' }, function() vim.opt.isfname = 'a,b,,,c' return { vim.opt.isfname:get(), vim.go.isfname } end) end) - -- isfname=a,b,c,^,,def it('can handle isfname ,^,,', function() - eq_exec_lua({ { '^,', 'a', 'b', 'c' }, 'a,b,^,,c' }, function() + eq_exec_lua({ { 'a', 'b', '^', 'c' }, 'a,b,^,,c' }, function() vim.opt.isfname = 'a,b,^,,c' return { vim.opt.isfname:get(), vim.go.isfname } end) diff --git a/test/functional/options/mousescroll_spec.lua b/test/functional/options/mousescroll_spec.lua index a437764f7e..9e3f507fb7 100644 --- a/test/functional/options/mousescroll_spec.lua +++ b/test/functional/options/mousescroll_spec.lua @@ -22,11 +22,8 @@ local screencol = function() end describe("'mousescroll'", function() - local invalid_arg = 'Vim(set):E474: Invalid argument: mousescroll=' - local digit_expected = 'Vim(set):E5080: Digit expected: mousescroll=' - - local function should_fail(val, errorstr) - eq(errorstr .. val, pcall_err(command, 'set mousescroll=' .. val)) + local function should_fail(val, msg) + eq(msg, pcall_err(command, 'set mousescroll=' .. val)) end local function should_succeed(val) @@ -40,12 +37,19 @@ describe("'mousescroll'", function() end) it('handles invalid values', function() - should_fail('', invalid_arg) -- empty string - should_fail('foo:123', invalid_arg) -- unknown direction - should_fail('hor:1,hor:2', invalid_arg) -- duplicate direction - should_fail('ver:99999999999999999999', invalid_arg) -- integer overflow - should_fail('ver:bar', digit_expected) -- expected digit - should_fail('ver:-1', digit_expected) -- negative count + -- empty string (no direction set) + should_fail('', 'Vim(set):E474: Invalid argument: mousescroll=') + -- unknown direction + should_fail('foo:123', "Vim(set):E474: Unknown item 'foo': mousescroll=foo:123") + -- integer overflow + should_fail( + 'ver:99999999999999999999', + "Vim(set):E474: 'ver' number is out of range: mousescroll=ver:99999999999999999999" + ) + -- expected digit + should_fail('ver:bar', "Vim(set):E474: 'ver' requires a number: mousescroll=ver:bar") + -- negative count + should_fail('ver:-1', "Vim(set):E474: 'ver' requires a number: mousescroll=ver:-1") end) it('handles valid values', function() @@ -54,10 +58,11 @@ describe("'mousescroll'", function() should_succeed('ver:1') -- only vertical should_succeed('hor:0,ver:0') -- zero should_succeed('hor:2147483647') -- large count + should_succeed('hor:1,hor:2') -- duplicate direction: last wins end) it('default set correctly', function() - eq('ver:3,hor:6', eval('&mousescroll')) + eq('hor:6,ver:3', eval('&mousescroll')) eq(10, screenrow()) scroll('up') diff --git a/test/functional/legacy/options_spec.lua b/test/functional/options/options_spec.lua similarity index 52% rename from test/functional/legacy/options_spec.lua rename to test/functional/options/options_spec.lua index 15833a548d..647e05f503 100644 --- a/test/functional/legacy/options_spec.lua +++ b/test/functional/options/options_spec.lua @@ -8,6 +8,7 @@ local command, clear = n.command, n.clear local source, expect = n.source, n.expect local matches = t.matches local pcall_err = t.pcall_err +local eq = t.eq describe('options', function() setup(clear) @@ -17,7 +18,7 @@ describe('options', function() end) end) -describe('set', function() +describe('options :set', function() before_each(clear) it("should keep two comma when 'path' is changed", function() @@ -32,7 +33,7 @@ describe('set', function() foo,,bar]]) end) - it('winminheight works', function() + it("'winminheight'", function() local _ = Screen.new(20, 11) source([[ set wmh=0 stal=2 @@ -44,7 +45,7 @@ describe('set', function() matches('E36: Not enough room', pcall_err(command, 'set wmh=1')) end) - it('winminheight works with tabline', function() + it("'winminheight' with tabline", function() local _ = Screen.new(20, 11) source([[ set wmh=0 stal=2 @@ -57,7 +58,7 @@ describe('set', function() matches('E36: Not enough room', pcall_err(command, 'set wmh=1')) end) - it('scroll works', function() + it("'scroll'", function() local screen = Screen.new(42, 16) source([[ set scroll=2 @@ -75,7 +76,46 @@ describe('set', function() end) it('foldcolumn and signcolumn to empty string is disallowed', function() - matches('E474: Invalid argument: fdc=', pcall_err(command, 'set fdc=')) + matches("E474: Invalid value ''.*fdc=", pcall_err(command, 'set fdc=')) matches('E474: Invalid argument: scl=', pcall_err(command, 'set scl=')) end) end) + +describe('options validation', function() + before_each(clear) + + -- Improved error messages for structured "key:value" options ("schema" in options.lua). + it('reports specific errors for structured (schema) options', function() + eq("Vim(set):E474: Unknown item 'foo': diffopt=foo", pcall_err(command, 'set diffopt=foo')) + eq( + "Vim(set):E474: 'context' requires a number: diffopt=context:x", + pcall_err(command, 'set diffopt=context:x') + ) + eq( + 'Vim(set):E474: ' + .. "'algorithm' must be one of: myers, minimal, patience, histogram: diffopt=algorithm:bad", + pcall_err(command, 'set diffopt=algorithm:bad') + ) + eq( + "Vim(set):E474: 'filler' does not take a value: diffopt=filler:1", + pcall_err(command, 'set diffopt=filler:1') + ) + eq( + "Vim(set):E474: 'ver' number is out of range: mousescroll=ver:99999999999", + pcall_err(command, 'set mousescroll=ver:99999999999') + ) + end) + + -- Enum / flag-list options name the offending value and list the valid ones. + it('reports specific errors for enum and flag-list options', function() + eq( + "Vim(set):E474: Invalid value 'x', expected one of: single, double: ambiwidth=x", + pcall_err(command, 'set ambiwidth=x') + ) + eq( + 'Vim(set):E474: ' + .. "Invalid value 'x', expected one of: yes, auto, no, breaksymlink, breakhardlink: backupcopy=x", + pcall_err(command, 'set backupcopy=x') + ) + end) +end) diff --git a/test/old/testdir/test_options.vim b/test/old/testdir/test_options.vim index e937c18385..2607574e77 100644 --- a/test/old/testdir/test_options.vim +++ b/test/old/testdir/test_options.vim @@ -2232,7 +2232,7 @@ func Test_opt_winminheight() endfunc func Test_opt_winminheight_term() - " See test/functional/legacy/options_spec.lua + " See test/functional/options/options_spec.lua CheckRunVimInTerminal " The tabline should be taken into account. @@ -2253,7 +2253,7 @@ func Test_opt_winminheight_term() endfunc func Test_opt_winminheight_term_tabs() - " See test/functional/legacy/options_spec.lua + " See test/functional/options/options_spec.lua CheckRunVimInTerminal " The tabline should be taken into account. @@ -2295,7 +2295,7 @@ endfunc " Test that resetting laststatus does change scroll option func Test_opt_reset_scroll() - " See test/functional/legacy/options_spec.lua + " See test/functional/options/options_spec.lua CheckRunVimInTerminal let vimrc =<< trim [CODE] set scroll=2 @@ -2982,49 +2982,49 @@ func Test_comma_option_key_value() " += replaces existing item with same key set diffopt=internal,filler,algorithm:patience set diffopt+=algorithm:histogram - call assert_equal('internal,filler,algorithm:histogram', &diffopt) + call assert_equal('algorithm:histogram,filler,internal', &diffopt) " += with exact duplicate does nothing set diffopt=internal,filler,algorithm:patience set diffopt+=algorithm:patience - call assert_equal('internal,filler,algorithm:patience', &diffopt) + call assert_equal('algorithm:patience,filler,internal', &diffopt) " += with multiple items, each processed individually set diffopt=algorithm:patience,filler set diffopt+=algorithm:histogram,filler - call assert_equal('filler,algorithm:histogram', &diffopt) + call assert_equal('algorithm:histogram,filler', &diffopt) " += with non-colon item appends normally set diffopt=internal,filler set diffopt+=iwhite - call assert_equal('internal,filler,iwhite', &diffopt) + call assert_equal('filler,internal,iwhite', &diffopt) " += repeated updates set diffopt=internal,filler,algorithm:patience set diffopt+=algorithm:histogram set diffopt+=algorithm:minimal set diffopt+=algorithm:myers - call assert_equal('internal,filler,algorithm:myers', &diffopt) + call assert_equal('algorithm:myers,filler,internal', &diffopt) " += all exact duplicates does nothing set diffopt=internal,filler,algorithm:patience set diffopt+=algorithm:patience,filler - call assert_equal('internal,filler,algorithm:patience', &diffopt) + call assert_equal('algorithm:patience,filler,internal', &diffopt) " -= with "key:" removes item regardless of value set diffopt=internal,filler,algorithm:patience set diffopt-=algorithm: - call assert_equal('internal,filler', &diffopt) + call assert_equal('filler,internal', &diffopt) " -= with "key:value" also matches by key set diffopt=internal,filler,algorithm:patience set diffopt-=algorithm:histogram - call assert_equal('internal,filler', &diffopt) + call assert_equal('filler,internal', &diffopt) " -= without colon does not match "key:value" items set diffopt=internal,filler,algorithm:patience set diffopt-=algorithm - call assert_equal('internal,filler,algorithm:patience', &diffopt) + call assert_equal('algorithm:patience,filler,internal', &diffopt) " -= with multiple non-colon items (order independent) set diffopt=internal,filler,closeoff @@ -3039,22 +3039,22 @@ func Test_comma_option_key_value() " -= with multiple items: non-colon and colon mixed set diffopt& diffopt=internal,filler,closeoff,indent-heuristic,inline:char set diffopt-=indent-heuristic,inline:char - call assert_equal('internal,filler,closeoff', &diffopt) + call assert_equal('closeoff,filler,internal', &diffopt) " -= with multiple items: colon and non-colon mixed (reverse order) set diffopt& diffopt=internal,filler,closeoff,indent-heuristic,inline:char set diffopt-=inline:char,indent-heuristic - call assert_equal('internal,filler,closeoff', &diffopt) + call assert_equal('closeoff,filler,internal', &diffopt) " += with multiple non-colon items set diffopt=internal,filler set diffopt+=closeoff,iwhite - call assert_equal('internal,filler,closeoff,iwhite', &diffopt) + call assert_equal('closeoff,filler,internal,iwhite', &diffopt) " += with multiple non-colon items, some already exist set diffopt=internal,filler,closeoff set diffopt+=filler,iwhite - call assert_equal('internal,filler,closeoff,iwhite', &diffopt) + call assert_equal('closeoff,filler,internal,iwhite', &diffopt) " -= with multiple items including key match set diffopt=internal,filler,algorithm:patience @@ -3064,12 +3064,12 @@ func Test_comma_option_key_value() " -= key match when item is at the beginning set diffopt=algorithm:patience,internal,filler set diffopt-=algorithm: - call assert_equal('internal,filler', &diffopt) + call assert_equal('filler,internal', &diffopt) " -= key match when item is at the end set diffopt=internal,filler,algorithm:patience set diffopt-=algorithm: - call assert_equal('internal,filler', &diffopt) + call assert_equal('filler,internal', &diffopt) " -= key match when item is the only item set diffopt=algorithm:patience @@ -3079,17 +3079,17 @@ func Test_comma_option_key_value() " ^= prepends new item set diffopt=internal,filler set diffopt^=algorithm:histogram - call assert_equal('algorithm:histogram,internal,filler', &diffopt) + call assert_equal('algorithm:histogram,filler,internal', &diffopt) " ^= replaces item and prepends set diffopt=internal,filler,algorithm:patience set diffopt^=algorithm:histogram - call assert_equal('algorithm:histogram,internal,filler', &diffopt) + call assert_equal('algorithm:histogram,filler,internal', &diffopt) " ^= with exact duplicate does nothing set diffopt=internal,filler,algorithm:patience set diffopt^=algorithm:patience - call assert_equal('internal,filler,algorithm:patience', &diffopt) + call assert_equal('algorithm:patience,filler,internal', &diffopt) set diffopt&