diff options
-rw-r--r-- | runtime/doc/news.txt | 3 | ||||
-rw-r--r-- | runtime/lua/vim/_editor.lua | 64 | ||||
-rw-r--r-- | test/functional/lua/command_line_completion_spec.lua | 171 |
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) |