diff --git a/runtime/doc/index.txt b/runtime/doc/index.txt index 0c46eecf87..493e00c956 100644 --- a/runtime/doc/index.txt +++ b/runtime/doc/index.txt @@ -1420,6 +1420,7 @@ Tag Command Action ~ |:lpfile| :lpf[ile] go to last location in previous file |:lrewind| :lr[ewind] go to the specified location, default first one |:ls| :ls list all buffers +|:lsp| :lsp language server protocol |:ltag| :lt[ag] jump to tag and add matching tags to the location list |:lunmap| :lu[nmap] like ":unmap!" but includes Lang-Arg mode diff --git a/runtime/doc/lsp.txt b/runtime/doc/lsp.txt index 4f3392e187..82e09758de 100644 --- a/runtime/doc/lsp.txt +++ b/runtime/doc/lsp.txt @@ -116,6 +116,22 @@ To remove or override BUFFER-LOCAL defaults, define a |LspAttach| handler: >lua end, }) < + +COMMANDS *:lsp* + +:lsp restart {names} *:lsp-restart* + Restarts the given language servers. If no names are given, all active + servers are restarted. + +:lsp start {names} *:lsp-start* + Starts the given language servers. If no names are given, all configured + with |vim.lsp.config()| with filetype matching the current buffer are + started. + +:lsp stop {names} *:lsp-stop* + Stops and disables the given language servers. If no names are given, all + servers on the current buffer are stopped. + ============================================================================== CONFIG *lsp-config* @@ -1023,12 +1039,6 @@ enable({name}, {enable}) *vim.lsp.enable()* vim.lsp.enable({'lua_ls', 'pyright'}) < - Example: *lsp-restart* Passing `false` stops and detaches the client(s). - Thus you can "restart" LSP by disabling and re-enabling a given config: >lua - vim.lsp.enable('clangd', false) - vim.lsp.enable('clangd', true) -< - Example: To dynamically decide whether LSP is activated, define a |lsp-root_dir()| function which calls `on_dir()` only when you want that config to activate: >lua diff --git a/runtime/doc/news.txt b/runtime/doc/news.txt index d83f58318d..4259f035f1 100644 --- a/runtime/doc/news.txt +++ b/runtime/doc/news.txt @@ -291,6 +291,7 @@ LSP • Support for `workspace/diagnostic/refresh`: https://microsoft.github.io/language-server-protocol/specification/#diagnostic_refresh - Support for dynamic registration for `textDocument/diagnostic` +• |:lsp| command to restart/stop LSP servers. LUA diff --git a/runtime/doc/vim_diff.txt b/runtime/doc/vim_diff.txt index 11e4500310..a3c8ce9973 100644 --- a/runtime/doc/vim_diff.txt +++ b/runtime/doc/vim_diff.txt @@ -307,6 +307,7 @@ Commands: - |:EditQuery| - |:Inspect| - |:InspectTree| +- |:lsp| - |:Man| is available by default, with many improvements such as completion - |:match| can be invoked before highlight group is defined - |:restart| diff --git a/runtime/lua/vim/lsp.lua b/runtime/lua/vim/lsp.lua index 233bbe3f3d..b06fd6041c 100644 --- a/runtime/lua/vim/lsp.lua +++ b/runtime/lua/vim/lsp.lua @@ -528,14 +528,6 @@ end --- vim.lsp.enable({'lua_ls', 'pyright'}) --- ``` --- ---- Example: [lsp-restart]() Passing `false` stops and detaches the client(s). Thus you can ---- "restart" LSP by disabling and re-enabling a given config: ---- ---- ```lua ---- vim.lsp.enable('clangd', false) ---- vim.lsp.enable('clangd', true) ---- ``` ---- --- Example: To _dynamically_ decide whether LSP is activated, define a |lsp-root_dir()| function --- which calls `on_dir()` only when you want that config to activate: --- diff --git a/runtime/lua/vim/lsp/_cmd.lua b/runtime/lua/vim/lsp/_cmd.lua new file mode 100644 index 0000000000..91b9b365f1 --- /dev/null +++ b/runtime/lua/vim/lsp/_cmd.lua @@ -0,0 +1,142 @@ +local lsp = vim.lsp + +local M = {} + +--- @param filter? vim.lsp.get_clients.Filter +--- @return string[] +local function get_client_names(filter) + return vim + .iter(lsp.get_clients(filter)) + :map(function(client) + return client.name + end) + :filter(function(name) + return vim.lsp.config[name] ~= nil + end) + :totable() +end + +---@return string[] +local function get_config_names() + local config_names = vim + .iter(vim.api.nvim_get_runtime_file('lsp/*.lua', true)) + ---@param path string + :map(function(path) + local file_name = path:match('[^/]*.lua$') + return file_name:sub(0, #file_name - 4) + end) + :totable() + + ---@diagnostic disable-next-line: invisible + vim.list_extend(config_names, vim.tbl_keys(vim.lsp.config._configs)) + return vim.list.unique(config_names) +end + +local complete_args = { + start = get_config_names, + stop = get_client_names, + restart = get_client_names, +} + +local function ex_lsp_start(servers) + -- Default to enabling all servers matching the filetype of the current buffer. + -- This assumes that they've been explicitly configured through `vim.lsp.config`, + -- otherwise they won't be present in the private `vim.lsp.config._configs` table. + if #servers == 0 then + local filetype = vim.bo.filetype + ---@diagnostic disable-next-line: invisible + for name, _ in pairs(vim.lsp.config._configs) do + local filetypes = vim.lsp.config[name].filetypes + if filetypes and vim.tbl_contains(filetypes, filetype) then + table.insert(servers, name) + end + end + end + + vim.lsp.enable(servers) +end + +---@param clients string[] +local function ex_lsp_stop(clients) + -- Default to disabling all servers on current buffer + if #clients == 0 then + clients = get_client_names { bufnr = vim.api.nvim_get_current_buf() } + end + + for _, name in ipairs(clients) do + if vim.lsp.config[name] == nil then + vim.notify(("Invalid server name '%s'"):format(name)) + else + vim.lsp.enable(name, false) + end + end +end + +---@param clients string[] +local function ex_lsp_restart(clients) + -- Default to restarting all active servers + if #clients == 0 then + clients = get_client_names() + end + + for _, name in ipairs(clients) do + if vim.lsp.config[name] == nil then + vim.notify(("Invalid server name '%s'"):format(name)) + else + vim.lsp.enable(name, false) + end + end + + local timer = assert(vim.uv.new_timer()) + timer:start(500, 0, function() + for _, name in ipairs(clients) do + vim.schedule_wrap(function(x) + vim.lsp.enable(x) + end)(name) + end + end) +end + +local actions = { + start = ex_lsp_start, + restart = ex_lsp_restart, + stop = ex_lsp_stop, +} + +local available_subcmds = vim.tbl_keys(actions) + +--- Use for `:lsp {subcmd} {clients}` command +---@param args string +M._ex_lsp = function(args) + local fargs = vim.api.nvim_parse_cmd('lsp ' .. args, {}).args + if not fargs then + return + end + local subcmd = fargs[1] + if not vim.list_contains(available_subcmds, subcmd) then + vim.notify(("Invalid subcommand '%s'"):format(subcmd), vim.log.levels.ERROR) + return + end + + local clients = { unpack(fargs, 2) } + + actions[subcmd](clients) +end + +--- Completion logic for `:lsp` command +--- @param line string content of the current command line +--- @return string[] list of completions +function M._ex_lsp_complete(line) + local splited = vim.split(line, '%s+') + if #splited == 2 then + return available_subcmds + else + local subcmd = splited[2] + ---@param n string + return vim.tbl_map(function(n) + return vim.fn.escape(n, [[" |]]) + end, complete_args[subcmd]()) + end +end + +return M diff --git a/src/nvim/cmdexpand.c b/src/nvim/cmdexpand.c index 4cc4413380..4f6be69c3b 100644 --- a/src/nvim/cmdexpand.c +++ b/src/nvim/cmdexpand.c @@ -1301,6 +1301,7 @@ char *addstar(char *fname, size_t len, int context) || ((context == EXPAND_TAGS_LISTFILES || context == EXPAND_TAGS) && fname[0] == '/') || context == EXPAND_CHECKHEALTH + || context == EXPAND_LSP || context == EXPAND_LUA) { retval = xstrnsave(fname, len); } else { @@ -2311,6 +2312,10 @@ static const char *set_context_by_cmdname(const char *cmd, cmdidx_T cmdidx, expa xp->xp_context = EXPAND_CHECKHEALTH; break; + case CMD_lsp: + xp->xp_context = EXPAND_LSP; + break; + case CMD_retab: xp->xp_context = EXPAND_RETAB; xp->xp_pattern = (char *)arg; @@ -2849,6 +2854,43 @@ static char *get_healthcheck_names(expand_T *xp FUNC_ATTR_UNUSED, int idx) return NULL; } +/// Completion for |:lsp| command. +/// +/// Given to ExpandGeneric() to obtain `:lsp` completion. +/// @param[in] idx Index of the item. +/// @param[in] xp Not used. +static char *get_lsp_arg(expand_T *xp FUNC_ATTR_UNUSED, int idx) +{ + static Object names = OBJECT_INIT; + static char *last_xp_line = NULL; + static unsigned last_gen = 0; + + if (last_xp_line == NULL || strcmp(last_xp_line, + xp->xp_line) != 0 + || last_gen != get_cmdline_last_prompt_id()) { + xfree(last_xp_line); + last_xp_line = xstrdup(xp->xp_line); + MAXSIZE_TEMP_ARRAY(args, 1); + Error err = ERROR_INIT; + + ADD_C(args, CSTR_AS_OBJ(xp->xp_line)); + // Build the current command line as a Lua string argument + Object res = NLUA_EXEC_STATIC("return require'vim.lsp._cmd'._ex_lsp_complete(...)", args, + kRetObject, NULL, + &err); + api_clear_error(&err); + api_free_object(names); + names = res; + last_gen = get_cmdline_last_prompt_id(); + } + + if (names.type == kObjectTypeArray && idx < (int)names.data.array.size + && names.data.array.items[idx].type == kObjectTypeString) { + return names.data.array.items[idx].data.string.data; + } + return NULL; +} + /// Do the expansion based on xp->xp_context and "rmp". static int ExpandOther(char *pat, expand_T *xp, regmatch_T *rmp, char ***matches, int *numMatches) { @@ -2891,6 +2933,7 @@ static int ExpandOther(char *pat, expand_T *xp, regmatch_T *rmp, char ***matches { EXPAND_SCRIPTNAMES, get_scriptnames_arg, true, false }, { EXPAND_RETAB, get_retab_arg, true, true }, { EXPAND_CHECKHEALTH, get_healthcheck_names, true, false }, + [32] = { EXPAND_LSP, get_lsp_arg, true, false }, }; int ret = FAIL; diff --git a/src/nvim/cmdexpand_defs.h b/src/nvim/cmdexpand_defs.h index 454160e696..e95792f9ca 100644 --- a/src/nvim/cmdexpand_defs.h +++ b/src/nvim/cmdexpand_defs.h @@ -117,6 +117,7 @@ enum { EXPAND_RETAB, EXPAND_CHECKHEALTH, EXPAND_LUA, + EXPAND_LSP, }; /// Type used by ExpandGeneric() diff --git a/src/nvim/ex_cmds.lua b/src/nvim/ex_cmds.lua index 079c440438..1b5ce946d8 100644 --- a/src/nvim/ex_cmds.lua +++ b/src/nvim/ex_cmds.lua @@ -1670,6 +1670,12 @@ M.cmds = { addr_type = 'ADDR_NONE', func = 'buflist_list', }, + { + command = 'lsp', + flags = bit.bor(NEEDARG, EXTRA, TRLBAR), + addr_type = 'ADDR_NONE', + func = 'ex_lsp', + }, { command = 'move', flags = bit.bor(RANGE, WHOLEFOLD, EXTRA, TRLBAR, CMDWIN, LOCK_OK, MODIFY), diff --git a/src/nvim/ex_docmd.c b/src/nvim/ex_docmd.c index fc7681535c..bf9c5e3b78 100644 --- a/src/nvim/ex_docmd.c +++ b/src/nvim/ex_docmd.c @@ -8048,6 +8048,18 @@ static void ex_terminal(exarg_T *eap) do_cmdline_cmd(ex_cmd); } +/// ":lsp {subcmd} {clients}" +static void ex_lsp(exarg_T *eap) +{ + Error err = ERROR_INIT; + MAXSIZE_TEMP_ARRAY(args, 1); + + ADD_C(args, CSTR_AS_OBJ(eap->arg)); + + NLUA_EXEC_STATIC("require'vim.lsp._cmd'._ex_lsp(...)", args, kRetNilBool, NULL, &err); + api_clear_error(&err); +} + /// ":fclose" static void ex_fclose(exarg_T *eap) { diff --git a/test/old/testdir/test_cmdline.vim b/test/old/testdir/test_cmdline.vim index 0a2b7b6141..18cce8e25f 100644 --- a/test/old/testdir/test_cmdline.vim +++ b/test/old/testdir/test_cmdline.vim @@ -1289,7 +1289,7 @@ func Test_cmdline_complete_various() " completion for a command with a trailing command call feedkeys(":ls | ls\\\"\", 'xt') - call assert_equal("\"ls | ls", @:) + call assert_equal("\"ls | ls lsp", @:) " completion for a command with an CTRL-V escaped argument call feedkeys(":ls \\a\\\"\", 'xt')