neovim

Neovim text editor
git clone https://git.dasho.dev/neovim.git
Log | Files | Refs | README

commit 2495015455f74d3e4f0cce8023941deddf0be189
parent b6b793634a63dce403bd0ee2e7d7972f15d79294
Author: Justin M. Keyes <justinkz@gmail.com>
Date:   Sun, 20 Jul 2025 12:41:35 -0400

Merge #35002 refactor(lsp): extract `Client._on_detach` to reduce duplicated code


Diffstat:
Mruntime/lua/vim/lsp.lua | 127++++++++-----------------------------------------------------------------------
Mruntime/lua/vim/lsp/client.lua | 85+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 97 insertions(+), 115 deletions(-)

diff --git a/runtime/lua/vim/lsp.lua b/runtime/lua/vim/lsp.lua @@ -84,9 +84,6 @@ function lsp._buf_get_line_ending(bufnr) return format_line_ending[vim.bo[bufnr].fileformat] or '\n' end --- Tracks all clients created via lsp.start_client -local all_clients = {} --- @type table<integer,vim.lsp.Client> - local client_errors_base = table.maxn(lsp.rpc.client_errors) local client_errors_offset = 0 @@ -175,78 +172,6 @@ local function reuse_client_default(client, config) return true end ---- Reset defaults set by `set_defaults`. ---- Must only be called if the last client attached to a buffer exits. -local function reset_defaults(bufnr) - if vim.bo[bufnr].tagfunc == 'v:lua.vim.lsp.tagfunc' then - vim.bo[bufnr].tagfunc = nil - end - if vim.bo[bufnr].omnifunc == 'v:lua.vim.lsp.omnifunc' then - vim.bo[bufnr].omnifunc = nil - end - if vim.bo[bufnr].formatexpr == 'v:lua.vim.lsp.formatexpr()' then - vim.bo[bufnr].formatexpr = nil - end - vim._with({ buf = bufnr }, function() - local keymap = vim.fn.maparg('K', 'n', false, true) - if keymap and keymap.callback == vim.lsp.buf.hover and keymap.buffer == 1 then - vim.keymap.del('n', 'K', { buffer = bufnr }) - end - end) -end - ---- @param code integer ---- @param signal integer ---- @param client_id integer -local function on_client_exit(code, signal, client_id) - local client = all_clients[client_id] - - vim.schedule(function() - for bufnr in pairs(client.attached_buffers) do - if client and client.attached_buffers[bufnr] and api.nvim_buf_is_valid(bufnr) then - api.nvim_exec_autocmds('LspDetach', { - buffer = bufnr, - modeline = false, - data = { client_id = client_id }, - }) - end - - client.attached_buffers[bufnr] = nil - - if #lsp.get_clients({ bufnr = bufnr, _uninitialized = true }) == 0 then - reset_defaults(bufnr) - end - end - - local namespace = vim.lsp.diagnostic.get_namespace(client_id) - vim.diagnostic.reset(namespace) - end) - - local name = client.name or 'unknown' - - -- Schedule the deletion of the client object so that it exists in the execution of LspDetach - -- autocommands - vim.schedule(function() - all_clients[client_id] = nil - - -- Client can be absent if executable starts, but initialize fails - -- init/attach won't have happened - if client then - changetracking.reset(client) - end - if code ~= 0 or (signal ~= 0 and signal ~= 15) then - local msg = string.format( - 'Client %s quit with exit code %s and signal %s. Check log for errors: %s', - name, - code, - signal, - lsp.get_log_path() - ) - vim.notify(msg, vim.log.levels.WARN) - end - end) -end - --- Creates and initializes a client with the given configuration. --- @param config vim.lsp.ClientConfig Configuration for the server. --- @return integer? client_id |vim.lsp.get_client_by_id()| Note: client may not be @@ -261,11 +186,6 @@ local function create_and_init_client(config) local client = assert(res) - --- @diagnostic disable-next-line: invisible - table.insert(client._on_exit_cbs, on_client_exit) - - all_clients[client.id] = client - client:initialize() return client.id, nil @@ -743,7 +663,7 @@ function lsp.start(config, opts) return end - for _, client in pairs(all_clients) do + for _, client in pairs(lsp.client._all) do if reuse_client(client, config) then if opts.attach == false then return client.id @@ -914,29 +834,6 @@ local function text_document_did_save_handler(bufnr) end end ----@param bufnr integer resolved buffer ----@param client vim.lsp.Client -local function buf_detach_client(bufnr, client) - api.nvim_exec_autocmds('LspDetach', { - buffer = bufnr, - modeline = false, - data = { client_id = client.id }, - }) - - changetracking.reset_buf(client, bufnr) - - if client:supports_method(ms.textDocument_didClose) then - local uri = vim.uri_from_bufnr(bufnr) - local params = { textDocument = { uri = uri } } - client:notify(ms.textDocument_didClose, params) - end - - client.attached_buffers[bufnr] = nil - - local namespace = lsp.diagnostic.get_namespace(client.id) - vim.diagnostic.reset(namespace, bufnr) -end - --- @type table<integer,true> local attached_buffers = {} @@ -1013,7 +910,7 @@ local function buf_attach(bufnr) on_detach = function() local clients = lsp.get_clients({ bufnr = bufnr, _uninitialized = true }) for _, client in ipairs(clients) do - buf_detach_client(bufnr, client) + client:_on_detach(bufnr) end attached_buffers[bufnr] = nil util.buf_versions[bufnr] = nil @@ -1076,7 +973,7 @@ function lsp.buf_detach_client(bufnr, client_id) validate('client_id', client_id, 'number') bufnr = vim._resolve_bufnr(bufnr) - local client = all_clients[client_id] + local client = lsp.get_client_by_id(client_id) if not client or not client.attached_buffers[bufnr] then vim.notify( string.format( @@ -1087,7 +984,7 @@ function lsp.buf_detach_client(bufnr, client_id) ) return else - buf_detach_client(bufnr, client) + client:_on_detach(bufnr) end end @@ -1106,7 +1003,7 @@ end --- ---@return vim.lsp.Client? client rpc object function lsp.get_client_by_id(client_id) - return all_clients[client_id] + return lsp.client._all[client_id] end --- Returns list of buffers attached to client_id. @@ -1114,7 +1011,7 @@ end ---@param client_id integer client id ---@return integer[] buffers list of buffer ids function lsp.get_buffers_by_client_id(client_id) - local client = all_clients[client_id] + local client = lsp.get_client_by_id(client_id) return client and vim.tbl_keys(client.attached_buffers) or {} end @@ -1142,7 +1039,7 @@ function lsp.stop_client(client_id, force) end else --- @cast id -vim.lsp.Client - local client = all_clients[id] + local client = lsp.get_client_by_id(id) if client then client:stop(force) end @@ -1184,7 +1081,7 @@ function lsp.get_clients(filter) local bufnr = filter.bufnr and vim._resolve_bufnr(filter.bufnr) - for _, client in pairs(all_clients) do + for _, client in pairs(lsp.client._all) do if client and (filter.id == nil or client.id == filter.id) @@ -1210,7 +1107,7 @@ api.nvim_create_autocmd('VimLeavePre', { callback = function() local active_clients = lsp.get_clients() log.info('exit_handler', active_clients) - for _, client in pairs(all_clients) do + for _, client in pairs(lsp.client._all) do client:stop() end @@ -1308,8 +1205,8 @@ function lsp.buf_request(bufnr, method, params, handler, on_unsupported) local function _cancel_all_requests() for client_id, request_id in pairs(client_request_ids) do - local client = all_clients[client_id] - if client.requests[request_id] then + local client = lsp.get_client_by_id(client_id) + if client and client.requests[request_id] then client:cancel_request(request_id) end end @@ -1571,7 +1468,7 @@ end function lsp.client_is_stopped(client_id) vim.deprecate('vim.lsp.client_is_stopped()', 'vim.lsp.get_client_by_id()', '0.14') assert(client_id, 'missing client_id param') - return not all_clients[client_id] + return not lsp.get_client_by_id(client_id) end --- Gets a map of client_id:client pairs for the given buffer, where each value diff --git a/runtime/lua/vim/lsp/client.lua b/runtime/lua/vim/lsp/client.lua @@ -6,6 +6,10 @@ local ms = lsp.protocol.Methods local changetracking = lsp._changetracking local validate = vim.validate +--- Tracks all clients initialized. +---@type table<integer,vim.lsp.Client> +local all_clients = {} + --- @alias vim.lsp.client.on_init_cb fun(client: vim.lsp.Client, init_result: lsp.InitializeResult) --- @alias vim.lsp.client.on_attach_cb fun(client: vim.lsp.Client, bufnr: integer) --- @alias vim.lsp.client.on_exit_cb fun(code: integer, signal: integer, client_id: integer) @@ -493,6 +497,9 @@ end --- @nodoc function Client:initialize() + -- Register all initialized clients. + all_clients[self.id] = self + local config = self.config local root_uri --- @type string? @@ -1186,12 +1193,87 @@ function Client:_on_error(code, err) end end +---@param bufnr integer resolved buffer +function Client:_on_detach(bufnr) + if self.attached_buffers[bufnr] and api.nvim_buf_is_valid(bufnr) then + api.nvim_exec_autocmds('LspDetach', { + buffer = bufnr, + modeline = false, + data = { client_id = self.id }, + }) + end + + changetracking.reset_buf(self, bufnr) + + if self:supports_method(ms.textDocument_didClose) then + local uri = vim.uri_from_bufnr(bufnr) + local params = { textDocument = { uri = uri } } + self:notify(ms.textDocument_didClose, params) + end + + self.attached_buffers[bufnr] = nil + + local namespace = lsp.diagnostic.get_namespace(self.id) + vim.diagnostic.reset(namespace, bufnr) +end + +--- Reset defaults set by `set_defaults`. +--- Must only be called if the last client attached to a buffer exits. +local function reset_defaults(bufnr) + if vim.bo[bufnr].tagfunc == 'v:lua.vim.lsp.tagfunc' then + vim.bo[bufnr].tagfunc = nil + end + if vim.bo[bufnr].omnifunc == 'v:lua.vim.lsp.omnifunc' then + vim.bo[bufnr].omnifunc = nil + end + if vim.bo[bufnr].formatexpr == 'v:lua.vim.lsp.formatexpr()' then + vim.bo[bufnr].formatexpr = nil + end + vim._with({ buf = bufnr }, function() + local keymap = vim.fn.maparg('K', 'n', false, true) + if keymap and keymap.callback == vim.lsp.buf.hover and keymap.buffer == 1 then + vim.keymap.del('n', 'K', { buffer = bufnr }) + end + end) +end + --- @private --- Invoked on client exit. --- --- @param code integer) exit code of the process --- @param signal integer the signal used to terminate (if any) function Client:_on_exit(code, signal) + vim.schedule(function() + for bufnr in pairs(self.attached_buffers) do + self:_on_detach(bufnr) + if #lsp.get_clients({ bufnr = bufnr, _uninitialized = true }) == 0 then + reset_defaults(bufnr) + end + end + end) + + -- Schedule the deletion of the client object so that it exists in the execution of LspDetach + -- autocommands + vim.schedule(function() + all_clients[self.id] = nil + + -- Client can be absent if executable starts, but initialize fails + -- init/attach won't have happened + if self then + changetracking.reset(self) + end + if code ~= 0 or (signal ~= 0 and signal ~= 15) then + local msg = string.format( + 'Client %s quit with exit code %s and signal %s. Check log for errors: %s', + self and self.name or 'unknown', + code, + signal, + lsp.get_log_path() + ) + vim.notify(msg, vim.log.levels.WARN) + end + end) + self:_run_callbacks( self._on_exit_cbs, lsp.client_errors.ON_EXIT_CALLBACK_ERROR, @@ -1240,4 +1322,7 @@ function Client:_remove_workspace_folder(dir) end end +-- Export for internal use only. +Client._all = all_clients + return Client