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

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