aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--runtime/doc/lua.txt81
-rw-r--r--runtime/lua/vim/_meta.lua869
-rw-r--r--src/nvim/option.c54
-rw-r--r--src/nvim/testdir/test_options.vim14
-rw-r--r--test/functional/lua/vim_spec.lua4
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'))