From 3f238b39cfdf27657b2d9452c6ffd28f8209c95f Mon Sep 17 00:00:00 2001 From: Lewis Russell Date: Thu, 21 Mar 2024 15:15:20 +0000 Subject: refactor(lsp): simplify client tracking - Remove: - uninitialized_clients - active_clients - all_buffer_active_clients - Add: - all_clients - Use `lsp.get_clients()` to get buffer clients. --- runtime/lua/vim/lsp.lua | 338 +++++++++++++++++++++--------------------------- 1 file changed, 151 insertions(+), 187 deletions(-) (limited to 'runtime/lua/vim/lsp.lua') diff --git a/runtime/lua/vim/lsp.lua b/runtime/lua/vim/lsp.lua index d5c376ba44..b302e1543a 100644 --- a/runtime/lua/vim/lsp.lua +++ b/runtime/lua/vim/lsp.lua @@ -1,7 +1,5 @@ local api = vim.api -local tbl_isempty, tbl_extend = vim.tbl_isempty, vim.tbl_extend local validate = vim.validate -local if_nil = vim.F.if_nil local lsp = vim._defer_require('vim.lsp', { _changetracking = ..., --- @module 'vim.lsp._changetracking' @@ -108,40 +106,7 @@ function lsp._buf_get_line_ending(bufnr) end -- Tracks all clients created via lsp.start_client -local active_clients = {} --- @type table -local all_buffer_active_clients = {} --- @type table> -local uninitialized_clients = {} --- @type table - ----@param bufnr? integer ----@param fn fun(client: vim.lsp.Client, client_id: integer, bufnr: integer) -local function for_each_buffer_client(bufnr, fn, restrict_client_ids) - validate({ - fn = { fn, 'f' }, - restrict_client_ids = { restrict_client_ids, 't', true }, - }) - bufnr = resolve_bufnr(bufnr) - local client_ids = all_buffer_active_clients[bufnr] - if not client_ids or tbl_isempty(client_ids) then - return - end - - if restrict_client_ids and #restrict_client_ids > 0 then - local filtered_client_ids = {} --- @type table - for client_id in pairs(client_ids) do - if vim.list_contains(restrict_client_ids, client_id) then - filtered_client_ids[client_id] = true - end - end - client_ids = filtered_client_ids - end - - for client_id in pairs(client_ids) do - local client = active_clients[client_id] - if client then - fn(client, client_id, bufnr) - end - end -end +local all_clients = {} --- @type table local client_errors_base = table.maxn(lsp.rpc.client_errors) local client_errors_offset = 0 @@ -156,7 +121,7 @@ end --- Can be used to look up the string from a the number or the number --- from the string. --- @nodoc -lsp.client_errors = tbl_extend( +lsp.client_errors = vim.tbl_extend( 'error', lsp.rpc.client_errors, client_error('BEFORE_INIT_CALLBACK_ERROR'), @@ -258,12 +223,10 @@ function lsp.start(config, opts) local bufnr = resolve_bufnr(opts.bufnr) - for _, clients in ipairs({ uninitialized_clients, lsp.get_clients() }) do - for _, client in pairs(clients) do - if reuse_client(client, config) then - lsp.buf_attach_client(bufnr, client.id) - return client.id - end + for _, client in pairs(all_clients) do + if reuse_client(client, config) then + lsp.buf_attach_client(bufnr, client.id) + return client.id end end @@ -385,17 +348,11 @@ end --- @param client vim.lsp.Client local function on_client_init(client) - local id = client.id - uninitialized_clients[id] = nil - -- Only assign after initialized. - active_clients[id] = client -- If we had been registered before we start, then send didOpen This can -- happen if we attach to buffers before initialize finishes or if -- someone restarts a client. - for bufnr, client_ids in pairs(all_buffer_active_clients) do - if client_ids[id] then - client.on_attach(bufnr) - end + for bufnr in pairs(client.attached_buffers) do + client:_on_attach(bufnr) end end @@ -403,28 +360,26 @@ end --- @param signal integer --- @param client_id integer local function on_client_exit(code, signal, client_id) - local client = active_clients[client_id] or uninitialized_clients[client_id] - - for bufnr, client_ids in pairs(all_buffer_active_clients) do - if client_ids[client_id] then - vim.schedule(function() - if client and client.attached_buffers[bufnr] then - api.nvim_exec_autocmds('LspDetach', { - buffer = bufnr, - modeline = false, - data = { client_id = client_id }, - }) - end + local client = all_clients[client_id] + + for bufnr in pairs(client.attached_buffers) do + vim.schedule(function() + if client and client.attached_buffers[bufnr] then + api.nvim_exec_autocmds('LspDetach', { + buffer = bufnr, + modeline = false, + data = { client_id = client_id }, + }) + end - local namespace = vim.lsp.diagnostic.get_namespace(client_id) - vim.diagnostic.reset(namespace, bufnr) + local namespace = vim.lsp.diagnostic.get_namespace(client_id) + vim.diagnostic.reset(namespace, bufnr) + client.attached_buffers[bufnr] = nil - client_ids[client_id] = nil - if vim.tbl_isempty(client_ids) then - reset_defaults(bufnr) - end - end) - end + if #lsp.get_clients({ bufnr = bufnr, _uninitialized = true }) == 0 then + reset_defaults(bufnr) + end + end) end local name = client.name or 'unknown' @@ -432,8 +387,7 @@ local function on_client_exit(code, signal, client_id) -- Schedule the deletion of the client object so that it exists in the execution of LspDetach -- autocommands vim.schedule(function() - active_clients[client_id] = nil - uninitialized_clients[client_id] = nil + all_clients[client_id] = nil -- Client can be absent if executable starts, but initialize fails -- init/attach won't have happened @@ -466,12 +420,12 @@ function lsp.start_client(config) end --- @diagnostic disable-next-line: invisible - table.insert(client._on_init_cbs, on_client_init) + table.insert(client._on_init_cbs, 1, on_client_init) + --- @diagnostic disable-next-line: invisible table.insert(client._on_exit_cbs, on_client_exit) - -- Store the uninitialized_clients for cleanup in case we exit before initialize finishes. - uninitialized_clients[client.id] = client + all_clients[client.id] = client client:initialize() @@ -495,7 +449,7 @@ local function text_document_did_change_handler( new_lastline ) -- Detach (nvim_buf_attach) via returning True to on_lines if no clients are attached - if tbl_isempty(all_buffer_active_clients[bufnr] or {}) then + if #lsp.get_clients({ bufnr = bufnr }) == 0 then return true end util.buf_versions[bufnr] = changedtick @@ -543,6 +497,80 @@ local function text_document_did_save_handler(bufnr) end end +--- @param bufnr integer +--- @param client_id integer +local function buf_attach(bufnr, client_id) + local uri = vim.uri_from_bufnr(bufnr) + local augroup = ('lsp_c_%d_b_%d_save'):format(client_id, bufnr) + local group = api.nvim_create_augroup(augroup, { clear = true }) + api.nvim_create_autocmd('BufWritePre', { + group = group, + buffer = bufnr, + desc = 'vim.lsp: textDocument/willSave', + callback = function(ctx) + for _, client in ipairs(lsp.get_clients({ bufnr = ctx.buf })) do + local params = { + textDocument = { + uri = uri, + }, + reason = protocol.TextDocumentSaveReason.Manual, + } + if vim.tbl_get(client.server_capabilities, 'textDocumentSync', 'willSave') then + client.notify(ms.textDocument_willSave, params) + end + if vim.tbl_get(client.server_capabilities, 'textDocumentSync', 'willSaveWaitUntil') then + local result, err = + client.request_sync(ms.textDocument_willSaveWaitUntil, params, 1000, ctx.buf) + if result and result.result then + util.apply_text_edits(result.result, ctx.buf, client.offset_encoding) + elseif err then + log.error(vim.inspect(err)) + end + end + end + end, + }) + api.nvim_create_autocmd('BufWritePost', { + group = group, + buffer = bufnr, + desc = 'vim.lsp: textDocument/didSave handler', + callback = function(ctx) + text_document_did_save_handler(ctx.buf) + end, + }) + -- First time, so attach and set up stuff. + api.nvim_buf_attach(bufnr, false, { + on_lines = text_document_did_change_handler, + on_reload = function() + local params = { textDocument = { uri = uri } } + for _, client in ipairs(lsp.get_clients({ bufnr = bufnr })) do + changetracking.reset_buf(client, bufnr) + if vim.tbl_get(client.server_capabilities, 'textDocumentSync', 'openClose') then + client.notify(ms.textDocument_didClose, params) + end + client:_text_document_did_open_handler(bufnr) + end + end, + on_detach = function() + local params = { textDocument = { uri = uri } } + for _, client in ipairs(lsp.get_clients({ bufnr = bufnr })) do + changetracking.reset_buf(client, bufnr) + if vim.tbl_get(client.server_capabilities, 'textDocumentSync', 'openClose') then + client.notify(ms.textDocument_didClose, params) + end + end + for _, client in ipairs(all_clients) do + client.attached_buffers[bufnr] = nil + end + util.buf_versions[bufnr] = nil + end, + -- TODO if we know all of the potential clients ahead of time, then we + -- could conditionally set this. + -- utf_sizes = size_index > 1; + utf_sizes = true, + }) +end + --- Implements the `textDocument/did…` notifications required to track a buffer --- for any language server. --- @@ -561,92 +589,26 @@ function lsp.buf_attach_client(bufnr, client_id) log.warn(string.format('buf_attach_client called on unloaded buffer (id: %d): ', bufnr)) return false end - local buffer_client_ids = all_buffer_active_clients[bufnr] -- This is our first time attaching to this buffer. - if not buffer_client_ids then - buffer_client_ids = {} - all_buffer_active_clients[bufnr] = buffer_client_ids + if #lsp.get_clients({ bufnr = bufnr, _uninitialized = true }) == 0 then + buf_attach(bufnr, client_id) + end - local uri = vim.uri_from_bufnr(bufnr) - local augroup = ('lsp_c_%d_b_%d_save'):format(client_id, bufnr) - local group = api.nvim_create_augroup(augroup, { clear = true }) - api.nvim_create_autocmd('BufWritePre', { - group = group, - buffer = bufnr, - desc = 'vim.lsp: textDocument/willSave', - callback = function(ctx) - for _, client in ipairs(lsp.get_clients({ bufnr = ctx.buf })) do - local params = { - textDocument = { - uri = uri, - }, - reason = protocol.TextDocumentSaveReason.Manual, - } - if vim.tbl_get(client.server_capabilities, 'textDocumentSync', 'willSave') then - client.notify(ms.textDocument_willSave, params) - end - if vim.tbl_get(client.server_capabilities, 'textDocumentSync', 'willSaveWaitUntil') then - local result, err = - client.request_sync(ms.textDocument_willSaveWaitUntil, params, 1000, ctx.buf) - if result and result.result then - util.apply_text_edits(result.result, ctx.buf, client.offset_encoding) - elseif err then - log.error(vim.inspect(err)) - end - end - end - end, - }) - api.nvim_create_autocmd('BufWritePost', { - group = group, - buffer = bufnr, - desc = 'vim.lsp: textDocument/didSave handler', - callback = function(ctx) - text_document_did_save_handler(ctx.buf) - end, - }) - -- First time, so attach and set up stuff. - api.nvim_buf_attach(bufnr, false, { - on_lines = text_document_did_change_handler, - on_reload = function() - local params = { textDocument = { uri = uri } } - for _, client in ipairs(lsp.get_clients({ bufnr = bufnr })) do - changetracking.reset_buf(client, bufnr) - if vim.tbl_get(client.server_capabilities, 'textDocumentSync', 'openClose') then - client.notify(ms.textDocument_didClose, params) - end - client:_text_document_did_open_handler(bufnr) - end - end, - on_detach = function() - local params = { textDocument = { uri = uri } } - for _, client in ipairs(lsp.get_clients({ bufnr = bufnr })) do - changetracking.reset_buf(client, bufnr) - if vim.tbl_get(client.server_capabilities, 'textDocumentSync', 'openClose') then - client.notify(ms.textDocument_didClose, params) - end - client.attached_buffers[bufnr] = nil - end - util.buf_versions[bufnr] = nil - all_buffer_active_clients[bufnr] = nil - end, - -- TODO if we know all of the potential clients ahead of time, then we - -- could conditionally set this. - -- utf_sizes = size_index > 1; - utf_sizes = true, - }) + local client = lsp.get_client_by_id(client_id) + if not client then + return false end - if buffer_client_ids[client_id] then + if client.attached_buffers[bufnr] then return true end - -- This is our first time attaching this client to this buffer. - buffer_client_ids[client_id] = true - local client = active_clients[client_id] + client.attached_buffers[bufnr] = true + + -- This is our first time attaching this client to this buffer. -- Send didOpen for the client if it is initialized. If it isn't initialized -- then it will send didOpen on initialize. - if client then + if client.initialized then client:_on_attach(bufnr) end return true @@ -665,7 +627,7 @@ function lsp.buf_detach_client(bufnr, client_id) }) bufnr = resolve_bufnr(bufnr) - local client = lsp.get_client_by_id(client_id) + local client = all_clients[client_id] if not client or not client.attached_buffers[bufnr] then vim.notify( string.format( @@ -694,11 +656,6 @@ function lsp.buf_detach_client(bufnr, client_id) client.attached_buffers[bufnr] = nil util.buf_versions[bufnr] = nil - all_buffer_active_clients[bufnr][client_id] = nil - if #vim.tbl_keys(all_buffer_active_clients[bufnr]) == 0 then - all_buffer_active_clients[bufnr] = nil - end - local namespace = lsp.diagnostic.get_namespace(client_id) vim.diagnostic.reset(namespace, bufnr) end @@ -708,7 +665,7 @@ end ---@param bufnr (integer) Buffer handle, or 0 for current ---@param client_id (integer) the client id function lsp.buf_is_attached(bufnr, client_id) - return (all_buffer_active_clients[resolve_bufnr(bufnr)] or {})[client_id] == true + return lsp.get_clients({ bufnr = bufnr, id = client_id, _uninitialized = true })[1] ~= nil end --- Gets a client by id, or nil if the id is invalid. @@ -718,7 +675,7 @@ end --- ---@return (nil|vim.lsp.Client) client rpc object function lsp.get_client_by_id(client_id) - return active_clients[client_id] or uninitialized_clients[client_id] + return all_clients[client_id] end --- Returns list of buffers attached to client_id. @@ -726,7 +683,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 = lsp.get_client_by_id(client_id) + local client = all_clients[client_id] return client and vim.tbl_keys(client.attached_buffers) or {} end @@ -742,17 +699,22 @@ end --- By default asks the server to shutdown, unless stop was requested --- already for this client, then force-shutdown is attempted. --- ----@param client_id integer|vim.lsp.Client id or |vim.lsp.Client| object, or list thereof ----@param force boolean|nil shutdown forcefully +---@param client_id integer|integer[]|vim.lsp.Client[] id, list of id's, or list of |vim.lsp.Client| objects +---@param force? boolean shutdown forcefully function lsp.stop_client(client_id, force) + --- @type integer[]|vim.lsp.Client[] local ids = type(client_id) == 'table' and client_id or { client_id } for _, id in ipairs(ids) do - if type(id) == 'table' and id.stop ~= nil then - id.stop(force) - elseif active_clients[id] then - active_clients[id].stop(force) - elseif uninitialized_clients[id] then - uninitialized_clients[id].stop(true) + if type(id) == 'table' then + if id.stop then + id.stop(force) + end + else + --- @cast id -vim.lsp.Client + local client = all_clients[id] + if client then + client.stop(force) + end end end end @@ -772,6 +734,9 @@ end --- --- Only return clients supporting the given method --- @field method? string +--- +--- Also return uninitialized clients. +--- @field package _uninitialized? boolean --- Get active clients. --- @@ -784,15 +749,16 @@ function lsp.get_clients(filter) local clients = {} --- @type vim.lsp.Client[] - local t = filter.bufnr and (all_buffer_active_clients[resolve_bufnr(filter.bufnr)] or {}) - or active_clients - for client_id in pairs(t) do - local client = active_clients[client_id] + local bufnr = filter.bufnr and resolve_bufnr(filter.bufnr) + + for _, client in pairs(all_clients) do if client and (filter.id == nil or client.id == filter.id) + and (filter.bufnr == nil or client.attached_buffers[bufnr]) and (filter.name == nil or client.name == filter.name) and (filter.method == nil or client.supports_method(filter.method, { bufnr = filter.bufnr })) + and (filter._uninitialized or client.initialized) then clients[#clients + 1] = client end @@ -810,15 +776,9 @@ end api.nvim_create_autocmd('VimLeavePre', { desc = 'vim.lsp: exit handler', callback = function() + local active_clients = lsp.get_clients() log.info('exit_handler', active_clients) - for _, client in pairs(uninitialized_clients) do - client.stop(true) - end - -- TODO handle v:dying differently? - if tbl_isempty(active_clients) then - return - end - for _, client in pairs(active_clients) do + for _, client in pairs(all_clients) do client.stop() end @@ -827,7 +787,7 @@ api.nvim_create_autocmd('VimLeavePre', { local send_kill = false for client_id, client in pairs(active_clients) do - local timeout = if_nil(client.flags.exit_timeout, false) + local timeout = client.flags.exit_timeout if timeout then send_kill = true timeouts[client_id] = timeout @@ -910,7 +870,7 @@ function lsp.buf_request(bufnr, method, params, handler) local function _cancel_all_requests() for client_id, request_id in pairs(client_request_ids) do - local client = active_clients[client_id] + local client = all_clients[client_id] client.cancel_request(request_id) end end @@ -1106,7 +1066,7 @@ end ---@return boolean stopped true if client is stopped, false otherwise. function lsp.client_is_stopped(client_id) assert(client_id, 'missing client_id param') - return active_clients[client_id] == nil and not uninitialized_clients[client_id] + return not all_clients[client_id] end --- Gets a map of client_id:client pairs for the given buffer, where each value @@ -1172,7 +1132,11 @@ function lsp.for_each_buffer_client(bufnr, fn) 'lsp.get_clients({ bufnr = bufnr }) with regular loop', '0.12' ) - return for_each_buffer_client(bufnr, fn) + bufnr = resolve_bufnr(bufnr) + + for _, client in pairs(lsp.get_clients({ bufnr = bufnr })) do + fn(client, client.id, bufnr) + end end --- Function to manage overriding defaults for LSP handlers. -- cgit From 934f38682afd5925df675485b96ac9a2d3b8dd57 Mon Sep 17 00:00:00 2001 From: Lewis Russell Date: Mon, 25 Mar 2024 20:16:42 +0000 Subject: Revert "refactor(lsp): simplify client tracking" This reverts commit 3f238b39cfdf27657b2d9452c6ffd28f8209c95f. --- runtime/lua/vim/lsp.lua | 338 +++++++++++++++++++++++++++--------------------- 1 file changed, 187 insertions(+), 151 deletions(-) (limited to 'runtime/lua/vim/lsp.lua') diff --git a/runtime/lua/vim/lsp.lua b/runtime/lua/vim/lsp.lua index b302e1543a..d5c376ba44 100644 --- a/runtime/lua/vim/lsp.lua +++ b/runtime/lua/vim/lsp.lua @@ -1,5 +1,7 @@ local api = vim.api +local tbl_isempty, tbl_extend = vim.tbl_isempty, vim.tbl_extend local validate = vim.validate +local if_nil = vim.F.if_nil local lsp = vim._defer_require('vim.lsp', { _changetracking = ..., --- @module 'vim.lsp._changetracking' @@ -106,7 +108,40 @@ function lsp._buf_get_line_ending(bufnr) end -- Tracks all clients created via lsp.start_client -local all_clients = {} --- @type table +local active_clients = {} --- @type table +local all_buffer_active_clients = {} --- @type table> +local uninitialized_clients = {} --- @type table + +---@param bufnr? integer +---@param fn fun(client: vim.lsp.Client, client_id: integer, bufnr: integer) +local function for_each_buffer_client(bufnr, fn, restrict_client_ids) + validate({ + fn = { fn, 'f' }, + restrict_client_ids = { restrict_client_ids, 't', true }, + }) + bufnr = resolve_bufnr(bufnr) + local client_ids = all_buffer_active_clients[bufnr] + if not client_ids or tbl_isempty(client_ids) then + return + end + + if restrict_client_ids and #restrict_client_ids > 0 then + local filtered_client_ids = {} --- @type table + for client_id in pairs(client_ids) do + if vim.list_contains(restrict_client_ids, client_id) then + filtered_client_ids[client_id] = true + end + end + client_ids = filtered_client_ids + end + + for client_id in pairs(client_ids) do + local client = active_clients[client_id] + if client then + fn(client, client_id, bufnr) + end + end +end local client_errors_base = table.maxn(lsp.rpc.client_errors) local client_errors_offset = 0 @@ -121,7 +156,7 @@ end --- Can be used to look up the string from a the number or the number --- from the string. --- @nodoc -lsp.client_errors = vim.tbl_extend( +lsp.client_errors = tbl_extend( 'error', lsp.rpc.client_errors, client_error('BEFORE_INIT_CALLBACK_ERROR'), @@ -223,10 +258,12 @@ function lsp.start(config, opts) local bufnr = resolve_bufnr(opts.bufnr) - for _, client in pairs(all_clients) do - if reuse_client(client, config) then - lsp.buf_attach_client(bufnr, client.id) - return client.id + for _, clients in ipairs({ uninitialized_clients, lsp.get_clients() }) do + for _, client in pairs(clients) do + if reuse_client(client, config) then + lsp.buf_attach_client(bufnr, client.id) + return client.id + end end end @@ -348,11 +385,17 @@ end --- @param client vim.lsp.Client local function on_client_init(client) + local id = client.id + uninitialized_clients[id] = nil + -- Only assign after initialized. + active_clients[id] = client -- If we had been registered before we start, then send didOpen This can -- happen if we attach to buffers before initialize finishes or if -- someone restarts a client. - for bufnr in pairs(client.attached_buffers) do - client:_on_attach(bufnr) + for bufnr, client_ids in pairs(all_buffer_active_clients) do + if client_ids[id] then + client.on_attach(bufnr) + end end end @@ -360,26 +403,28 @@ end --- @param signal integer --- @param client_id integer local function on_client_exit(code, signal, client_id) - local client = all_clients[client_id] - - for bufnr in pairs(client.attached_buffers) do - vim.schedule(function() - if client and client.attached_buffers[bufnr] then - api.nvim_exec_autocmds('LspDetach', { - buffer = bufnr, - modeline = false, - data = { client_id = client_id }, - }) - end + local client = active_clients[client_id] or uninitialized_clients[client_id] + + for bufnr, client_ids in pairs(all_buffer_active_clients) do + if client_ids[client_id] then + vim.schedule(function() + if client and client.attached_buffers[bufnr] then + api.nvim_exec_autocmds('LspDetach', { + buffer = bufnr, + modeline = false, + data = { client_id = client_id }, + }) + end - local namespace = vim.lsp.diagnostic.get_namespace(client_id) - vim.diagnostic.reset(namespace, bufnr) - client.attached_buffers[bufnr] = nil + local namespace = vim.lsp.diagnostic.get_namespace(client_id) + vim.diagnostic.reset(namespace, bufnr) - if #lsp.get_clients({ bufnr = bufnr, _uninitialized = true }) == 0 then - reset_defaults(bufnr) - end - end) + client_ids[client_id] = nil + if vim.tbl_isempty(client_ids) then + reset_defaults(bufnr) + end + end) + end end local name = client.name or 'unknown' @@ -387,7 +432,8 @@ local function on_client_exit(code, signal, client_id) -- 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 + active_clients[client_id] = nil + uninitialized_clients[client_id] = nil -- Client can be absent if executable starts, but initialize fails -- init/attach won't have happened @@ -420,12 +466,12 @@ function lsp.start_client(config) end --- @diagnostic disable-next-line: invisible - table.insert(client._on_init_cbs, 1, on_client_init) - + table.insert(client._on_init_cbs, on_client_init) --- @diagnostic disable-next-line: invisible table.insert(client._on_exit_cbs, on_client_exit) - all_clients[client.id] = client + -- Store the uninitialized_clients for cleanup in case we exit before initialize finishes. + uninitialized_clients[client.id] = client client:initialize() @@ -449,7 +495,7 @@ local function text_document_did_change_handler( new_lastline ) -- Detach (nvim_buf_attach) via returning True to on_lines if no clients are attached - if #lsp.get_clients({ bufnr = bufnr }) == 0 then + if tbl_isempty(all_buffer_active_clients[bufnr] or {}) then return true end util.buf_versions[bufnr] = changedtick @@ -497,80 +543,6 @@ local function text_document_did_save_handler(bufnr) end end ---- @param bufnr integer ---- @param client_id integer -local function buf_attach(bufnr, client_id) - local uri = vim.uri_from_bufnr(bufnr) - local augroup = ('lsp_c_%d_b_%d_save'):format(client_id, bufnr) - local group = api.nvim_create_augroup(augroup, { clear = true }) - api.nvim_create_autocmd('BufWritePre', { - group = group, - buffer = bufnr, - desc = 'vim.lsp: textDocument/willSave', - callback = function(ctx) - for _, client in ipairs(lsp.get_clients({ bufnr = ctx.buf })) do - local params = { - textDocument = { - uri = uri, - }, - reason = protocol.TextDocumentSaveReason.Manual, - } - if vim.tbl_get(client.server_capabilities, 'textDocumentSync', 'willSave') then - client.notify(ms.textDocument_willSave, params) - end - if vim.tbl_get(client.server_capabilities, 'textDocumentSync', 'willSaveWaitUntil') then - local result, err = - client.request_sync(ms.textDocument_willSaveWaitUntil, params, 1000, ctx.buf) - if result and result.result then - util.apply_text_edits(result.result, ctx.buf, client.offset_encoding) - elseif err then - log.error(vim.inspect(err)) - end - end - end - end, - }) - api.nvim_create_autocmd('BufWritePost', { - group = group, - buffer = bufnr, - desc = 'vim.lsp: textDocument/didSave handler', - callback = function(ctx) - text_document_did_save_handler(ctx.buf) - end, - }) - -- First time, so attach and set up stuff. - api.nvim_buf_attach(bufnr, false, { - on_lines = text_document_did_change_handler, - on_reload = function() - local params = { textDocument = { uri = uri } } - for _, client in ipairs(lsp.get_clients({ bufnr = bufnr })) do - changetracking.reset_buf(client, bufnr) - if vim.tbl_get(client.server_capabilities, 'textDocumentSync', 'openClose') then - client.notify(ms.textDocument_didClose, params) - end - client:_text_document_did_open_handler(bufnr) - end - end, - on_detach = function() - local params = { textDocument = { uri = uri } } - for _, client in ipairs(lsp.get_clients({ bufnr = bufnr })) do - changetracking.reset_buf(client, bufnr) - if vim.tbl_get(client.server_capabilities, 'textDocumentSync', 'openClose') then - client.notify(ms.textDocument_didClose, params) - end - end - for _, client in ipairs(all_clients) do - client.attached_buffers[bufnr] = nil - end - util.buf_versions[bufnr] = nil - end, - -- TODO if we know all of the potential clients ahead of time, then we - -- could conditionally set this. - -- utf_sizes = size_index > 1; - utf_sizes = true, - }) -end - --- Implements the `textDocument/did…` notifications required to track a buffer --- for any language server. --- @@ -589,26 +561,92 @@ function lsp.buf_attach_client(bufnr, client_id) log.warn(string.format('buf_attach_client called on unloaded buffer (id: %d): ', bufnr)) return false end + local buffer_client_ids = all_buffer_active_clients[bufnr] -- This is our first time attaching to this buffer. - if #lsp.get_clients({ bufnr = bufnr, _uninitialized = true }) == 0 then - buf_attach(bufnr, client_id) - end + if not buffer_client_ids then + buffer_client_ids = {} + all_buffer_active_clients[bufnr] = buffer_client_ids - local client = lsp.get_client_by_id(client_id) - if not client then - return false + local uri = vim.uri_from_bufnr(bufnr) + local augroup = ('lsp_c_%d_b_%d_save'):format(client_id, bufnr) + local group = api.nvim_create_augroup(augroup, { clear = true }) + api.nvim_create_autocmd('BufWritePre', { + group = group, + buffer = bufnr, + desc = 'vim.lsp: textDocument/willSave', + callback = function(ctx) + for _, client in ipairs(lsp.get_clients({ bufnr = ctx.buf })) do + local params = { + textDocument = { + uri = uri, + }, + reason = protocol.TextDocumentSaveReason.Manual, + } + if vim.tbl_get(client.server_capabilities, 'textDocumentSync', 'willSave') then + client.notify(ms.textDocument_willSave, params) + end + if vim.tbl_get(client.server_capabilities, 'textDocumentSync', 'willSaveWaitUntil') then + local result, err = + client.request_sync(ms.textDocument_willSaveWaitUntil, params, 1000, ctx.buf) + if result and result.result then + util.apply_text_edits(result.result, ctx.buf, client.offset_encoding) + elseif err then + log.error(vim.inspect(err)) + end + end + end + end, + }) + api.nvim_create_autocmd('BufWritePost', { + group = group, + buffer = bufnr, + desc = 'vim.lsp: textDocument/didSave handler', + callback = function(ctx) + text_document_did_save_handler(ctx.buf) + end, + }) + -- First time, so attach and set up stuff. + api.nvim_buf_attach(bufnr, false, { + on_lines = text_document_did_change_handler, + on_reload = function() + local params = { textDocument = { uri = uri } } + for _, client in ipairs(lsp.get_clients({ bufnr = bufnr })) do + changetracking.reset_buf(client, bufnr) + if vim.tbl_get(client.server_capabilities, 'textDocumentSync', 'openClose') then + client.notify(ms.textDocument_didClose, params) + end + client:_text_document_did_open_handler(bufnr) + end + end, + on_detach = function() + local params = { textDocument = { uri = uri } } + for _, client in ipairs(lsp.get_clients({ bufnr = bufnr })) do + changetracking.reset_buf(client, bufnr) + if vim.tbl_get(client.server_capabilities, 'textDocumentSync', 'openClose') then + client.notify(ms.textDocument_didClose, params) + end + client.attached_buffers[bufnr] = nil + end + util.buf_versions[bufnr] = nil + all_buffer_active_clients[bufnr] = nil + end, + -- TODO if we know all of the potential clients ahead of time, then we + -- could conditionally set this. + -- utf_sizes = size_index > 1; + utf_sizes = true, + }) end - if client.attached_buffers[bufnr] then + if buffer_client_ids[client_id] then return true end - - client.attached_buffers[bufnr] = true - -- This is our first time attaching this client to this buffer. + buffer_client_ids[client_id] = true + + local client = active_clients[client_id] -- Send didOpen for the client if it is initialized. If it isn't initialized -- then it will send didOpen on initialize. - if client.initialized then + if client then client:_on_attach(bufnr) end return true @@ -627,7 +665,7 @@ function lsp.buf_detach_client(bufnr, client_id) }) bufnr = 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( @@ -656,6 +694,11 @@ function lsp.buf_detach_client(bufnr, client_id) client.attached_buffers[bufnr] = nil util.buf_versions[bufnr] = nil + all_buffer_active_clients[bufnr][client_id] = nil + if #vim.tbl_keys(all_buffer_active_clients[bufnr]) == 0 then + all_buffer_active_clients[bufnr] = nil + end + local namespace = lsp.diagnostic.get_namespace(client_id) vim.diagnostic.reset(namespace, bufnr) end @@ -665,7 +708,7 @@ end ---@param bufnr (integer) Buffer handle, or 0 for current ---@param client_id (integer) the client id function lsp.buf_is_attached(bufnr, client_id) - return lsp.get_clients({ bufnr = bufnr, id = client_id, _uninitialized = true })[1] ~= nil + return (all_buffer_active_clients[resolve_bufnr(bufnr)] or {})[client_id] == true end --- Gets a client by id, or nil if the id is invalid. @@ -675,7 +718,7 @@ end --- ---@return (nil|vim.lsp.Client) client rpc object function lsp.get_client_by_id(client_id) - return all_clients[client_id] + return active_clients[client_id] or uninitialized_clients[client_id] end --- Returns list of buffers attached to client_id. @@ -683,7 +726,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 @@ -699,22 +742,17 @@ end --- By default asks the server to shutdown, unless stop was requested --- already for this client, then force-shutdown is attempted. --- ----@param client_id integer|integer[]|vim.lsp.Client[] id, list of id's, or list of |vim.lsp.Client| objects ----@param force? boolean shutdown forcefully +---@param client_id integer|vim.lsp.Client id or |vim.lsp.Client| object, or list thereof +---@param force boolean|nil shutdown forcefully function lsp.stop_client(client_id, force) - --- @type integer[]|vim.lsp.Client[] local ids = type(client_id) == 'table' and client_id or { client_id } for _, id in ipairs(ids) do - if type(id) == 'table' then - if id.stop then - id.stop(force) - end - else - --- @cast id -vim.lsp.Client - local client = all_clients[id] - if client then - client.stop(force) - end + if type(id) == 'table' and id.stop ~= nil then + id.stop(force) + elseif active_clients[id] then + active_clients[id].stop(force) + elseif uninitialized_clients[id] then + uninitialized_clients[id].stop(true) end end end @@ -734,9 +772,6 @@ end --- --- Only return clients supporting the given method --- @field method? string ---- ---- Also return uninitialized clients. ---- @field package _uninitialized? boolean --- Get active clients. --- @@ -749,16 +784,15 @@ function lsp.get_clients(filter) local clients = {} --- @type vim.lsp.Client[] - local bufnr = filter.bufnr and resolve_bufnr(filter.bufnr) - - for _, client in pairs(all_clients) do + local t = filter.bufnr and (all_buffer_active_clients[resolve_bufnr(filter.bufnr)] or {}) + or active_clients + for client_id in pairs(t) do + local client = active_clients[client_id] if client and (filter.id == nil or client.id == filter.id) - and (filter.bufnr == nil or client.attached_buffers[bufnr]) and (filter.name == nil or client.name == filter.name) and (filter.method == nil or client.supports_method(filter.method, { bufnr = filter.bufnr })) - and (filter._uninitialized or client.initialized) then clients[#clients + 1] = client end @@ -776,9 +810,15 @@ end api.nvim_create_autocmd('VimLeavePre', { desc = 'vim.lsp: exit handler', 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(uninitialized_clients) do + client.stop(true) + end + -- TODO handle v:dying differently? + if tbl_isempty(active_clients) then + return + end + for _, client in pairs(active_clients) do client.stop() end @@ -787,7 +827,7 @@ api.nvim_create_autocmd('VimLeavePre', { local send_kill = false for client_id, client in pairs(active_clients) do - local timeout = client.flags.exit_timeout + local timeout = if_nil(client.flags.exit_timeout, false) if timeout then send_kill = true timeouts[client_id] = timeout @@ -870,7 +910,7 @@ function lsp.buf_request(bufnr, method, params, handler) local function _cancel_all_requests() for client_id, request_id in pairs(client_request_ids) do - local client = all_clients[client_id] + local client = active_clients[client_id] client.cancel_request(request_id) end end @@ -1066,7 +1106,7 @@ end ---@return boolean stopped true if client is stopped, false otherwise. function lsp.client_is_stopped(client_id) assert(client_id, 'missing client_id param') - return not all_clients[client_id] + return active_clients[client_id] == nil and not uninitialized_clients[client_id] end --- Gets a map of client_id:client pairs for the given buffer, where each value @@ -1132,11 +1172,7 @@ function lsp.for_each_buffer_client(bufnr, fn) 'lsp.get_clients({ bufnr = bufnr }) with regular loop', '0.12' ) - bufnr = resolve_bufnr(bufnr) - - for _, client in pairs(lsp.get_clients({ bufnr = bufnr })) do - fn(client, client.id, bufnr) - end + return for_each_buffer_client(bufnr, fn) end --- Function to manage overriding defaults for LSP handlers. -- cgit From 00e71d3da3464df2b4c4f33bfd5fac6d88e7c867 Mon Sep 17 00:00:00 2001 From: Lewis Russell Date: Thu, 21 Mar 2024 15:15:20 +0000 Subject: refactor(lsp): simplify client tracking - Remove: - uninitialized_clients - active_clients - all_buffer_active_clients - Add: - all_clients - Use `lsp.get_clients()` to get buffer clients. --- runtime/lua/vim/lsp.lua | 343 +++++++++++++++++++++--------------------------- 1 file changed, 147 insertions(+), 196 deletions(-) (limited to 'runtime/lua/vim/lsp.lua') diff --git a/runtime/lua/vim/lsp.lua b/runtime/lua/vim/lsp.lua index d5c376ba44..0c9de607fe 100644 --- a/runtime/lua/vim/lsp.lua +++ b/runtime/lua/vim/lsp.lua @@ -1,7 +1,5 @@ local api = vim.api -local tbl_isempty, tbl_extend = vim.tbl_isempty, vim.tbl_extend local validate = vim.validate -local if_nil = vim.F.if_nil local lsp = vim._defer_require('vim.lsp', { _changetracking = ..., --- @module 'vim.lsp._changetracking' @@ -108,40 +106,7 @@ function lsp._buf_get_line_ending(bufnr) end -- Tracks all clients created via lsp.start_client -local active_clients = {} --- @type table -local all_buffer_active_clients = {} --- @type table> -local uninitialized_clients = {} --- @type table - ----@param bufnr? integer ----@param fn fun(client: vim.lsp.Client, client_id: integer, bufnr: integer) -local function for_each_buffer_client(bufnr, fn, restrict_client_ids) - validate({ - fn = { fn, 'f' }, - restrict_client_ids = { restrict_client_ids, 't', true }, - }) - bufnr = resolve_bufnr(bufnr) - local client_ids = all_buffer_active_clients[bufnr] - if not client_ids or tbl_isempty(client_ids) then - return - end - - if restrict_client_ids and #restrict_client_ids > 0 then - local filtered_client_ids = {} --- @type table - for client_id in pairs(client_ids) do - if vim.list_contains(restrict_client_ids, client_id) then - filtered_client_ids[client_id] = true - end - end - client_ids = filtered_client_ids - end - - for client_id in pairs(client_ids) do - local client = active_clients[client_id] - if client then - fn(client, client_id, bufnr) - end - end -end +local all_clients = {} --- @type table local client_errors_base = table.maxn(lsp.rpc.client_errors) local client_errors_offset = 0 @@ -156,7 +121,7 @@ end --- Can be used to look up the string from a the number or the number --- from the string. --- @nodoc -lsp.client_errors = tbl_extend( +lsp.client_errors = vim.tbl_extend( 'error', lsp.rpc.client_errors, client_error('BEFORE_INIT_CALLBACK_ERROR'), @@ -258,12 +223,10 @@ function lsp.start(config, opts) local bufnr = resolve_bufnr(opts.bufnr) - for _, clients in ipairs({ uninitialized_clients, lsp.get_clients() }) do - for _, client in pairs(clients) do - if reuse_client(client, config) then - lsp.buf_attach_client(bufnr, client.id) - return client.id - end + for _, client in pairs(all_clients) do + if reuse_client(client, config) then + lsp.buf_attach_client(bufnr, client.id) + return client.id end end @@ -383,48 +346,30 @@ local function reset_defaults(bufnr) end) end ---- @param client vim.lsp.Client -local function on_client_init(client) - local id = client.id - uninitialized_clients[id] = nil - -- Only assign after initialized. - active_clients[id] = client - -- If we had been registered before we start, then send didOpen This can - -- happen if we attach to buffers before initialize finishes or if - -- someone restarts a client. - for bufnr, client_ids in pairs(all_buffer_active_clients) do - if client_ids[id] then - client.on_attach(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 = active_clients[client_id] or uninitialized_clients[client_id] - - for bufnr, client_ids in pairs(all_buffer_active_clients) do - if client_ids[client_id] then - vim.schedule(function() - if client and client.attached_buffers[bufnr] then - api.nvim_exec_autocmds('LspDetach', { - buffer = bufnr, - modeline = false, - data = { client_id = client_id }, - }) - end + local client = all_clients[client_id] + + for bufnr in pairs(client.attached_buffers) do + vim.schedule(function() + if client and client.attached_buffers[bufnr] then + api.nvim_exec_autocmds('LspDetach', { + buffer = bufnr, + modeline = false, + data = { client_id = client_id }, + }) + end - local namespace = vim.lsp.diagnostic.get_namespace(client_id) - vim.diagnostic.reset(namespace, bufnr) + local namespace = vim.lsp.diagnostic.get_namespace(client_id) + vim.diagnostic.reset(namespace, bufnr) + client.attached_buffers[bufnr] = nil - client_ids[client_id] = nil - if vim.tbl_isempty(client_ids) then - reset_defaults(bufnr) - end - end) - end + if #lsp.get_clients({ bufnr = bufnr, _uninitialized = true }) == 0 then + reset_defaults(bufnr) + end + end) end local name = client.name or 'unknown' @@ -432,8 +377,7 @@ local function on_client_exit(code, signal, client_id) -- Schedule the deletion of the client object so that it exists in the execution of LspDetach -- autocommands vim.schedule(function() - active_clients[client_id] = nil - uninitialized_clients[client_id] = nil + all_clients[client_id] = nil -- Client can be absent if executable starts, but initialize fails -- init/attach won't have happened @@ -465,13 +409,10 @@ function lsp.start_client(config) return end - --- @diagnostic disable-next-line: invisible - table.insert(client._on_init_cbs, on_client_init) --- @diagnostic disable-next-line: invisible table.insert(client._on_exit_cbs, on_client_exit) - -- Store the uninitialized_clients for cleanup in case we exit before initialize finishes. - uninitialized_clients[client.id] = client + all_clients[client.id] = client client:initialize() @@ -495,7 +436,7 @@ local function text_document_did_change_handler( new_lastline ) -- Detach (nvim_buf_attach) via returning True to on_lines if no clients are attached - if tbl_isempty(all_buffer_active_clients[bufnr] or {}) then + if #lsp.get_clients({ bufnr = bufnr }) == 0 then return true end util.buf_versions[bufnr] = changedtick @@ -543,6 +484,80 @@ local function text_document_did_save_handler(bufnr) end end +--- @param bufnr integer +--- @param client_id integer +local function buf_attach(bufnr, client_id) + local uri = vim.uri_from_bufnr(bufnr) + local augroup = ('lsp_c_%d_b_%d_save'):format(client_id, bufnr) + local group = api.nvim_create_augroup(augroup, { clear = true }) + api.nvim_create_autocmd('BufWritePre', { + group = group, + buffer = bufnr, + desc = 'vim.lsp: textDocument/willSave', + callback = function(ctx) + for _, client in ipairs(lsp.get_clients({ bufnr = ctx.buf })) do + local params = { + textDocument = { + uri = uri, + }, + reason = protocol.TextDocumentSaveReason.Manual, + } + if vim.tbl_get(client.server_capabilities, 'textDocumentSync', 'willSave') then + client.notify(ms.textDocument_willSave, params) + end + if vim.tbl_get(client.server_capabilities, 'textDocumentSync', 'willSaveWaitUntil') then + local result, err = + client.request_sync(ms.textDocument_willSaveWaitUntil, params, 1000, ctx.buf) + if result and result.result then + util.apply_text_edits(result.result, ctx.buf, client.offset_encoding) + elseif err then + log.error(vim.inspect(err)) + end + end + end + end, + }) + api.nvim_create_autocmd('BufWritePost', { + group = group, + buffer = bufnr, + desc = 'vim.lsp: textDocument/didSave handler', + callback = function(ctx) + text_document_did_save_handler(ctx.buf) + end, + }) + -- First time, so attach and set up stuff. + api.nvim_buf_attach(bufnr, false, { + on_lines = text_document_did_change_handler, + on_reload = function() + local params = { textDocument = { uri = uri } } + for _, client in ipairs(lsp.get_clients({ bufnr = bufnr })) do + changetracking.reset_buf(client, bufnr) + if vim.tbl_get(client.server_capabilities, 'textDocumentSync', 'openClose') then + client.notify(ms.textDocument_didClose, params) + end + client:_text_document_did_open_handler(bufnr) + end + end, + on_detach = function() + local params = { textDocument = { uri = uri } } + for _, client in ipairs(lsp.get_clients({ bufnr = bufnr })) do + changetracking.reset_buf(client, bufnr) + if vim.tbl_get(client.server_capabilities, 'textDocumentSync', 'openClose') then + client.notify(ms.textDocument_didClose, params) + end + end + for _, client in ipairs(all_clients) do + client.attached_buffers[bufnr] = nil + end + util.buf_versions[bufnr] = nil + end, + -- TODO if we know all of the potential clients ahead of time, then we + -- could conditionally set this. + -- utf_sizes = size_index > 1; + utf_sizes = true, + }) +end + --- Implements the `textDocument/did…` notifications required to track a buffer --- for any language server. --- @@ -561,92 +576,26 @@ function lsp.buf_attach_client(bufnr, client_id) log.warn(string.format('buf_attach_client called on unloaded buffer (id: %d): ', bufnr)) return false end - local buffer_client_ids = all_buffer_active_clients[bufnr] -- This is our first time attaching to this buffer. - if not buffer_client_ids then - buffer_client_ids = {} - all_buffer_active_clients[bufnr] = buffer_client_ids + if #lsp.get_clients({ bufnr = bufnr, _uninitialized = true }) == 0 then + buf_attach(bufnr, client_id) + end - local uri = vim.uri_from_bufnr(bufnr) - local augroup = ('lsp_c_%d_b_%d_save'):format(client_id, bufnr) - local group = api.nvim_create_augroup(augroup, { clear = true }) - api.nvim_create_autocmd('BufWritePre', { - group = group, - buffer = bufnr, - desc = 'vim.lsp: textDocument/willSave', - callback = function(ctx) - for _, client in ipairs(lsp.get_clients({ bufnr = ctx.buf })) do - local params = { - textDocument = { - uri = uri, - }, - reason = protocol.TextDocumentSaveReason.Manual, - } - if vim.tbl_get(client.server_capabilities, 'textDocumentSync', 'willSave') then - client.notify(ms.textDocument_willSave, params) - end - if vim.tbl_get(client.server_capabilities, 'textDocumentSync', 'willSaveWaitUntil') then - local result, err = - client.request_sync(ms.textDocument_willSaveWaitUntil, params, 1000, ctx.buf) - if result and result.result then - util.apply_text_edits(result.result, ctx.buf, client.offset_encoding) - elseif err then - log.error(vim.inspect(err)) - end - end - end - end, - }) - api.nvim_create_autocmd('BufWritePost', { - group = group, - buffer = bufnr, - desc = 'vim.lsp: textDocument/didSave handler', - callback = function(ctx) - text_document_did_save_handler(ctx.buf) - end, - }) - -- First time, so attach and set up stuff. - api.nvim_buf_attach(bufnr, false, { - on_lines = text_document_did_change_handler, - on_reload = function() - local params = { textDocument = { uri = uri } } - for _, client in ipairs(lsp.get_clients({ bufnr = bufnr })) do - changetracking.reset_buf(client, bufnr) - if vim.tbl_get(client.server_capabilities, 'textDocumentSync', 'openClose') then - client.notify(ms.textDocument_didClose, params) - end - client:_text_document_did_open_handler(bufnr) - end - end, - on_detach = function() - local params = { textDocument = { uri = uri } } - for _, client in ipairs(lsp.get_clients({ bufnr = bufnr })) do - changetracking.reset_buf(client, bufnr) - if vim.tbl_get(client.server_capabilities, 'textDocumentSync', 'openClose') then - client.notify(ms.textDocument_didClose, params) - end - client.attached_buffers[bufnr] = nil - end - util.buf_versions[bufnr] = nil - all_buffer_active_clients[bufnr] = nil - end, - -- TODO if we know all of the potential clients ahead of time, then we - -- could conditionally set this. - -- utf_sizes = size_index > 1; - utf_sizes = true, - }) + local client = lsp.get_client_by_id(client_id) + if not client then + return false end - if buffer_client_ids[client_id] then + if client.attached_buffers[bufnr] then return true end - -- This is our first time attaching this client to this buffer. - buffer_client_ids[client_id] = true - local client = active_clients[client_id] + client.attached_buffers[bufnr] = true + + -- This is our first time attaching this client to this buffer. -- Send didOpen for the client if it is initialized. If it isn't initialized -- then it will send didOpen on initialize. - if client then + if client.initialized then client:_on_attach(bufnr) end return true @@ -665,7 +614,7 @@ function lsp.buf_detach_client(bufnr, client_id) }) bufnr = resolve_bufnr(bufnr) - local client = lsp.get_client_by_id(client_id) + local client = all_clients[client_id] if not client or not client.attached_buffers[bufnr] then vim.notify( string.format( @@ -694,11 +643,6 @@ function lsp.buf_detach_client(bufnr, client_id) client.attached_buffers[bufnr] = nil util.buf_versions[bufnr] = nil - all_buffer_active_clients[bufnr][client_id] = nil - if #vim.tbl_keys(all_buffer_active_clients[bufnr]) == 0 then - all_buffer_active_clients[bufnr] = nil - end - local namespace = lsp.diagnostic.get_namespace(client_id) vim.diagnostic.reset(namespace, bufnr) end @@ -708,7 +652,7 @@ end ---@param bufnr (integer) Buffer handle, or 0 for current ---@param client_id (integer) the client id function lsp.buf_is_attached(bufnr, client_id) - return (all_buffer_active_clients[resolve_bufnr(bufnr)] or {})[client_id] == true + return lsp.get_clients({ bufnr = bufnr, id = client_id, _uninitialized = true })[1] ~= nil end --- Gets a client by id, or nil if the id is invalid. @@ -718,7 +662,7 @@ end --- ---@return (nil|vim.lsp.Client) client rpc object function lsp.get_client_by_id(client_id) - return active_clients[client_id] or uninitialized_clients[client_id] + return all_clients[client_id] end --- Returns list of buffers attached to client_id. @@ -726,7 +670,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 = lsp.get_client_by_id(client_id) + local client = all_clients[client_id] return client and vim.tbl_keys(client.attached_buffers) or {} end @@ -742,17 +686,22 @@ end --- By default asks the server to shutdown, unless stop was requested --- already for this client, then force-shutdown is attempted. --- ----@param client_id integer|vim.lsp.Client id or |vim.lsp.Client| object, or list thereof ----@param force boolean|nil shutdown forcefully +---@param client_id integer|integer[]|vim.lsp.Client[] id, list of id's, or list of |vim.lsp.Client| objects +---@param force? boolean shutdown forcefully function lsp.stop_client(client_id, force) + --- @type integer[]|vim.lsp.Client[] local ids = type(client_id) == 'table' and client_id or { client_id } for _, id in ipairs(ids) do - if type(id) == 'table' and id.stop ~= nil then - id.stop(force) - elseif active_clients[id] then - active_clients[id].stop(force) - elseif uninitialized_clients[id] then - uninitialized_clients[id].stop(true) + if type(id) == 'table' then + if id.stop then + id.stop(force) + end + else + --- @cast id -vim.lsp.Client + local client = all_clients[id] + if client then + client.stop(force) + end end end end @@ -772,6 +721,9 @@ end --- --- Only return clients supporting the given method --- @field method? string +--- +--- Also return uninitialized clients. +--- @field package _uninitialized? boolean --- Get active clients. --- @@ -784,15 +736,16 @@ function lsp.get_clients(filter) local clients = {} --- @type vim.lsp.Client[] - local t = filter.bufnr and (all_buffer_active_clients[resolve_bufnr(filter.bufnr)] or {}) - or active_clients - for client_id in pairs(t) do - local client = active_clients[client_id] + local bufnr = filter.bufnr and resolve_bufnr(filter.bufnr) + + for _, client in pairs(all_clients) do if client and (filter.id == nil or client.id == filter.id) + and (filter.bufnr == nil or client.attached_buffers[bufnr]) and (filter.name == nil or client.name == filter.name) and (filter.method == nil or client.supports_method(filter.method, { bufnr = filter.bufnr })) + and (filter._uninitialized or client.initialized) then clients[#clients + 1] = client end @@ -810,15 +763,9 @@ end api.nvim_create_autocmd('VimLeavePre', { desc = 'vim.lsp: exit handler', callback = function() + local active_clients = lsp.get_clients() log.info('exit_handler', active_clients) - for _, client in pairs(uninitialized_clients) do - client.stop(true) - end - -- TODO handle v:dying differently? - if tbl_isempty(active_clients) then - return - end - for _, client in pairs(active_clients) do + for _, client in pairs(all_clients) do client.stop() end @@ -827,7 +774,7 @@ api.nvim_create_autocmd('VimLeavePre', { local send_kill = false for client_id, client in pairs(active_clients) do - local timeout = if_nil(client.flags.exit_timeout, false) + local timeout = client.flags.exit_timeout if timeout then send_kill = true timeouts[client_id] = timeout @@ -910,7 +857,7 @@ function lsp.buf_request(bufnr, method, params, handler) local function _cancel_all_requests() for client_id, request_id in pairs(client_request_ids) do - local client = active_clients[client_id] + local client = all_clients[client_id] client.cancel_request(request_id) end end @@ -1106,7 +1053,7 @@ end ---@return boolean stopped true if client is stopped, false otherwise. function lsp.client_is_stopped(client_id) assert(client_id, 'missing client_id param') - return active_clients[client_id] == nil and not uninitialized_clients[client_id] + return not all_clients[client_id] end --- Gets a map of client_id:client pairs for the given buffer, where each value @@ -1172,7 +1119,11 @@ function lsp.for_each_buffer_client(bufnr, fn) 'lsp.get_clients({ bufnr = bufnr }) with regular loop', '0.12' ) - return for_each_buffer_client(bufnr, fn) + bufnr = resolve_bufnr(bufnr) + + for _, client in pairs(lsp.get_clients({ bufnr = bufnr })) do + fn(client, client.id, bufnr) + end end --- Function to manage overriding defaults for LSP handlers. -- cgit From d9235efa76229708586d3c9db3dcbac46127ca0a Mon Sep 17 00:00:00 2001 From: Lewis Russell Date: Tue, 2 Apr 2024 11:56:29 +0100 Subject: refactor(lsp): move workspace folder logic into the client - Changed `reuse_client` to check workspace folders in addition to root_dir. --- runtime/lua/vim/lsp.lua | 28 +++++++++++++++++++++++----- 1 file changed, 23 insertions(+), 5 deletions(-) (limited to 'runtime/lua/vim/lsp.lua') diff --git a/runtime/lua/vim/lsp.lua b/runtime/lua/vim/lsp.lua index 0c9de607fe..eb604caacd 100644 --- a/runtime/lua/vim/lsp.lua +++ b/runtime/lua/vim/lsp.lua @@ -164,6 +164,28 @@ local function once(fn) end end +--- @param client vim.lsp.Client +--- @param config vim.lsp.ClientConfig +--- @return boolean +local function reuse_client_default(client, config) + if client.name ~= config.name then + return false + end + + if config.root_dir then + for _, dir in ipairs(client.workspace_folders or {}) do + -- note: do not need to check client.root_dir since that should be client.workspace_folders[1] + if config.root_dir == dir.name then + return true + end + end + end + + -- TODO(lewis6991): also check config.workspace_folders + + return false +end + --- @class vim.lsp.start.Opts --- @inlinedoc --- @@ -216,11 +238,7 @@ end --- @return integer? client_id function lsp.start(config, opts) opts = opts or {} - local reuse_client = opts.reuse_client - or function(client, conf) - return client.root_dir == conf.root_dir and client.name == conf.name - end - + local reuse_client = opts.reuse_client or reuse_client_default local bufnr = resolve_bufnr(opts.bufnr) for _, client in pairs(all_clients) do -- cgit From f190f758ac58d9cc955368e047b070e0a2261033 Mon Sep 17 00:00:00 2001 From: Yinzuo Jiang Date: Sat, 20 Apr 2024 21:40:01 +0800 Subject: feat(lsp): add vim.lsp.buf.subtypes(), vim.lsp.buf.supertypes() (#28388) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Mathias Fußenegger Co-authored-by: Maria José Solano --- runtime/lua/vim/lsp.lua | 3 +++ 1 file changed, 3 insertions(+) (limited to 'runtime/lua/vim/lsp.lua') diff --git a/runtime/lua/vim/lsp.lua b/runtime/lua/vim/lsp.lua index eb604caacd..ab22c5901a 100644 --- a/runtime/lua/vim/lsp.lua +++ b/runtime/lua/vim/lsp.lua @@ -43,6 +43,9 @@ lsp._request_name_to_capability = { [ms.textDocument_prepareCallHierarchy] = { 'callHierarchyProvider' }, [ms.callHierarchy_incomingCalls] = { 'callHierarchyProvider' }, [ms.callHierarchy_outgoingCalls] = { 'callHierarchyProvider' }, + [ms.textDocument_prepareTypeHierarchy] = { 'typeHierarchyProvider' }, + [ms.typeHierarchy_subtypes] = { 'typeHierarchyProvider' }, + [ms.typeHierarchy_supertypes] = { 'typeHierarchyProvider' }, [ms.textDocument_rename] = { 'renameProvider' }, [ms.textDocument_prepareRename] = { 'renameProvider', 'prepareProvider' }, [ms.textDocument_codeAction] = { 'codeActionProvider' }, -- cgit From 38b9c322c97b63f53caef7a651211fc9312d055e Mon Sep 17 00:00:00 2001 From: Gregory Anders <8965202+gpanders@users.noreply.github.com> Date: Wed, 24 Apr 2024 21:43:46 -0500 Subject: feat(fs): add vim.fs.root (#28477) vim.fs.root() is a function for finding a project root relative to a buffer using one or more "root markers". This is useful for LSP and could be useful for other "projects" designs, as well as for any plugins which work with a "projects" concept. --- runtime/lua/vim/lsp.lua | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) (limited to 'runtime/lua/vim/lsp.lua') diff --git a/runtime/lua/vim/lsp.lua b/runtime/lua/vim/lsp.lua index ab22c5901a..8403aa0ee6 100644 --- a/runtime/lua/vim/lsp.lua +++ b/runtime/lua/vim/lsp.lua @@ -210,7 +210,7 @@ end --- vim.lsp.start({ --- name = 'my-server-name', --- cmd = {'name-of-language-server-executable'}, ---- root_dir = vim.fs.dirname(vim.fs.find({'pyproject.toml', 'setup.py'}, { upward = true })[1]), +--- root_dir = vim.fs.root(0, {'pyproject.toml', 'setup.py'}), --- }) --- ``` --- @@ -219,9 +219,9 @@ end --- - `name` arbitrary name for the LSP client. Should be unique per language server. --- - `cmd` command string[] or function, described at |vim.lsp.start_client()|. --- - `root_dir` path to the project root. By default this is used to decide if an existing client ---- should be re-used. The example above uses |vim.fs.find()| and |vim.fs.dirname()| to detect the ---- root by traversing the file system upwards starting from the current directory until either ---- a `pyproject.toml` or `setup.py` file is found. +--- should be re-used. The example above uses |vim.fs.root()| and |vim.fs.dirname()| to detect +--- the root by traversing the file system upwards starting from the current directory until +--- either a `pyproject.toml` or `setup.py` file is found. --- - `workspace_folders` list of `{ uri:string, name: string }` tables specifying the project root --- folders used by the language server. If `nil` the property is derived from `root_dir` for --- convenience. -- cgit From 37d8e504593646c81542f8c66f0d608e0a59f036 Mon Sep 17 00:00:00 2001 From: Gregory Anders <8965202+gpanders@users.noreply.github.com> Date: Fri, 26 Apr 2024 08:15:44 -0500 Subject: fix(lsp): add "silent" option to vim.lsp.start (#28478) vim.notify cannot be suppressed and it is not always necessary to display a visible warning to the user if the RPC process fails to start. For instance, a user may have the same LSP configuration across systems, some of which may not have all of the LSP server executables installed. In that case, the user receives a notification every time a file is opened that they cannot suppress. Instead of using vim.notify in vim.lsp.rpc, propagate a normal error up through the call stack and use vim.notify in vim.lsp.start() only if the "silent" option is not set. This also updates lsp.start_client() to return an error message as its second return value if an error occurred, rather than calling vim.notify directly. Callers of lsp.start_client() will need to update call sites appropriately if they wish to report errors to the user (or even better, switch to vim.lsp.start). --- runtime/lua/vim/lsp.lua | 43 +++++++++++++++++++++++++++---------------- 1 file changed, 27 insertions(+), 16 deletions(-) (limited to 'runtime/lua/vim/lsp.lua') diff --git a/runtime/lua/vim/lsp.lua b/runtime/lua/vim/lsp.lua index 8403aa0ee6..fcb1ad5b4b 100644 --- a/runtime/lua/vim/lsp.lua +++ b/runtime/lua/vim/lsp.lua @@ -195,10 +195,13 @@ end --- Predicate used to decide if a client should be re-used. Used on all --- running clients. The default implementation re-uses a client if name and --- root_dir matches. ---- @field reuse_client fun(client: vim.lsp.Client, config: table): boolean +--- @field reuse_client fun(client: vim.lsp.Client, config: vim.lsp.ClientConfig): boolean --- --- Buffer handle to attach to if starting or re-using a client (0 for current). --- @field bufnr integer +--- +--- Suppress error reporting if the LSP server fails to start (default false). +--- @field silent boolean --- Create a new LSP client and start a language server or reuses an already --- running client if one is found matching `name` and `root_dir`. @@ -246,19 +249,25 @@ function lsp.start(config, opts) for _, client in pairs(all_clients) do if reuse_client(client, config) then - lsp.buf_attach_client(bufnr, client.id) - return client.id + if lsp.buf_attach_client(bufnr, client.id) then + return client.id + end end end - local client_id = lsp.start_client(config) + local client_id, err = lsp.start_client(config) + if err then + if not opts.silent then + vim.notify(err, vim.log.levels.WARN) + end + return nil + end - if not client_id then - return -- lsp.start_client will have printed an error + if client_id and lsp.buf_attach_client(bufnr, client_id) then + return client_id end - lsp.buf_attach_client(bufnr, client_id) - return client_id + return nil end --- Consumes the latest progress messages from all clients and formats them as a string. @@ -420,16 +429,18 @@ end --- Starts and initializes a client with the given configuration. --- @param config vim.lsp.ClientConfig Configuration for the server. ---- @return integer|nil client_id |vim.lsp.get_client_by_id()| Note: client may not be ---- fully initialized. Use `on_init` to do any actions once ---- the client has been initialized. +--- @return integer? client_id |vim.lsp.get_client_by_id()| Note: client may not be +--- fully initialized. Use `on_init` to do any actions once +--- the client has been initialized. +--- @return string? # Error message, if any function lsp.start_client(config) - local client = require('vim.lsp.client').create(config) - - if not client then - return + local ok, res = pcall(require('vim.lsp.client').create, config) + if not ok then + return nil, res --[[@as string]] end + local client = assert(res) + --- @diagnostic disable-next-line: invisible table.insert(client._on_exit_cbs, on_client_exit) @@ -437,7 +448,7 @@ function lsp.start_client(config) client:initialize() - return client.id + return client.id, nil end --- Notify all attached clients that a buffer has changed. -- cgit From b2c26a875b9dfd17fd05cf01cf5cc13eb2a10dfd Mon Sep 17 00:00:00 2001 From: Lewis Russell Date: Fri, 26 Apr 2024 14:58:17 +0100 Subject: fix(lsp): ensure buffer is not attached more than once Fixes regression introduced in #28030 If an LSP server is restarted, then the associated `nvim_buf_attach` call will not detach if no buffer changes are sent between the client stopping and a new one being created. This leads to `nvim_buf_attach` being called multiple times for the same buffer, which then leads to changetracking sending duplicate requests to the server (one per attach). To solve this, introduce separate tracking (client agnostic) on which buffers have had calls to `nvim_buf_attach`. --- runtime/lua/vim/lsp.lua | 55 +++++++++++++++++++++---------------------------- 1 file changed, 23 insertions(+), 32 deletions(-) (limited to 'runtime/lua/vim/lsp.lua') diff --git a/runtime/lua/vim/lsp.lua b/runtime/lua/vim/lsp.lua index fcb1ad5b4b..25feeb0e8d 100644 --- a/runtime/lua/vim/lsp.lua +++ b/runtime/lua/vim/lsp.lua @@ -451,30 +451,6 @@ function lsp.start_client(config) return client.id, nil end ---- Notify all attached clients that a buffer has changed. ----@param _ integer ----@param bufnr integer ----@param changedtick integer ----@param firstline integer ----@param lastline integer ----@param new_lastline integer ----@return true? -local function text_document_did_change_handler( - _, - bufnr, - changedtick, - firstline, - lastline, - new_lastline -) - -- Detach (nvim_buf_attach) via returning True to on_lines if no clients are attached - if #lsp.get_clients({ bufnr = bufnr }) == 0 then - return true - end - util.buf_versions[bufnr] = changedtick - changetracking.send_changes(bufnr, firstline, lastline, new_lastline) -end - ---Buffer lifecycle handler for textDocument/didSave --- @param bufnr integer local function text_document_did_save_handler(bufnr) @@ -516,11 +492,18 @@ local function text_document_did_save_handler(bufnr) end end +--- @type table +local attached_buffers = {} + --- @param bufnr integer ---- @param client_id integer -local function buf_attach(bufnr, client_id) +local function buf_attach(bufnr) + if attached_buffers[bufnr] then + return + end + attached_buffers[bufnr] = true + local uri = vim.uri_from_bufnr(bufnr) - local augroup = ('lsp_c_%d_b_%d_save'):format(client_id, bufnr) + local augroup = ('lsp_b_%d_save'):format(bufnr) local group = api.nvim_create_augroup(augroup, { clear = true }) api.nvim_create_autocmd('BufWritePre', { group = group, @@ -559,7 +542,14 @@ local function buf_attach(bufnr, client_id) }) -- First time, so attach and set up stuff. api.nvim_buf_attach(bufnr, false, { - on_lines = text_document_did_change_handler, + on_lines = function(_, _, changedtick, firstline, lastline, new_lastline) + if #lsp.get_clients({ bufnr = bufnr }) == 0 then + return true -- detach + end + util.buf_versions[bufnr] = changedtick + changetracking.send_changes(bufnr, firstline, lastline, new_lastline) + end, + on_reload = function() local params = { textDocument = { uri = uri } } for _, client in ipairs(lsp.get_clients({ bufnr = bufnr })) do @@ -570,6 +560,7 @@ local function buf_attach(bufnr, client_id) client:_text_document_did_open_handler(bufnr) end end, + on_detach = function() local params = { textDocument = { uri = uri } } for _, client in ipairs(lsp.get_clients({ bufnr = bufnr })) do @@ -582,7 +573,9 @@ local function buf_attach(bufnr, client_id) client.attached_buffers[bufnr] = nil end util.buf_versions[bufnr] = nil + attached_buffers[bufnr] = nil end, + -- TODO if we know all of the potential clients ahead of time, then we -- could conditionally set this. -- utf_sizes = size_index > 1; @@ -608,16 +601,14 @@ function lsp.buf_attach_client(bufnr, client_id) log.warn(string.format('buf_attach_client called on unloaded buffer (id: %d): ', bufnr)) return false end - -- This is our first time attaching to this buffer. - if #lsp.get_clients({ bufnr = bufnr, _uninitialized = true }) == 0 then - buf_attach(bufnr, client_id) - end local client = lsp.get_client_by_id(client_id) if not client then return false end + buf_attach(bufnr) + if client.attached_buffers[bufnr] then return true end -- cgit From 688860741589b4583129e426f4df0523f9213275 Mon Sep 17 00:00:00 2001 From: Gregory Anders <8965202+gpanders@users.noreply.github.com> Date: Fri, 26 Apr 2024 11:12:49 -0500 Subject: feat(lsp): add more LSP defaults (#28500) - crn for rename - crr for code actions - gr for references - (in Insert mode) for signature help --- runtime/lua/vim/lsp.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'runtime/lua/vim/lsp.lua') diff --git a/runtime/lua/vim/lsp.lua b/runtime/lua/vim/lsp.lua index 25feeb0e8d..3f459491f3 100644 --- a/runtime/lua/vim/lsp.lua +++ b/runtime/lua/vim/lsp.lua @@ -348,7 +348,7 @@ function lsp._set_defaults(client, bufnr) and is_empty_or_default(bufnr, 'keywordprg') and vim.fn.maparg('K', 'n', false, false) == '' then - vim.keymap.set('n', 'K', vim.lsp.buf.hover, { buffer = bufnr }) + vim.keymap.set('n', 'K', vim.lsp.buf.hover, { buffer = bufnr, desc = 'vim.lsp.buf.hover()' }) end end) if client.supports_method(ms.textDocument_diagnostic) then -- cgit From 9b8a0755390b7eb3ad369f3a0a42eb9aecb8cbe2 Mon Sep 17 00:00:00 2001 From: Mathias Fußenegger Date: Fri, 26 Apr 2024 20:26:21 +0200 Subject: fix(lsp): change `silent` in lsp.start.Opts to optional (#28524) --- runtime/lua/vim/lsp.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'runtime/lua/vim/lsp.lua') diff --git a/runtime/lua/vim/lsp.lua b/runtime/lua/vim/lsp.lua index 3f459491f3..c6d6c8a0cd 100644 --- a/runtime/lua/vim/lsp.lua +++ b/runtime/lua/vim/lsp.lua @@ -201,7 +201,7 @@ end --- @field bufnr integer --- --- Suppress error reporting if the LSP server fails to start (default false). ---- @field silent boolean +--- @field silent? boolean --- Create a new LSP client and start a language server or reuses an already --- running client if one is found matching `name` and `root_dir`. -- cgit From efb44e0cad294f51e330d57d7590d38de5cec62c Mon Sep 17 00:00:00 2001 From: Maria José Solano Date: Sat, 4 May 2024 15:08:17 -0700 Subject: docs: fix lua type warnings (#28633) --- runtime/lua/vim/lsp.lua | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) (limited to 'runtime/lua/vim/lsp.lua') diff --git a/runtime/lua/vim/lsp.lua b/runtime/lua/vim/lsp.lua index c6d6c8a0cd..325c30ca38 100644 --- a/runtime/lua/vim/lsp.lua +++ b/runtime/lua/vim/lsp.lua @@ -310,6 +310,7 @@ local function is_empty_or_default(bufnr, option) end local info = api.nvim_get_option_info2(option, { buf = bufnr }) + ---@param e vim.fn.getscriptinfo.ret local scriptinfo = vim.tbl_filter(function(e) return e.sid == info.last_set_sid end, vim.fn.getscriptinfo()) @@ -515,7 +516,7 @@ local function buf_attach(bufnr) textDocument = { uri = uri, }, - reason = protocol.TextDocumentSaveReason.Manual, + reason = protocol.TextDocumentSaveReason.Manual, ---@type integer } if vim.tbl_get(client.server_capabilities, 'textDocumentSync', 'willSave') then client.notify(ms.textDocument_willSave, params) @@ -899,7 +900,7 @@ end --- a `client_id:result` map. ---@return function cancel Function that cancels all requests. function lsp.buf_request_all(bufnr, method, params, handler) - local results = {} --- @type table + local results = {} --- @type table local result_count = 0 local expected_result_count = 0 @@ -940,7 +941,7 @@ end ---@return table? result Map of client_id:request_result. ---@return string? err On timeout, cancel, or error, `err` is a string describing the failure reason, and `result` is nil. function lsp.buf_request_sync(bufnr, method, params, timeout_ms) - local request_results + local request_results ---@type table local cancel = lsp.buf_request_all(bufnr, method, params, function(it) request_results = it -- cgit From 5eee633c97055fc8c7617f2914835f2860b92d9c Mon Sep 17 00:00:00 2001 From: Mathias Fußenegger Date: Tue, 14 May 2024 19:38:22 +0200 Subject: fix(lsp): don't start additional client if attach failed (#28744) If a client for a server was already running and lsp.start was called in an unloaded buffer it started another client instead of bailing out. --- runtime/lua/vim/lsp.lua | 2 ++ 1 file changed, 2 insertions(+) (limited to 'runtime/lua/vim/lsp.lua') diff --git a/runtime/lua/vim/lsp.lua b/runtime/lua/vim/lsp.lua index 325c30ca38..e2af317823 100644 --- a/runtime/lua/vim/lsp.lua +++ b/runtime/lua/vim/lsp.lua @@ -251,6 +251,8 @@ function lsp.start(config, opts) if reuse_client(client, config) then if lsp.buf_attach_client(bufnr, client.id) then return client.id + else + return nil end end end -- cgit From 7acf39ddab8ebdb63ebf78ec980149d20783fd4b Mon Sep 17 00:00:00 2001 From: dundargoc <33953936+dundargoc@users.noreply.github.com> Date: Wed, 15 May 2024 01:18:33 +0200 Subject: docs: misc (#28609) Closes https://github.com/neovim/neovim/issues/28484. Closes https://github.com/neovim/neovim/issues/28719. Co-authored-by: Chris Co-authored-by: Gregory Anders Co-authored-by: Jake B <16889000+jakethedev@users.noreply.github.com> Co-authored-by: Jonathan Raines Co-authored-by: Yi Ming Co-authored-by: Zane Dufour Co-authored-by: zeertzjq --- runtime/lua/vim/lsp.lua | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) (limited to 'runtime/lua/vim/lsp.lua') diff --git a/runtime/lua/vim/lsp.lua b/runtime/lua/vim/lsp.lua index e2af317823..8103ed4d21 100644 --- a/runtime/lua/vim/lsp.lua +++ b/runtime/lua/vim/lsp.lua @@ -897,12 +897,12 @@ end ---@param bufnr (integer) Buffer handle, or 0 for current. ---@param method (string) LSP method name ---@param params (table|nil) Parameters to send to the server ----@param handler fun(results: table) (function) +---@param handler fun(results: table) (function) --- Handler called after all requests are completed. Server results are passed as --- a `client_id:result` map. ---@return function cancel Function that cancels all requests. function lsp.buf_request_all(bufnr, method, params, handler) - local results = {} --- @type table + local results = {} --- @type table local result_count = 0 local expected_result_count = 0 @@ -940,7 +940,7 @@ end ---@param params table? Parameters to send to the server ---@param timeout_ms integer? Maximum time in milliseconds to wait for a result. --- (default: `1000`) ----@return table? result Map of client_id:request_result. +---@return table? result Map of client_id:request_result. ---@return string? err On timeout, cancel, or error, `err` is a string describing the failure reason, and `result` is nil. function lsp.buf_request_sync(bufnr, method, params, timeout_ms) local request_results ---@type table -- cgit From 8263ed46706671e6a9a21cbb5f9555dd42ff8085 Mon Sep 17 00:00:00 2001 From: Ilia Choly Date: Tue, 21 May 2024 12:16:53 -0400 Subject: fix(lsp): add textDocument/documentLink to capability map (#28838) --- runtime/lua/vim/lsp.lua | 2 ++ 1 file changed, 2 insertions(+) (limited to 'runtime/lua/vim/lsp.lua') diff --git a/runtime/lua/vim/lsp.lua b/runtime/lua/vim/lsp.lua index 8103ed4d21..8751cff902 100644 --- a/runtime/lua/vim/lsp.lua +++ b/runtime/lua/vim/lsp.lua @@ -64,6 +64,8 @@ lsp._request_name_to_capability = { [ms.textDocument_inlayHint] = { 'inlayHintProvider' }, [ms.textDocument_diagnostic] = { 'diagnosticProvider' }, [ms.inlayHint_resolve] = { 'inlayHintProvider', 'resolveProvider' }, + [ms.textDocument_documentLink] = { 'documentLinkProvider' }, + [ms.documentLink_resolve] = { 'documentLinkProvider', 'resolveProvider' }, } -- TODO improve handling of scratch buffers with LSP attached. -- cgit From 879d17ea8d62c199ea0c91c5f37a4f25495be7ce Mon Sep 17 00:00:00 2001 From: Ilia Choly Date: Tue, 21 May 2024 14:02:48 -0400 Subject: fix(lsp): detach all clients on_reload to force buf_state reload (#28875) Problem: The changetracking state can de-sync when reloading a buffer with more than one LSP client attached. Solution: Fully detach all clients from the buffer to force buf_state to be re-created. --- runtime/lua/vim/lsp.lua | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) (limited to 'runtime/lua/vim/lsp.lua') diff --git a/runtime/lua/vim/lsp.lua b/runtime/lua/vim/lsp.lua index 8751cff902..6f12bc457b 100644 --- a/runtime/lua/vim/lsp.lua +++ b/runtime/lua/vim/lsp.lua @@ -556,12 +556,15 @@ local function buf_attach(bufnr) end, on_reload = function() + local clients = lsp.get_clients({ bufnr = bufnr }) local params = { textDocument = { uri = uri } } - for _, client in ipairs(lsp.get_clients({ bufnr = bufnr })) do + for _, client in ipairs(clients) do changetracking.reset_buf(client, bufnr) if vim.tbl_get(client.server_capabilities, 'textDocumentSync', 'openClose') then client.notify(ms.textDocument_didClose, params) end + end + for _, client in ipairs(clients) do client:_text_document_did_open_handler(bufnr) end end, -- cgit From 339129ebc9503883a3f060d3eff620d67a9eadaf Mon Sep 17 00:00:00 2001 From: Ilia Choly Date: Sun, 19 May 2024 13:03:06 -0400 Subject: refactor(lsp): use supports_method where applicable --- runtime/lua/vim/lsp.lua | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) (limited to 'runtime/lua/vim/lsp.lua') diff --git a/runtime/lua/vim/lsp.lua b/runtime/lua/vim/lsp.lua index 6f12bc457b..ad1794e98d 100644 --- a/runtime/lua/vim/lsp.lua +++ b/runtime/lua/vim/lsp.lua @@ -66,6 +66,10 @@ lsp._request_name_to_capability = { [ms.inlayHint_resolve] = { 'inlayHintProvider', 'resolveProvider' }, [ms.textDocument_documentLink] = { 'documentLinkProvider' }, [ms.documentLink_resolve] = { 'documentLinkProvider', 'resolveProvider' }, + [ms.textDocument_didClose] = { 'textDocumentSync', 'openClose' }, + [ms.textDocument_didOpen] = { 'textDocumentSync', 'openClose' }, + [ms.textDocument_willSave] = { 'textDocumentSync', 'willSave' }, + [ms.textDocument_willSaveWaitUntil] = { 'textDocumentSync', 'willSaveWaitUntil' }, } -- TODO improve handling of scratch buffers with LSP attached. @@ -522,10 +526,10 @@ local function buf_attach(bufnr) }, reason = protocol.TextDocumentSaveReason.Manual, ---@type integer } - if vim.tbl_get(client.server_capabilities, 'textDocumentSync', 'willSave') then + if client.supports_method(ms.textDocument_willSave) then client.notify(ms.textDocument_willSave, params) end - if vim.tbl_get(client.server_capabilities, 'textDocumentSync', 'willSaveWaitUntil') then + if client.supports_method(ms.textDocument_willSaveWaitUntil) then local result, err = client.request_sync(ms.textDocument_willSaveWaitUntil, params, 1000, ctx.buf) if result and result.result then @@ -560,7 +564,7 @@ local function buf_attach(bufnr) local params = { textDocument = { uri = uri } } for _, client in ipairs(clients) do changetracking.reset_buf(client, bufnr) - if vim.tbl_get(client.server_capabilities, 'textDocumentSync', 'openClose') then + if client.supports_method(ms.textDocument_didClose) then client.notify(ms.textDocument_didClose, params) end end @@ -573,7 +577,7 @@ local function buf_attach(bufnr) local params = { textDocument = { uri = uri } } for _, client in ipairs(lsp.get_clients({ bufnr = bufnr })) do changetracking.reset_buf(client, bufnr) - if vim.tbl_get(client.server_capabilities, 'textDocumentSync', 'openClose') then + if client.supports_method(ms.textDocument_didClose) then client.notify(ms.textDocument_didClose, params) end end @@ -665,7 +669,7 @@ function lsp.buf_detach_client(bufnr, client_id) changetracking.reset_buf(client, bufnr) - if vim.tbl_get(client.server_capabilities, 'textDocumentSync', 'openClose') then + 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) -- cgit From 5ac8db10f0428c976dfc9a4935a74d0fe160995c Mon Sep 17 00:00:00 2001 From: Andre Toerien <49614525+AThePeanut4@users.noreply.github.com> Date: Thu, 23 May 2024 12:03:47 +0200 Subject: fix(lsp): trigger LspDetach on buffer delete (#28795) Co-authored-by: Mathias Fussenegger --- runtime/lua/vim/lsp.lua | 9 +++++++++ 1 file changed, 9 insertions(+) (limited to 'runtime/lua/vim/lsp.lua') diff --git a/runtime/lua/vim/lsp.lua b/runtime/lua/vim/lsp.lua index ad1794e98d..700c10abc8 100644 --- a/runtime/lua/vim/lsp.lua +++ b/runtime/lua/vim/lsp.lua @@ -576,10 +576,19 @@ local function buf_attach(bufnr) on_detach = function() local params = { textDocument = { uri = uri } } for _, client in ipairs(lsp.get_clients({ bufnr = bufnr })) do + 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 client.notify(ms.textDocument_didClose, params) end + + local namespace = lsp.diagnostic.get_namespace(client.id) + vim.diagnostic.reset(namespace, bufnr) end for _, client in ipairs(all_clients) do client.attached_buffers[bufnr] = nil -- cgit From 2908f71dc9e9591f97e0f9d70dbc8d8b18f9e475 Mon Sep 17 00:00:00 2001 From: Mathias Fußenegger Date: Thu, 23 May 2024 15:17:03 +0200 Subject: refactor(lsp): reuse buf_detach_client logic in on_detach (#28939) --- runtime/lua/vim/lsp.lua | 68 +++++++++++++++++++++---------------------------- 1 file changed, 29 insertions(+), 39 deletions(-) (limited to 'runtime/lua/vim/lsp.lua') diff --git a/runtime/lua/vim/lsp.lua b/runtime/lua/vim/lsp.lua index 700c10abc8..1592fd3151 100644 --- a/runtime/lua/vim/lsp.lua +++ b/runtime/lua/vim/lsp.lua @@ -501,6 +501,30 @@ 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 + util.buf_versions[bufnr] = nil + + local namespace = lsp.diagnostic.get_namespace(client.id) + vim.diagnostic.reset(namespace, bufnr) +end + --- @type table local attached_buffers = {} @@ -574,26 +598,10 @@ local function buf_attach(bufnr) end, on_detach = function() - local params = { textDocument = { uri = uri } } - for _, client in ipairs(lsp.get_clients({ bufnr = bufnr })) do - 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 - client.notify(ms.textDocument_didClose, params) - end - - local namespace = lsp.diagnostic.get_namespace(client.id) - vim.diagnostic.reset(namespace, bufnr) - end - for _, client in ipairs(all_clients) do - client.attached_buffers[bufnr] = nil + local clients = lsp.get_clients({ bufnr = bufnr, _uninitialized = true }) + for _, client in ipairs(clients) do + buf_detach_client(bufnr, client) end - util.buf_versions[bufnr] = nil attached_buffers[bufnr] = nil end, @@ -668,27 +676,9 @@ function lsp.buf_detach_client(bufnr, client_id) ) ) return + else + buf_detach_client(bufnr, client) end - - 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 - util.buf_versions[bufnr] = nil - - local namespace = lsp.diagnostic.get_namespace(client_id) - vim.diagnostic.reset(namespace, bufnr) end --- Checks if a buffer is attached for a particular client. -- cgit