diff options
-rw-r--r-- | runtime/doc/lua.txt | 81 | ||||
-rw-r--r-- | runtime/lua/vim/_meta.lua | 869 | ||||
-rw-r--r-- | src/nvim/option.c | 54 | ||||
-rw-r--r-- | src/nvim/testdir/test_options.vim | 14 | ||||
-rw-r--r-- | test/functional/lua/vim_spec.lua | 4 |
5 files changed, 431 insertions, 591 deletions
diff --git a/runtime/doc/lua.txt b/runtime/doc/lua.txt index a6a51c1e36..88547edfe5 100644 --- a/runtime/doc/lua.txt +++ b/runtime/doc/lua.txt @@ -1090,6 +1090,20 @@ Example: > vim.g.foo = nil -- Delete (:unlet) the Vimscript variable. vim.b[2].foo = 6 -- Set b:foo for buffer 2 < + +Note that setting dictionary fields directly will not write them back into +Nvim. This is because the index into the namespace simply returns a copy. +Instead the whole dictionary must be written as one. This can be achieved by +creating a short-lived temporary. + +Example: > + + vim.g.my_dict.field1 = 'value' -- Does not work + + local my_dict = vim.g.my_dict -- + my_dict.field1 = 'value' -- Instead do + vim.g.my_dict = my_dict -- + vim.g *vim.g* Global (|g:|) editor variables. Key with no value returns `nil`. @@ -1133,32 +1147,23 @@ Vim options can be accessed through |vim.o|, which behaves like Vimscript Examples: ~ To set a boolean toggle: - In Vimscript: - `set number` - - In Lua: - `vim.o.number = true` + Vimscript: `set number` + Lua: `vim.o.number = true` To set a string value: - In Vimscript: - `set wildignore=*.o,*.a,__pycache__` - - In Lua: - `vim.o.wildignore = '*.o,*.a,__pycache__'` - -Similarly, there exist |vim.bo| and |vim.wo| for setting buffer-local and -window-local options, respectively, similarly to |:setlocal|. There is also -|vim.go| that only sets the global value of a |global-local| option, see -|:setglobal|. The following table summarizes this relation. - - lua command global_value local_value ~ -vim.o :set set set -vim.bo/vim.wo :setlocal - set -vim.go :setglobal set - + Vimscript: `set wildignore=*.o,*.a,__pycache__` + Lua: `vim.o.wildignore = '*.o,*.a,__pycache__'` +Similarly, there is |vim.bo| and |vim.wo| for setting buffer-scoped and +window-scoped options. Note that this must NOT be confused with +|local-options| and |:setlocal|. There is also |vim.go| that only accesses the +global value of a |global-local| option, see |:setglobal|. vim.o *vim.o* - Get or set editor options, like |:set|. Invalid key is an error. + Get or set an |option|. Like `:set`. Invalid key is an error. + + Note: this works on both buffer-scoped and window-scoped options using the + current buffer and window. Example: > vim.o.cmdheight = 4 @@ -1166,14 +1171,12 @@ vim.o *vim.o* print(vim.o.foo) -- error: invalid key < vim.go *vim.go* - Get or set an |option|. Invalid key is an error. - - This is a wrapper around |nvim_set_option_value()| and - |nvim_get_option_value()|. + Get or set a global |option|. Like `:setglobal`. Invalid key is + an error. - NOTE: This is different from |vim.o| because this ONLY sets the global - option, which generally produces confusing behavior for options with - |global-local| values. + Note: this is different from |vim.o| because this accesses the global + option value and thus is mostly useful for use with |global-local| + options. Example: > vim.go.cmdheight = 4 @@ -1181,12 +1184,11 @@ vim.go *vim.go* print(vim.go.bar) -- error: invalid key < vim.bo[{bufnr}] *vim.bo* - Get or set buffer-scoped |local-options| for the buffer with number {bufnr}. - If [{bufnr}] is omitted, use the current buffer. Invalid {bufnr} or key is - an error. + Get or set buffer-scoped |options| for the buffer with number {bufnr}. + Like `:set` and `:setlocal`. If [{bufnr}] is omitted then the current + buffer is used. Invalid {bufnr} or key is an error. - This is a wrapper around |nvim_set_option_value()| and - |nvim_get_option_value()| with `opts = {scope = local, buf = bufnr}` . + Note: this is equivalent to both `:set` and `:setlocal`. Example: > local bufnr = vim.api.nvim_get_current_buf() @@ -1195,13 +1197,14 @@ vim.bo[{bufnr}] * print(vim.bo.baz) -- error: invalid key < vim.wo[{winid}] *vim.wo* - Get or set window-scoped |local-options| for the window with handle {winid}. - If [{winid}] is omitted, use the current window. Invalid {winid} or key - is an error. - - This is a wrapper around |nvim_set_option_value()| and - |nvim_get_option_value()| with `opts = {scope = local, win = winid}` . + Get or set window-scoped |options| for the window with handle {winid}. + Like `:set`. If [{winid}] is omitted then the current window is used. + Invalid {winid} or key is an error. + Note: this does not access |local-options| (`:setlocal`) instead use: > + nvim_get_option_value(OPTION, { scope = 'local', win = winid }) + nvim_set_option_value(OPTION, VALUE, { scope = 'local', win = winid } +< Example: > local winid = vim.api.nvim_get_current_win() vim.wo[winid].number = true -- same as vim.wo.number = true diff --git a/runtime/lua/vim/_meta.lua b/runtime/lua/vim/_meta.lua index 0f45c916dc..dada918d69 100644 --- a/runtime/lua/vim/_meta.lua +++ b/runtime/lua/vim/_meta.lua @@ -2,172 +2,115 @@ local vim = assert(vim) local a = vim.api -local validate = vim.validate - -local SET_TYPES = setmetatable({ - SET = 0, - LOCAL = 1, - GLOBAL = 2, -}, { __index = error }) - -local options_info = nil -local buf_options = nil -local glb_options = nil -local win_options = nil - -local function _setup() - if options_info ~= nil then - return - end - options_info = {} - for _, v in pairs(a.nvim_get_all_options_info()) do - options_info[v.name] = v - if v.shortname ~= '' then - options_info[v.shortname] = v - end - end - local function get_scoped_options(scope) - local result = {} - for name, option_info in pairs(options_info) do - if option_info.scope == scope then - result[name] = true +-- TODO(tjdevries): Improve option metadata so that this doesn't have to be hardcoded. +-- Can be done in a separate PR. +local key_value_options = { + fillchars = true, + fcs = true, + listchars = true, + lcs = true, + winhighlight = true, + winhl = true, +} + +--- 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 result + return 'string' end - - buf_options = get_scoped_options('buf') - glb_options = get_scoped_options('global') - win_options = get_scoped_options('win') + return info.type end -local function make_meta_accessor(get, set, del, validator) - validator = validator or function() - return true - end - - validate({ - get = { get, 'f' }, - set = { set, 'f' }, - del = { del, 'f', true }, - validator = { validator, 'f' }, - }) +local options_info = setmetatable({}, { + __index = function(t, k) + local info = a.nvim_get_option_info(k) + info.metatype = get_option_metatype(k, info) + rawset(t, k, info) + return rawget(t, k) + end, +}) - local mt = {} - function mt:__newindex(k, v) - if not validator(k) then - return +vim.env = setmetatable({}, { + __index = function(_, k) + local v = vim.fn.getenv(k) + if v == vim.NIL then + return nil end + return v + end, - if del and v == nil then - return del(k) - end - return set(k, v) - end - function mt:__index(k) - if not validator(k) then - return - end + __newindex = function(_, k, v) + vim.fn.setenv(k, v) + end, +}) - return get(k) +local function opt_validate(option_name, target_scope) + local scope = options_info[option_name].scope + if scope ~= target_scope then + local scope_to_string = { buf = 'buffer', win = 'window' } + error( + string.format( + [['%s' is a %s option, not a %s option. See ":help %s"]], + option_name, + scope_to_string[scope] or scope, + scope_to_string[target_scope] or target_scope, + option_name + ) + ) end - return setmetatable({}, mt) end -vim.env = make_meta_accessor(function(k) - local v = vim.fn.getenv(k) - if v == vim.NIL then - return nil - end - return v -end, vim.fn.setenv) - -do -- buffer option accessor - local function new_buf_opt_accessor(bufnr) - local function get(k) - if bufnr == nil and type(k) == 'number' then - return new_buf_opt_accessor(k) - end - - return a.nvim_get_option_value(k, { buf = bufnr or 0 }) - end - - local function set(k, v) - return a.nvim_set_option_value(k, v, { buf = bufnr or 0 }) - end - - return make_meta_accessor(get, set, nil, function(k) - if type(k) == 'string' then - _setup() - if win_options[k] then - error( - string.format([['%s' is a window option, not a buffer option. See ":help %s"]], k, k) - ) - elseif glb_options[k] then - error( - string.format([['%s' is a global option, not a buffer option. See ":help %s"]], k, k) - ) - end +local function new_opt_accessor(handle, scope) + return setmetatable({}, { + __index = function(_, k) + if handle == nil and type(k) == 'number' then + return new_opt_accessor(k, scope) end + opt_validate(k, scope) + return a.nvim_get_option_value(k, { [scope] = handle or 0 }) + end, - return true - end) - end - - vim.bo = new_buf_opt_accessor(nil) + __newindex = function(_, k, v) + opt_validate(k, scope) + return a.nvim_set_option_value(k, v, { [scope] = handle or 0 }) + end, + }) end -do -- window option accessor - local function new_win_opt_accessor(winnr) - local function get(k) - if winnr == nil and type(k) == 'number' then - return new_win_opt_accessor(k) - end - return a.nvim_get_option_value(k, { win = winnr or 0 }) - end - - local function set(k, v) - return a.nvim_set_option_value(k, v, { win = winnr or 0 }) - end - - return make_meta_accessor(get, set, nil, function(k) - if type(k) == 'string' then - _setup() - if buf_options[k] then - error( - string.format([['%s' is a buffer option, not a window option. See ":help %s"]], k, k) - ) - elseif glb_options[k] then - error( - string.format([['%s' is a global option, not a window option. See ":help %s"]], k, k) - ) - end - end - - return true - end) - end - - vim.wo = new_win_opt_accessor(nil) -end +vim.bo = new_opt_accessor(nil, 'buf') +vim.wo = new_opt_accessor(nil, 'win') -- vim global option -- this ONLY sets the global option. like `setglobal` -vim.go = make_meta_accessor(function(k) - return a.nvim_get_option_value(k, { scope = 'global' }) -end, function(k, v) - return a.nvim_set_option_value(k, v, { scope = 'global' }) -end) +vim.go = setmetatable({}, { + __index = function(_, k) + return a.nvim_get_option_value(k, { scope = 'global' }) + end, + __newindex = function(_, k, v) + return a.nvim_set_option_value(k, v, { scope = 'global' }) + end, +}) -- vim `set` style options. -- it has no additional metamethod magic. -vim.o = make_meta_accessor(function(k) - return a.nvim_get_option_value(k, {}) -end, function(k, v) - return a.nvim_set_option_value(k, v, {}) -end) +vim.o = setmetatable({}, { + __index = function(_, k) + return a.nvim_get_option_value(k, {}) + end, + __newindex = function(_, k, v) + return a.nvim_set_option_value(k, v, {}) + end, +}) ---@brief [[ --- vim.opt, vim.opt_local and vim.opt_global implementation @@ -178,11 +121,8 @@ end) ---@brief ]] --- Preserves the order and does not mutate the original list -local remove_duplicate_values = function(t) +local function remove_duplicate_values(t) local result, seen = {}, {} - if type(t) == 'function' then - error(debug.traceback('asdf')) - end for _, v in ipairs(t) do if not seen[v] then table.insert(result, v) @@ -194,64 +134,6 @@ local remove_duplicate_values = function(t) return result end --- TODO(tjdevries): Improve option metadata so that this doesn't have to be hardcoded. --- Can be done in a separate PR. -local key_value_options = { - fillchars = true, - fcs = true, - listchars = true, - lcs = true, - winhighlight = true, - winhl = true, -} - ----@class OptionTypes ---- Option Type Enum -local OptionTypes = setmetatable({ - BOOLEAN = 0, - NUMBER = 1, - STRING = 2, - ARRAY = 3, - MAP = 4, - SET = 5, -}, { - __index = function(_, k) - error('Not a valid OptionType: ' .. k) - end, - __newindex = function(_, k) - error('Cannot set a new OptionType: ' .. k) - end, -}) - ---- Convert a vimoption_T style dictionary to the correct OptionType associated with it. ----@return OptionType -local get_option_type = function(name, info) - if info.type == 'boolean' then - return OptionTypes.BOOLEAN - elseif info.type == 'number' then - return OptionTypes.NUMBER - elseif info.type == 'string' then - if not info.commalist and not info.flaglist then - return OptionTypes.STRING - end - - if key_value_options[name] then - assert(info.commalist, 'Must be a comma list to use key:value style') - return OptionTypes.MAP - end - - if info.flaglist then - return OptionTypes.SET - elseif info.commalist then - return OptionTypes.ARRAY - end - - error('Fallthrough in OptionTypes') - else - error('Not a known info.type:' .. info.type) - end -end - -- Check whether the OptionTypes is allowed for vim.opt -- If it does not match, throw an error which indicates which option causes the error. local function assert_valid_value(name, value, types) @@ -272,373 +154,323 @@ local function assert_valid_value(name, value, types) ) end +local function passthrough(_, x) + return x +end + +local function tbl_merge(left, right) + return vim.tbl_extend('force', left, right) +end + +local function tbl_remove(t, value) + if type(value) == 'string' then + t[value] = nil + else + for _, v in ipairs(value) do + t[v] = nil + end + end + + return t +end + local valid_types = { - [OptionTypes.BOOLEAN] = { 'boolean' }, - [OptionTypes.NUMBER] = { 'number' }, - [OptionTypes.STRING] = { 'string' }, - [OptionTypes.SET] = { 'string', 'table' }, - [OptionTypes.ARRAY] = { 'string', 'table' }, - [OptionTypes.MAP] = { 'string', 'table' }, + boolean = { 'boolean' }, + number = { 'number' }, + string = { 'string' }, + set = { 'string', 'table' }, + array = { 'string', 'table' }, + map = { 'string', 'table' }, } ---- Convert a lua value to a vimoption_T value -local convert_value_to_vim = (function() - -- Map of functions to take a Lua style value and convert to vimoption_T style value. - -- Each function takes (info, lua_value) -> vim_value - local to_vim_value = { - [OptionTypes.BOOLEAN] = function(_, value) - return value - end, - [OptionTypes.NUMBER] = function(_, value) - return value - end, - [OptionTypes.STRING] = function(_, value) - return value - end, - - [OptionTypes.SET] = function(info, value) - if type(value) == 'string' then - return value - end +-- Map of functions to take a Lua style value and convert to vimoption_T style value. +-- Each function takes (info, lua_value) -> vim_value +local to_vim_value = { + boolean = passthrough, + number = passthrough, + string = passthrough, - if info.flaglist and info.commalist then - local keys = {} - for k, v in pairs(value) do - if v then - table.insert(keys, k) - end - end + set = function(info, value) + if type(value) == 'string' then + return value + end - table.sort(keys) - return table.concat(keys, ',') - else - local result = '' - for k, v in pairs(value) do - if v then - result = result .. k - end + if info.flaglist and info.commalist then + local keys = {} + for k, v in pairs(value) do + if v then + table.insert(keys, k) end - - return result end - end, - [OptionTypes.ARRAY] = function(info, value) - if type(value) == 'string' then - return value - end - if not info.allows_duplicates then - value = remove_duplicate_values(value) + table.sort(keys) + return table.concat(keys, ',') + else + local result = '' + for k, v in pairs(value) do + if v then + result = result .. k + end end - return table.concat(value, ',') - end, - [OptionTypes.MAP] = function(_, value) - if type(value) == 'string' then - return value - end + return result + end + end, - local result = {} - for opt_key, opt_value in pairs(value) do - table.insert(result, string.format('%s:%s', opt_key, opt_value)) - end + array = function(info, value) + if type(value) == 'string' then + return value + end + if not info.allows_duplicates then + value = remove_duplicate_values(value) + end + return table.concat(value, ',') + end, - table.sort(result) - return table.concat(result, ',') - end, - } + map = function(_, value) + if type(value) == 'string' then + return value + end - return function(name, info, value) - if value == nil then - return vim.NIL + local result = {} + for opt_key, opt_value in pairs(value) do + table.insert(result, string.format('%s:%s', opt_key, opt_value)) end - local option_type = get_option_type(name, info) - assert_valid_value(name, value, valid_types[option_type]) + table.sort(result) + return table.concat(result, ',') + end, +} - return to_vim_value[option_type](info, value) +--- Convert a lua value to a vimoption_T value +local function convert_value_to_vim(name, info, value) + if value == nil then + return vim.NIL end -end)() ---- Converts a vimoption_T style value to a Lua value -local convert_value_to_lua = (function() - -- 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 = { - [OptionTypes.BOOLEAN] = function(_, value) - return value - end, - [OptionTypes.NUMBER] = function(_, value) - return value - end, - [OptionTypes.STRING] = function(_, value) - return value - end, + assert_valid_value(name, value, valid_types[info.metatype]) - [OptionTypes.ARRAY] = function(info, value) - if type(value) == 'table' then - if not info.allows_duplicates then - value = remove_duplicate_values(value) - end + return to_vim_value[info.metatype](info, value) +end - return value - 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, - -- Empty strings mean that there is nothing there, - -- so empty table should be returned. - if value == '' then - return {} + array = function(info, value) + if type(value) == 'table' then + if not info.allows_duplicates then + value = remove_duplicate_values(value) end - -- Handles unescaped commas in a list. - if string.find(value, ',,,') then - local comma_split = vim.split(value, ',,,') - local left = comma_split[1] - local right = comma_split[2] + return value + end - local result = {} - vim.list_extend(result, vim.split(left, ',')) - table.insert(result, ',') - vim.list_extend(result, vim.split(right, ',')) + -- Empty strings mean that there is nothing there, + -- so empty table should be returned. + if value == '' then + return {} + end - table.sort(result) + -- Handles unescaped commas in a list. + if string.find(value, ',,,') then + local left, right = unpack(vim.split(value, ',,,')) - return result - end + local result = {} + vim.list_extend(result, vim.split(left, ',')) + table.insert(result, ',') + vim.list_extend(result, vim.split(right, ',')) - if string.find(value, ',^,,', 1, true) then - local comma_split = vim.split(value, ',^,,', true) - local left = comma_split[1] - local right = comma_split[2] + table.sort(result) - local result = {} - vim.list_extend(result, vim.split(left, ',')) - table.insert(result, '^,') - vim.list_extend(result, vim.split(right, ',')) + return result + end - table.sort(result) + if string.find(value, ',^,,', 1, true) then + local left, right = unpack(vim.split(value, ',^,,', true)) - return result - end + local result = {} + vim.list_extend(result, vim.split(left, ',')) + table.insert(result, '^,') + vim.list_extend(result, vim.split(right, ',')) - return vim.split(value, ',') - end, + table.sort(result) - [OptionTypes.SET] = function(info, value) - if type(value) == 'table' then - return value - end + return result + end - -- Empty strings mean that there is nothing there, - -- so empty table should be returned. - if value == '' then - return {} - end + return vim.split(value, ',') + end, - assert(info.flaglist, 'That is the only one I know how to handle') + set = function(info, value) + if type(value) == 'table' then + return value + end - if info.flaglist and info.commalist then - local split_value = vim.split(value, ',') - local result = {} - for _, v in ipairs(split_value) do - result[v] = true - end + -- Empty strings mean that there is nothing there, + -- so empty table should be returned. + if value == '' then + return {} + end - return result - else - local result = {} - for i = 1, #value do - result[value:sub(i, i)] = true - end + assert(info.flaglist, 'That is the only one I know how to handle') - return result + if info.flaglist and info.commalist then + local split_value = vim.split(value, ',') + local result = {} + for _, v in ipairs(split_value) do + result[v] = true end - end, - [OptionTypes.MAP] = function(info, raw_value) - if type(raw_value) == 'table' then - return raw_value + return result + else + local result = {} + for i = 1, #value do + result[value:sub(i, i)] = true end - assert(info.commalist, 'Only commas are supported currently') - - local result = {} + return result + end + end, - local comma_split = vim.split(raw_value, ',') - for _, key_value_str in ipairs(comma_split) do - local key, value = unpack(vim.split(key_value_str, ':')) - key = vim.trim(key) + map = function(info, raw_value) + if type(raw_value) == 'table' then + return raw_value + end - result[key] = value - end + assert(info.commalist, 'Only commas are supported currently') - return result - end, - } + local result = {} - return function(name, info, option_value) - return to_lua_value[get_option_type(name, info)](info, option_value) - end -end)() + local comma_split = vim.split(raw_value, ',') + for _, key_value_str in ipairs(comma_split) do + local key, value = unpack(vim.split(key_value_str, ':')) + key = vim.trim(key) ---- Handles the mutation of various different values. -local value_mutator = function(name, info, current, new, mutator) - return mutator[get_option_type(name, info)](current, new) -end + result[key] = value + end ---- Handles the '^' operator -local prepend_value = (function() - local methods = { - [OptionTypes.NUMBER] = function() - error("The '^' operator is not currently supported for") - end, + return result + end, +} - [OptionTypes.STRING] = function(left, right) - return right .. left - end, +--- Converts a vimoption_T style value to a Lua value +local function convert_value_to_lua(info, option_value) + return to_lua_value[info.metatype](info, option_value) +end - [OptionTypes.ARRAY] = function(left, right) - for i = #right, 1, -1 do - table.insert(left, 1, right[i]) - end +local prepend_methods = { + number = function() + error("The '^' operator is not currently supported for") + end, - return left - end, + string = function(left, right) + return right .. left + end, - [OptionTypes.MAP] = function(left, right) - return vim.tbl_extend('force', left, right) - end, + array = function(left, right) + for i = #right, 1, -1 do + table.insert(left, 1, right[i]) + end - [OptionTypes.SET] = function(left, right) - return vim.tbl_extend('force', left, right) - end, - } + return left + end, - return function(name, info, current, new) - return value_mutator( - name, - info, - convert_value_to_lua(name, info, current), - convert_value_to_lua(name, info, new), - methods - ) - end -end)() + map = tbl_merge, + set = tbl_merge, +} ---- Handles the '+' operator -local add_value = (function() - local methods = { - [OptionTypes.NUMBER] = function(left, right) - return left + right - end, +--- Handles the '^' operator +local function prepend_value(info, current, new) + return prepend_methods[info.metatype]( + convert_value_to_lua(info, current), + convert_value_to_lua(info, new) + ) +end - [OptionTypes.STRING] = function(left, right) - return left .. right - end, +local add_methods = { + number = function(left, right) + return left + right + end, - [OptionTypes.ARRAY] = function(left, right) - for _, v in ipairs(right) do - table.insert(left, v) - end + string = function(left, right) + return left .. right + end, - return left - end, + array = function(left, right) + for _, v in ipairs(right) do + table.insert(left, v) + end - [OptionTypes.MAP] = function(left, right) - return vim.tbl_extend('force', left, right) - end, + return left + end, - [OptionTypes.SET] = function(left, right) - return vim.tbl_extend('force', left, right) - end, - } + map = tbl_merge, + set = tbl_merge, +} - return function(name, info, current, new) - return value_mutator( - name, - info, - convert_value_to_lua(name, info, current), - convert_value_to_lua(name, info, new), - methods - ) - end -end)() +--- Handles the '+' operator +local function add_value(info, current, new) + return add_methods[info.metatype]( + convert_value_to_lua(info, current), + convert_value_to_lua(info, new) + ) +end ---- Handles the '-' operator -local remove_value = (function() - local remove_one_item = function(t, val) - if vim.tbl_islist(t) then - local remove_index = nil - for i, v in ipairs(t) do - if v == val then - remove_index = i - end +local function remove_one_item(t, val) + if vim.tbl_islist(t) then + local remove_index = nil + for i, v in ipairs(t) do + if v == val then + remove_index = i end + end - if remove_index then - table.remove(t, remove_index) - end - else - t[val] = nil + if remove_index then + table.remove(t, remove_index) end + else + t[val] = nil end +end - local methods = { - [OptionTypes.NUMBER] = function(left, right) - return left - right - end, - - [OptionTypes.STRING] = function() - error('Subtraction not supported for strings.') - end, - - [OptionTypes.ARRAY] = function(left, right) - if type(right) == 'string' then - remove_one_item(left, right) - else - for _, v in ipairs(right) do - remove_one_item(left, v) - end - end +local remove_methods = { + number = function(left, right) + return left - right + end, - return left - end, + string = function() + error('Subtraction not supported for strings.') + end, - [OptionTypes.MAP] = function(left, right) - if type(right) == 'string' then - left[right] = nil - else - for _, v in ipairs(right) do - left[v] = nil - end + array = function(left, right) + if type(right) == 'string' then + remove_one_item(left, right) + else + for _, v in ipairs(right) do + remove_one_item(left, v) end + end - return left - end, - - [OptionTypes.SET] = function(left, right) - if type(right) == 'string' then - left[right] = nil - else - for _, v in ipairs(right) do - left[v] = nil - end - end + return left + end, - return left - end, - } + map = tbl_remove, + set = tbl_remove, +} - return function(name, info, current, new) - return value_mutator(name, info, convert_value_to_lua(name, info, current), new, methods) - end -end)() +--- Handles the '-' operator +local function remove_value(info, current, new) + return remove_methods[info.metatype](convert_value_to_lua(info, current), new) +end -local create_option_metatable = function(set_type) - local set_mt, option_mt +local function create_option_accessor(scope) + local option_mt - local make_option = function(name, value) - _setup() + local function make_option(name, value) local info = assert(options_info[name], 'Not a valid option name: ' .. name) if type(value) == 'table' and getmetatable(value) == option_mt then @@ -654,67 +486,58 @@ local create_option_metatable = function(set_type) }, option_mt) end - local scope - if set_type == SET_TYPES.GLOBAL then - scope = 'global' - elseif set_type == SET_TYPES.LOCAL then - scope = 'local' - end - option_mt = { -- To set a value, instead use: -- opt[my_option] = value _set = function(self) local value = convert_value_to_vim(self._name, self._info, self._value) a.nvim_set_option_value(self._name, value, { scope = scope }) - - return self end, get = function(self) - return convert_value_to_lua(self._name, self._info, self._value) + return convert_value_to_lua(self._info, self._value) end, append = function(self, right) - return self:__add(right):_set() + self._value = add_value(self._info, self._value, right) + self:_set() end, __add = function(self, right) - return make_option(self._name, add_value(self._name, self._info, self._value, right)) + return make_option(self._name, add_value(self._info, self._value, right)) end, prepend = function(self, right) - return self:__pow(right):_set() + self._value = prepend_value(self._info, self._value, right) + self:_set() end, __pow = function(self, right) - return make_option(self._name, prepend_value(self._name, self._info, self._value, right)) + return make_option(self._name, prepend_value(self._info, self._value, right)) end, remove = function(self, right) - return self:__sub(right):_set() + self._value = remove_value(self._info, self._value, right) + self:_set() end, __sub = function(self, right) - return make_option(self._name, remove_value(self._name, self._info, self._value, right)) + return make_option(self._name, remove_value(self._info, self._value, right)) end, } option_mt.__index = option_mt - set_mt = { + return setmetatable({}, { __index = function(_, k) return make_option(k, a.nvim_get_option_value(k, { scope = scope })) end, __newindex = function(_, k, v) - local opt = make_option(k, v) - opt:_set() + make_option(k, v):_set() end, - } - - return set_mt + }) end -vim.opt = setmetatable({}, create_option_metatable(SET_TYPES.SET)) -vim.opt_local = setmetatable({}, create_option_metatable(SET_TYPES.LOCAL)) -vim.opt_global = setmetatable({}, create_option_metatable(SET_TYPES.GLOBAL)) +vim.opt = create_option_accessor() +vim.opt_local = create_option_accessor('local') +vim.opt_global = create_option_accessor('global') diff --git a/src/nvim/option.c b/src/nvim/option.c index 2a45f3b38b..5b67f0e471 100644 --- a/src/nvim/option.c +++ b/src/nvim/option.c @@ -779,10 +779,11 @@ void ex_set(exarg_T *eap) /// Part of do_set() for string options. /// @return FAIL on failure, do not process further options. -static int do_set_string(int opt_idx, int opt_flags, char **arg, int nextchar, set_op_T op_arg, +static int do_set_string(int opt_idx, int opt_flags, char **argp, int nextchar, set_op_T op_arg, uint32_t flags, char *varp_arg, char *errbuf, size_t errbuflen, int *value_checked, char **errmsg) { + char *arg = *argp; set_op_T op = op_arg; char *varp = varp_arg; char *save_arg = NULL; @@ -849,15 +850,13 @@ static int do_set_string(int opt_idx, int opt_flags, char **arg, int nextchar, s } else if (nextchar == '<') { // set to global val newval = xstrdup(*(char **)get_varp_scope(&(options[opt_idx]), OPT_GLOBAL)); } else { - (*arg)++; // jump to after the '=' or ':' + arg++; // jump to after the '=' or ':' // Set 'keywordprg' to ":help" if an empty // value was passed to :set by the user. - // Misuse errbuf[] for the resulting string. - if (varp == (char *)&p_kp && (**arg == NUL || **arg == ' ')) { - STRCPY(errbuf, ":help"); - save_arg = *arg; - *arg = errbuf; + if (varp == (char *)&p_kp && (*arg == NUL || *arg == ' ')) { + save_arg = arg; + arg = ":help"; } else if (varp == (char *)&p_bs && ascii_isdigit(**(char_u **)varp)) { // Convert 'backspace' number to string, for // adding, prepending and removing string. @@ -887,11 +886,11 @@ static int do_set_string(int opt_idx, int opt_flags, char **arg, int nextchar, s origval_g = *(char_u **)varp; } oldval = *(char_u **)varp; - } else if (varp == (char *)&p_ww && ascii_isdigit(**arg)) { + } else if (varp == (char *)&p_ww && ascii_isdigit(*arg)) { // Convert 'whichwrap' number to string, for backwards compatibility // with Vim 3.0. *whichwrap = NUL; - int i = getdigits_int(arg, true, 0); + int i = getdigits_int(&arg, true, 0); if (i & 1) { xstrlcat(whichwrap, "b,", sizeof(whichwrap)); } @@ -910,12 +909,12 @@ static int do_set_string(int opt_idx, int opt_flags, char **arg, int nextchar, s if (*whichwrap != NUL) { // remove trailing , whichwrap[strlen(whichwrap) - 1] = NUL; } - save_arg = *arg; - *arg = whichwrap; - } else if (**arg == '>' && (varp == (char *)&p_dir || varp == (char *)&p_bdir)) { + save_arg = arg; + arg = whichwrap; + } else if (*arg == '>' && (varp == (char *)&p_dir || varp == (char *)&p_bdir)) { // Remove '>' before 'dir' and 'bdir', for backwards compatibility with // version 3.0 - (*arg)++; + arg++; } // Copy the new string into allocated memory. @@ -923,7 +922,7 @@ static int do_set_string(int opt_idx, int opt_flags, char **arg, int nextchar, s // backslashes. // get a bit too much - newlen = (unsigned)strlen(*arg) + 1; + newlen = (unsigned)strlen(arg) + 1; if (op != OP_NONE) { newlen += (unsigned)STRLEN(origval) + 1; } @@ -935,26 +934,26 @@ static int do_set_string(int opt_idx, int opt_flags, char **arg, int nextchar, s // are not removed, and keep backslash at start, for "\\machine\path", // but do remove it for "\\\\machine\\path". // The reverse is found in ExpandOldSetting(). - while (**arg && !ascii_iswhite(**arg)) { - if (**arg == '\\' && (*arg)[1] != NUL + while (*arg != NUL && !ascii_iswhite(*arg)) { + if (*arg == '\\' && arg[1] != NUL #ifdef BACKSLASH_IN_FILENAME && !((flags & P_EXPAND) - && vim_isfilec((*arg)[1]) - && !ascii_iswhite((*arg)[1]) - && ((*arg)[1] != '\\' - || (s == newval && (*arg)[2] != '\\'))) + && vim_isfilec(arg[1]) + && !ascii_iswhite(arg[1]) + && (arg[1] != '\\' + || (s == newval && arg[2] != '\\'))) #endif ) { - (*arg)++; // remove backslash + arg++; // remove backslash } - int i = utfc_ptr2len(*arg); + int i = utfc_ptr2len(arg); if (i > 1) { // copy multibyte char - memmove(s, *arg, (size_t)i); - *arg += i; + memmove(s, arg, (size_t)i); + arg += i; s += i; } else { - *s++ = *(*arg)++; + *s++ = *arg++; } } *s = NUL; @@ -1061,8 +1060,8 @@ static int do_set_string(int opt_idx, int opt_flags, char **arg, int nextchar, s } } - if (save_arg != NULL) { // number for 'whichwrap' - *arg = save_arg; + if (save_arg != NULL) { + arg = save_arg; // arg was temporarily changed, restore it } } @@ -1117,6 +1116,7 @@ static int do_set_string(int opt_idx, int opt_flags, char **arg, int nextchar, s xfree(saved_origval_g); xfree(saved_newval); + *argp = arg; return *errmsg == NULL ? OK : FAIL; } diff --git a/src/nvim/testdir/test_options.vim b/src/nvim/testdir/test_options.vim index 952975df32..ada6d2406b 100644 --- a/src/nvim/testdir/test_options.vim +++ b/src/nvim/testdir/test_options.vim @@ -1209,4 +1209,18 @@ func Test_switchbuf_reset() only! endfunc +" :set empty string for global 'keywordprg' falls back to ":help" +func Test_keywordprg_empty() + let k = &keywordprg + set keywordprg=man + call assert_equal('man', &keywordprg) + set keywordprg= + call assert_equal(':help', &keywordprg) + set keywordprg=man + call assert_equal('man', &keywordprg) + call assert_equal("\n keywordprg=:help", execute('set kp= kp?')) + let &keywordprg = k +endfunc + + " vim: shiftwidth=2 sts=2 expandtab diff --git a/test/functional/lua/vim_spec.lua b/test/functional/lua/vim_spec.lua index 00ce44f48a..3184f01ef4 100644 --- a/test/functional/lua/vim_spec.lua +++ b/test/functional/lua/vim_spec.lua @@ -1421,7 +1421,7 @@ describe('lua stdlib', function() ]] eq('', funcs.luaeval "vim.bo.filetype") eq(true, funcs.luaeval "vim.bo[BUF].modifiable") - matches("unknown option 'nosuchopt'$", + matches("no such option: 'nosuchopt'$", pcall_err(exec_lua, 'return vim.bo.nosuchopt')) matches("Expected lua string$", pcall_err(exec_lua, 'return vim.bo[0][0].autoread')) @@ -1442,7 +1442,7 @@ describe('lua stdlib', function() eq(0, funcs.luaeval "vim.wo.cole") eq(0, funcs.luaeval "vim.wo[0].cole") eq(0, funcs.luaeval "vim.wo[1001].cole") - matches("unknown option 'notanopt'$", + matches("no such option: 'notanopt'$", pcall_err(exec_lua, 'return vim.wo.notanopt')) matches("Expected lua string$", pcall_err(exec_lua, 'return vim.wo[0][0].list')) |