lsp: vim.lsp.diagnostic (#12655)

Breaking Changes:
- Deprecated all `vim.lsp.util.{*diagnostics*}()` functions.
    - Instead, all functions must be found in vim.lsp.diagnostic
    - For now, they issue a warning ONCE per neovim session. In a
      "little while" we will remove them completely.
- `vim.lsp.callbacks` has moved to `vim.lsp.handlers`.
    - For a "little while" we will just redirect `vim.lsp.callbacks` to
      `vim.lsp.handlers`. However, we will remove this at some point, so
      it is recommended that you change all of your references to
      `callbacks` into `handlers`.
    - This also means that for functions like |vim.lsp.start_client()|
      and similar, keyword style arguments have moved from "callbacks"
      to "handlers". Once again, these are currently being forward, but
      will cease to be forwarded in a "little while".
- Changed the highlight groups for LspDiagnostic highlight as they were
  inconsistently named.
    - For more information, see |lsp-highlight-diagnostics|
- Changed the sign group names as well, to be consistent with
  |lsp-highlight-diagnostics|

General Enhancements:
- Rewrote much of the getting started help document for lsp. It also
  provides a much nicer configuration strategy, so as to not recommend
  globally overwriting builtin neovim mappings.

LSP Enhancements:
- Introduced the concept of |lsp-handlers| which will allow much better
  customization for users without having to copy & paste entire files /
  functions / etc.

Diagnostic Enhancements:
- "goto next diagnostic" |vim.lsp.diagnostic.goto_next()|
- "goto prev diagnostic" |vim.lsp.diagnostic.goto_prev()|
    - For each of the gotos, auto open diagnostics is available as a
      configuration option
- Configurable diagnostic handling:
    - See |vim.lsp.diagnostic.on_publish_diagnostics()|
    - Delay display until after insert mode
    - Configure signs
    - Configure virtual text
    - Configure underline
- Set the location list with the buffers diagnostics.
    - See |vim.lsp.diagnostic.set_loclist()|
- Better performance for getting counts and line diagnostics
    - They are now cached on save, to enhance lookups.
    - Particularly useful for checking in statusline, etc.
- Actual testing :)
    - See ./test/functional/plugin/lsp/diagnostic_spec.lua
- Added `guisp` for underline highlighting

NOTE: "a little while" means enough time to feel like most plugins and
plugin authors have had a chance to refactor their code to use the
updated calls. Then we will remove them completely. There is no need to
keep them, because we don't have any released version of neovim that
exposes these APIs. I'm trying to be nice to people following HEAD :)

Co-authored: [Twitch Chat 2020](https://twitch.tv/teej_dv)
This commit is contained in:
TJ DeVries
2020-11-12 22:21:34 -05:00
committed by GitHub
parent 4ae31c46f7
commit f75be5e9d5
22 changed files with 3752 additions and 1367 deletions

24
runtime/lua/vim/F.lua Normal file
View File

@@ -0,0 +1,24 @@
local F = {}
--- Returns {a} if it is not nil, otherwise returns {b}.
---
--@param a
--@param b
function F.if_nil(a, b)
if a == nil then return b end
return a
end
-- Use in combination with pcall
function F.ok_or_nil(status, ...)
if not status then return end
return ...
end
-- Nil pcall.
function F.npcall(fn, ...)
return F.ok_or_nil(pcall(fn, ...))
end
return F

View File

@@ -2,6 +2,22 @@ local api = vim.api
local highlight = {}
--@private
function highlight.create(higroup, hi_info, default)
local options = {}
-- TODO: Add validation
for k, v in pairs(hi_info) do
table.insert(options, string.format("%s=%s", k, v))
end
vim.cmd(string.format([[highlight %s %s %s]], default and "default" or "", higroup, table.concat(options, " ")))
end
--@private
function highlight.link(higroup, link_to, force)
vim.cmd(string.format([[highlight%s link %s %s]], force and "!" or " default", higroup, link_to))
end
--- Highlight range between two positions
---
--@param bufnr number of buffer to apply highlighting to

View File

@@ -1,4 +1,4 @@
local default_callbacks = require 'vim.lsp.callbacks'
local default_handlers = require 'vim.lsp.handlers'
local log = require 'vim.lsp.log'
local lsp_rpc = require 'vim.lsp.rpc'
local protocol = require 'vim.lsp.protocol'
@@ -13,16 +13,21 @@ local validate = vim.validate
local lsp = {
protocol = protocol;
callbacks = default_callbacks;
-- TODO(tjdevries): Add in the warning that `callbacks` is no longer supported.
-- util.warn_once("vim.lsp.callbacks is deprecated. Use vim.lsp.handlers instead.")
handlers = default_handlers;
callbacks = default_handlers;
buf = require'vim.lsp.buf';
diagnostic = require'vim.lsp.diagnostic';
util = util;
-- Allow raw RPC access.
rpc = lsp_rpc;
-- Export these directly from rpc.
rpc_response_error = lsp_rpc.rpc_response_error;
-- You probably won't need this directly, since __tostring is set for errors
-- by the RPC.
-- format_rpc_error = lsp_rpc.format_rpc_error;
}
-- maps request name to the required resolved_capability in the client.
@@ -72,7 +77,7 @@ local function resolve_bufnr(bufnr)
end
--@private
--- callback called by the client when trying to call a method that's not
--- Called by the client when trying to call a method that's not
--- supported in any of the servers registered for the current buffer.
--@param method (string) name of the method
function lsp._unsupported_method(method)
@@ -115,14 +120,14 @@ local all_buffer_active_clients = {}
local uninitialized_clients = {}
--@private
--- Invokes a callback for each LSP client attached to the buffer {bufnr}.
--- Invokes a function for each LSP client attached to the buffer {bufnr}.
---
--@param bufnr (Number) of buffer
--@param callback (function({client}, {client_id}, {bufnr}) Function to run on
--@param fn (function({client}, {client_id}, {bufnr}) Function to run on
---each client attached to that buffer.
local function for_each_buffer_client(bufnr, callback)
local function for_each_buffer_client(bufnr, fn)
validate {
callback = { callback, 'f' };
fn = { fn, 'f' };
}
bufnr = resolve_bufnr(bufnr)
local client_ids = all_buffer_active_clients[bufnr]
@@ -132,7 +137,7 @@ local function for_each_buffer_client(bufnr, callback)
for client_id in pairs(client_ids) do
local client = active_clients[client_id]
if client then
callback(client, client_id, bufnr)
fn(client, client_id, bufnr)
end
end
end
@@ -209,7 +214,9 @@ local function validate_client_config(config)
}
validate {
root_dir = { config.root_dir, is_dir, "directory" };
-- TODO(remove-callbacks)
callbacks = { config.callbacks, "t", true };
handlers = { config.handlers, "t", true };
capabilities = { config.capabilities, "t", true };
cmd_cwd = { config.cmd_cwd, optional_validator(is_dir), "directory" };
cmd_env = { config.cmd_env, "t", true };
@@ -220,13 +227,23 @@ local function validate_client_config(config)
before_init = { config.before_init, "f", true };
offset_encoding = { config.offset_encoding, "s", true };
}
-- TODO(remove-callbacks)
if config.handlers and config.callbacks then
error(debug.traceback(
"Unable to configure LSP with both 'config.handlers' and 'config.callbacks'. Use 'config.handlers' exclusively."
))
end
local cmd, cmd_args = lsp._cmd_parts(config.cmd)
local offset_encoding = valid_encodings.UTF16
if config.offset_encoding then
offset_encoding = validate_encoding(config.offset_encoding)
end
return {
cmd = cmd; cmd_args = cmd_args;
cmd = cmd;
cmd_args = cmd_args;
offset_encoding = offset_encoding;
}
end
@@ -276,12 +293,11 @@ end
---
--- - Methods:
---
--- - request(method, params, [callback], bufnr)
--- - request(method, params, [handler], bufnr)
--- Sends a request to the server.
--- This is a thin wrapper around {client.rpc.request} with some additional
--- checking.
--- If {callback} is not specified, it will use {client.callbacks} to try to
--- find a callback. If one is not found there, then an error will occur.
--- If {handler} is not specified, If one is not found there, then an error will occur.
--- Returns: {status}, {[client_id]}. {status} is a boolean indicating if
--- the notification was successful. If it is `false`, then it will always
--- be `false` (the client has shutdown).
@@ -325,8 +341,7 @@ end
--- with the server. You can modify this in the `config`'s `on_init` method
--- before text is sent to the server.
---
--- - {callbacks} (table): The callbacks used by the client as
--- described in |lsp-callbacks|.
--- - {handlers} (table): The handlers used by the client as described in |lsp-handler|.
---
--- - {config} (table): copy of the table that was passed by the user
--- to |vim.lsp.start_client()|.
@@ -378,15 +393,7 @@ end
--- `{[vim.type_idx]=vim.types.dictionary}`, else it will be encoded as an
--- array.
---
--@param callbacks Map of language server method names to
--- `function(err, method, params, client_id)` handler. Invoked for:
--- - Notifications to the server, where `err` will always be `nil`.
--- - Requests by the server. For these you can respond by returning
--- two values: `result, err` where err must be shaped like a RPC error,
--- i.e. `{ code, message, data? }`. Use |vim.lsp.rpc_response_error()| to
--- help with this.
--- - Default callback for client requests not explicitly specifying
--- a callback.
--@param handlers Map of language server method names to |lsp-handler|
---
--@param init_options Values to pass in the initialization request
--- as `initializationOptions`. See `initialize` in the LSP spec.
@@ -437,52 +444,51 @@ function lsp.start_client(config)
local client_id = next_client_id()
local callbacks = config.callbacks or {}
-- TODO(remove-callbacks)
local handlers = config.handlers or config.callbacks or {}
local name = config.name or tostring(client_id)
local log_prefix = string.format("LSP[%s]", name)
local handlers = {}
local dispatch = {}
--@private
--- Returns the callback associated with an LSP method. Returns the default
--- callback if the user hasn't set a custom one.
--- Returns the handler associated with an LSP method.
--- Returns the default handler if the user hasn't set a custom one.
---
--@param method (string) LSP method name
--@returns (fn) The callback for the given method, if defined, or the default
---from |lsp-callbacks|
local function resolve_callback(method)
return callbacks[method] or default_callbacks[method]
--@returns (fn) The handler for the given method, if defined, or the default from |vim.lsp.handlers|
local function resolve_handler(method)
return handlers[method] or default_handlers[method]
end
--@private
--- Handles a notification sent by an LSP server by invoking the
--- corresponding callback.
--- corresponding handler.
---
--@param method (string) LSP method name
--@param params (table) The parameters for that method.
function handlers.notification(method, params)
function dispatch.notification(method, params)
local _ = log.debug() and log.debug('notification', method, params)
local callback = resolve_callback(method)
if callback then
local handler = resolve_handler(method)
if handler then
-- Method name is provided here for convenience.
callback(nil, method, params, client_id)
handler(nil, method, params, client_id)
end
end
--@private
--- Handles a request from an LSP server by invoking the corresponding
--- callback.
--- Handles a request from an LSP server by invoking the corresponding handler.
---
--@param method (string) LSP method name
--@param params (table) The parameters for that method
function handlers.server_request(method, params)
function dispatch.server_request(method, params)
local _ = log.debug() and log.debug('server_request', method, params)
local callback = resolve_callback(method)
if callback then
local _ = log.debug() and log.debug("server_request: found callback for", method)
return callback(nil, method, params, client_id)
local handler = resolve_handler(method)
if handler then
local _ = log.debug() and log.debug("server_request: found handler for", method)
return handler(nil, method, params, client_id)
end
local _ = log.debug() and log.debug("server_request: no callback found for", method)
local _ = log.debug() and log.debug("server_request: no handler found for", method)
return nil, lsp.rpc_response_error(protocol.ErrorCodes.MethodNotFound)
end
@@ -493,7 +499,7 @@ function lsp.start_client(config)
--@param err (...) Other arguments may be passed depending on the error kind
--@see |vim.lsp.client_errors| for possible errors. Use
---`vim.lsp.client_errors[code]` to get a human-friendly name.
function handlers.on_error(code, err)
function dispatch.on_error(code, err)
local _ = log.error() and log.error(log_prefix, "on_error", { code = lsp.client_errors[code], err = err })
err_message(log_prefix, ': Error ', lsp.client_errors[code], ': ', vim.inspect(err))
if config.on_error then
@@ -510,7 +516,7 @@ function lsp.start_client(config)
---
--@param code (number) exit code of the process
--@param signal (number) the signal used to terminate (if any)
function handlers.on_exit(code, signal)
function dispatch.on_exit(code, signal)
active_clients[client_id] = nil
uninitialized_clients[client_id] = nil
local active_buffers = {}
@@ -523,7 +529,7 @@ function lsp.start_client(config)
-- Buffer level cleanup
vim.schedule(function()
for _, bufnr in ipairs(active_buffers) do
util.buf_clear_diagnostics(bufnr)
lsp.diagnostic.clear(bufnr)
end
end)
if config.on_exit then
@@ -532,7 +538,7 @@ function lsp.start_client(config)
end
-- Start the RPC client.
local rpc = lsp_rpc.start(cmd, cmd_args, handlers, {
local rpc = lsp_rpc.start(cmd, cmd_args, dispatch, {
cwd = config.cmd_cwd;
env = config.cmd_env;
})
@@ -542,12 +548,14 @@ function lsp.start_client(config)
name = name;
rpc = rpc;
offset_encoding = offset_encoding;
callbacks = callbacks;
config = config;
-- TODO(remove-callbacks)
callbacks = handlers;
handlers = handlers;
}
-- Store the uninitialized_clients for cleanup in case we exit before
-- initialize finishes.
-- Store the uninitialized_clients for cleanup in case we exit before initialize finishes.
uninitialized_clients[client_id] = client;
--@private
@@ -641,13 +649,11 @@ function lsp.start_client(config)
--- Sends a request to the server.
---
--- This is a thin wrapper around {client.rpc.request} with some additional
--- checks for capabilities and callback availability.
--- checks for capabilities and handler availability.
---
--@param method (string) LSP method name.
--@param params (table) LSP request params.
--@param callback (function, optional) Response handler for this method.
---If {callback} is not specified, it will use {client.callbacks} to try to
---find a callback. If one is not found there, then an error will occur.
--@param handler (function, optional) Response |lsp-handler| for this method.
--@param bufnr (number) Buffer handle (0 for current).
--@returns ({status}, [request_id]): {status} is a bool indicating
---whether the request was successful. If it is `false`, then it will
@@ -656,16 +662,14 @@ function lsp.start_client(config)
---second result. You can use this with `client.cancel_request(request_id)`
---to cancel the-request.
--@see |vim.lsp.buf_request()|
function client.request(method, params, callback, bufnr)
-- FIXME: callback is optional, but bufnr is apparently not? Shouldn't that
-- require a `select('#', ...)` call?
if not callback then
callback = resolve_callback(method)
or error(string.format("not found: %q request callback for client %q.", method, client.name))
function client.request(method, params, handler, bufnr)
if not handler then
handler = resolve_handler(method)
or error(string.format("not found: %q request handler for client %q.", method, client.name))
end
local _ = log.debug() and log.debug(log_prefix, "client.request", client_id, method, params, callback, bufnr)
local _ = log.debug() and log.debug(log_prefix, "client.request", client_id, method, params, handler, bufnr)
return rpc.request(method, params, function(err, result)
callback(err, method, result, client_id, bufnr)
handler(err, method, result, client_id, bufnr)
end)
end
@@ -995,19 +999,18 @@ nvim_command("autocmd VimLeavePre * lua vim.lsp._vim_exit_handler()")
--@param bufnr (number) Buffer handle, or 0 for current.
--@param method (string) LSP method name
--@param params (optional, table) Parameters to send to the server
--@param callback (optional, functionnil) Handler
-- `function(err, method, params, client_id)` for this request. Defaults
-- to the client callback in `client.callbacks`. See |lsp-callbacks|.
--@param handler (optional, function) See |lsp-handler|
-- If nil, follows resolution strategy defined in |lsp-handler-configuration|
--
--@returns 2-tuple:
--- - Map of client-id:request-id pairs for all successful requests.
--- - Function which can be used to cancel all the requests. You could instead
--- iterate all clients and call their `cancel_request()` methods.
function lsp.buf_request(bufnr, method, params, callback)
function lsp.buf_request(bufnr, method, params, handler)
validate {
bufnr = { bufnr, 'n', true };
method = { method, 's' };
callback = { callback, 'f', true };
handler = { handler, 'f', true };
}
local client_request_ids = {}
@@ -1015,7 +1018,7 @@ function lsp.buf_request(bufnr, method, params, callback)
for_each_buffer_client(bufnr, function(client, client_id, resolved_bufnr)
if client.supports_method(method) then
method_supported = true
local request_success, request_id = client.request(method, params, callback, resolved_bufnr)
local request_success, request_id = client.request(method, params, handler, resolved_bufnr)
-- This could only fail if the client shut down in the time since we looked
-- it up and we did the request, which should be rare.
@@ -1025,13 +1028,13 @@ function lsp.buf_request(bufnr, method, params, callback)
end
end)
-- if no clients support the given method, call the callback with the proper
-- if no clients support the given method, call the handler with the proper
-- error message.
if not method_supported then
local unsupported_err = lsp._unsupported_method(method)
local cb = callback or lsp.callbacks[method]
if cb then
cb(unsupported_err, method, bufnr)
handler = handler or lsp.handlers[method]
if handler then
handler(unsupported_err, method, bufnr)
end
return
end
@@ -1064,11 +1067,11 @@ end
function lsp.buf_request_sync(bufnr, method, params, timeout_ms)
local request_results = {}
local result_count = 0
local function _callback(err, _method, result, client_id)
local function _sync_handler(err, _, result, client_id)
request_results[client_id] = { error = err, result = result }
result_count = result_count + 1
end
local client_request_ids, cancel = lsp.buf_request(bufnr, method, params, _callback)
local client_request_ids, cancel = lsp.buf_request(bufnr, method, params, _sync_handler)
local expected_result_count = 0
for _ in pairs(client_request_ids) do
expected_result_count = expected_result_count + 1
@@ -1209,22 +1212,53 @@ function lsp.get_log_path()
return log.get_filename()
end
-- Defines the LspDiagnostics signs if they're not defined already.
do
--@private
--- Defines a sign if it isn't already defined.
--@param name (String) Name of the sign
--@param properties (table) Properties to attach to the sign
local function define_default_sign(name, properties)
if vim.tbl_isempty(vim.fn.sign_getdefined(name)) then
vim.fn.sign_define(name, properties)
--- Call {fn} for every client attached to {bufnr}
function lsp.for_each_buffer_client(bufnr, fn)
return for_each_buffer_client(bufnr, fn)
end
--- Function to manage overriding defaults for LSP handlers.
--@param handler (function) See |lsp-handler|
--@param override_config (table) Table containing the keys to override behavior of the {handler}
function lsp.with(handler, override_config)
return function(err, method, params, client_id, bufnr, config)
return handler(err, method, params, client_id, bufnr, vim.tbl_deep_extend("force", config or {}, override_config))
end
end
--- Helper function to use when implementing a handler.
--- This will check that all of the keys in the user configuration
--- are valid keys and make sense to include for this handler.
---
--- Will error on invalid keys (i.e. keys that do not exist in the options)
function lsp._with_extend(name, options, user_config)
user_config = user_config or {}
local resulting_config = {}
for k, v in pairs(user_config) do
if options[k] == nil then
error(debug.traceback(string.format(
"Invalid option for `%s`: %s. Valid options are:\n%s",
name,
k,
vim.inspect(vim.tbl_keys(options))
)))
end
resulting_config[k] = v
end
for k, v in pairs(options) do
if resulting_config[k] == nil then
resulting_config[k] = v
end
end
define_default_sign('LspDiagnosticsErrorSign', {text='E', texthl='LspDiagnosticsErrorSign', linehl='', numhl=''})
define_default_sign('LspDiagnosticsWarningSign', {text='W', texthl='LspDiagnosticsWarningSign', linehl='', numhl=''})
define_default_sign('LspDiagnosticsInformationSign', {text='I', texthl='LspDiagnosticsInformationSign', linehl='', numhl=''})
define_default_sign('LspDiagnosticsHintSign', {text='H', texthl='LspDiagnosticsHintSign', linehl='', numhl=''})
return resulting_config
end
-- Define the LspDiagnostics signs if they're not defined already.
require('vim.lsp.diagnostic')._define_default_signs_and_highlights()
return lsp
-- vim:sw=2 ts=2 et

View File

@@ -30,9 +30,7 @@ end
---
--@param method (string) LSP method name
--@param params (optional, table) Parameters to send to the server
--@param callback (optional, functionnil) Handler
-- `function(err, method, params, client_id)` for this request. Defaults
-- to the client callback in `client.callbacks`. See |lsp-callbacks|.
--@param handler (optional, functionnil) See |lsp-handler|. Follows |lsp-handler-resolution|
--
--@returns 2-tuple:
--- - Map of client-id:request-id pairs for all successful requests.
@@ -40,12 +38,12 @@ end
--- iterate all clients and call their `cancel_request()` methods.
---
--@see |vim.lsp.buf_request()|
local function request(method, params, callback)
local function request(method, params, handler)
validate {
method = {method, 's'};
callback = {callback, 'f', true};
handler = {handler, 'f', true};
}
return vim.lsp.buf_request(0, method, params, callback)
return vim.lsp.buf_request(0, method, params, handler)
end
--- Checks whether the language servers attached to the current buffer are
@@ -64,6 +62,7 @@ function M.hover()
end
--- Jumps to the declaration of the symbol under the cursor.
--@note Many servers do not implement this method. Generally, see |vim.lsp.buf.definition()| instead.
---
function M.declaration()
local params = util.make_position_params()
@@ -279,7 +278,7 @@ end
--@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_codeAction
function M.code_action(context)
validate { context = { context, 't', true } }
context = context or { diagnostics = util.get_line_diagnostics() }
context = context or { diagnostics = vim.lsp.diagnostic.get_line_diagnostics() }
local params = util.make_range_params()
params.context = context
request('textDocument/codeAction', params)
@@ -294,7 +293,7 @@ end
---Defaults to the end of the last visual selection.
function M.range_code_action(context, start_pos, end_pos)
validate { context = { context, 't', true } }
context = context or { diagnostics = util.get_line_diagnostics() }
context = context or { diagnostics = vim.lsp.diagnostic.get_line_diagnostics() }
local params = util.make_given_range_params(start_pos, end_pos)
params.context = context
request('textDocument/codeAction', params)

View File

@@ -1,345 +1,4 @@
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
util._warn_once("require('vim.lsp.callbacks') is deprecated. Use vim.lsp.handlers instead.")
return require('vim.lsp.handlers')

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,310 @@
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
M['textDocument/publishDiagnostics'] = function(...)
return require('vim.lsp.diagnostic').on_publish_diagnostics(...)
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_handler = 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_handler
--@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#workspace_symbol
M['workspace/symbol'] = symbol_handler
--@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 handler 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_handler(_, 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_handler
--@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_definition
M['textDocument/definition'] = location_handler
--@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_typeDefinition
M['textDocument/typeDefinition'] = location_handler
--@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_implementation
M['textDocument/implementation'] = location_handler
--@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 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
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_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
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_handler('from')
--@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#callHierarchy/outgoingCalls
M['callHierarchy/outgoingCalls'] = make_call_hierarchy_handler('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, config)
local _ = log.debug() and log.debug('default_handler', method, {
params = params, client_id = client_id, err = err, bufnr = bufnr, config = config
})
if err then
error(tostring(err))
end
return fn(err, method, params, client_id, bufnr, config)
end
end
return M
-- vim:sw=2 ts=2 et

View File

@@ -1,18 +1,9 @@
-- Protocol for the Microsoft Language Server Protocol (mslsp)
local if_nil = vim.F.if_nil
local protocol = {}
--@private
--- Returns {a} if it is not nil, otherwise returns {b}.
---
--@param a
--@param b
local function ifnil(a, b)
if a == nil then return b end
return a
end
--[=[
--@private
--- Useful for interfacing with:
@@ -909,12 +900,12 @@ function protocol.resolve_capabilities(server_capabilities)
}
elseif type(textDocumentSync) == 'table' then
text_document_sync_properties = {
text_document_open_close = ifnil(textDocumentSync.openClose, false);
text_document_did_change = ifnil(textDocumentSync.change, TextDocumentSyncKind.None);
text_document_will_save = ifnil(textDocumentSync.willSave, false);
text_document_will_save_wait_until = ifnil(textDocumentSync.willSaveWaitUntil, false);
text_document_save = ifnil(textDocumentSync.save, false);
text_document_save_include_text = ifnil(type(textDocumentSync.save) == 'table'
text_document_open_close = if_nil(textDocumentSync.openClose, false);
text_document_did_change = if_nil(textDocumentSync.change, TextDocumentSyncKind.None);
text_document_will_save = if_nil(textDocumentSync.willSave, false);
text_document_will_save_wait_until = if_nil(textDocumentSync.willSaveWaitUntil, false);
text_document_save = if_nil(textDocumentSync.save, false);
text_document_save_include_text = if_nil(type(textDocumentSync.save) == 'table'
and textDocumentSync.save.includeText, false);
}
else

View File

@@ -231,41 +231,42 @@ local function rpc_response_error(code, message, data)
})
end
local default_handlers = {}
local default_dispatchers = {}
--@private
--- Default handler for notifications sent to an LSP server.
--- Default dispatcher for notifications sent to an LSP server.
---
--@param method (string) The invoked LSP method
--@param params (table): Parameters for the invoked LSP method
function default_handlers.notification(method, params)
function default_dispatchers.notification(method, params)
local _ = log.debug() and log.debug('notification', method, params)
end
--@private
--- Default handler for requests sent to an LSP server.
--- Default dispatcher for requests sent to an LSP server.
---
--@param method (string) The invoked LSP method
--@param params (table): Parameters for the invoked LSP method
--@returns `nil` and `vim.lsp.protocol.ErrorCodes.MethodNotFound`.
function default_handlers.server_request(method, params)
function default_dispatchers.server_request(method, params)
local _ = log.debug() and log.debug('server_request', method, params)
return nil, rpc_response_error(protocol.ErrorCodes.MethodNotFound)
end
--@private
--- Default handler for when a client exits.
--- Default dispatcher for when a client exits.
---
--@param code (number): Exit code
--@param signal (number): Number describing the signal used to terminate (if
---any)
function default_handlers.on_exit(code, signal)
function default_dispatchers.on_exit(code, signal)
local _ = log.info() and log.info("client_exit", { code = code, signal = signal })
end
--@private
--- Default handler for client errors.
--- Default dispatcher for client errors.
---
--@param code (number): Error code
--@param err (any): Details about the error
---any)
function default_handlers.on_error(code, err)
function default_dispatchers.on_error(code, err)
local _ = log.error() and log.error('client_error:', client_errors[code], err)
end
@@ -274,8 +275,8 @@ end
---
--@param cmd (string) Command to start the LSP server.
--@param cmd_args (table) List of additional string arguments to pass to {cmd}.
--@param handlers (table, optional) Handlers for LSP message types. Valid
---handler names are:
--@param dispatchers (table, optional) Dispatchers for LSP message types. Valid
---dispatcher names are:
--- - `"notification"`
--- - `"server_request"`
--- - `"on_error"`
@@ -294,39 +295,39 @@ end
--- - {pid} (number) The LSP server's PID.
--- - {handle} A handle for low-level interaction with the LSP server process
--- |vim.loop|.
local function start(cmd, cmd_args, handlers, extra_spawn_params)
local function start(cmd, cmd_args, dispatchers, extra_spawn_params)
local _ = log.info() and log.info("Starting RPC client", {cmd = cmd, args = cmd_args, extra = extra_spawn_params})
validate {
cmd = { cmd, 's' };
cmd_args = { cmd_args, 't' };
handlers = { handlers, 't', true };
dispatchers = { dispatchers, 't', true };
}
if not (vim.fn.executable(cmd) == 1) then
error(string.format("The given command %q is not executable.", cmd))
end
if handlers then
local user_handlers = handlers
handlers = {}
for handle_name, default_handler in pairs(default_handlers) do
local user_handler = user_handlers[handle_name]
if user_handler then
if type(user_handler) ~= 'function' then
error(string.format("handler.%s must be a function", handle_name))
if dispatchers then
local user_dispatchers = dispatchers
dispatchers = {}
for dispatch_name, default_dispatch in pairs(default_dispatchers) do
local user_dispatcher = user_dispatchers[dispatch_name]
if user_dispatcher then
if type(user_dispatcher) ~= 'function' then
error(string.format("dispatcher.%s must be a function", dispatch_name))
end
-- server_request is wrapped elsewhere.
if not (handle_name == 'server_request'
or handle_name == 'on_exit') -- TODO this blocks the loop exiting for some reason.
if not (dispatch_name == 'server_request'
or dispatch_name == 'on_exit') -- TODO this blocks the loop exiting for some reason.
then
user_handler = schedule_wrap(user_handler)
user_dispatcher = schedule_wrap(user_dispatcher)
end
handlers[handle_name] = user_handler
dispatchers[dispatch_name] = user_dispatcher
else
handlers[handle_name] = default_handler
dispatchers[dispatch_name] = default_dispatch
end
end
else
handlers = default_handlers
dispatchers = default_dispatchers
end
local stdin = uv.new_pipe(false)
@@ -339,8 +340,7 @@ local function start(cmd, cmd_args, handlers, extra_spawn_params)
local handle, pid
do
--@private
--- Callback for |vim.loop.spawn()| Closes all streams and runs the
--- `on_exit` handler.
--- Callback for |vim.loop.spawn()| Closes all streams and runs the `on_exit` dispatcher.
--@param code (number) Exit code
--@param signal (number) Signal that was used to terminate (if any)
local function onexit(code, signal)
@@ -350,7 +350,7 @@ local function start(cmd, cmd_args, handlers, extra_spawn_params)
handle:close()
-- Make sure that message_callbacks can be gc'd.
message_callbacks = nil
handlers.on_exit(code, signal)
dispatchers.on_exit(code, signal)
end
local spawn_params = {
args = cmd_args;
@@ -448,7 +448,7 @@ local function start(cmd, cmd_args, handlers, extra_spawn_params)
local function on_error(errkind, ...)
assert(client_errors[errkind])
-- TODO what to do if this fails?
pcall(handlers.on_error, errkind, ...)
pcall(dispatchers.on_error, errkind, ...)
end
--@private
local function pcall_handler(errkind, status, head, ...)
@@ -471,7 +471,7 @@ local function start(cmd, cmd_args, handlers, extra_spawn_params)
local function handle_body(body)
local decoded, err = json_decode(body)
if not decoded then
on_error(client_errors.INVALID_SERVER_JSON, err)
-- on_error(client_errors.INVALID_SERVER_JSON, err)
return
end
local _ = log.debug() and log.debug("decoded", decoded)
@@ -484,7 +484,7 @@ local function start(cmd, cmd_args, handlers, extra_spawn_params)
schedule(function()
local status, result
status, result, err = try_call(client_errors.SERVER_REQUEST_HANDLER_ERROR,
handlers.server_request, decoded.method, decoded.params)
dispatchers.server_request, decoded.method, decoded.params)
local _ = log.debug() and log.debug("server_request: callback result", { status = status, result = result, err = err })
if status then
if not (result or err) then
@@ -551,7 +551,7 @@ local function start(cmd, cmd_args, handlers, extra_spawn_params)
-- Notification
decoded.params = convert_NIL(decoded.params)
try_call(client_errors.NOTIFICATION_HANDLER_ERROR,
handlers.notification, decoded.method, decoded.params)
dispatchers.notification, decoded.method, decoded.params)
else
-- Invalid server message
on_error(client_errors.INVALID_SERVER_MESSAGE, decoded)

View File

@@ -5,50 +5,32 @@ local api = vim.api
local list_extend = vim.list_extend
local highlight = require 'vim.highlight'
local npcall = vim.F.npcall
local split = vim.split
local _warned = {}
local warn_once = function(message)
if not _warned[message] then
vim.api.nvim_err_writeln(message)
_warned[message] = true
end
end
local M = {}
-- FIXME: DOC: Expose in vimdocs
--- Diagnostics received from the server via `textDocument/publishDiagnostics`
-- by buffer.
--
-- {<bufnr>: {diagnostics}}
--
-- This contains only entries for active buffers. Entries for detached buffers
-- are discarded.
--
-- If you override the `textDocument/publishDiagnostic` callback,
-- this will be empty unless you call `buf_diagnostics_save_positions`.
--
--
-- Diagnostic is:
--
-- {
-- range: Range
-- message: string
-- severity?: DiagnosticSeverity
-- code?: number | string
-- source?: string
-- tags?: DiagnosticTag[]
-- relatedInformation?: DiagnosticRelatedInformation[]
-- }
M.diagnostics_by_buf = {}
-- TODO(remove-callbacks)
M.diagnostics_by_buf = setmetatable({}, {
__index = function(_, bufnr)
warn_once("diagnostics_by_buf is deprecated. Use 'vim.lsp.diagnostic.get'")
return vim.lsp.diagnostic.get(bufnr)
end
})
local split = vim.split
--@private
local function split_lines(value)
return split(value, '\n', true)
end
--@private
local function ok_or_nil(status, ...)
if not status then return end
return ...
end
--@private
local function npcall(fn, ...)
return ok_or_nil(pcall(fn, ...))
end
--- Replaces text in a range with new text.
---
--- CAUTION: Changes in-place!
@@ -121,10 +103,18 @@ local function get_line_byte_from_position(bufnr, position)
-- When on the first character, we can ignore the difference between byte and
-- character
if col > 0 then
if not api.nvim_buf_is_loaded(bufnr) then
vim.fn.bufload(bufnr)
end
local line = position.line
local lines = api.nvim_buf_get_lines(bufnr, line, line + 1, false)
if #lines > 0 then
return vim.str_byteindex(lines[1], col)
local ok, result = pcall(vim.str_byteindex, lines[1], col)
if ok then
return result
end
end
end
return col
@@ -700,13 +690,13 @@ end
--- Trims empty lines from input and pad left and right with spaces
---
--@param contents table of lines to trim and pad
--@param opts dictionary with optional fields
-- - pad_left number of columns to pad contents at left (default 1)
-- - pad_right number of columns to pad contents at right (default 1)
-- - pad_top number of lines to pad contents at top (default 0)
-- - pad_bottom number of lines to pad contents at bottom (default 0)
--@returns contents table of trimmed and padded lines
---@param contents table of lines to trim and pad
---@param opts dictionary with optional fields
--- - pad_left number of columns to pad contents at left (default 1)
--- - pad_right number of columns to pad contents at right (default 1)
--- - pad_top number of lines to pad contents at top (default 0)
--- - pad_bottom number of lines to pad contents at bottom (default 0)
---@return contents table of trimmed and padded lines
function M._trim_and_pad(contents, opts)
validate {
contents = { contents, 't' };
@@ -742,19 +732,19 @@ end
--- regions to improve readability.
--- The result is shown in a floating preview.
---
--@param contents table of lines to show in window
--@param opts dictionary with optional fields
-- - height of floating window
-- - width of floating window
-- - wrap_at character to wrap at for computing height
-- - max_width maximal width of floating window
-- - max_height maximal height of floating window
-- - pad_left number of columns to pad contents at left
-- - pad_right number of columns to pad contents at right
-- - pad_top number of lines to pad contents at top
-- - pad_bottom number of lines to pad contents at bottom
-- - separator insert separator after code block
--@returns width,height size of float
---@param contents table of lines to show in window
---@param opts dictionary with optional fields
--- - height of floating window
--- - width of floating window
--- - wrap_at character to wrap at for computing height
--- - max_width maximal width of floating window
--- - max_height maximal height of floating window
--- - pad_left number of columns to pad contents at left
--- - pad_right number of columns to pad contents at right
--- - pad_top number of lines to pad contents at top
--- - pad_bottom number of lines to pad contents at bottom
--- - separator insert separator after code block
---@returns width,height size of float
function M.fancy_floating_markdown(contents, opts)
validate {
contents = { contents, 't' };
@@ -971,171 +961,81 @@ function M.open_floating_preview(contents, filetype, opts)
return floating_bufnr, floating_winnr
end
-- TODO(remove-callbacks)
do
local diagnostic_ns = api.nvim_create_namespace("vim_lsp_diagnostics")
local reference_ns = api.nvim_create_namespace("vim_lsp_references")
local sign_ns = 'vim_lsp_signs'
local underline_highlight_name = "LspDiagnosticsUnderline"
vim.cmd(string.format("highlight default %s gui=underline cterm=underline", underline_highlight_name))
for kind, _ in pairs(protocol.DiagnosticSeverity) do
if type(kind) == 'string' then
vim.cmd(string.format("highlight default link %s%s %s", underline_highlight_name, kind, underline_highlight_name))
end
end
local severity_highlights = {}
local severity_floating_highlights = {}
local default_severity_highlight = {
[protocol.DiagnosticSeverity.Error] = { guifg = "Red" };
[protocol.DiagnosticSeverity.Warning] = { guifg = "Orange" };
[protocol.DiagnosticSeverity.Information] = { guifg = "LightBlue" };
[protocol.DiagnosticSeverity.Hint] = { guifg = "LightGrey" };
}
-- Initialize default severity highlights
for severity, hi_info in pairs(default_severity_highlight) do
local severity_name = protocol.DiagnosticSeverity[severity]
local highlight_name = "LspDiagnostics"..severity_name
local floating_highlight_name = highlight_name.."Floating"
-- Try to fill in the foreground color with a sane default.
local cmd_parts = {"highlight", "default", highlight_name}
for k, v in pairs(hi_info) do
table.insert(cmd_parts, k.."="..v)
end
api.nvim_command(table.concat(cmd_parts, ' '))
api.nvim_command('highlight link ' .. highlight_name .. 'Sign ' .. highlight_name)
api.nvim_command('highlight link ' .. highlight_name .. 'Floating ' .. highlight_name)
severity_highlights[severity] = highlight_name
severity_floating_highlights[severity] = floating_highlight_name
end
--- Clears diagnostics for a buffer.
---
--@param bufnr (number) buffer id
function M.buf_clear_diagnostics(bufnr)
validate { bufnr = {bufnr, 'n', true} }
bufnr = bufnr == 0 and api.nvim_get_current_buf() or bufnr
-- clear sign group
vim.fn.sign_unplace(sign_ns, {buffer=bufnr})
-- clear virtual text namespace
api.nvim_buf_clear_namespace(bufnr, diagnostic_ns, 0, -1)
end
--- Gets the name of a severity's highlight group.
---
--@param severity A member of `vim.lsp.protocol.DiagnosticSeverity`
--@returns (string) Highlight group name
--@deprecated
function M.get_severity_highlight_name(severity)
return severity_highlights[severity]
warn_once("vim.lsp.util.get_severity_highlight_name is deprecated.")
return vim.lsp.diagnostic._get_severity_highlight_name(severity)
end
--- Gets list of diagnostics for the current line.
---
--@returns (table) list of `Diagnostic` tables
--@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#diagnostic
--@deprecated
function M.buf_clear_diagnostics(bufnr, client_id)
warn_once("buf_clear_diagnostics is deprecated. Use vim.lsp.diagnostic.clear")
return vim.lsp.diagnostic.clear(bufnr, client_id)
end
--@deprecated
function M.get_line_diagnostics()
warn_once("get_line_diagnostics is deprecated. Use vim.lsp.diagnostic.get_line_diagnostics")
local bufnr = api.nvim_get_current_buf()
local linenr = api.nvim_win_get_cursor(0)[1] - 1
local line_nr = api.nvim_win_get_cursor(0)[1] - 1
local buffer_diagnostics = M.diagnostics_by_buf[bufnr]
if not buffer_diagnostics then
return {}
end
local diagnostics_by_line = M.diagnostics_group_by_line(buffer_diagnostics)
return diagnostics_by_line[linenr] or {}
return vim.lsp.diagnostic.get_line_diagnostics(bufnr, line_nr)
end
--- Displays the diagnostics for the current line in a floating hover
--- window.
--@deprecated
function M.show_line_diagnostics()
-- local marks = api.nvim_buf_get_extmarks(bufnr, diagnostic_ns, {line, 0}, {line, -1}, {})
-- if #marks == 0 then
-- return
-- end
local lines = {"Diagnostics:"}
local highlights = {{0, "Bold"}}
local line_diagnostics = M.get_line_diagnostics()
if vim.tbl_isempty(line_diagnostics) then return end
warn_once("show_line_diagnostics is deprecated. Use vim.lsp.diagnostic.show_line_diagnostics")
for i, diagnostic in ipairs(line_diagnostics) do
-- for i, mark in ipairs(marks) do
-- local mark_id = mark[1]
-- local diagnostic = buffer_diagnostics[mark_id]
local bufnr = api.nvim_get_current_buf()
local line_nr = api.nvim_win_get_cursor(0)[1] - 1
-- TODO(ashkan) make format configurable?
local prefix = string.format("%d. ", i)
local hiname = severity_floating_highlights[diagnostic.severity]
assert(hiname, 'unknown severity: ' .. tostring(diagnostic.severity))
local message_lines = split_lines(diagnostic.message)
table.insert(lines, prefix..message_lines[1])
table.insert(highlights, {#prefix + 1, hiname})
for j = 2, #message_lines do
table.insert(lines, message_lines[j])
table.insert(highlights, {0, hiname})
end
end
local popup_bufnr, winnr = M.open_floating_preview(lines, 'plaintext')
for i, hi in ipairs(highlights) do
local prefixlen, hiname = unpack(hi)
-- Start highlight after the prefix
api.nvim_buf_add_highlight(popup_bufnr, -1, hiname, i-1, prefixlen, -1)
end
return popup_bufnr, winnr
return vim.lsp.diagnostic.show_line_diagnostics(bufnr, line_nr)
end
--- Saves diagnostics into vim.lsp.util.diagnostics_by_buf[{bufnr}].
---
--@param bufnr (number) buffer id for which the diagnostics are for
--@param diagnostics list of `Diagnostic`s received from the LSP server
function M.buf_diagnostics_save_positions(bufnr, diagnostics)
validate {
bufnr = {bufnr, 'n', true};
diagnostics = {diagnostics, 't', true};
}
if not diagnostics then return end
bufnr = bufnr == 0 and api.nvim_get_current_buf() or bufnr
if not M.diagnostics_by_buf[bufnr] then
-- Clean up our data when the buffer unloads.
api.nvim_buf_attach(bufnr, false, {
on_detach = function(b)
M.diagnostics_by_buf[b] = nil
end
})
end
M.diagnostics_by_buf[bufnr] = diagnostics
--@deprecated
function M.buf_diagnostics_save_positions(bufnr, diagnostics, client_id)
warn_once("buf_diagnostics_save_positions is deprecated. Use vim.lsp.diagnostic.save")
return vim.lsp.diagnostic.save(diagnostics, bufnr, client_id)
end
--- Highlights a list of diagnostics in a buffer by underlining them.
---
--@param bufnr (number) buffer id
--@param diagnostics (list of `Diagnostic`s)
function M.buf_diagnostics_underline(bufnr, diagnostics)
for _, diagnostic in ipairs(diagnostics) do
local start = diagnostic.range["start"]
local finish = diagnostic.range["end"]
local hlmap = {
[protocol.DiagnosticSeverity.Error]='Error',
[protocol.DiagnosticSeverity.Warning]='Warning',
[protocol.DiagnosticSeverity.Information]='Information',
[protocol.DiagnosticSeverity.Hint]='Hint',
}
highlight.range(bufnr, diagnostic_ns,
underline_highlight_name..hlmap[diagnostic.severity],
{start.line, start.character},
{finish.line, finish.character}
)
end
--@deprecated
function M.buf_diagnostics_get_positions(bufnr, client_id)
warn_once("buf_diagnostics_get_positions is deprecated. Use vim.lsp.diagnostic.get")
return vim.lsp.diagnostic.get(bufnr, client_id)
end
--@deprecated
function M.buf_diagnostics_underline(bufnr, diagnostics, client_id)
warn_once("buf_diagnostics_underline is deprecated. Use 'vim.lsp.diagnostic.set_underline'")
return vim.lsp.diagnostic.set_underline(diagnostics, bufnr, client_id)
end
--@deprecated
function M.buf_diagnostics_virtual_text(bufnr, diagnostics, client_id)
warn_once("buf_diagnostics_virtual_text is deprecated. Use 'vim.lsp.diagnostic.set_virtual_text'")
return vim.lsp.diagnostic.set_virtual_text(diagnostics, bufnr, client_id)
end
--@deprecated
function M.buf_diagnostics_signs(bufnr, diagnostics, client_id)
warn_once("buf_diagnostics_signs is deprecated. Use 'vim.lsp.diagnostics.set_signs'")
return vim.lsp.diagnostic.set_signs(diagnostics, bufnr, client_id)
end
--@deprecated
function M.buf_diagnostics_count(kind, client_id)
warn_once("buf_diagnostics_count is deprecated. Use 'vim.lsp.diagnostic.get_count'")
return vim.lsp.diagnostic.get_count(vim.api.nvim_get_current_buf(), client_id, kind)
end
end
do --[[ References ]]
local reference_ns = api.nvim_create_namespace("vim_lsp_references")
--- Removes document highlights from a buffer.
---
--@param bufnr buffer id
@@ -1162,109 +1062,6 @@ do
highlight.range(bufnr, reference_ns, document_highlight_kind[kind], start_pos, end_pos)
end
end
--- Groups a list of diagnostics by line.
---
--@param diagnostics (table) list of `Diagnostic`s
--@returns (table) dictionary mapping lines to lists of diagnostics valid on
---those lines
--@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#diagnostic
function M.diagnostics_group_by_line(diagnostics)
if not diagnostics then return end
local diagnostics_by_line = {}
for _, diagnostic in ipairs(diagnostics) do
local start = diagnostic.range.start
-- TODO: Are diagnostics only valid for a single line? I don't understand
-- why this would be okay otherwise
local line_diagnostics = diagnostics_by_line[start.line]
if not line_diagnostics then
line_diagnostics = {}
diagnostics_by_line[start.line] = line_diagnostics
end
table.insert(line_diagnostics, diagnostic)
end
return diagnostics_by_line
end
--- Given a list of diagnostics, sets the corresponding virtual text for a
--- buffer.
---
--@param bufnr buffer id
--@param diagnostics (table) list of `Diagnostic`s
function M.buf_diagnostics_virtual_text(bufnr, diagnostics)
if not diagnostics then
return
end
local buffer_line_diagnostics = M.diagnostics_group_by_line(diagnostics)
for line, line_diags in pairs(buffer_line_diagnostics) do
local virt_texts = {}
for i = 1, #line_diags - 1 do
table.insert(virt_texts, {"", severity_highlights[line_diags[i].severity]})
end
local last = line_diags[#line_diags]
-- TODO(ashkan) use first line instead of subbing 2 spaces?
table.insert(virt_texts, {""..last.message:gsub("\r", ""):gsub("\n", " "), severity_highlights[last.severity]})
api.nvim_buf_set_virtual_text(bufnr, diagnostic_ns, line, virt_texts, {})
end
end
--- Returns the number of diagnostics of given kind for current buffer.
---
--- Useful for showing diagnostic counts in statusline. eg:
---
--- <pre>
--- function! LspStatus() abort
--- let sl = ''
--- if luaeval('not vim.tbl_isempty(vim.lsp.buf_get_clients(0))')
--- let sl.='%#MyStatuslineLSP#E:'
--- let sl.='%#MyStatuslineLSPErrors#%{luaeval("vim.lsp.util.buf_diagnostics_count([[Error]])")}'
--- let sl.='%#MyStatuslineLSP# W:'
--- let sl.='%#MyStatuslineLSPWarnings#%{luaeval("vim.lsp.util.buf_diagnostics_count([[Warning]])")}'
--- else
--- let sl.='%#MyStatuslineLSPErrors#off'
--- endif
--- return sl
--- endfunction
--- let &l:statusline = '%#MyStatuslineLSP#LSP '.LspStatus()
--- </pre>
---
--@param kind Diagnostic severity kind: See |vim.lsp.protocol.DiagnosticSeverity|
--@returns Count of diagnostics
function M.buf_diagnostics_count(kind)
local bufnr = vim.api.nvim_get_current_buf()
local diagnostics = M.diagnostics_by_buf[bufnr]
if not diagnostics then return end
local count = 0
for _, diagnostic in pairs(diagnostics) do
if protocol.DiagnosticSeverity[kind] == diagnostic.severity then
count = count + 1
end
end
return count
end
local diagnostic_severity_map = {
[protocol.DiagnosticSeverity.Error] = "LspDiagnosticsErrorSign";
[protocol.DiagnosticSeverity.Warning] = "LspDiagnosticsWarningSign";
[protocol.DiagnosticSeverity.Information] = "LspDiagnosticsInformationSign";
[protocol.DiagnosticSeverity.Hint] = "LspDiagnosticsHintSign";
}
--- Places signs for each diagnostic in the sign column.
---
--- Sign characters can be customized with the following commands:
---
--- <pre>
--- sign define LspDiagnosticsErrorSign text=E texthl=LspDiagnosticsError linehl= numhl=
--- sign define LspDiagnosticsWarningSign text=W texthl=LspDiagnosticsWarning linehl= numhl=
--- sign define LspDiagnosticsInformationSign text=I texthl=LspDiagnosticsInformation linehl= numhl=
--- sign define LspDiagnosticsHintSign text=H texthl=LspDiagnosticsHint linehl= numhl=
--- </pre>
function M.buf_diagnostics_signs(bufnr, diagnostics)
for _, diagnostic in ipairs(diagnostics) do
vim.fn.sign_place(0, sign_ns, diagnostic_severity_map[diagnostic.severity], bufnr, {lnum=(diagnostic.range.start.line+1)})
end
end
end
local position_sort = sort_by_key(function(v)
@@ -1561,6 +1358,9 @@ function M.character_offset(buf, row, col)
return str_utfindex(line, col)
end
M._get_line_byte_from_position = get_line_byte_from_position
M._warn_once = warn_once
M.buf_versions = {}
return M

View File

@@ -96,7 +96,7 @@ end
---
--- Examples:
--- <pre>
--- split(":aa::b:", ":") --> {'','aa','','bb',''}
--- split(":aa::b:", ":") --> {'','aa','','b',''}
--- split("axaby", "ab?") --> {'','x','y'}
--- split(x*yz*o, "*", true) --> {'x','yz','o'}
--- </pre>
@@ -496,7 +496,7 @@ do
}
local function _is_type(val, t)
return t == 'callable' and vim.is_callable(val) or type(val) == t
return type(val) == t or (t == 'callable' and vim.is_callable(val))
end
local function is_valid(opt)