feat(options): allow setting function values to vim.o/go/bo

Problem:
Assigning function values to options is tedious.

Solution:
Allow setting function values to options directly.
This commit is contained in:
Lewis Russell 2024-11-20 10:21:35 +00:00
parent d27bbebf8e
commit 6fb4d0050a
6 changed files with 242 additions and 23 deletions

View File

@ -746,8 +746,11 @@ formatexpr({opts}) *vim.lsp.formatexpr()*
Currently only supports a single client. This can be set via
`setlocal formatexpr=v:lua.vim.lsp.formatexpr()` or (more typically) in
`on_attach` via
`vim.bo[bufnr].formatexpr = 'v:lua.vim.lsp.formatexpr(#{timeout_ms:250})'`.
`on_attach` via: >lua
vim.bo[bufnr].formatexpr = function()
return vim.lsp.formatexpr({ timeout_ms = 250 })
end
<
Parameters: ~
• {opts} (`table?`) A table with the following fields:

View File

@ -1293,6 +1293,14 @@ 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|.
Unlike |nvim_set_option_value()|, |vim.o|, |vim.go|, |vim.bo|
and |vim.wo| can be assigned function values.
Example: >lua
vim.bo.tagfunc = vim.lsp.tagfunc
vim.wo[1000].foldexpr = vim.treesitter.foldexpr
<
*vim.opt_local*
*vim.opt_global*

View File

@ -134,6 +134,145 @@ local function get_options_info(name)
return info
end
local func_opts = {
-- Options that can be set as a function
func = {
completefunc = true,
findfunc = true,
omnifunc = true,
operatorfunc = true,
quickfixtextfunc = true,
tagfunc = true,
thesaurusfunc = true,
},
-- Options that can be set as an expression
expr = {
diffexpr = true,
foldexpr = true,
formatexpr = true,
includeexpr = true,
indentexpr = true,
modelineexpr = true,
patchexpr = true,
},
-- Options that can be set as an expression with a prefix of '%!'
pct_bang_expr = {
statuscolumn = true,
statusline = true,
tabline = true,
winbar = true,
},
-- Options that can be set as an ex command if prefixed with ':'
ex_cmd_expr = {
keywordprg = true,
},
}
--- @param v integer?
--- @return integer
local function resolve_win(v)
return assert((not v or v == 0) and api.nvim_get_current_win() or v)
end
--- @type table<string,boolean>
local all_func_opts = {}
for _, v in pairs(func_opts) do
for k in pairs(v) do
all_func_opts[k] = true
end
end
--- @type table<string,function?>
vim._func_opts = setmetatable({}, { __mode = 'v' })
--- If the option name is a function option convert it to a string
--- format and store the function value.
--- @param name string
--- @param value any
--- @param info vim.api.keyset.get_option_info
--- @param opts vim.api.keyset.option
local function apply_func_opt(name, value, info, opts)
local idxs --- @type any[]
-- Find a table keep a references to the function value for an option.
-- We store a strong reference to the function in vim.g/b/w so it can be correctly garbage
-- collected when the buffer/window is closed. However because `v:lua` does not support
-- indexing with [], we need to store a weak reference to the function in another table under a
-- single string key.
if info.scope == 'global' or info.global_local and opts.scope == 'global' then
idxs = { 'g', '_func_opts' }
elseif info.scope == 'buf' then
idxs = { 'b', vim._resolve_bufnr(opts.buf), '_func_opts' }
else
assert(info.scope == 'win')
idxs = { 'w', resolve_win(opts.win), '_func_opts' }
if opts.scope == 'local' then
idxs[#idxs + 1] = vim._resolve_bufnr(opts.buf)
end
end
local fvalue = type(value) == 'function' and value or nil
-- If we have a function value, ensure the table for the strong references exists
if fvalue then
local t = vim --- @type table<string,any>
for _, k in ipairs(idxs) do
t[k] = t[k] or {}
t = t[k]
end
end
--- @type table<string,function?>
local t = vim.tbl_get(vim, unpack(idxs))
if t then
-- Note fvalue as nil is used to GC
t[name] = fvalue
end
if not fvalue then
return value
end
local vlua_key_parts = {} --- @type string[]
for _, k in ipairs(idxs) do
vlua_key_parts[#vlua_key_parts + 1] = k ~= '_func_opts' and k or nil
end
vlua_key_parts[#vlua_key_parts + 1] = name
local vlua_key = table.concat(vlua_key_parts, '_')
vim._func_opts[vlua_key] = fvalue
local expr_pfx = (
func_opts.pct_bang_expr[name] and '%!'
or func_opts.ex_cmd_expr[name] and ':call '
or ''
)
local call_sfx = func_opts.func[name] and '' or '()'
return ('%sv:lua.vim._func_opts.%s%s'):format(expr_pfx, vlua_key, call_sfx)
end
--- @param name string
--- @param value any
--- @param opts vim.api.keyset.option
local function set_option_value(name, value, opts)
local info = api.nvim_get_option_info2(name, {})
-- Resolve name if it is a short name
name = info.name
if all_func_opts[name] then
value = apply_func_opt(name, value, info, opts)
end
api.nvim_set_option_value(name, value, opts)
end
--- Environment variables defined in the editor session.
--- See |expand-env| and |:let-environment| for the Vimscript behavior.
--- Invalid or unset key returns `nil`.
@ -158,6 +297,7 @@ vim.env = setmetatable({}, {
end,
})
--- @param bufnr? integer
local function new_buf_opt_accessor(bufnr)
return setmetatable({}, {
__index = function(_, k)
@ -167,8 +307,10 @@ local function new_buf_opt_accessor(bufnr)
return api.nvim_get_option_value(k, { buf = bufnr or 0 })
end,
--- @param k string
--- @param v any
__newindex = function(_, k, v)
return api.nvim_set_option_value(k, v, { buf = bufnr or 0 })
return set_option_value(k, v, { buf = bufnr or 0 })
end,
})
end
@ -196,7 +338,7 @@ local function new_win_opt_accessor(winid, bufnr)
end,
__newindex = function(_, k, v)
return api.nvim_set_option_value(k, v, {
return set_option_value(k, v, {
scope = bufnr and 'local' or nil,
win = winid or 0,
})
@ -227,6 +369,14 @@ end
--- 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|.
---
--- Unlike |nvim_set_option_value()|, |vim.o|, |vim.go|, |vim.bo|
--- and |vim.wo| can be assigned function values.
---
--- Example: >lua
--- vim.bo.tagfunc = vim.lsp.tagfunc
--- vim.wo[1000].foldexpr = vim.treesitter.foldexpr
--- <
--- </pre>
--- Get or set |options|. Like `:set`. Invalid key is an error.
@ -245,8 +395,10 @@ vim.o = setmetatable({}, {
__index = function(_, k)
return api.nvim_get_option_value(k, {})
end,
--- @param k string
--- @param v any
__newindex = function(_, k, v)
return api.nvim_set_option_value(k, v, {})
return set_option_value(k, v, {})
end,
})
@ -268,8 +420,10 @@ vim.go = setmetatable({}, {
__index = function(_, k)
return api.nvim_get_option_value(k, { scope = 'global' })
end,
--- @param k string
--- @param v any
__newindex = function(_, k, v)
return api.nvim_set_option_value(k, v, { scope = 'global' })
return set_option_value(k, v, { scope = 'global' })
end,
})

View File

@ -1066,8 +1066,13 @@ end
---
--- Currently only supports a single client. This can be set via
--- `setlocal formatexpr=v:lua.vim.lsp.formatexpr()` or (more typically) in `on_attach`
--- via `vim.bo[bufnr].formatexpr = 'v:lua.vim.lsp.formatexpr(#{timeout_ms:250})'`.
--- via:
---
--- ```lua
--- vim.bo[bufnr].formatexpr = function()
--- return vim.lsp.formatexpr({ timeout_ms = 250 })
--- end
--- ```
---@param opts? vim.lsp.formatexpr.Opts
function lsp.formatexpr(opts)
opts = opts or {}

View File

@ -149,7 +149,7 @@ describe('lua stdlib', function()
vim.api.nvim_buf_set_var(0, 'nullvar', vim.NIL)
vim.api.nvim_buf_set_var(0, 'to_delete', { hello = 'world' })
_G.BUF = vim.api.nvim_create_buf(false, true)
vim.api.nvim_buf_set_var(BUF, 'testing', 'bye')
vim.api.nvim_buf_set_var(_G.BUF, 'testing', 'bye')
end)
eq('hi', fn.luaeval 'vim.b.testing')
@ -164,7 +164,10 @@ describe('lua stdlib', function()
return { vim.b.nonexistent == vim.NIL, vim.b.nullvar == vim.NIL }
end)
matches([[attempt to index .* nil value]], pcall_err(exec_lua, 'return vim.b[BUF][0].testing'))
matches(
[[attempt to index .* nil value]],
pcall_err(exec_lua, 'return vim.b[BUF][0].testing')
)
eq({ hello = 'world' }, fn.luaeval 'vim.b.to_delete')
exec_lua [[
@ -174,12 +177,18 @@ describe('lua stdlib', function()
exec_lua(function()
local counter = 0
local function add_counter() counter = counter + 1 end
local function get_counter() return counter end
local function add_counter()
counter = counter + 1
end
local function get_counter()
return counter
end
vim.b.AddCounter = add_counter
vim.b.GetCounter = get_counter
vim.b.fn = {add = add_counter, get = get_counter}
vim.b.AddParens = function(s) return '(' .. s .. ')' end
vim.b.fn = { add = add_counter, get = get_counter }
vim.b.AddParens = function(s)
return '(' .. s .. ')'
end
end)
eq(0, eval('b:GetCounter()'))
@ -199,12 +208,18 @@ describe('lua stdlib', function()
exec_lua(function()
local counter = 0
local function add_counter() counter = counter + 1 end
local function get_counter() return counter end
local function add_counter()
counter = counter + 1
end
local function get_counter()
return counter
end
vim.api.nvim_buf_set_var(0, 'AddCounter', add_counter)
vim.api.nvim_buf_set_var(0, 'GetCounter', get_counter)
vim.api.nvim_buf_set_var(0, 'fn', {add = add_counter, get = get_counter})
vim.api.nvim_buf_set_var(0, 'AddParens', function(s) return '(' .. s .. ')' end)
vim.api.nvim_buf_set_var(0, 'fn', { add = add_counter, get = get_counter })
vim.api.nvim_buf_set_var(0, 'AddParens', function(s)
return '(' .. s .. ')'
end)
end)
eq(0, eval('b:GetCounter()'))
@ -261,7 +276,10 @@ describe('lua stdlib', function()
eq(NIL, fn.luaeval 'vim.w.nonexistent')
eq(NIL, fn.luaeval 'vim.w[WIN].nonexistent')
matches([[attempt to index .* nil value]], pcall_err(exec_lua, 'return vim.w[WIN][0].testing'))
matches(
[[attempt to index .* nil value]],
pcall_err(exec_lua, 'return vim.w[WIN][0].testing')
)
eq({ hello = 'world' }, fn.luaeval 'vim.w.to_delete')
exec_lua [[
@ -456,7 +474,10 @@ describe('lua stdlib', function()
eq({ 'one', 'two' }, eval('v:oldfiles'))
exec_lua([[vim.v.oldfiles = {}]])
eq({}, eval('v:oldfiles'))
eq('Setting v:oldfiles to value with wrong type', pcall_err(exec_lua, [[vim.v.oldfiles = 'a']]))
eq(
'Setting v:oldfiles to value with wrong type',
pcall_err(exec_lua, [[vim.v.oldfiles = 'a']])
)
eq({}, eval('v:oldfiles'))
feed('i foo foo foo<Esc>0/foo<CR>')
@ -528,6 +549,13 @@ describe('lua stdlib', function()
matches('Expected Lua string$', pcall_err(exec_lua, 'return vim.bo[0][0].autoread'))
matches('Invalid buffer id: %-1$', pcall_err(exec_lua, 'return vim.bo[-1].filetype'))
end)
it('can set function values', function()
eq_exec_lua('v:lua.vim._func_opts.b_1_tagfunc', function()
vim.bo.tagfunc = function() end
return vim.bo.tagfunc
end)
end)
end)
describe('vim.wo', function()
@ -567,7 +595,26 @@ describe('lua stdlib', function()
it('errors', function()
matches('only bufnr=0 is supported', pcall_err(exec_lua, 'vim.wo[0][10].signcolumn = "no"'))
matches('only bufnr=0 is supported', pcall_err(exec_lua, 'local a = vim.wo[0][10].signcolumn'))
matches(
'only bufnr=0 is supported',
pcall_err(exec_lua, 'local a = vim.wo[0][10].signcolumn')
)
end)
it('can set function values', function()
eq_exec_lua({ '%!v:lua.vim._func_opts.w_1000_statusline()', 'HELLO' }, function()
vim.wo.statusline = function()
return 'HELLO'
end
return { vim.wo.statusline, vim.api.nvim_eval_statusline(vim.wo.statusline, {}).str }
end)
eq_exec_lua('v:lua.vim._func_opts.w_1000_1_foldexpr()', function()
vim.wo[0][0].foldexpr = function()
return 'HELLO'
end
return vim.wo[0].foldexpr
end)
end)
end)
@ -1073,7 +1120,10 @@ describe('lua stdlib', function()
)
matches(
"Invalid option type 'function' for 'listchars'",
pcall_err(exec_lua, [[vim.opt.listchars = function() return "eol:~,space:.,tab:>~" end]])
pcall_err(
exec_lua,
[[vim.opt.listchars = function() return "eol:~,space:.,tab:>~" end]]
)
)
end)
@ -1198,5 +1248,4 @@ describe('lua stdlib', function()
end)
end)
end)
end)

View File

@ -86,7 +86,7 @@ function M.handler(bytecode, upvalues, ...)
set_upvalues(f, upvalues)
-- Run in pcall so we can return any print messages
local ret = { pcall(f, ...) } --- @type any[]
local ret = { xpcall(f, debug.traceback, ...) } --- @type any[]
_G.print = orig_print