diff --git a/doc/lspconfig.txt b/doc/lspconfig.txt index db94a7106b..f722193399 100644 --- a/doc/lspconfig.txt +++ b/doc/lspconfig.txt @@ -75,6 +75,9 @@ the following added keys: - {single_file_support} (`bool`) (default: nil) Determines if a server is started without a matching root directory. See |lspconfig-single-file-support|. +- {silent} (`bool`) (default: false) Whether to suppress error reporting if the + LSP server fails to start. + - {on_new_config} (`function(new_config, new_root_dir)`) Function executed after a root directory is detected. This is used to modify the server configuration (including `cmd` itself). Most commonly, this is used to diff --git a/lua/lspconfig/configs.lua b/lua/lspconfig/configs.lua index 77d078626f..2a19991f6a 100644 --- a/lua/lspconfig/configs.lua +++ b/lua/lspconfig/configs.lua @@ -8,6 +8,7 @@ local configs = {} --- @class lspconfig.Config : vim.lsp.ClientConfig --- @field enabled? boolean --- @field single_file_support? boolean +--- @field silent? boolean --- @field filetypes? string[] --- @field filetype? string --- @field on_new_config? fun(new_config: lspconfig.Config?, new_root_dir: string) @@ -105,7 +106,7 @@ function configs.__newindex(t, config_name, config_def) api.nvim_create_autocmd(event_conf.event, { pattern = event_conf.pattern or '*', callback = function(opt) - M.manager:try_add(opt.buf) + M.manager:try_add(opt.buf, nil, config.silent) end, group = lsp_group, desc = string.format( @@ -176,7 +177,7 @@ function configs.__newindex(t, config_name, config_def) return end local pseudo_root = #bufname == 0 and pwd or util.path.dirname(util.path.sanitize(bufname)) - M.manager:add(pseudo_root, true, bufnr) + M.manager:add(pseudo_root, true, bufnr, config.silent) end end) end diff --git a/lua/lspconfig/manager.lua b/lua/lspconfig/manager.lua index 7d6fb88cfb..d5db2833b9 100644 --- a/lua/lspconfig/manager.lua +++ b/lua/lspconfig/manager.lua @@ -8,7 +8,7 @@ local util = require 'lspconfig.util' ---@param client vim.lsp.Client ---@param root_dir string ---@return boolean -local function check_in_workspace(client, root_dir) +local function is_dir_in_workspace_folders(client, root_dir) if not client.workspace_folders then return false end @@ -23,7 +23,7 @@ local function check_in_workspace(client, root_dir) end --- @class lspconfig.Manager ---- @field _clients table +--- @field _clients table> root dir -> (client name -> client) --- @field config lspconfig.Config --- @field make_config fun(root_dir: string): lspconfig.Config local M = {} @@ -42,66 +42,42 @@ function M.new(config, make_config) end --- @private ---- @param clients table ---- @param root_dir string ---- @param client_name string ---- @return vim.lsp.Client? -local function get_client(clients, root_dir, client_name) - if vim.tbl_isempty(clients) then - return - end - - if clients[root_dir] then - for _, id in pairs(clients[root_dir]) do - local client = lsp.get_client_by_id(id) - if client and client.name == client_name then - return client - end - end - end - - for _, ids in pairs(clients) do - for _, id in ipairs(ids) do - local client = lsp.get_client_by_id(id) - if client and client.name == client_name then - return client - end - end - end -end - ---- @private ---- @param bufnr integer --- @param root string ---- @param client_id integer -function M:_attach_and_cache(bufnr, root, client_id) +--- @param client vim.lsp.Client +function M:_cache_client(root, client) local clients = self._clients - lsp.buf_attach_client(bufnr, client_id) if not clients[root] then clients[root] = {} end - if not vim.tbl_contains(clients[root], client_id) then - clients[root][#clients[root] + 1] = client_id + if not clients[root][client.name] then + clients[root][client.name] = client end end --- @private ---- @param bufnr integer --- @param root_dir string --- @param client vim.lsp.Client -function M:_register_workspace_folders(bufnr, root_dir, client) +function M:_notify_workspace_folder_added(root_dir, client) + if is_dir_in_workspace_folders(client, root_dir) then + return + end + + local supported = vim.tbl_get(client, 'server_capabilities', 'workspace', 'workspaceFolders', 'supported') + if not supported then + return + end + local params = { event = { added = { { uri = vim.uri_from_fname(root_dir), name = root_dir } }, removed = {}, }, } - client.rpc.notify('workspace/didChangeWorkspaceFolders', params) + client.rpc.notify(lsp.protocol.Methods.workspace_didChangeWorkspaceFolders, params) if not client.workspace_folders then client.workspace_folders = {} end client.workspace_folders[#client.workspace_folders + 1] = params.event.added[1] - self:_attach_and_cache(bufnr, root_dir, client.id) end --- @private @@ -109,7 +85,8 @@ end --- @param new_config lspconfig.Config --- @param root_dir string --- @param single_file boolean -function M:_start_new_client(bufnr, new_config, root_dir, single_file) +--- @param silent boolean +function M:_start_client(bufnr, new_config, root_dir, single_file, silent) -- do nothing if the client is not enabled if new_config.enabled == false then return @@ -125,13 +102,14 @@ function M:_start_new_client(bufnr, new_config, root_dir, single_file) return end - local clients = self._clients + new_config.on_init = util.add_hook_before(new_config.on_init, function(client) + self:_notify_workspace_folder_added(root_dir, client) + end) new_config.on_exit = util.add_hook_before(new_config.on_exit, function() - for index, id in pairs(clients[root_dir]) do - local exist = assert(lsp.get_client_by_id(id)) - if exist.name == new_config.name then - table.remove(clients[root_dir], index) + for name in pairs(self._clients[root_dir]) do + if name == new_config.name then + self._clients[root_dir][name] = nil end end end) @@ -150,88 +128,46 @@ function M:_start_new_client(bufnr, new_config, root_dir, single_file) new_config.workspace_folders = nil end - -- TODO: Replace lsp.start_client with lsp.start - local client_id, err = lsp.start_client(new_config) - if not client_id then - if err then - vim.notify(err, vim.log.levels.WARN) - end - return - end - self:_attach_and_cache(bufnr, root_dir, client_id) -end + local client_id = lsp.start(new_config, { + bufnr = bufnr, + silent = silent, + reuse_client = function(existing_client) + if (self._clients[root_dir] or {})[existing_client.name] then + self:_notify_workspace_folder_added(root_dir, existing_client) + return true + end ---- @private ---- @param bufnr integer ---- @param new_config lspconfig.Config ---- @param root_dir string ---- @param client vim.lsp.Client ---- @param single_file boolean -function M:_attach_or_spawn(bufnr, new_config, root_dir, client, single_file) - if check_in_workspace(client, root_dir) then - return self:_attach_and_cache(bufnr, root_dir, client.id) - end + for _, dir_clients in pairs(self._clients) do + if dir_clients[existing_client.name] then + self:_notify_workspace_folder_added(root_dir, existing_client) + return true + end + end - local supported = vim.tbl_get(client, 'server_capabilities', 'workspace', 'workspaceFolders', 'supported') - if supported then - return self:_register_workspace_folders(bufnr, root_dir, client) + return false + end, + }) + if client_id then + self:_cache_client(root_dir, assert(lsp.get_client_by_id(client_id))) end - self:_start_new_client(bufnr, new_config, root_dir, single_file) -end - ---- @private ---- @param bufnr integer ---- @param new_config lspconfig.Config ---- @param root_dir string ---- @param client vim.lsp.Client ---- @param single_file boolean -function M:_attach_after_client_initialized(bufnr, new_config, root_dir, client, single_file) - local timer = assert(uv.new_timer()) - timer:start( - 0, - 10, - vim.schedule_wrap(function() - if client.initialized and client.server_capabilities and not timer:is_closing() then - self:_attach_or_spawn(bufnr, new_config, root_dir, client, single_file) - timer:stop() - timer:close() - end - end) - ) end ---@param root_dir string ---@param single_file boolean ---@param bufnr integer -function M:add(root_dir, single_file, bufnr) +---@param silent boolean +function M:add(root_dir, single_file, bufnr, silent) root_dir = util.path.sanitize(root_dir) local new_config = self.make_config(root_dir) - local client = get_client(self._clients, root_dir, new_config.name) - - if not client then - return self:_start_new_client(bufnr, new_config, root_dir, single_file) - end - - if self._clients[root_dir] or single_file then - lsp.buf_attach_client(bufnr, client.id) - return - end - - -- make sure neovim had exchanged capabilities from language server - -- it's useful to check server support workspaceFolders or not - if client.initialized and client.server_capabilities then - self:_attach_or_spawn(bufnr, new_config, root_dir, client, single_file) - else - self:_attach_after_client_initialized(bufnr, new_config, root_dir, client, single_file) - end + self:_start_client(bufnr, new_config, root_dir, single_file, silent) end --- @return vim.lsp.Client[] function M:clients() local res = {} - for _, client_ids in pairs(self._clients) do - for _, id in ipairs(client_ids) do - res[#res + 1] = lsp.get_client_by_id(id) + for _, dir_clients in pairs(self._clients) do + for _, client in pairs(dir_clients) do + res[#res + 1] = client end end return res @@ -241,7 +177,8 @@ end --- a new client if one doesn't already exist for `bufnr`. --- @param bufnr integer --- @param project_root? string -function M:try_add(bufnr, project_root) +--- @param silent boolean +function M:try_add(bufnr, project_root, silent) bufnr = bufnr or api.nvim_get_current_buf() if vim.bo[bufnr].buftype == 'nofile' then @@ -258,7 +195,7 @@ function M:try_add(bufnr, project_root) end if project_root then - self:add(project_root, false, bufnr) + self:add(project_root, false, bufnr, silent) return end @@ -281,10 +218,10 @@ function M:try_add(bufnr, project_root) end if root_dir then - self:add(root_dir, false, bufnr) + self:add(root_dir, false, bufnr, silent) elseif self.config.single_file_support then local pseudo_root = #bufname == 0 and pwd or util.path.dirname(buf_path) - self:add(pseudo_root, true, bufnr) + self:add(pseudo_root, true, bufnr, silent) end end) end @@ -297,7 +234,7 @@ function M:try_add_wrapper(bufnr, project_root) local config = self.config -- `config.filetypes = nil` means all filetypes are valid. if not config.filetypes or vim.tbl_contains(config.filetypes, vim.bo[bufnr].filetype) then - self:try_add(bufnr, project_root) + self:try_add(bufnr, project_root, config.silent) end end