Request and notification handlers in the LSP client
LSP handlers are functions that process responses from language servers. Each LSP method can have a custom handler to control how Neovim reacts to server responses.
vim.lsp.handlers['textDocument/hover'] = function(err, result, ctx, config) if err then vim.notify('Hover error: ' .. err.message, vim.log.levels.ERROR) return end if not result or not result.contents then vim.notify('No hover information', vim.log.levels.INFO) return end -- Use default handler with custom config local opts = vim.tbl_extend('force', config or {}, { border = 'rounded', max_width = 80, }) return vim.lsp.handlers.hover(err, result, ctx, opts)end
vim.lsp.handlers['textDocument/codeAction'] = function(err, result, ctx, config) if err then return end -- Only show quickfix actions local quickfixes = vim.tbl_filter(function(action) return action.kind and action.kind:match('^quickfix') end, result or {}) if #quickfixes == 0 then vim.notify('No quickfix actions available', vim.log.levels.INFO) return end -- Use default handler with filtered results return vim.lsp.handlers['textDocument/codeAction'](err, quickfixes, ctx, config)end
vim.lsp.handlers['window/showMessageRequest'] = function(err, result, ctx) if not result or not result.actions then return vim.NIL end vim.ui.select(result.actions, { prompt = result.message, format_item = function(action) return action.title end, }, function(choice) if choice then -- User selected an action, return it to server return choice end return vim.NIL end)end
vim.lsp.handlers['$/progress'] = function(err, result, ctx) local client = vim.lsp.get_client_by_id(ctx.client_id) local value = result.value if value.kind == 'begin' then print(string.format('[%s] %s: %s', client.name, value.title, value.message or '')) elseif value.kind == 'report' then if value.percentage then print(string.format('[%s] %d%%', client.name, value.percentage)) end elseif value.kind == 'end' then print(string.format('[%s] %s: done', client.name, value.title)) endend
vim.lsp.handlers['textDocument/definition'] = function(err, result, ctx) if err then vim.notify('Error finding definition', vim.log.levels.ERROR) return end if not result or vim.tbl_isempty(result) then vim.notify('Definition not found', vim.log.levels.WARN) return end local client = vim.lsp.get_client_by_id(ctx.client_id) -- Single result: jump directly if not vim.islist(result) then vim.lsp.util.jump_to_location(result, client.offset_encoding) return end -- Multiple results: use quickfix if #result == 1 then vim.lsp.util.jump_to_location(result[1], client.offset_encoding) else vim.fn.setqflist({}, ' ', { title = 'LSP Definitions', items = vim.lsp.util.locations_to_items(result, client.offset_encoding), }) vim.cmd.copen() endend
vim.lsp.handlers['textDocument/references'] = function(err, result, ctx, config) if not result or vim.tbl_isempty(result) then vim.notify('No references found', vim.log.levels.INFO) return end -- Filter out test files local filtered = vim.tbl_filter(function(ref) return not ref.uri:match('_test%.%w+$') end, result) local client = vim.lsp.get_client_by_id(ctx.client_id) local items = vim.lsp.util.locations_to_items(filtered, client.offset_encoding) vim.fn.setloclist(0, {}, ' ', { title = string.format('References (%d)', #items), items = items, }) vim.cmd.lopen()end
vim.lsp.handlers['textDocument/completion'] = function(err, result, ctx) if err or not result then return end -- Normalize to CompletionList local items = result.items or result -- Filter items based on custom criteria local filtered = vim.tbl_filter(function(item) -- Only show functions and methods return item.kind == 3 or item.kind == 2 -- Function or Method end, items) if #filtered == 0 then return end -- Process completion items vim.lsp.completion._convert_to_completion_items(filtered, ctx.bufnr)end
vim.lsp.handlers['textDocument/formatting'] = function(err, result, ctx) local client = vim.lsp.get_client_by_id(ctx.client_id) if client.name == 'null-ls' then -- Special handling for null-ls vim.notify('Formatted with null-ls', vim.log.levels.INFO) elseif client.name == 'prettier' then -- Special handling for prettier vim.notify('Formatted with prettier', vim.log.levels.INFO) end -- Apply edits if result then vim.lsp.util.apply_text_edits(result, ctx.bufnr, client.offset_encoding) endend
local function safe_handler(handler_fn) return function(err, result, ctx, config) if err then local client = vim.lsp.get_client_by_id(ctx.client_id) vim.notify( string.format('LSP[%s] %s error: %s', client.name, ctx.method, err.message), vim.log.levels.ERROR ) return end local ok, ret = pcall(handler_fn, err, result, ctx, config) if not ok then vim.notify( string.format('Handler error for %s: %s', ctx.method, ret), vim.log.levels.ERROR ) end return ret endendvim.lsp.handlers['textDocument/hover'] = safe_handler(function(err, result, ctx, config) -- Custom hover logicend)
vim.lsp.start({ name = 'my-server', cmd = { 'language-server' }, handlers = { ['textDocument/publishDiagnostics'] = function(err, result, ctx) -- Only for this client print('Diagnostics from my-server:', #result.diagnostics) -- Still process with default handler vim.lsp.diagnostic.on_publish_diagnostics(err, result, ctx) end, },})