mirror of
https://github.com/neovim/neovim
synced 2025-07-16 01:01:49 +00:00
feat(lua): completion for vim.fn, vim.v, vim.o #30472
Problem: Lua accessors for - global, local, and special variables (`vim.{g,t,w,b,v}.*`), and - options (`vim.{o,bo,wo,opt,opt_local,opt_global}.*`), do not have command-line completion, unlike their vimscript counterparts (e.g., `g:`, `b:`, `:set`, `:setlocal`, `:call <fn>`, etc.). Completion for vimscript functions (`vim.fn.*`) is incomplete and does not list all the available functions. Solution: Implement completion for vimscript function, variable and option accessors in `vim._expand_pat` through: - `getcompletion()` for variable and vimscript function accessors, and - `nvim_get_all_options_info()` for option accessors. Note/Remark: - Short names for options are yet to be implemented. - Completions for accessors with handles (e.g. `vim.b[0]`, `vim.wo[0]`) are also yet to be implemented, and are left as future work, which involves some refactoring of options. - For performance reasons, we may want to introduce caching for completing options, but this is not considered at this time since the number of the available options is not very big (only ~350) and Lua completion for option accessors appears to be pretty fast. - Can we have a more "general" framework for customizing completions? In the future, we may want to improve the implementation by moving the core logic for generating completion candidates to each accessor (or its metatable) or through some central interface, rather than writing all the accessor-specific completion implementations in a single function: `vim._expand_pat`.
This commit is contained in:
@ -78,6 +78,9 @@ LUA
|
||||
|
||||
• API functions now consistently return an empty dictionary as
|
||||
|vim.empty_dict()|. Earlier, a |lua-special-tbl| was sometimes used.
|
||||
• Command-line completions for: `vim.g`, `vim.t`, `vim.w`, `vim.b`, `vim.v`,
|
||||
`vim.o`, `vim.wo`, `vim.bo`, `vim.opt`, `vim.opt_local`, `vim.opt_global`,
|
||||
and `vim.fn`.
|
||||
|
||||
OPTIONS
|
||||
|
||||
|
@ -787,7 +787,7 @@ function vim._expand_pat(pat, env)
|
||||
if mt and type(mt.__index) == 'table' then
|
||||
field = rawget(mt.__index, key)
|
||||
elseif final_env == vim and (vim._submodules[key] or vim._extra[key]) then
|
||||
field = vim[key]
|
||||
field = vim[key] --- @type any
|
||||
end
|
||||
end
|
||||
final_env = field
|
||||
@ -798,14 +798,24 @@ function vim._expand_pat(pat, env)
|
||||
end
|
||||
|
||||
local keys = {} --- @type table<string,true>
|
||||
|
||||
--- @param obj table<any,any>
|
||||
local function insert_keys(obj)
|
||||
for k, _ in pairs(obj) do
|
||||
if type(k) == 'string' and string.sub(k, 1, string.len(match_part)) == match_part then
|
||||
if
|
||||
type(k) == 'string'
|
||||
and string.sub(k, 1, string.len(match_part)) == match_part
|
||||
and k:match('^[_%w]+$') ~= nil -- filter out invalid identifiers for field, e.g. 'foo#bar'
|
||||
then
|
||||
keys[k] = true
|
||||
end
|
||||
end
|
||||
end
|
||||
---@param acc table<string,any>
|
||||
local function _fold_to_map(acc, k, v)
|
||||
acc[k] = (v or true)
|
||||
return acc
|
||||
end
|
||||
|
||||
if type(final_env) == 'table' then
|
||||
insert_keys(final_env)
|
||||
@ -814,11 +824,61 @@ function vim._expand_pat(pat, env)
|
||||
if mt and type(mt.__index) == 'table' then
|
||||
insert_keys(mt.__index)
|
||||
end
|
||||
|
||||
if final_env == vim then
|
||||
insert_keys(vim._submodules)
|
||||
insert_keys(vim._extra)
|
||||
end
|
||||
|
||||
-- Completion for dict accessors (special vim variables and vim.fn)
|
||||
if mt and vim.tbl_contains({ vim.g, vim.t, vim.w, vim.b, vim.v, vim.fn }, final_env) then
|
||||
local prefix, type = unpack(
|
||||
vim.fn == final_env and { '', 'function' }
|
||||
or vim.g == final_env and { 'g:', 'var' }
|
||||
or vim.t == final_env and { 't:', 'var' }
|
||||
or vim.w == final_env and { 'w:', 'var' }
|
||||
or vim.b == final_env and { 'b:', 'var' }
|
||||
or vim.v == final_env and { 'v:', 'var' }
|
||||
or { nil, nil }
|
||||
)
|
||||
assert(prefix, "Can't resolve final_env")
|
||||
local vars = vim.fn.getcompletion(prefix .. match_part, type) --- @type string[]
|
||||
insert_keys(vim
|
||||
.iter(vars)
|
||||
:map(function(s) ---@param s string
|
||||
s = s:gsub('[()]+$', '') -- strip '(' and ')' for function completions
|
||||
return s:sub(#prefix + 1) -- strip the prefix, e.g., 'g:foo' => 'foo'
|
||||
end)
|
||||
:fold({}, _fold_to_map))
|
||||
end
|
||||
|
||||
-- Completion for option accessors (full names only)
|
||||
if
|
||||
mt
|
||||
and vim.tbl_contains(
|
||||
{ vim.o, vim.go, vim.bo, vim.wo, vim.opt, vim.opt_local, vim.opt_global },
|
||||
final_env
|
||||
)
|
||||
then
|
||||
--- @type fun(option_name: string, option: vim.api.keyset.get_option_info): boolean
|
||||
local filter = function(_, _)
|
||||
return true
|
||||
end
|
||||
if vim.bo == final_env then
|
||||
filter = function(_, option)
|
||||
return option.scope == 'buf'
|
||||
end
|
||||
elseif vim.wo == final_env then
|
||||
filter = function(_, option)
|
||||
return option.scope == 'win'
|
||||
end
|
||||
end
|
||||
|
||||
--- @type table<string, vim.api.keyset.get_option_info>
|
||||
local options = vim.api.nvim_get_all_options_info()
|
||||
insert_keys(vim.iter(options):filter(filter):fold({}, _fold_to_map))
|
||||
end
|
||||
|
||||
keys = vim.tbl_keys(keys)
|
||||
table.sort(keys)
|
||||
|
||||
|
@ -5,12 +5,14 @@ local clear = n.clear
|
||||
local eq = t.eq
|
||||
local exec_lua = n.exec_lua
|
||||
|
||||
--- @return { [1]: string[], [2]: integer }
|
||||
local get_completions = function(input, env)
|
||||
return exec_lua('return {vim._expand_pat(...)}', input, env)
|
||||
return exec_lua('return { vim._expand_pat(...) }', input, env)
|
||||
end
|
||||
|
||||
--- @return { [1]: string[], [2]: integer }
|
||||
local get_compl_parts = function(parts)
|
||||
return exec_lua('return {vim._expand_pat_get_parts(...)}', parts)
|
||||
return exec_lua('return { vim._expand_pat_get_parts(...) }', parts)
|
||||
end
|
||||
|
||||
before_each(clear)
|
||||
@ -123,6 +125,171 @@ describe('nlua_expand_pat', function()
|
||||
)
|
||||
end)
|
||||
|
||||
describe('should complete vim.fn', function()
|
||||
it('correctly works for simple completion', function()
|
||||
local actual = get_completions('vim.fn.did')
|
||||
local expected = {
|
||||
{ 'did_filetype' },
|
||||
#'vim.fn.',
|
||||
}
|
||||
eq(expected, actual)
|
||||
end)
|
||||
it('should not suggest items with #', function()
|
||||
exec_lua [[
|
||||
-- ensure remote#host#... functions exist
|
||||
vim.cmd [=[
|
||||
runtime! autoload/remote/host.vim
|
||||
]=]
|
||||
-- make a dummy call to ensure vim.fn contains an entry: remote#host#...
|
||||
vim.fn['remote#host#IsRunning']('python3')
|
||||
]]
|
||||
local actual = get_completions('vim.fn.remo')
|
||||
local expected = {
|
||||
{ 'remove' }, -- there should be no completion "remote#host#..."
|
||||
#'vim.fn.',
|
||||
}
|
||||
eq(expected, actual)
|
||||
end)
|
||||
end)
|
||||
|
||||
describe('should complete for variable accessors for', function()
|
||||
it('vim.v', function()
|
||||
local actual = get_completions('vim.v.t_')
|
||||
local expected = {
|
||||
{ 't_blob', 't_bool', 't_dict', 't_float', 't_func', 't_list', 't_number', 't_string' },
|
||||
#'vim.v.',
|
||||
}
|
||||
eq(expected, actual)
|
||||
end)
|
||||
|
||||
it('vim.g', function()
|
||||
exec_lua [[
|
||||
vim.cmd [=[
|
||||
let g:nlua_foo = 'completion'
|
||||
let g:nlua_foo_bar = 'completion'
|
||||
let g:nlua_foo#bar = 'nocompletion' " should be excluded from lua completion
|
||||
]=]
|
||||
]]
|
||||
local actual = get_completions('vim.g.nlua')
|
||||
local expected = {
|
||||
{ 'nlua_foo', 'nlua_foo_bar' },
|
||||
#'vim.g.',
|
||||
}
|
||||
eq(expected, actual)
|
||||
end)
|
||||
|
||||
it('vim.b', function()
|
||||
exec_lua [[
|
||||
vim.b.nlua_foo_buf = 'bar'
|
||||
vim.b.some_other_vars = 'bar'
|
||||
]]
|
||||
local actual = get_completions('vim.b.nlua')
|
||||
local expected = {
|
||||
{ 'nlua_foo_buf' },
|
||||
#'vim.b.',
|
||||
}
|
||||
eq(expected, actual)
|
||||
end)
|
||||
|
||||
it('vim.w', function()
|
||||
exec_lua [[
|
||||
vim.w.nlua_win_var = 42
|
||||
]]
|
||||
local actual = get_completions('vim.w.nlua')
|
||||
local expected = {
|
||||
{ 'nlua_win_var' },
|
||||
#'vim.w.',
|
||||
}
|
||||
eq(expected, actual)
|
||||
end)
|
||||
|
||||
it('vim.t', function()
|
||||
exec_lua [[
|
||||
vim.t.nlua_tab_var = 42
|
||||
]]
|
||||
local actual = get_completions('vim.t.')
|
||||
local expected = {
|
||||
{ 'nlua_tab_var' },
|
||||
#'vim.t.',
|
||||
}
|
||||
eq(expected, actual)
|
||||
end)
|
||||
end)
|
||||
|
||||
describe('should complete for option accessors for', function()
|
||||
-- for { vim.o, vim.go, vim.opt, vim.opt_local, vim.opt_global }
|
||||
local test_opt = function(accessor)
|
||||
do
|
||||
local actual = get_completions(accessor .. '.file')
|
||||
local expected = {
|
||||
'fileencoding',
|
||||
'fileencodings',
|
||||
'fileformat',
|
||||
'fileformats',
|
||||
'fileignorecase',
|
||||
'filetype',
|
||||
}
|
||||
eq({ expected, #accessor + 1 }, actual, accessor .. '.file')
|
||||
end
|
||||
do
|
||||
local actual = get_completions(accessor .. '.winh')
|
||||
local expected = {
|
||||
'winheight',
|
||||
'winhighlight',
|
||||
}
|
||||
eq({ expected, #accessor + 1 }, actual, accessor .. '.winh')
|
||||
end
|
||||
end
|
||||
|
||||
test_opt('vim.o')
|
||||
test_opt('vim.go')
|
||||
test_opt('vim.opt')
|
||||
test_opt('vim.opt_local')
|
||||
test_opt('vim.opt_global')
|
||||
|
||||
it('vim.o, suggesting all the known options', function()
|
||||
local completions = get_completions('vim.o.')[1] ---@type string[]
|
||||
eq(
|
||||
exec_lua [[
|
||||
return vim.tbl_count(vim.api.nvim_get_all_options_info())
|
||||
]],
|
||||
#completions
|
||||
)
|
||||
end)
|
||||
|
||||
it('vim.bo', function()
|
||||
do
|
||||
local actual = get_completions('vim.bo.file')
|
||||
local compls = {
|
||||
-- should contain buffer options only
|
||||
'fileencoding',
|
||||
'fileformat',
|
||||
'filetype',
|
||||
}
|
||||
eq({ compls, #'vim.bo.' }, actual)
|
||||
end
|
||||
do
|
||||
local actual = get_completions('vim.bo.winh')
|
||||
local compls = {}
|
||||
eq({ compls, #'vim.bo.' }, actual)
|
||||
end
|
||||
end)
|
||||
|
||||
it('vim.wo', function()
|
||||
do
|
||||
local actual = get_completions('vim.wo.file')
|
||||
local compls = {}
|
||||
eq({ compls, #'vim.wo.' }, actual)
|
||||
end
|
||||
do
|
||||
local actual = get_completions('vim.wo.winh')
|
||||
-- should contain window options only
|
||||
local compls = { 'winhighlight' }
|
||||
eq({ compls, #'vim.wo.' }, actual)
|
||||
end
|
||||
end)
|
||||
end)
|
||||
|
||||
it('should return everything if the input is of length 0', function()
|
||||
eq({ { 'other', 'vim' }, 0 }, get_completions('', { vim = true, other = true }))
|
||||
end)
|
||||
|
Reference in New Issue
Block a user