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:
Jongwook Choi
2024-10-04 09:48:31 -04:00
committed by GitHub
parent 86c5c8724b
commit d5ae5c84e9
3 changed files with 234 additions and 4 deletions

View File

@ -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

View File

@ -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)

View File

@ -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)