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:
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