mirror of
https://github.com/neovim/neovim.git
synced 2025-09-08 04:18:18 +00:00

To avoid loading buffers https://github.com/neovim/neovim/pull/12440 changed the logic to not process diagnostics for unloaded buffers. This is problematic for language servers where compile errors or build errors are reported via diagnostics. These errors may prevent the language server from providing all functions and it is difficult for users to debug it without having access to the errors. For example, with eclipse.jdt.ls there may be a problem with gradle (the build tool for java), it results in a diagnostics like this: org.gradle.toolingapi/build.gradle|1 col 1| Could not run build action using Gradle distribution 'https://services.gradle.org/distributions/gradle-4.8.1-bin.zip'. This would be invisible to users unless the user happens to open the right file. In this case the user would actually never see the error, because the language server isn't attached to the build configuration files. This changes the behaviour to at least store the diagnostics. The other operations which are more expensive are still skipped.
346 lines
14 KiB
Lua
346 lines
14 KiB
Lua
local log = require 'vim.lsp.log'
|
|
local protocol = require 'vim.lsp.protocol'
|
|
local util = require 'vim.lsp.util'
|
|
local vim = vim
|
|
local api = vim.api
|
|
local buf = require 'vim.lsp.buf'
|
|
|
|
local M = {}
|
|
|
|
-- FIXME: DOC: Expose in vimdocs
|
|
|
|
--@private
|
|
--- Writes to error buffer.
|
|
--@param ... (table of strings) Will be concatenated before being written
|
|
local function err_message(...)
|
|
api.nvim_err_writeln(table.concat(vim.tbl_flatten{...}))
|
|
api.nvim_command("redraw")
|
|
end
|
|
|
|
--@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#workspace_executeCommand
|
|
M['workspace/executeCommand'] = function(err, _)
|
|
if err then
|
|
error("Could not execute code action: "..err.message)
|
|
end
|
|
end
|
|
|
|
--@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_codeAction
|
|
M['textDocument/codeAction'] = function(_, _, actions)
|
|
if actions == nil or vim.tbl_isempty(actions) then
|
|
print("No code actions available")
|
|
return
|
|
end
|
|
|
|
local option_strings = {"Code 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
|
|
end
|
|
local action_chosen = actions[choice]
|
|
-- textDocument/codeAction can return either Command[] or CodeAction[].
|
|
-- If it is a CodeAction, it can have either an edit, a command or both.
|
|
-- Edits should be executed first
|
|
if action_chosen.edit or type(action_chosen.command) == "table" then
|
|
if action_chosen.edit then
|
|
util.apply_workspace_edit(action_chosen.edit)
|
|
end
|
|
if type(action_chosen.command) == "table" then
|
|
buf.execute_command(action_chosen.command)
|
|
end
|
|
else
|
|
buf.execute_command(action_chosen)
|
|
end
|
|
end
|
|
|
|
--@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#workspace_applyEdit
|
|
M['workspace/applyEdit'] = function(_, _, workspace_edit)
|
|
if not workspace_edit then return end
|
|
-- TODO(ashkan) Do something more with label?
|
|
if workspace_edit.label then
|
|
print("Workspace edit", workspace_edit.label)
|
|
end
|
|
local status, result = pcall(util.apply_workspace_edit, workspace_edit.edit)
|
|
return {
|
|
applied = status;
|
|
failureReason = result;
|
|
}
|
|
end
|
|
|
|
--@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_publishDiagnostics
|
|
M['textDocument/publishDiagnostics'] = function(_, _, result)
|
|
if not result then return end
|
|
local uri = result.uri
|
|
local bufnr = vim.uri_to_bufnr(uri)
|
|
if not bufnr then
|
|
err_message("LSP.publishDiagnostics: Couldn't find buffer for ", uri)
|
|
return
|
|
end
|
|
|
|
-- https://microsoft.github.io/language-server-protocol/specifications/specification-current/#diagnostic
|
|
-- The diagnostic's severity. Can be omitted. If omitted it is up to the
|
|
-- client to interpret diagnostics as error, warning, info or hint.
|
|
-- TODO: Replace this with server-specific heuristics to infer severity.
|
|
for _, diagnostic in ipairs(result.diagnostics) do
|
|
if diagnostic.severity == nil then
|
|
diagnostic.severity = protocol.DiagnosticSeverity.Error
|
|
end
|
|
end
|
|
|
|
util.buf_clear_diagnostics(bufnr)
|
|
|
|
-- Always save the diagnostics, even if the buf is not loaded.
|
|
-- Language servers may report compile or build errors via diagnostics
|
|
-- Users should be able to find these, even if they're in files which
|
|
-- are not loaded.
|
|
util.buf_diagnostics_save_positions(bufnr, result.diagnostics)
|
|
|
|
-- Unloaded buffers should not handle diagnostics.
|
|
-- When the buffer is loaded, we'll call on_attach, which sends textDocument/didOpen.
|
|
-- This should trigger another publish of the diagnostics.
|
|
--
|
|
-- In particular, this stops a ton of spam when first starting a server for current
|
|
-- unloaded buffers.
|
|
if not api.nvim_buf_is_loaded(bufnr) then
|
|
return
|
|
end
|
|
util.buf_diagnostics_underline(bufnr, result.diagnostics)
|
|
util.buf_diagnostics_virtual_text(bufnr, result.diagnostics)
|
|
util.buf_diagnostics_signs(bufnr, result.diagnostics)
|
|
vim.api.nvim_command("doautocmd User LspDiagnosticsChanged")
|
|
end
|
|
|
|
--@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_references
|
|
M['textDocument/references'] = function(_, _, result)
|
|
if not result then return end
|
|
util.set_qflist(util.locations_to_items(result))
|
|
api.nvim_command("copen")
|
|
api.nvim_command("wincmd p")
|
|
end
|
|
|
|
--@private
|
|
--- Prints given list of symbols to the quickfix list.
|
|
--@param _ (not used)
|
|
--@param _ (not used)
|
|
--@param result (list of Symbols) LSP method name
|
|
--@param result (table) result of LSP method; a location or a list of locations.
|
|
---(`textDocument/definition` can return `Location` or `Location[]`
|
|
local symbol_callback = function(_, _, result, _, bufnr)
|
|
if not result or vim.tbl_isempty(result) then return end
|
|
|
|
util.set_qflist(util.symbols_to_items(result, bufnr))
|
|
api.nvim_command("copen")
|
|
api.nvim_command("wincmd p")
|
|
end
|
|
--@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_documentSymbol
|
|
M['textDocument/documentSymbol'] = symbol_callback
|
|
--@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#workspace_symbol
|
|
M['workspace/symbol'] = symbol_callback
|
|
|
|
--@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_rename
|
|
M['textDocument/rename'] = function(_, _, result)
|
|
if not result then return end
|
|
util.apply_workspace_edit(result)
|
|
end
|
|
|
|
--@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_rangeFormatting
|
|
M['textDocument/rangeFormatting'] = function(_, _, result)
|
|
if not result then return end
|
|
util.apply_text_edits(result)
|
|
end
|
|
|
|
--@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_formatting
|
|
M['textDocument/formatting'] = function(_, _, result)
|
|
if not result then return end
|
|
util.apply_text_edits(result)
|
|
end
|
|
|
|
--@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_completion
|
|
M['textDocument/completion'] = function(_, _, result)
|
|
if vim.tbl_isempty(result or {}) then return end
|
|
local row, col = unpack(api.nvim_win_get_cursor(0))
|
|
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
|
|
|
|
--@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_hover
|
|
M['textDocument/hover'] = function(_, method, result)
|
|
util.focusable_float(method, function()
|
|
if not (result and result.contents) then
|
|
-- return { 'No information available' }
|
|
return
|
|
end
|
|
local markdown_lines = util.convert_input_to_markdown_lines(result.contents)
|
|
markdown_lines = util.trim_empty_lines(markdown_lines)
|
|
if vim.tbl_isempty(markdown_lines) then
|
|
-- return { 'No information available' }
|
|
return
|
|
end
|
|
local bufnr, winnr = util.fancy_floating_markdown(markdown_lines, {
|
|
pad_left = 1; pad_right = 1;
|
|
})
|
|
util.close_preview_autocmd({"CursorMoved", "BufHidden", "InsertCharPre"}, winnr)
|
|
return bufnr, winnr
|
|
end)
|
|
end
|
|
|
|
--@private
|
|
--- Jumps to a location. Used as a callback for multiple LSP methods.
|
|
--@param _ (not used)
|
|
--@param method (string) LSP method name
|
|
--@param result (table) result of LSP method; a location or a list of locations.
|
|
---(`textDocument/definition` can return `Location` or `Location[]`
|
|
local function location_callback(_, method, result)
|
|
if result == nil or vim.tbl_isempty(result) then
|
|
local _ = log.info() and log.info(method, 'No location found')
|
|
return nil
|
|
end
|
|
|
|
-- textDocument/definition can return Location or Location[]
|
|
-- https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_definition
|
|
|
|
if vim.tbl_islist(result) then
|
|
util.jump_to_location(result[1])
|
|
|
|
if #result > 1 then
|
|
util.set_qflist(util.locations_to_items(result))
|
|
api.nvim_command("copen")
|
|
api.nvim_command("wincmd p")
|
|
end
|
|
else
|
|
util.jump_to_location(result)
|
|
end
|
|
end
|
|
|
|
--@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_declaration
|
|
M['textDocument/declaration'] = location_callback
|
|
--@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_definition
|
|
M['textDocument/definition'] = location_callback
|
|
--@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_typeDefinition
|
|
M['textDocument/typeDefinition'] = location_callback
|
|
--@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_implementation
|
|
M['textDocument/implementation'] = location_callback
|
|
|
|
--@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_signatureHelp
|
|
M['textDocument/signatureHelp'] = function(_, method, result)
|
|
-- When use `autocmd CompleteDone <silent><buffer> lua vim.lsp.buf.signature_help()` to call signatureHelp callback
|
|
-- 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
|
|
print('No signature help available')
|
|
return
|
|
end
|
|
local lines = util.convert_signature_help_to_markdown_lines(result)
|
|
lines = util.trim_empty_lines(lines)
|
|
if vim.tbl_isempty(lines) then
|
|
print('No signature help available')
|
|
return
|
|
end
|
|
util.focusable_preview(method, function()
|
|
return lines, util.try_trim_markdown_code_blocks(lines)
|
|
end)
|
|
end
|
|
|
|
--@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_documentHighlight
|
|
M['textDocument/documentHighlight'] = function(_, _, result, _)
|
|
if not result then return end
|
|
local bufnr = api.nvim_get_current_buf()
|
|
util.buf_highlight_references(bufnr, result)
|
|
end
|
|
|
|
--@private
|
|
---
|
|
--- Displays call hierarchy in the quickfix window.
|
|
---
|
|
--@param direction `"from"` for incoming calls and `"to"` for outgoing calls
|
|
--@returns `CallHierarchyIncomingCall[]` if {direction} is `"from"`,
|
|
--@returns `CallHierarchyOutgoingCall[]` if {direction} is `"to"`,
|
|
local make_call_hierarchy_callback = 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
|
|
util.set_qflist(items)
|
|
api.nvim_command("copen")
|
|
api.nvim_command("wincmd p")
|
|
end
|
|
end
|
|
|
|
--@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#callHierarchy/incomingCalls
|
|
M['callHierarchy/incomingCalls'] = make_call_hierarchy_callback('from')
|
|
|
|
--@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#callHierarchy/outgoingCalls
|
|
M['callHierarchy/outgoingCalls'] = make_call_hierarchy_callback('to')
|
|
|
|
--@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#window/logMessage
|
|
M['window/logMessage'] = function(_, _, result, client_id)
|
|
local message_type = result.type
|
|
local message = result.message
|
|
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 the 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 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['window/showMessage'] = function(_, _, result, client_id)
|
|
local message_type = result.type
|
|
local message = result.message
|
|
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 the 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
|
|
|
|
-- Add boilerplate error validation and logging for all of these.
|
|
for k, fn in pairs(M) do
|
|
M[k] = function(err, method, params, client_id, bufnr)
|
|
log.debug('default_callback', method, { params = params, client_id = client_id, err = err, bufnr = bufnr })
|
|
if err then
|
|
error(tostring(err))
|
|
end
|
|
return fn(err, method, params, client_id, bufnr)
|
|
end
|
|
end
|
|
|
|
return M
|
|
-- vim:sw=2 ts=2 et
|