feat(lsp): add LspAttach and LspDetach autocommands

The current approach of using `on_attach` callbacks for configuring
buffers for LSP is suboptimal:

1. It does not use the standard Nvim interface for driving and hooking
   into events (i.e. autocommands)
2. There is no way for "third parties" (e.g. plugins) to hook into the
   event. This means that *all* buffer configuration must go into the
   user-supplied on_attach callback. This also makes it impossible for
   these configurations to be modular, since it all must happen in the
   same place.
3. There is currently no way to do something when a client detaches from
   a buffer (there is no `on_detach` callback).

The solution is to use the traditional method of event handling in Nvim:
autocommands. When a LSP client is attached to a buffer, fire a
`LspAttach`. Likewise, when a client detaches from a buffer fire a
`LspDetach` event.

This enables plugins to easily add LSP-specific configuration to buffers
as well as enabling users to make their own configurations more modular
(e.g. by creating multiple LspAttach autocommands that each do
something unique).
This commit is contained in:
Gregory Anders 2022-05-09 12:00:27 -06:00
parent 8a9ab88945
commit 2ffafc7aa9
4 changed files with 112 additions and 9 deletions

View File

@ -461,6 +461,39 @@ LspSignatureActiveParameter
==============================================================================
EVENTS *lsp-events*
*LspAttach*
After an LSP client attaches to a buffer. The |autocmd-pattern| is the
name of the buffer. When used from Lua, the client ID is passed to the
callback in the "data" table. Example: >
vim.api.nvim_create_autocmd("LspAttach", {
callback = function(args)
local bufnr = args.buf
local client = vim.lsp.get_client_by_id(args.data.client_id)
if client.server_capabilities.completionProvider then
vim.bo[bufnr].omnifunc = "v:lua.vim.lsp.omnifunc"
end
if client.server_capabilities.definitionProvider then
vim.bo[bufnr].tagfunc = "v:lua.vim.lsp.tagfunc"
end
end,
})
<
*LspDetach*
Just before an LSP client detaches from a buffer. The |autocmd-pattern| is the
name of the buffer. When used from Lua, the client ID is passed to the
callback in the "data" table. Example: >
vim.api.nvim_create_autocmd("LspDetach", {
callback = function(args)
local client = vim.lsp.get_client_by_id(args.data.client_id)
-- Do something with the client
vim.cmd("setlocal tagfunc< omnifunc<")
end,
})
<
In addition, the following |User| |autocommands| are provided:
LspProgressUpdate *LspProgressUpdate*
Upon receipt of a progress notification from the server. See
|vim.lsp.util.get_progress_messages()|.

View File

@ -1,5 +1,3 @@
local if_nil = vim.F.if_nil
local default_handlers = require('vim.lsp.handlers')
local log = require('vim.lsp.log')
local lsp_rpc = require('vim.lsp.rpc')
@ -8,11 +6,16 @@ local util = require('vim.lsp.util')
local sync = require('vim.lsp.sync')
local vim = vim
local nvim_err_writeln, nvim_buf_get_lines, nvim_command, nvim_buf_get_option =
vim.api.nvim_err_writeln, vim.api.nvim_buf_get_lines, vim.api.nvim_command, vim.api.nvim_buf_get_option
local nvim_err_writeln, nvim_buf_get_lines, nvim_command, nvim_buf_get_option, nvim_exec_autocmds =
vim.api.nvim_err_writeln,
vim.api.nvim_buf_get_lines,
vim.api.nvim_command,
vim.api.nvim_buf_get_option,
vim.api.nvim_exec_autocmds
local uv = vim.loop
local tbl_isempty, tbl_extend = vim.tbl_isempty, vim.tbl_extend
local validate = vim.validate
local if_nil = vim.F.if_nil
local lsp = {
protocol = protocol,
@ -867,15 +870,27 @@ function lsp.start_client(config)
pcall(config.on_exit, code, signal, client_id)
end
for bufnr, client_ids in pairs(all_buffer_active_clients) do
if client_ids[client_id] then
vim.schedule(function()
nvim_exec_autocmds('LspDetach', {
buffer = bufnr,
modeline = false,
data = { client_id = client_id },
})
local namespace = vim.lsp.diagnostic.get_namespace(client_id)
vim.diagnostic.reset(namespace, bufnr)
end)
client_ids[client_id] = nil
end
end
active_clients[client_id] = nil
uninitialized_clients[client_id] = nil
lsp.diagnostic.reset(client_id, all_buffer_active_clients)
changetracking.reset(client_id)
for _, client_ids in pairs(all_buffer_active_clients) do
client_ids[client_id] = nil
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', client_id, code, signal)
vim.schedule(function()
@ -1213,6 +1228,13 @@ function lsp.start_client(config)
---@param bufnr (number) Buffer number
function client._on_attach(bufnr)
text_document_did_open_handler(bufnr, client)
nvim_exec_autocmds('LspAttach', {
buffer = bufnr,
modeline = false,
data = { client_id = client.id },
})
if config.on_attach then
-- TODO(ashkan) handle errors.
pcall(config.on_attach, client, bufnr)
@ -1359,6 +1381,12 @@ function lsp.buf_detach_client(bufnr, client_id)
return
end
nvim_exec_autocmds('LspDetach', {
buffer = bufnr,
modeline = false,
data = { client_id = client_id },
})
changetracking.reset_buf(client, bufnr)
if vim.tbl_get(client.server_capabilities, 'textDocumentSync', 'openClose') then

View File

@ -70,6 +70,8 @@ return {
'InsertEnter', -- when entering Insert mode
'InsertLeave', -- just after leaving Insert mode
'InsertLeavePre', -- just before leaving Insert mode
'LspAttach', -- after an LSP client attaches to a buffer
'LspDetach', -- after an LSP client detaches from a buffer
'MenuPopup', -- just before popup menu is displayed
'ModeChanged', -- after changing the mode
'OptionSet', -- after setting any option
@ -133,6 +135,8 @@ return {
nvim_specific = {
BufModifiedSet=true,
DiagnosticChanged=true,
LspAttach=true,
LspDetach=true,
RecordingEnter=true,
RecordingLeave=true,
Signal=true,

View File

@ -18,6 +18,7 @@ local NIL = helpers.NIL
local read_file = require('test.helpers').read_file
local write_file = require('test.helpers').write_file
local isCI = helpers.isCI
local meths = helpers.meths
-- Use these to get access to a coroutine so that I can run async tests and use
-- yield.
@ -341,6 +342,43 @@ describe('LSP', function()
}
end)
it('should fire autocommands on attach and detach', function()
local client
test_rpc_server {
test_name = "basic_init";
on_setup = function()
exec_lua [[
BUFFER = vim.api.nvim_create_buf(false, true)
vim.api.nvim_create_autocmd('LspAttach', {
callback = function(args)
local client = vim.lsp.get_client_by_id(args.data.client_id)
vim.g.lsp_attached = client.name
end,
})
vim.api.nvim_create_autocmd('LspDetach', {
callback = function(args)
local client = vim.lsp.get_client_by_id(args.data.client_id)
vim.g.lsp_detached = client.name
end,
})
]]
end;
on_init = function(_client)
client = _client
eq(true, exec_lua("return lsp.buf_attach_client(BUFFER, TEST_RPC_CLIENT_ID)"))
client.notify('finish')
end;
on_handler = function(_, _, ctx)
if ctx.method == 'finish' then
eq('basic_init', meths.get_var('lsp_attached'))
exec_lua("return lsp.buf_detach_client(BUFFER, TEST_RPC_CLIENT_ID)")
eq('basic_init', meths.get_var('lsp_detached'))
client.stop()
end
end;
}
end)
it('client should return settings via workspace/configuration handler', function()
local expected_handlers = {
{NIL, {}, {method="shutdown", client_id=1}};