Files
neovim/runtime/lua/vim/lsp/handlers.lua
dundargoc 24cea4c7f7 refactor: create function for deferred loading
The benefit of this is that users only pay for what they use. If e.g.
only `vim.lsp.buf_get_clients()` is called then they don't need to load
all modules under `vim.lsp` which could lead to significant startuptime
saving.

Also `vim.lsp.module` is a bit nicer to user compared to
`require("vim.lsp.module")`.

This isn't used for some nested modules such as `filetype` as it breaks
tests with error messages such as "attempt to index field 'detect'".
It's not entirely certain the reason for this, but it is likely it is
due to filetype being precompiled which would imply deferred loading
isn't needed for performance reasons.
2024-02-02 13:09:12 +01:00

682 lines
25 KiB
Lua

local log = require('vim.lsp.log')
local protocol = require('vim.lsp.protocol')
local ms = protocol.Methods
local util = require('vim.lsp.util')
local api = vim.api
--- @type table<string,lsp.Handler>
local M = {}
-- FIXME: DOC: Expose in vimdocs
--- Writes to error buffer.
---@param ... string Will be concatenated before being written
local function err_message(...)
vim.notify(table.concat(vim.tbl_flatten({ ... })), vim.log.levels.ERROR)
api.nvim_command('redraw')
end
--see: https://microsoft.github.io/language-server-protocol/specifications/specification-current/#workspace_executeCommand
M[ms.workspace_executeCommand] = function(_, _, _, _)
-- Error handling is done implicitly by wrapping all handlers; see end of this file
end
--see: https://microsoft.github.io/language-server-protocol/specifications/specification-current/#progress
---@param result lsp.ProgressParams
---@param ctx lsp.HandlerContext
M[ms.dollar_progress] = function(_, result, ctx)
local client = vim.lsp.get_client_by_id(ctx.client_id)
if not client then
err_message('LSP[id=', tostring(ctx.client_id), '] client has shut down during progress update')
return vim.NIL
end
local kind = nil
local value = result.value
if type(value) == 'table' then
kind = value.kind
-- Carry over title of `begin` messages to `report` and `end` messages
-- So that consumers always have it available, even if they consume a
-- subset of the full sequence
if kind == 'begin' then
client.progress.pending[result.token] = value.title
else
value.title = client.progress.pending[result.token]
if kind == 'end' then
client.progress.pending[result.token] = nil
end
end
end
client.progress:push(result)
api.nvim_exec_autocmds('LspProgress', {
pattern = kind,
modeline = false,
data = { client_id = ctx.client_id, result = result },
})
end
--see: https://microsoft.github.io/language-server-protocol/specifications/specification-current/#window_workDoneProgress_create
---@param result lsp.WorkDoneProgressCreateParams
---@param ctx lsp.HandlerContext
M[ms.window_workDoneProgress_create] = function(_, result, ctx)
local client = vim.lsp.get_client_by_id(ctx.client_id)
if not client then
err_message('LSP[id=', tostring(ctx.client_id), '] client has shut down during progress update')
return vim.NIL
end
client.progress:push(result)
return vim.NIL
end
--see: https://microsoft.github.io/language-server-protocol/specifications/specification-current/#window_showMessageRequest
---@param result lsp.ShowMessageRequestParams
M[ms.window_showMessageRequest] = function(_, result)
local actions = result.actions or {}
local co, is_main = coroutine.running()
if co and not is_main then
local opts = {
prompt = result.message .. ': ',
format_item = function(action)
return (action.title:gsub('\r\n', '\\r\\n')):gsub('\n', '\\n')
end,
}
vim.ui.select(actions, opts, function(choice)
-- schedule to ensure resume doesn't happen _before_ yield with
-- default synchronous vim.ui.select
vim.schedule(function()
coroutine.resume(co, choice or vim.NIL)
end)
end)
return coroutine.yield()
else
local option_strings = { result.message, '\nRequest Actions:' }
for i, action in ipairs(actions) do
local title = action.title:gsub('\r\n', '\\r\\n')
title = title:gsub('\n', '\\n')
table.insert(option_strings, string.format('%d. %s', i, title))
end
local choice = vim.fn.inputlist(option_strings)
if choice < 1 or choice > #actions then
return vim.NIL
else
return actions[choice]
end
end
end
--see: https://microsoft.github.io/language-server-protocol/specifications/specification-current/#client_registerCapability
M[ms.client_registerCapability] = function(_, result, ctx)
local client_id = ctx.client_id
local client = assert(vim.lsp.get_client_by_id(client_id))
client.dynamic_capabilities:register(result.registrations)
for bufnr, _ in pairs(client.attached_buffers) do
vim.lsp._set_defaults(client, bufnr)
end
---@type string[]
local unsupported = {}
for _, reg in ipairs(result.registrations) do
if reg.method == ms.workspace_didChangeWatchedFiles then
vim.lsp._watchfiles.register(reg, ctx)
elseif not client.dynamic_capabilities:supports_registration(reg.method) then
unsupported[#unsupported + 1] = reg.method
end
end
if #unsupported > 0 then
local warning_tpl = 'The language server %s triggers a registerCapability '
.. 'handler for %s despite dynamicRegistration set to false. '
.. 'Report upstream, this warning is harmless'
local client_name = client and client.name or string.format('id=%d', client_id)
local warning = string.format(warning_tpl, client_name, table.concat(unsupported, ', '))
log.warn(warning)
end
return vim.NIL
end
--see: https://microsoft.github.io/language-server-protocol/specifications/specification-current/#client_unregisterCapability
M[ms.client_unregisterCapability] = function(_, result, ctx)
local client_id = ctx.client_id
local client = assert(vim.lsp.get_client_by_id(client_id))
client.dynamic_capabilities:unregister(result.unregisterations)
for _, unreg in ipairs(result.unregisterations) do
if unreg.method == ms.workspace_didChangeWatchedFiles then
vim.lsp._watchfiles.unregister(unreg, ctx)
end
end
return vim.NIL
end
--see: https://microsoft.github.io/language-server-protocol/specifications/specification-current/#workspace_applyEdit
M[ms.workspace_applyEdit] = function(_, workspace_edit, ctx)
assert(
workspace_edit,
'workspace/applyEdit must be called with `ApplyWorkspaceEditParams`. Server is violating the specification'
)
-- TODO(ashkan) Do something more with label?
local client_id = ctx.client_id
local client = assert(vim.lsp.get_client_by_id(client_id))
if workspace_edit.label then
print('Workspace edit', workspace_edit.label)
end
local status, result =
pcall(util.apply_workspace_edit, workspace_edit.edit, client.offset_encoding)
return {
applied = status,
failureReason = result,
}
end
---@param table table e.g., { foo = { bar = "z" } }
---@param section string indicating the field of the table, e.g., "foo.bar"
---@return any|nil setting value read from the table, or `nil` not found
local function lookup_section(table, section)
local keys = vim.split(section, '.', { plain = true }) --- @type string[]
return vim.tbl_get(table, unpack(keys))
end
--see: https://microsoft.github.io/language-server-protocol/specifications/specification-current/#workspace_configuration
M[ms.workspace_configuration] = function(_, result, ctx)
local client_id = ctx.client_id
local client = vim.lsp.get_client_by_id(client_id)
if not client then
err_message(
'LSP[',
client_id,
'] client has shut down after sending a workspace/configuration request'
)
return
end
if not result.items then
return {}
end
local response = {}
for _, item in ipairs(result.items) do
if item.section then
local value = lookup_section(client.config.settings, item.section)
-- For empty sections with no explicit '' key, return settings as is
if value == nil and item.section == '' then
value = client.config.settings
end
if value == nil then
value = vim.NIL
end
table.insert(response, value)
end
end
return response
end
--see: https://microsoft.github.io/language-server-protocol/specifications/specification-current/#workspace_workspaceFolders
M[ms.workspace_workspaceFolders] = function(_, _, ctx)
local client_id = ctx.client_id
local client = vim.lsp.get_client_by_id(client_id)
if not client then
err_message('LSP[id=', client_id, '] client has shut down after sending the message')
return
end
return client.workspace_folders or vim.NIL
end
M[ms.textDocument_publishDiagnostics] = function(...)
return vim.lsp.diagnostic.on_publish_diagnostics(...)
end
M[ms.textDocument_diagnostic] = function(...)
return vim.lsp.diagnostic.on_diagnostic(...)
end
M[ms.textDocument_codeLens] = function(...)
return vim.lsp.codelens.on_codelens(...)
end
M[ms.textDocument_inlayHint] = function(...)
return vim.lsp.inlay_hint.on_inlayhint(...)
end
--see: https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_references
M[ms.textDocument_references] = function(_, result, ctx, config)
if not result or vim.tbl_isempty(result) then
vim.notify('No references found')
return
end
local client = assert(vim.lsp.get_client_by_id(ctx.client_id))
config = config or {}
local title = 'References'
local items = util.locations_to_items(result, client.offset_encoding)
if config.loclist then
vim.fn.setloclist(0, {}, ' ', { title = title, items = items, context = ctx })
api.nvim_command('lopen')
elseif config.on_list then
assert(type(config.on_list) == 'function', 'on_list is not a function')
config.on_list({ title = title, items = items, context = ctx })
else
vim.fn.setqflist({}, ' ', { title = title, items = items, context = ctx })
api.nvim_command('botright copen')
end
end
--- Return a function that converts LSP responses to list items and opens the list
---
--- The returned function has an optional {config} parameter that accepts a table
--- with the following keys:
---
--- loclist: (boolean) use the location list (default is to use the quickfix list)
---
---@param map_result function `((resp, bufnr) -> list)` to convert the response
---@param entity string name of the resource used in a `not found` error message
---@param title_fn fun(ctx: lsp.HandlerContext): string Function to call to generate list title
---@return lsp.Handler
local function response_to_list(map_result, entity, title_fn)
return function(_, result, ctx, config)
if not result or vim.tbl_isempty(result) then
vim.notify('No ' .. entity .. ' found')
return
end
config = config or {}
local title = title_fn(ctx)
local items = map_result(result, ctx.bufnr)
if config.loclist then
vim.fn.setloclist(0, {}, ' ', { title = title, items = items, context = ctx })
api.nvim_command('lopen')
elseif config.on_list then
assert(type(config.on_list) == 'function', 'on_list is not a function')
config.on_list({ title = title, items = items, context = ctx })
else
vim.fn.setqflist({}, ' ', { title = title, items = items, context = ctx })
api.nvim_command('botright copen')
end
end
end
--see: https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_documentSymbol
M[ms.textDocument_documentSymbol] = response_to_list(
util.symbols_to_items,
'document symbols',
function(ctx)
local fname = vim.fn.fnamemodify(vim.uri_to_fname(ctx.params.textDocument.uri), ':.')
return string.format('Symbols in %s', fname)
end
)
--see: https://microsoft.github.io/language-server-protocol/specifications/specification-current/#workspace_symbol
M[ms.workspace_symbol] = response_to_list(util.symbols_to_items, 'symbols', function(ctx)
return string.format("Symbols matching '%s'", ctx.params.query)
end)
--see: https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_rename
M[ms.textDocument_rename] = function(_, result, ctx, _)
if not result then
vim.notify("Language server couldn't provide rename result", vim.log.levels.INFO)
return
end
local client = assert(vim.lsp.get_client_by_id(ctx.client_id))
util.apply_workspace_edit(result, client.offset_encoding)
end
--see: https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_rangeFormatting
M[ms.textDocument_rangeFormatting] = function(_, result, ctx, _)
if not result then
return
end
local client = assert(vim.lsp.get_client_by_id(ctx.client_id))
util.apply_text_edits(result, ctx.bufnr, client.offset_encoding)
end
--see: https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_formatting
M[ms.textDocument_formatting] = function(_, result, ctx, _)
if not result then
return
end
local client = assert(vim.lsp.get_client_by_id(ctx.client_id))
util.apply_text_edits(result, ctx.bufnr, client.offset_encoding)
end
--see: https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_completion
M[ms.textDocument_completion] = function(_, result, _, _)
if vim.tbl_isempty(result or {}) then
return
end
local cursor = api.nvim_win_get_cursor(0)
local row, col = cursor[1], cursor[2]
local line = assert(api.nvim_buf_get_lines(0, row - 1, row, false)[1])
local line_to_cursor = line:sub(col + 1)
local textMatch = vim.fn.match(line_to_cursor, '\\k*$')
local prefix = line_to_cursor:sub(textMatch + 1)
local matches = util.text_document_completion_list_to_complete_items(result, prefix)
vim.fn.complete(textMatch + 1, matches)
end
--- |lsp-handler| for the method "textDocument/hover"
---
--- ```lua
--- vim.lsp.handlers["textDocument/hover"] = vim.lsp.with(
--- vim.lsp.handlers.hover, {
--- -- Use a sharp border with `FloatBorder` highlights
--- border = "single",
--- -- add the title in hover float window
--- title = "hover"
--- }
--- )
--- ```
---
---@param ctx lsp.HandlerContext
---@param config table Configuration table.
--- - border: (default=nil)
--- - Add borders to the floating window
--- - See |vim.lsp.util.open_floating_preview()| for more options.
function M.hover(_, result, ctx, config)
config = config or {}
config.focus_id = ctx.method
if api.nvim_get_current_buf() ~= ctx.bufnr then
-- Ignore result since buffer changed. This happens for slow language servers.
return
end
if not (result and result.contents) then
if config.silent ~= true then
vim.notify('No information available')
end
return
end
local format = 'markdown'
local contents ---@type string[]
if type(result.contents) == 'table' and result.contents.kind == 'plaintext' then
format = 'plaintext'
contents = vim.split(result.contents.value or '', '\n', { trimempty = true })
else
contents = util.convert_input_to_markdown_lines(result.contents)
end
if vim.tbl_isempty(contents) then
if config.silent ~= true then
vim.notify('No information available')
end
return
end
return util.open_floating_preview(contents, format, config)
end
--see: https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_hover
M[ms.textDocument_hover] = M.hover
--- Jumps to a location. Used as a handler for multiple LSP methods.
---@param _ nil not used
---@param result (table) result of LSP method; a location or a list of locations.
---@param ctx (lsp.HandlerContext) table containing the context of the request, including the method
---(`textDocument/definition` can return `Location` or `Location[]`
local function location_handler(_, result, ctx, config)
if result == nil or vim.tbl_isempty(result) then
if log.info() then
log.info(ctx.method, 'No location found')
end
return nil
end
local client = assert(vim.lsp.get_client_by_id(ctx.client_id))
config = config or {}
-- textDocument/definition can return Location or Location[]
-- https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_definition
if not vim.tbl_islist(result) then
result = { result }
end
local title = 'LSP locations'
local items = util.locations_to_items(result, client.offset_encoding)
if config.on_list then
assert(type(config.on_list) == 'function', 'on_list is not a function')
config.on_list({ title = title, items = items })
return
end
if #result == 1 then
util.jump_to_location(result[1], client.offset_encoding, config.reuse_win)
return
end
vim.fn.setqflist({}, ' ', { title = title, items = items })
api.nvim_command('botright copen')
end
--see: https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_declaration
M[ms.textDocument_declaration] = location_handler
--see: https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_definition
M[ms.textDocument_definition] = location_handler
--see: https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_typeDefinition
M[ms.textDocument_typeDefinition] = location_handler
--see: https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_implementation
M[ms.textDocument_implementation] = location_handler
--- |lsp-handler| for the method "textDocument/signatureHelp".
---
--- The active parameter is highlighted with |hl-LspSignatureActiveParameter|.
---
--- ```lua
--- vim.lsp.handlers["textDocument/signatureHelp"] = vim.lsp.with(
--- vim.lsp.handlers.signature_help, {
--- -- Use a sharp border with `FloatBorder` highlights
--- border = "single"
--- }
--- )
--- ```
---
---@param result table Response from the language server
---@param ctx lsp.HandlerContext Client context
---@param config table Configuration table.
--- - border: (default=nil)
--- - Add borders to the floating window
--- - See |vim.lsp.util.open_floating_preview()| for more options
function M.signature_help(_, result, ctx, config)
config = config or {}
config.focus_id = ctx.method
if api.nvim_get_current_buf() ~= ctx.bufnr then
-- Ignore result since buffer changed. This happens for slow language servers.
return
end
-- When use `autocmd CompleteDone <silent><buffer> lua vim.lsp.buf.signature_help()` to call signatureHelp handler
-- If the completion item doesn't have signatures It will make noise. Change to use `print` that can use `<silent>` to ignore
if not (result and result.signatures and result.signatures[1]) then
if config.silent ~= true then
print('No signature help available')
end
return
end
local client = assert(vim.lsp.get_client_by_id(ctx.client_id))
local triggers =
vim.tbl_get(client.server_capabilities, 'signatureHelpProvider', 'triggerCharacters')
local ft = vim.bo[ctx.bufnr].filetype
local lines, hl = util.convert_signature_help_to_markdown_lines(result, ft, triggers)
if not lines or vim.tbl_isempty(lines) then
if config.silent ~= true then
print('No signature help available')
end
return
end
local fbuf, fwin = util.open_floating_preview(lines, 'markdown', config)
if hl then
-- Highlight the second line if the signature is wrapped in a Markdown code block.
local line = vim.startswith(lines[1], '```') and 1 or 0
api.nvim_buf_add_highlight(fbuf, -1, 'LspSignatureActiveParameter', line, unpack(hl))
end
return fbuf, fwin
end
--see: https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_signatureHelp
M[ms.textDocument_signatureHelp] = M.signature_help
--see: https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_documentHighlight
M[ms.textDocument_documentHighlight] = function(_, result, ctx, _)
if not result then
return
end
local client_id = ctx.client_id
local client = vim.lsp.get_client_by_id(client_id)
if not client then
return
end
util.buf_highlight_references(ctx.bufnr, result, client.offset_encoding)
end
---@private
---
--- Displays call hierarchy in the quickfix window.
---
---@param direction 'from'|'to' `"from"` for incoming calls and `"to"` for outgoing calls
---@return function
--- `CallHierarchyIncomingCall[]` if {direction} is `"from"`,
--- `CallHierarchyOutgoingCall[]` if {direction} is `"to"`,
local make_call_hierarchy_handler = function(direction)
return function(_, result)
if not result then
return
end
local items = {}
for _, call_hierarchy_call in pairs(result) do
local call_hierarchy_item = call_hierarchy_call[direction]
for _, range in pairs(call_hierarchy_call.fromRanges) do
table.insert(items, {
filename = assert(vim.uri_to_fname(call_hierarchy_item.uri)),
text = call_hierarchy_item.name,
lnum = range.start.line + 1,
col = range.start.character + 1,
})
end
end
vim.fn.setqflist({}, ' ', { title = 'LSP call hierarchy', items = items })
api.nvim_command('botright copen')
end
end
--see: https://microsoft.github.io/language-server-protocol/specifications/specification-current/#callHierarchy_incomingCalls
M[ms.callHierarchy_incomingCalls] = make_call_hierarchy_handler('from')
--see: https://microsoft.github.io/language-server-protocol/specifications/specification-current/#callHierarchy_outgoingCalls
M[ms.callHierarchy_outgoingCalls] = make_call_hierarchy_handler('to')
--see: https://microsoft.github.io/language-server-protocol/specifications/specification-current/#window_logMessage
M[ms.window_logMessage] = function(_, result, ctx, _)
local message_type = result.type
local message = result.message
local client_id = ctx.client_id
local client = vim.lsp.get_client_by_id(client_id)
local client_name = client and client.name or string.format('id=%d', client_id)
if not client then
err_message('LSP[', client_name, '] client has shut down after sending ', message)
end
if message_type == protocol.MessageType.Error then
log.error(message)
elseif message_type == protocol.MessageType.Warning then
log.warn(message)
elseif message_type == protocol.MessageType.Info or message_type == protocol.MessageType.Log then
log.info(message)
else
log.debug(message)
end
return result
end
--see: https://microsoft.github.io/language-server-protocol/specifications/specification-current/#window_showMessage
M[ms.window_showMessage] = function(_, result, ctx, _)
local message_type = result.type
local message = result.message
local client_id = ctx.client_id
local client = vim.lsp.get_client_by_id(client_id)
local client_name = client and client.name or string.format('id=%d', client_id)
if not client then
err_message('LSP[', client_name, '] client has shut down after sending ', message)
end
if message_type == protocol.MessageType.Error then
err_message('LSP[', client_name, '] ', message)
else
local message_type_name = protocol.MessageType[message_type]
api.nvim_out_write(string.format('LSP[%s][%s] %s\n', client_name, message_type_name, message))
end
return result
end
--see: https://microsoft.github.io/language-server-protocol/specifications/specification-current/#window_showDocument
M[ms.window_showDocument] = function(_, result, ctx, _)
local uri = result.uri
if result.external then
-- TODO(lvimuser): ask the user for confirmation
local ret, err = vim.ui.open(uri)
if ret == nil or ret.code ~= 0 then
return {
success = false,
error = {
code = protocol.ErrorCodes.UnknownErrorCode,
message = ret and ret.stderr or err,
},
}
end
return { success = true }
end
local client_id = ctx.client_id
local client = vim.lsp.get_client_by_id(client_id)
local client_name = client and client.name or string.format('id=%d', client_id)
if not client then
err_message('LSP[', client_name, '] client has shut down after sending ', ctx.method)
return vim.NIL
end
local location = {
uri = uri,
range = result.selection,
}
local success = util.show_document(location, client.offset_encoding, {
reuse_win = true,
focus = result.takeFocus,
})
return { success = success or false }
end
---@see https://microsoft.github.io/language-server-protocol/specification/#workspace_inlayHint_refresh
M[ms.workspace_inlayHint_refresh] = function(err, result, ctx, config)
return vim.lsp.inlay_hint.on_refresh(err, result, ctx, config)
end
-- Add boilerplate error validation and logging for all of these.
for k, fn in pairs(M) do
M[k] = function(err, result, ctx, config)
local _ = log.trace()
and log.trace('default_handler', ctx.method, {
err = err,
result = result,
ctx = vim.inspect(ctx),
config = config,
})
if err then
-- LSP spec:
-- interface ResponseError:
-- code: integer;
-- message: string;
-- data?: string | number | boolean | array | object | null;
-- Per LSP, don't show ContentModified error to the user.
if err.code ~= protocol.ErrorCodes.ContentModified then
local client = vim.lsp.get_client_by_id(ctx.client_id)
local client_name = client and client.name or string.format('client_id=%d', ctx.client_id)
err_message(client_name .. ': ' .. tostring(err.code) .. ': ' .. err.message)
end
return
end
return fn(err, result, ctx, config)
end
end
return M