local api = vim.api local fs = vim.fs local time = require('vim._core.time') local util = require('vim._core.util') local uv = vim.uv local N_ = vim.fn.gettext --- Parsed ex command arguments for builtin commands, passed from C via `nlua_call_excmd`. --- Inherits fields from user command args: name, args, bang, line1, line2, range, count, reg, smods. --- Note: For builtin commands `name` is the canonical command name. --- @class vim._core.ExCmdArgs : vim.api.keyset.create_user_command.command_args local M = {} --- Apply the `:filter[!] /pattern/` modifier to a single message. See also `message_filtered()`. --- --- @param filter { pattern: string, force: boolean }? ":filter" mod. --- @param msg string Message to test. --- @return boolean # True if `msg` should be skipped (not displayed). function M.filter(filter, msg) if not filter or filter.pattern == '' then return false end local match = vim.regex(filter.pattern):match_str(msg) ~= nil return match == filter.force end --- @return string[] local function get_client_names() return vim .iter(vim.lsp.get_clients()) :map(function(client) return client.name end) :unique() :totable() end --- @param filter vim.lsp.get_configs.Filter --- @return fun():string[] local function filtered_config_names(filter) return function() return vim .iter(vim.lsp.get_configs(filter)) :map(function(config) return config.name end) :totable() end end local complete_args = { enable = filtered_config_names { enabled = false }, disable = filtered_config_names { enabled = true }, restart = get_client_names, stop = get_client_names, } --- @param names string[] --- @param enable? boolean local function checked_enable(names, enable) for _, name in ipairs(names) do if name:find('*') == nil and vim.lsp.config[name] ~= nil then vim.lsp.enable(name, enable) else util.echo_err(N_('E5801: No client config named: %s'):format(name)) end end end --- @param config_names string[] local function ex_lsp_enable(config_names) -- Default to enabling all clients matching the filetype of the current buffer. if #config_names == 0 then local filetype = vim.bo.filetype for _, config in ipairs(vim.lsp.get_configs()) do local filetypes = config.filetypes if filetypes == nil or vim.list_contains(filetypes, filetype) then table.insert(config_names, config.name) end end if #config_names == 0 then if filetype == '' then util.echo_err(N_('E5802: Current buffer has no filetype')) else util.echo_err(N_('E5803: No configs for filetype: %s'):format(filetype)) end return end end checked_enable(config_names) end --- @param config_names string[] local function ex_lsp_disable(config_names) -- Default to disabling all clients attached to the current buffer. if #config_names == 0 then config_names = vim .iter(vim.lsp.get_clients { bufnr = api.nvim_get_current_buf() }) :map(function(client) return client.name end) :filter(function(name) return vim.lsp.config[name] ~= nil end) :totable() if #config_names == 0 then util.echo_err(N_('E5804: No configs with clients attached to current buffer')) return end end checked_enable(config_names, false) end --- @param client_names string[] --- @return vim.lsp.Client[] local function get_clients_from_names(client_names) -- Default to all active clients attached to the current buffer. if #client_names == 0 then local clients = vim.lsp.get_clients { bufnr = api.nvim_get_current_buf() } if #clients == 0 then util.echo_err(N_('E5805: No clients attached to current buffer')) end return clients else return vim .iter(client_names) :map(function(name) local clients = vim.lsp.get_clients { name = name } if #clients == 0 then util.echo_err(N_('E5806: No active clients matching name: %s'):format(name)) end return clients end) :flatten() :totable() end end --- @param client_names string[] local function ex_lsp_restart(client_names) local clients = get_clients_from_names(client_names) for _, client in ipairs(clients) do client:_restart(client.exit_timeout) end end --- @param client_names string[] local function ex_lsp_stop(client_names) local clients = get_clients_from_names(client_names) for _, client in ipairs(clients) do client:stop(client.exit_timeout) end end local actions = { enable = ex_lsp_enable, disable = ex_lsp_disable, restart = ex_lsp_restart, stop = ex_lsp_stop, } local available_subcmds = vim.tbl_keys(actions) --- Implements command: `:lsp {subcmd} {name}?`. --- @param eap vim._core.ExCmdArgs function M.ex_lsp(eap) local subcmd = eap.fargs[1] if not vim.list_contains(available_subcmds, subcmd) then util.echo_err(N_('E5800: Invalid :lsp subcommand: %s'):format(subcmd)) return end actions[subcmd]({ unpack(eap.fargs, 2) }) end --- Completion logic for `:lsp` command --- @param line string content of the current command line --- @return string[] list of completions function M.lsp_complete(_, line) local split = vim.split(line, '%s+') if #split == 2 then return available_subcmds else local subcmd = split[2] return vim .iter(complete_args[subcmd]()) :map(function(n) return vim.fn.escape(n, ' \t') end) :totable() end end --- @type string --- @diagnostic disable-next-line: assign-type-mismatch local log_dir = vim.fn.stdpath('log') --- Implements command: `:log {file}`. --- @param eap vim._core.ExCmdArgs function M.ex_log(eap) local filename = eap.args if filename == '' then util.wrapped_edit(log_dir, eap.smods) else local path --- @type string -- Special case for NVIM_LOG_FILE local nvim_log_file = vim.env.NVIM_LOG_FILE if filename == 'nvim' and nvim_log_file and nvim_log_file ~= '' then path = nvim_log_file else path = fs.joinpath(log_dir, filename .. '.log') end if not uv.fs_stat(path) then util.echo_err(N_('E5200: No such log file: %s'):format(path)) return end util.wrapped_edit(path, eap.smods) vim.cmd.normal { 'G', bang = true } end end --- Completion logic for `:log` command --- @return string[] completions function M.log_complete() local names = { 'nvim' } --- @type string[] for file, type in fs.dir(log_dir, { depth = vim._maxint }) do local name, matches = file:gsub('%.log$', '') if matches ~= 0 and type == 'file' and name ~= 'nvim' then names[#names + 1] = name end end return names end --- `:terminal [cmd]` --- @param eap vim._core.ExCmdArgs --- @param shell_argv? string[] Tokenized 'shell' from C (shell_build_argv), for the no-cmd case. function M.ex_terminal(eap, shell_argv) local smods = eap.smods local has_mods = (smods.tab or 0) > 0 or (smods.split or '') ~= '' or smods.horizontal or smods.vertical -- Use the invocation-time CWD, before creating a new buffer. local opts = { term = true, cwd = vim.fn.getcwd() } if has_mods then vim.cmd.new { mods = smods } else vim.cmd.enew { bang = eap.bang } end vim.fn.jobstart( shell_argv and shell_argv -- No `cmd`, run 'shell'. or eap.args, -- Run [cmd] in 'shell'. opts ) end function M.ex_uptime() -- os.time() might lead to uptime == -1 when this is called too quickly after startup local now = assert(uv.clock_gettime('realtime')) local uptime = math.floor((now.sec * 1e9 + now.nsec - vim.v.starttime) / 1e9) local uptime_display = time.fmt_rtime(uptime) api.nvim_echo({ { N_('Up %s'):format(uptime_display) } }, true) end --- `:oldfiles` and `:browse oldfiles`. Lists v:oldfiles (plain `:oldfiles`) or shows (async) --- vim.ui.select() picker (`:browse oldfiles`) and edits the chosen file. --- @param eap vim._core.ExCmdArgs function M.ex_oldfiles(eap) local files = vim.v.oldfiles if not files or #files == 0 then api.nvim_echo({ { N_('No old files') } }, false) return end if eap.smods.browse then vim.ui.select(files, { prompt = N_('Select an oldfile:'), kind = 'oldfiles', }, function(_, idx) if idx then api.nvim_cmd({ cmd = 'edit', args = { vim.fn.expand(assert(files[idx])) }, magic = { file = false, bar = true }, -- May contain '%' (e.g. swapfiles), don't expand. }, {}) end end) return end -- `:oldfiles`: list the entries. Honor `:filter /pat/[!]` per entry. local lines = {} ---@type [string][] for i, f in ipairs(files) do if not M.filter(eap.smods.filter, f) then lines[#lines + 1] = { ('%d: %s\n'):format(i, f) } end end if #lines == 0 then return end api.nvim_echo(lines, false) end --- Verify that all plugins in a list are installed. --- @param names string[] --- @return boolean success local function verify_plugin_list(names) local not_found = {} --- @type string[] local installed_plugin_names = vim.pack._get_names() for _, name in ipairs(names) do if not vim.list_contains(installed_plugin_names, name) then not_found[#not_found + 1] = name end end if #not_found ~= 0 then util.echo_err(N_('E5807: Plugin not installed: %s'):format(table.concat(not_found, ', '))) return false end return true end --- @param eap vim._core.ExCmdArgs function M.ex_packupdate(eap) local offline = false local target = nil --- @type string? local plugins = {} --- @type string[] for _, arg in ipairs(eap.fargs) do if not vim.startswith(arg, '++') then plugins[#plugins + 1] = arg elseif arg == '++offline' then offline = true elseif arg == '++lockfile' then target = 'lockfile' else util.echo_err(N_('E474: Invalid argument')) return end end if verify_plugin_list(plugins) then vim.pack.update( #plugins ~= 0 and plugins or nil, { force = eap.bang, offline = offline, target = target, _ex = true } ) end end --- @param pattern string --- @return string[] completions function M.packupdate_complete(pattern) if vim.startswith(pattern, '++') then return { '++lockfile', '++offline' } end return vim.pack._get_names() end --- @param eap vim._core.ExCmdArgs function M.ex_packdel(eap) local all = false local plugins = {} --- @type string[] for _, arg in ipairs(eap.fargs) do if not vim.startswith(arg, '++') then plugins[#plugins + 1] = arg elseif arg == '++all' then all = true else util.echo_err(N_('E474: Invalid argument')) return end end if all then if #plugins ~= 0 then util.echo_err(N_('E5811: Cannot specify plugin names when using ++all')) return end plugins = vim.pack._get_names(not eap.bang) end if all or verify_plugin_list(plugins) then vim.pack.del(plugins, { force = eap.bang, _ex = true }) end end --- @param pattern string --- @param line string content of the current command line --- @return string[] completions function M.packdel_complete(pattern, line) local cmd = api.nvim_parse_cmd(line) if #cmd.args == 1 and vim.startswith(pattern, '++') then return { '++all' } end if vim.list_contains(assert(cmd.args), '++all') then return {} end return vim.pack._get_names(not cmd.bang) end return M