aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--runtime/doc/news.txt3
-rw-r--r--runtime/lua/vim/_editor.lua64
-rw-r--r--test/functional/lua/command_line_completion_spec.lua171
3 files changed, 234 insertions, 4 deletions
diff --git a/runtime/doc/news.txt b/runtime/doc/news.txt
index d8e113190a..d2e66f4596 100644
--- a/runtime/doc/news.txt
+++ b/runtime/doc/news.txt
@@ -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
diff --git a/runtime/lua/vim/_editor.lua b/runtime/lua/vim/_editor.lua
index bdca97cfb8..2e829578a7 100644
--- a/runtime/lua/vim/_editor.lua
+++ b/runtime/lua/vim/_editor.lua
@@ -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)
diff --git a/test/functional/lua/command_line_completion_spec.lua b/test/functional/lua/command_line_completion_spec.lua
index 2ba432133b..f8786a45bb 100644
--- a/test/functional/lua/command_line_completion_spec.lua
+++ b/test/functional/lua/command_line_completion_spec.lua
@@ -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)