mirror of
https://github.com/neovim/neovim.git
synced 2025-09-07 11:58:17 +00:00
refactor(lsp): move client code to a regular Lua class
Problem: The LSP client code is implemented as a complicated closure-class (class defined in a single function). Solution: Move LSP client code to a more conventional Lua class and move to a separate file.
This commit is contained in:

committed by
Lewis Russell

parent
cca8a78ea2
commit
59cf827f99
@@ -15,6 +15,7 @@ local lsp = vim._defer_require('vim.lsp', {
|
|||||||
_tagfunc = ..., --- @module 'vim.lsp._tagfunc'
|
_tagfunc = ..., --- @module 'vim.lsp._tagfunc'
|
||||||
_watchfiles = ..., --- @module 'vim.lsp._watchfiles'
|
_watchfiles = ..., --- @module 'vim.lsp._watchfiles'
|
||||||
buf = ..., --- @module 'vim.lsp.buf'
|
buf = ..., --- @module 'vim.lsp.buf'
|
||||||
|
client = ..., --- @module 'vim.lsp.client'
|
||||||
codelens = ..., --- @module 'vim.lsp.codelens'
|
codelens = ..., --- @module 'vim.lsp.codelens'
|
||||||
diagnostic = ..., --- @module 'vim.lsp.diagnostic'
|
diagnostic = ..., --- @module 'vim.lsp.diagnostic'
|
||||||
handlers = ..., --- @module 'vim.lsp.handlers'
|
handlers = ..., --- @module 'vim.lsp.handlers'
|
||||||
@@ -259,7 +260,7 @@ end
|
|||||||
--- Validates a client configuration as given to |vim.lsp.start_client()|.
|
--- Validates a client configuration as given to |vim.lsp.start_client()|.
|
||||||
---
|
---
|
||||||
---@param config (lsp.ClientConfig)
|
---@param config (lsp.ClientConfig)
|
||||||
---@return (string|fun(dispatchers:vim.rpc.Dispatchers):vim.lsp.rpc.PublicClient?) Command
|
---@return (string|fun(dispatchers:vim.lsp.rpc.Dispatchers):vim.lsp.rpc.PublicClient?) Command
|
||||||
---@return string[] Arguments
|
---@return string[] Arguments
|
||||||
---@return string Encoding.
|
---@return string Encoding.
|
||||||
local function validate_client_config(config)
|
local function validate_client_config(config)
|
||||||
@@ -292,7 +293,7 @@ local function validate_client_config(config)
|
|||||||
'flags.debounce_text_changes must be a number with the debounce time in milliseconds'
|
'flags.debounce_text_changes must be a number with the debounce time in milliseconds'
|
||||||
)
|
)
|
||||||
|
|
||||||
local cmd, cmd_args --- @type (string|fun(dispatchers:vim.rpc.Dispatchers):vim.lsp.rpc.PublicClient), string[]
|
local cmd, cmd_args --- @type (string|fun(dispatchers:vim.lsp.rpc.Dispatchers):vim.lsp.rpc.PublicClient), string[]
|
||||||
local config_cmd = config.cmd
|
local config_cmd = config.cmd
|
||||||
if type(config_cmd) == 'function' then
|
if type(config_cmd) == 'function' then
|
||||||
cmd = config_cmd
|
cmd = config_cmd
|
||||||
@@ -341,42 +342,6 @@ local function once(fn)
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
--- Default handler for the 'textDocument/didOpen' LSP notification.
|
|
||||||
---
|
|
||||||
---@param bufnr integer Number of the buffer, or 0 for current
|
|
||||||
---@param client lsp.Client Client object
|
|
||||||
local function text_document_did_open_handler(bufnr, client)
|
|
||||||
changetracking.init(client, bufnr)
|
|
||||||
if not vim.tbl_get(client.server_capabilities, 'textDocumentSync', 'openClose') then
|
|
||||||
return
|
|
||||||
end
|
|
||||||
if not api.nvim_buf_is_loaded(bufnr) then
|
|
||||||
return
|
|
||||||
end
|
|
||||||
local filetype = vim.bo[bufnr].filetype
|
|
||||||
|
|
||||||
local params = {
|
|
||||||
textDocument = {
|
|
||||||
version = 0,
|
|
||||||
uri = vim.uri_from_bufnr(bufnr),
|
|
||||||
languageId = client.config.get_language_id(bufnr, filetype),
|
|
||||||
text = lsp._buf_get_full_text(bufnr),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
client.notify(ms.textDocument_didOpen, params)
|
|
||||||
util.buf_versions[bufnr] = params.textDocument.version
|
|
||||||
|
|
||||||
-- Next chance we get, we should re-do the diagnostics
|
|
||||||
vim.schedule(function()
|
|
||||||
-- Protect against a race where the buffer disappears
|
|
||||||
-- between `did_open_handler` and the scheduled function firing.
|
|
||||||
if api.nvim_buf_is_valid(bufnr) then
|
|
||||||
local namespace = vim.lsp.diagnostic.get_namespace(client.id)
|
|
||||||
vim.diagnostic.show(namespace, bufnr)
|
|
||||||
end
|
|
||||||
end)
|
|
||||||
end
|
|
||||||
|
|
||||||
-- FIXME: DOC: Shouldn't need to use a dummy function
|
-- FIXME: DOC: Shouldn't need to use a dummy function
|
||||||
--
|
--
|
||||||
--- LSP client object. You can get an active client object via
|
--- LSP client object. You can get an active client object via
|
||||||
@@ -556,7 +521,9 @@ function lsp.status()
|
|||||||
local percentage = nil
|
local percentage = nil
|
||||||
local messages = {} --- @type string[]
|
local messages = {} --- @type string[]
|
||||||
for _, client in ipairs(vim.lsp.get_clients()) do
|
for _, client in ipairs(vim.lsp.get_clients()) do
|
||||||
|
--- @diagnostic disable-next-line:no-unknown
|
||||||
for progress in client.progress do
|
for progress in client.progress do
|
||||||
|
--- @cast progress {token: lsp.ProgressToken, value: lsp.LSPAny}
|
||||||
local value = progress.value
|
local value = progress.value
|
||||||
if type(value) == 'table' and value.kind then
|
if type(value) == 'table' and value.kind then
|
||||||
local message = value.message and (value.title .. ': ' .. value.message) or value.title
|
local message = value.message and (value.title .. ': ' .. value.message) or value.title
|
||||||
@@ -655,6 +622,26 @@ end
|
|||||||
--- @field flags table
|
--- @field flags table
|
||||||
--- @field root_dir string
|
--- @field root_dir string
|
||||||
|
|
||||||
|
--- Reset defaults set by `set_defaults`.
|
||||||
|
--- Must only be called if the last client attached to a buffer exits.
|
||||||
|
local function reset_defaults(bufnr)
|
||||||
|
if vim.bo[bufnr].tagfunc == 'v:lua.vim.lsp.tagfunc' then
|
||||||
|
vim.bo[bufnr].tagfunc = nil
|
||||||
|
end
|
||||||
|
if vim.bo[bufnr].omnifunc == 'v:lua.vim.lsp.omnifunc' then
|
||||||
|
vim.bo[bufnr].omnifunc = nil
|
||||||
|
end
|
||||||
|
if vim.bo[bufnr].formatexpr == 'v:lua.vim.lsp.formatexpr()' then
|
||||||
|
vim.bo[bufnr].formatexpr = nil
|
||||||
|
end
|
||||||
|
api.nvim_buf_call(bufnr, function()
|
||||||
|
local keymap = vim.fn.maparg('K', 'n', false, true)
|
||||||
|
if keymap and keymap.callback == vim.lsp.buf.hover then
|
||||||
|
vim.keymap.del('n', 'K', { buffer = bufnr })
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
-- FIXME: DOC: Currently all methods on the `vim.lsp.client` object are
|
-- FIXME: DOC: Currently all methods on the `vim.lsp.client` object are
|
||||||
-- documented twice: Here, and on the methods themselves (e.g.
|
-- documented twice: Here, and on the methods themselves (e.g.
|
||||||
-- `client.request()`). This is a workaround for the vimdoc generator script
|
-- `client.request()`). This is a workaround for the vimdoc generator script
|
||||||
@@ -875,26 +862,6 @@ function lsp.start_client(config)
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
--- Reset defaults set by `set_defaults`.
|
|
||||||
--- Must only be called if the last client attached to a buffer exits.
|
|
||||||
local function reset_defaults(bufnr)
|
|
||||||
if vim.bo[bufnr].tagfunc == 'v:lua.vim.lsp.tagfunc' then
|
|
||||||
vim.bo[bufnr].tagfunc = nil
|
|
||||||
end
|
|
||||||
if vim.bo[bufnr].omnifunc == 'v:lua.vim.lsp.omnifunc' then
|
|
||||||
vim.bo[bufnr].omnifunc = nil
|
|
||||||
end
|
|
||||||
if vim.bo[bufnr].formatexpr == 'v:lua.vim.lsp.formatexpr()' then
|
|
||||||
vim.bo[bufnr].formatexpr = nil
|
|
||||||
end
|
|
||||||
api.nvim_buf_call(bufnr, function()
|
|
||||||
local keymap = vim.fn.maparg('K', 'n', false, true)
|
|
||||||
if keymap and keymap.callback == vim.lsp.buf.hover then
|
|
||||||
vim.keymap.del('n', 'K', { buffer = bufnr })
|
|
||||||
end
|
|
||||||
end)
|
|
||||||
end
|
|
||||||
|
|
||||||
---@private
|
---@private
|
||||||
--- Invoked on client exit.
|
--- Invoked on client exit.
|
||||||
---
|
---
|
||||||
@@ -971,456 +938,26 @@ function lsp.start_client(config)
|
|||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
---@class lsp.Client
|
config.capabilities = config.capabilities or protocol.make_client_capabilities()
|
||||||
local client = {
|
|
||||||
id = client_id,
|
|
||||||
name = name,
|
|
||||||
rpc = rpc,
|
|
||||||
offset_encoding = offset_encoding,
|
|
||||||
config = config,
|
|
||||||
attached_buffers = {}, --- @type table<integer,true>
|
|
||||||
|
|
||||||
handlers = handlers,
|
local client = require('vim.lsp.client').new(client_id, rpc, handlers, offset_encoding, config)
|
||||||
--- @type table<string,function>
|
|
||||||
commands = config.commands or {},
|
|
||||||
|
|
||||||
--- @type table<integer,{ type: string, bufnr: integer, method: string}>
|
|
||||||
requests = {},
|
|
||||||
|
|
||||||
--- Contains $/progress report messages.
|
|
||||||
--- They have the format {token: integer|string, value: any}
|
|
||||||
--- For "work done progress", value will be one of:
|
|
||||||
--- - lsp.WorkDoneProgressBegin,
|
|
||||||
--- - lsp.WorkDoneProgressReport (extended with title from Begin)
|
|
||||||
--- - lsp.WorkDoneProgressEnd (extended with title from Begin)
|
|
||||||
progress = vim.ringbuf(50),
|
|
||||||
|
|
||||||
--- @type lsp.ServerCapabilities
|
|
||||||
server_capabilities = {},
|
|
||||||
|
|
||||||
---@deprecated use client.progress instead
|
|
||||||
messages = { name = name, messages = {}, progress = {}, status = {} },
|
|
||||||
dynamic_capabilities = vim.lsp._dynamic.new(client_id),
|
|
||||||
}
|
|
||||||
|
|
||||||
---@type table<string|integer, string> title of unfinished progress sequences by token
|
|
||||||
client.progress.pending = {}
|
|
||||||
|
|
||||||
--- @type lsp.ClientCapabilities
|
|
||||||
client.config.capabilities = config.capabilities or protocol.make_client_capabilities()
|
|
||||||
|
|
||||||
-- 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
|
uninitialized_clients[client_id] = client
|
||||||
|
|
||||||
local function initialize()
|
client:initialize(function()
|
||||||
local valid_traces = {
|
uninitialized_clients[client_id] = nil
|
||||||
off = 'off',
|
-- Only assign after initialized.
|
||||||
messages = 'messages',
|
active_clients[client_id] = client
|
||||||
verbose = 'verbose',
|
-- If we had been registered before we start, then send didOpen This can
|
||||||
}
|
-- happen if we attach to buffers before initialize finishes or if
|
||||||
|
-- someone restarts a client.
|
||||||
local workspace_folders --- @type lsp.WorkspaceFolder[]?
|
for bufnr, client_ids in pairs(all_buffer_active_clients) do
|
||||||
local root_uri --- @type string?
|
if client_ids[client_id] then
|
||||||
local root_path --- @type string?
|
client.on_attach(bufnr)
|
||||||
if config.workspace_folders or config.root_dir then
|
|
||||||
if config.root_dir and not config.workspace_folders then
|
|
||||||
workspace_folders = {
|
|
||||||
{
|
|
||||||
uri = vim.uri_from_fname(config.root_dir),
|
|
||||||
name = string.format('%s', config.root_dir),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
else
|
|
||||||
workspace_folders = config.workspace_folders
|
|
||||||
end
|
|
||||||
root_uri = workspace_folders[1].uri
|
|
||||||
root_path = vim.uri_to_fname(root_uri)
|
|
||||||
else
|
|
||||||
workspace_folders = nil
|
|
||||||
root_uri = nil
|
|
||||||
root_path = nil
|
|
||||||
end
|
|
||||||
|
|
||||||
local initialize_params = {
|
|
||||||
-- The process Id of the parent process that started the server. Is null if
|
|
||||||
-- the process has not been started by another process. If the parent
|
|
||||||
-- process is not alive then the server should exit (see exit notification)
|
|
||||||
-- its process.
|
|
||||||
processId = uv.os_getpid(),
|
|
||||||
-- Information about the client
|
|
||||||
-- since 3.15.0
|
|
||||||
clientInfo = {
|
|
||||||
name = 'Neovim',
|
|
||||||
version = tostring(vim.version()),
|
|
||||||
},
|
|
||||||
-- The rootPath of the workspace. Is null if no folder is open.
|
|
||||||
--
|
|
||||||
-- @deprecated in favour of rootUri.
|
|
||||||
rootPath = root_path or vim.NIL,
|
|
||||||
-- The rootUri of the workspace. Is null if no folder is open. If both
|
|
||||||
-- `rootPath` and `rootUri` are set `rootUri` wins.
|
|
||||||
rootUri = root_uri or vim.NIL,
|
|
||||||
-- The workspace folders configured in the client when the server starts.
|
|
||||||
-- This property is only available if the client supports workspace folders.
|
|
||||||
-- It can be `null` if the client supports workspace folders but none are
|
|
||||||
-- configured.
|
|
||||||
workspaceFolders = workspace_folders or vim.NIL,
|
|
||||||
-- User provided initialization options.
|
|
||||||
initializationOptions = config.init_options,
|
|
||||||
-- The capabilities provided by the client (editor or tool)
|
|
||||||
capabilities = config.capabilities,
|
|
||||||
-- The initial trace setting. If omitted trace is disabled ("off").
|
|
||||||
-- trace = "off" | "messages" | "verbose";
|
|
||||||
trace = valid_traces[config.trace] or 'off',
|
|
||||||
}
|
|
||||||
if config.before_init then
|
|
||||||
local status, err = pcall(config.before_init, initialize_params, config)
|
|
||||||
if not status then
|
|
||||||
write_error(lsp.client_errors.BEFORE_INIT_CALLBACK_ERROR, err)
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
end)
|
||||||
--- @param method string
|
|
||||||
--- @param opts? {bufnr: integer?}
|
|
||||||
client.supports_method = function(method, opts)
|
|
||||||
opts = opts or {}
|
|
||||||
local required_capability = lsp._request_name_to_capability[method]
|
|
||||||
-- if we don't know about the method, assume that the client supports it.
|
|
||||||
if not required_capability then
|
|
||||||
return true
|
|
||||||
end
|
|
||||||
if vim.tbl_get(client.server_capabilities, unpack(required_capability)) then
|
|
||||||
return true
|
|
||||||
else
|
|
||||||
if client.dynamic_capabilities:supports_registration(method) then
|
|
||||||
return client.dynamic_capabilities:supports(method, opts)
|
|
||||||
end
|
|
||||||
return false
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
if log.trace() then
|
|
||||||
log.trace(log_prefix, 'initialize_params', initialize_params)
|
|
||||||
end
|
|
||||||
rpc.request('initialize', initialize_params, function(init_err, result)
|
|
||||||
assert(not init_err, tostring(init_err))
|
|
||||||
assert(result, 'server sent empty result')
|
|
||||||
rpc.notify('initialized', vim.empty_dict())
|
|
||||||
client.initialized = true
|
|
||||||
uninitialized_clients[client_id] = nil
|
|
||||||
client.workspace_folders = workspace_folders
|
|
||||||
|
|
||||||
-- These are the cleaned up capabilities we use for dynamically deciding
|
|
||||||
-- when to send certain events to clients.
|
|
||||||
client.server_capabilities =
|
|
||||||
assert(result.capabilities, "initialize result doesn't contain capabilities")
|
|
||||||
client.server_capabilities = assert(protocol.resolve_capabilities(client.server_capabilities))
|
|
||||||
|
|
||||||
if client.server_capabilities.positionEncoding then
|
|
||||||
client.offset_encoding = client.server_capabilities.positionEncoding
|
|
||||||
end
|
|
||||||
|
|
||||||
if next(config.settings) then
|
|
||||||
client.notify(ms.workspace_didChangeConfiguration, { settings = config.settings })
|
|
||||||
end
|
|
||||||
|
|
||||||
if config.on_init then
|
|
||||||
local status, err = pcall(config.on_init, client, result)
|
|
||||||
if not status then
|
|
||||||
write_error(lsp.client_errors.ON_INIT_CALLBACK_ERROR, err)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
if log.info() then
|
|
||||||
log.info(
|
|
||||||
log_prefix,
|
|
||||||
'server_capabilities',
|
|
||||||
{ server_capabilities = client.server_capabilities }
|
|
||||||
)
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Only assign after initialized.
|
|
||||||
active_clients[client_id] = client
|
|
||||||
-- If we had been registered before we start, then send didOpen This can
|
|
||||||
-- happen if we attach to buffers before initialize finishes or if
|
|
||||||
-- someone restarts a client.
|
|
||||||
for bufnr, client_ids in pairs(all_buffer_active_clients) do
|
|
||||||
if client_ids[client_id] then
|
|
||||||
client._on_attach(bufnr)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end)
|
|
||||||
end
|
|
||||||
|
|
||||||
---@nodoc
|
|
||||||
--- Sends a request to the server.
|
|
||||||
---
|
|
||||||
--- This is a thin wrapper around {client.rpc.request} with some additional
|
|
||||||
--- checks for capabilities and handler availability.
|
|
||||||
---
|
|
||||||
---@param method string LSP method name.
|
|
||||||
---@param params table|nil LSP request params.
|
|
||||||
---@param handler lsp.Handler|nil Response |lsp-handler| for this method.
|
|
||||||
---@param bufnr integer Buffer handle (0 for current).
|
|
||||||
---@return boolean status, integer|nil request_id {status} is a bool indicating
|
|
||||||
---whether the request was successful. If it is `false`, then it will
|
|
||||||
---always be `false` (the client has shutdown). If it was
|
|
||||||
---successful, then it will return {request_id} as the
|
|
||||||
---second result. You can use this with `client.cancel_request(request_id)`
|
|
||||||
---to cancel the-request.
|
|
||||||
---@see |vim.lsp.buf_request_all()|
|
|
||||||
function client.request(method, params, handler, bufnr)
|
|
||||||
if not handler then
|
|
||||||
handler = assert(
|
|
||||||
resolve_handler(method),
|
|
||||||
string.format('not found: %q request handler for client %q.', method, client.name)
|
|
||||||
)
|
|
||||||
end
|
|
||||||
-- Ensure pending didChange notifications are sent so that the server doesn't operate on a stale state
|
|
||||||
changetracking.flush(client, bufnr)
|
|
||||||
local version = util.buf_versions[bufnr]
|
|
||||||
bufnr = resolve_bufnr(bufnr)
|
|
||||||
if log.debug() then
|
|
||||||
log.debug(log_prefix, 'client.request', client_id, method, params, handler, bufnr)
|
|
||||||
end
|
|
||||||
local success, request_id = rpc.request(method, params, function(err, result)
|
|
||||||
local context = {
|
|
||||||
method = method,
|
|
||||||
client_id = client_id,
|
|
||||||
bufnr = bufnr,
|
|
||||||
params = params,
|
|
||||||
version = version,
|
|
||||||
}
|
|
||||||
handler(err, result, context)
|
|
||||||
end, function(request_id)
|
|
||||||
local request = client.requests[request_id]
|
|
||||||
request.type = 'complete'
|
|
||||||
nvim_exec_autocmds('LspRequest', {
|
|
||||||
buffer = api.nvim_buf_is_valid(bufnr) and bufnr or nil,
|
|
||||||
modeline = false,
|
|
||||||
data = { client_id = client_id, request_id = request_id, request = request },
|
|
||||||
})
|
|
||||||
client.requests[request_id] = nil
|
|
||||||
end)
|
|
||||||
|
|
||||||
if success and request_id then
|
|
||||||
local request = { type = 'pending', bufnr = bufnr, method = method }
|
|
||||||
client.requests[request_id] = request
|
|
||||||
nvim_exec_autocmds('LspRequest', {
|
|
||||||
buffer = bufnr,
|
|
||||||
modeline = false,
|
|
||||||
data = { client_id = client_id, request_id = request_id, request = request },
|
|
||||||
})
|
|
||||||
end
|
|
||||||
|
|
||||||
return success, request_id
|
|
||||||
end
|
|
||||||
|
|
||||||
---@private
|
|
||||||
--- Sends a request to the server and synchronously waits for the response.
|
|
||||||
---
|
|
||||||
--- This is a wrapper around {client.request}
|
|
||||||
---
|
|
||||||
---@param method (string) LSP method name.
|
|
||||||
---@param params (table) LSP request params.
|
|
||||||
---@param timeout_ms (integer|nil) Maximum time in milliseconds to wait for
|
|
||||||
--- a result. Defaults to 1000
|
|
||||||
---@param bufnr (integer) Buffer handle (0 for current).
|
|
||||||
---@return {err: lsp.ResponseError|nil, result:any}|nil, string|nil err # a dictionary, where
|
|
||||||
--- `err` and `result` come from the |lsp-handler|.
|
|
||||||
--- On timeout, cancel or error, returns `(nil, err)` where `err` is a
|
|
||||||
--- string describing the failure reason. If the request was unsuccessful
|
|
||||||
--- returns `nil`.
|
|
||||||
---@see |vim.lsp.buf_request_sync()|
|
|
||||||
function client.request_sync(method, params, timeout_ms, bufnr)
|
|
||||||
local request_result = nil
|
|
||||||
local function _sync_handler(err, result)
|
|
||||||
request_result = { err = err, result = result }
|
|
||||||
end
|
|
||||||
|
|
||||||
local success, request_id = client.request(method, params, _sync_handler, bufnr)
|
|
||||||
if not success then
|
|
||||||
return nil
|
|
||||||
end
|
|
||||||
|
|
||||||
local wait_result, reason = vim.wait(timeout_ms or 1000, function()
|
|
||||||
return request_result ~= nil
|
|
||||||
end, 10)
|
|
||||||
|
|
||||||
if not wait_result then
|
|
||||||
if request_id then
|
|
||||||
client.cancel_request(request_id)
|
|
||||||
end
|
|
||||||
return nil, wait_result_reason[reason]
|
|
||||||
end
|
|
||||||
return request_result
|
|
||||||
end
|
|
||||||
|
|
||||||
---@nodoc
|
|
||||||
--- Sends a notification to an LSP server.
|
|
||||||
---
|
|
||||||
---@param method string LSP method name.
|
|
||||||
---@param params table|nil LSP request params.
|
|
||||||
---@return boolean status true if the notification was successful.
|
|
||||||
---If it is false, then it will always be false
|
|
||||||
---(the client has shutdown).
|
|
||||||
function client.notify(method, params)
|
|
||||||
if method ~= ms.textDocument_didChange then
|
|
||||||
changetracking.flush(client)
|
|
||||||
end
|
|
||||||
|
|
||||||
local client_active = rpc.notify(method, params)
|
|
||||||
|
|
||||||
if client_active then
|
|
||||||
vim.schedule(function()
|
|
||||||
nvim_exec_autocmds('LspNotify', {
|
|
||||||
modeline = false,
|
|
||||||
data = {
|
|
||||||
client_id = client.id,
|
|
||||||
method = method,
|
|
||||||
params = params,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
end)
|
|
||||||
end
|
|
||||||
|
|
||||||
return client_active
|
|
||||||
end
|
|
||||||
|
|
||||||
---@nodoc
|
|
||||||
--- Cancels a request with a given request id.
|
|
||||||
---
|
|
||||||
---@param id (integer) id of request to cancel
|
|
||||||
---@return boolean status true if notification was successful. false otherwise
|
|
||||||
---@see |vim.lsp.client.notify()|
|
|
||||||
function client.cancel_request(id)
|
|
||||||
validate({ id = { id, 'n' } })
|
|
||||||
local request = client.requests[id]
|
|
||||||
if request and request.type == 'pending' then
|
|
||||||
request.type = 'cancel'
|
|
||||||
nvim_exec_autocmds('LspRequest', {
|
|
||||||
buffer = request.bufnr,
|
|
||||||
modeline = false,
|
|
||||||
data = { client_id = client_id, request_id = id, request = request },
|
|
||||||
})
|
|
||||||
end
|
|
||||||
return rpc.notify(ms.dollar_cancelRequest, { id = id })
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Track this so that we can escalate automatically if we've already tried a
|
|
||||||
-- graceful shutdown
|
|
||||||
local graceful_shutdown_failed = false
|
|
||||||
|
|
||||||
---@nodoc
|
|
||||||
--- Stops a client, optionally with force.
|
|
||||||
---
|
|
||||||
---By default, it will just ask the - server to shutdown without force. If
|
|
||||||
--- you request to stop a client which has previously been requested to
|
|
||||||
--- shutdown, it will automatically escalate and force shutdown.
|
|
||||||
---
|
|
||||||
---@param force boolean|nil
|
|
||||||
function client.stop(force)
|
|
||||||
if rpc.is_closing() then
|
|
||||||
return
|
|
||||||
end
|
|
||||||
if force or not client.initialized or graceful_shutdown_failed then
|
|
||||||
rpc.terminate()
|
|
||||||
return
|
|
||||||
end
|
|
||||||
-- Sending a signal after a process has exited is acceptable.
|
|
||||||
rpc.request(ms.shutdown, nil, function(err, _)
|
|
||||||
if err == nil then
|
|
||||||
rpc.notify(ms.exit)
|
|
||||||
else
|
|
||||||
-- If there was an error in the shutdown request, then term to be safe.
|
|
||||||
rpc.terminate()
|
|
||||||
graceful_shutdown_failed = true
|
|
||||||
end
|
|
||||||
end)
|
|
||||||
end
|
|
||||||
|
|
||||||
---@private
|
|
||||||
--- Checks whether a client is stopped.
|
|
||||||
---
|
|
||||||
---@return boolean # true if client is stopped or in the process of being
|
|
||||||
---stopped; false otherwise
|
|
||||||
function client.is_stopped()
|
|
||||||
return rpc.is_closing()
|
|
||||||
end
|
|
||||||
|
|
||||||
---@private
|
|
||||||
--- Execute a lsp command, either via client command function (if available)
|
|
||||||
--- or via workspace/executeCommand (if supported by the server)
|
|
||||||
---
|
|
||||||
---@param command lsp.Command
|
|
||||||
---@param context? {bufnr: integer}
|
|
||||||
---@param handler? lsp.Handler only called if a server command
|
|
||||||
function client._exec_cmd(command, context, handler)
|
|
||||||
context = vim.deepcopy(context or {}, true) --[[@as lsp.HandlerContext]]
|
|
||||||
context.bufnr = context.bufnr or api.nvim_get_current_buf()
|
|
||||||
context.client_id = client.id
|
|
||||||
local cmdname = command.command
|
|
||||||
local fn = client.commands[cmdname] or lsp.commands[cmdname]
|
|
||||||
if fn then
|
|
||||||
fn(command, context)
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
local command_provider = client.server_capabilities.executeCommandProvider
|
|
||||||
local commands = type(command_provider) == 'table' and command_provider.commands or {}
|
|
||||||
if not vim.list_contains(commands, cmdname) then
|
|
||||||
vim.notify_once(
|
|
||||||
string.format(
|
|
||||||
'Language server `%s` does not support command `%s`. This command may require a client extension.',
|
|
||||||
client.name,
|
|
||||||
cmdname
|
|
||||||
),
|
|
||||||
vim.log.levels.WARN
|
|
||||||
)
|
|
||||||
return
|
|
||||||
end
|
|
||||||
-- Not using command directly to exclude extra properties,
|
|
||||||
-- see https://github.com/python-lsp/python-lsp-server/issues/146
|
|
||||||
local params = {
|
|
||||||
command = command.command,
|
|
||||||
arguments = command.arguments,
|
|
||||||
}
|
|
||||||
client.request(ms.workspace_executeCommand, params, handler, context.bufnr)
|
|
||||||
end
|
|
||||||
|
|
||||||
---@private
|
|
||||||
--- Runs the on_attach function from the client's config if it was defined.
|
|
||||||
---@param bufnr integer Buffer number
|
|
||||||
function client._on_attach(bufnr)
|
|
||||||
text_document_did_open_handler(bufnr, client)
|
|
||||||
|
|
||||||
lsp._set_defaults(client, bufnr)
|
|
||||||
|
|
||||||
nvim_exec_autocmds('LspAttach', {
|
|
||||||
buffer = bufnr,
|
|
||||||
modeline = false,
|
|
||||||
data = { client_id = client.id },
|
|
||||||
})
|
|
||||||
|
|
||||||
if config.on_attach then
|
|
||||||
local status, err = pcall(config.on_attach, client, bufnr)
|
|
||||||
if not status then
|
|
||||||
write_error(lsp.client_errors.ON_ATTACH_ERROR, err)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
-- schedule the initialization of semantic tokens to give the above
|
|
||||||
-- on_attach and LspAttach callbacks the ability to schedule wrap the
|
|
||||||
-- opt-out (deleting the semanticTokensProvider from capabilities)
|
|
||||||
vim.schedule(function()
|
|
||||||
if vim.tbl_get(client.server_capabilities, 'semanticTokensProvider', 'full') then
|
|
||||||
lsp.semantic_tokens.start(bufnr, client.id)
|
|
||||||
end
|
|
||||||
end)
|
|
||||||
|
|
||||||
client.attached_buffers[bufnr] = true
|
|
||||||
end
|
|
||||||
|
|
||||||
initialize()
|
|
||||||
|
|
||||||
return client_id
|
return client_id
|
||||||
end
|
end
|
||||||
@@ -1564,7 +1101,7 @@ function lsp.buf_attach_client(bufnr, client_id)
|
|||||||
if vim.tbl_get(client.server_capabilities, 'textDocumentSync', 'openClose') then
|
if vim.tbl_get(client.server_capabilities, 'textDocumentSync', 'openClose') then
|
||||||
client.notify(ms.textDocument_didClose, params)
|
client.notify(ms.textDocument_didClose, params)
|
||||||
end
|
end
|
||||||
text_document_did_open_handler(bufnr, client)
|
client:_text_document_did_open_handler(bufnr)
|
||||||
end
|
end
|
||||||
end,
|
end,
|
||||||
on_detach = function()
|
on_detach = function()
|
||||||
@@ -1596,7 +1133,7 @@ function lsp.buf_attach_client(bufnr, client_id)
|
|||||||
-- Send didOpen for the client if it is initialized. If it isn't initialized
|
-- Send didOpen for the client if it is initialized. If it isn't initialized
|
||||||
-- then it will send didOpen on initialize.
|
-- then it will send didOpen on initialize.
|
||||||
if client then
|
if client then
|
||||||
client._on_attach(bufnr)
|
client:_on_attach(bufnr)
|
||||||
end
|
end
|
||||||
return true
|
return true
|
||||||
end
|
end
|
||||||
|
@@ -6,6 +6,7 @@ local glob = vim.glob
|
|||||||
local M = {}
|
local M = {}
|
||||||
|
|
||||||
--- @param client_id number
|
--- @param client_id number
|
||||||
|
--- @return lsp.DynamicCapabilities
|
||||||
function M.new(client_id)
|
function M.new(client_id)
|
||||||
return setmetatable({
|
return setmetatable({
|
||||||
capabilities = {},
|
capabilities = {},
|
||||||
@@ -37,7 +38,7 @@ function M:register(registrations)
|
|||||||
end
|
end
|
||||||
|
|
||||||
--- @param unregisterations lsp.Unregistration[]
|
--- @param unregisterations lsp.Unregistration[]
|
||||||
--- @private
|
--- @package
|
||||||
function M:unregister(unregisterations)
|
function M:unregister(unregisterations)
|
||||||
for _, unreg in ipairs(unregisterations) do
|
for _, unreg in ipairs(unregisterations) do
|
||||||
local method = unreg.method
|
local method = unreg.method
|
||||||
@@ -77,7 +78,7 @@ end
|
|||||||
|
|
||||||
--- @param method string
|
--- @param method string
|
||||||
--- @param opts? {bufnr: integer?}
|
--- @param opts? {bufnr: integer?}
|
||||||
--- @private
|
--- @package
|
||||||
function M:supports(method, opts)
|
function M:supports(method, opts)
|
||||||
return self:get(method, opts) ~= nil
|
return self:get(method, opts) ~= nil
|
||||||
end
|
end
|
||||||
|
@@ -652,7 +652,7 @@ local function on_code_action_results(results, opts)
|
|||||||
end
|
end
|
||||||
if action.command then
|
if action.command then
|
||||||
local command = type(action.command) == 'table' and action.command or action
|
local command = type(action.command) == 'table' and action.command or action
|
||||||
client._exec_cmd(command, ctx)
|
client:_exec_cmd(command, ctx)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
663
runtime/lua/vim/lsp/client.lua
Normal file
663
runtime/lua/vim/lsp/client.lua
Normal file
@@ -0,0 +1,663 @@
|
|||||||
|
local uv = vim.uv
|
||||||
|
local api = vim.api
|
||||||
|
local lsp = vim.lsp
|
||||||
|
local log = lsp.log
|
||||||
|
local ms = lsp.protocol.Methods
|
||||||
|
local changetracking = lsp._changetracking
|
||||||
|
|
||||||
|
--- @class lsp.Client.Progress: vim.Ringbuf<{token: integer|string, value: any}>
|
||||||
|
--- @field pending table<lsp.ProgressToken,lsp.LSPAny>
|
||||||
|
|
||||||
|
--- @class lsp.Client
|
||||||
|
---
|
||||||
|
--- The id allocated to the client.
|
||||||
|
--- @field id integer
|
||||||
|
---
|
||||||
|
--- If a name is specified on creation, that will be used. Otherwise it is just
|
||||||
|
--- the client id. This is used for logs and messages.
|
||||||
|
--- @field name string
|
||||||
|
---
|
||||||
|
--- RPC client object, for low level interaction with the client.
|
||||||
|
--- See |vim.lsp.rpc.start()|.
|
||||||
|
--- @field rpc vim.lsp.rpc.PublicClient
|
||||||
|
---
|
||||||
|
--- The encoding used for communicating with the server. You can modify this in
|
||||||
|
--- the `config`'s `on_init` method before text is sent to the server.
|
||||||
|
--- @field offset_encoding string
|
||||||
|
---
|
||||||
|
--- The handlers used by the client as described in |lsp-handler|.
|
||||||
|
--- @field handlers table<string,lsp.Handler>
|
||||||
|
---
|
||||||
|
--- The current pending requests in flight to the server. Entries are key-value
|
||||||
|
--- pairs with the key being the request ID while the value is a table with
|
||||||
|
--- `type`, `bufnr`, and `method` key-value pairs. `type` is either "pending"
|
||||||
|
--- for an active request, or "cancel" for a cancel request. It will be
|
||||||
|
--- "complete" ephemerally while executing |LspRequest| autocmds when replies
|
||||||
|
--- are received from the server.
|
||||||
|
--- @field requests table<integer,{ type: string, bufnr: integer, method: string}>
|
||||||
|
---
|
||||||
|
--- copy of the table that was passed by the user
|
||||||
|
--- to |vim.lsp.start_client()|.
|
||||||
|
--- @field config lsp.ClientConfig
|
||||||
|
---
|
||||||
|
--- Response from the server sent on
|
||||||
|
--- initialize` describing the server's capabilities.
|
||||||
|
--- @field server_capabilities lsp.ServerCapabilities
|
||||||
|
---
|
||||||
|
--- A ring buffer (|vim.ringbuf()|) containing progress messages
|
||||||
|
--- sent by the server.
|
||||||
|
--- @field progress lsp.Client.Progress
|
||||||
|
---
|
||||||
|
--- @field initialized true?
|
||||||
|
--- @field workspace_folders lsp.WorkspaceFolder[]?
|
||||||
|
--- @field attached_buffers table<integer,true>
|
||||||
|
--- @field commands table<string,function>
|
||||||
|
--- @field private _log_prefix string
|
||||||
|
--- Track this so that we can escalate automatically if we've already tried a
|
||||||
|
--- graceful shutdown
|
||||||
|
--- @field private _graceful_shutdown_failed true?
|
||||||
|
---
|
||||||
|
--- @field dynamic_capabilities lsp.DynamicCapabilities
|
||||||
|
---
|
||||||
|
--- Sends a request to the server.
|
||||||
|
--- This is a thin wrapper around {client.rpc.request} with some additional
|
||||||
|
--- checking.
|
||||||
|
--- 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).
|
||||||
|
--- If {status} is `true`, the function returns {request_id} as the second
|
||||||
|
--- result. You can use this with `client.cancel_request(request_id)` to cancel
|
||||||
|
--- the request.
|
||||||
|
--- @field request fun(method: string, params: table?, handler: lsp.Handler?, bufnr: integer): boolean, integer?
|
||||||
|
---
|
||||||
|
--- Sends a request to the server and synchronously waits for the response.
|
||||||
|
--- This is a wrapper around {client.request}
|
||||||
|
--- Returns: { err=err, result=result }, a dictionary, where `err` and `result`
|
||||||
|
--- come from the |lsp-handler|. On timeout, cancel or error, returns `(nil,
|
||||||
|
--- err)` where `err` is a string describing the failure reason. If the request
|
||||||
|
--- was unsuccessful returns `nil`.
|
||||||
|
--- @field request_sync fun(method: string, params: table?, timeout_ms: integer?, bufnr: integer): {err: lsp.ResponseError|nil, result:any}|nil, string|nil err # a dictionary, where
|
||||||
|
---
|
||||||
|
--- Sends a notification to an LSP server.
|
||||||
|
--- Returns: a boolean to indicate if the notification was successful. If
|
||||||
|
--- it is false, then it will always be false (the client has shutdown).
|
||||||
|
--- @field notify fun(method: string, params: table?): boolean
|
||||||
|
---
|
||||||
|
--- Cancels a request with a given request id.
|
||||||
|
--- Returns: same as `notify()`.
|
||||||
|
--- @field cancel_request fun(id: integer): boolean
|
||||||
|
---
|
||||||
|
--- Stops a client, optionally with force.
|
||||||
|
--- By default, it will just ask the server to shutdown without force.
|
||||||
|
--- If you request to stop a client which has previously been requested to
|
||||||
|
--- shutdown, it will automatically escalate and force shutdown.
|
||||||
|
--- @field stop fun(force?: boolean)
|
||||||
|
---
|
||||||
|
--- Runs the on_attach function from the client's config if it was defined.
|
||||||
|
--- Useful for buffer-local setup.
|
||||||
|
--- @field on_attach fun(bufnr: integer)
|
||||||
|
---
|
||||||
|
--- Checks if a client supports a given method.
|
||||||
|
--- Always returns true for unknown off-spec methods.
|
||||||
|
--- [opts] is a optional `{bufnr?: integer}` table.
|
||||||
|
--- Some language server capabilities can be file specific.
|
||||||
|
--- @field supports_method fun(method: string, opts?: {bufnr: integer?}): boolean
|
||||||
|
---
|
||||||
|
--- Checks whether a client is stopped.
|
||||||
|
--- Returns: true if the client is fully stopped.
|
||||||
|
--- @field is_stopped fun(): boolean
|
||||||
|
local Client = {}
|
||||||
|
Client.__index = Client
|
||||||
|
|
||||||
|
--- @param cls table
|
||||||
|
--- @param meth any
|
||||||
|
--- @return function
|
||||||
|
local function method_wrapper(cls, meth)
|
||||||
|
return function(...)
|
||||||
|
return meth(cls, ...)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
--- @package
|
||||||
|
--- @param id integer
|
||||||
|
--- @param rpc vim.lsp.rpc.PublicClient
|
||||||
|
--- @param handlers table<string,lsp.Handler>
|
||||||
|
--- @param offset_encoding string
|
||||||
|
--- @param config lsp.ClientConfig
|
||||||
|
--- @return lsp.Client
|
||||||
|
function Client.new(id, rpc, handlers, offset_encoding, config)
|
||||||
|
local name = config.name
|
||||||
|
|
||||||
|
--- @class lsp.Client
|
||||||
|
local self = {
|
||||||
|
id = id,
|
||||||
|
config = config,
|
||||||
|
handlers = handlers,
|
||||||
|
rpc = rpc,
|
||||||
|
offset_encoding = offset_encoding,
|
||||||
|
name = name,
|
||||||
|
_log_prefix = string.format('LSP[%s]', name),
|
||||||
|
requests = {},
|
||||||
|
commands = config.commands or {},
|
||||||
|
attached_buffers = {},
|
||||||
|
server_capabilities = {},
|
||||||
|
dynamic_capabilities = vim.lsp._dynamic.new(id),
|
||||||
|
|
||||||
|
--- Contains $/progress report messages.
|
||||||
|
--- They have the format {token: integer|string, value: any}
|
||||||
|
--- For "work done progress", value will be one of:
|
||||||
|
--- - lsp.WorkDoneProgressBegin,
|
||||||
|
--- - lsp.WorkDoneProgressReport (extended with title from Begin)
|
||||||
|
--- - lsp.WorkDoneProgressEnd (extended with title from Begin)
|
||||||
|
progress = vim.ringbuf(50) --[[@as lsp.Client.Progress]],
|
||||||
|
|
||||||
|
--- @deprecated use client.progress instead
|
||||||
|
messages = { name = name, messages = {}, progress = {}, status = {} },
|
||||||
|
}
|
||||||
|
|
||||||
|
self.request = method_wrapper(self, Client._request)
|
||||||
|
self.request_sync = method_wrapper(self, Client._request_sync)
|
||||||
|
self.notify = method_wrapper(self, Client._notify)
|
||||||
|
self.cancel_request = method_wrapper(self, Client._cancel_request)
|
||||||
|
self.stop = method_wrapper(self, Client._stop)
|
||||||
|
self.is_stopped = method_wrapper(self, Client._is_stopped)
|
||||||
|
self.on_attach = method_wrapper(self, Client._on_attach)
|
||||||
|
self.supports_method = method_wrapper(self, Client._supports_method)
|
||||||
|
|
||||||
|
---@type table<string|integer, string> title of unfinished progress sequences by token
|
||||||
|
self.progress.pending = {}
|
||||||
|
|
||||||
|
return setmetatable(self, Client)
|
||||||
|
end
|
||||||
|
|
||||||
|
--- @private
|
||||||
|
--- @param cb fun()
|
||||||
|
function Client:initialize(cb)
|
||||||
|
local valid_traces = {
|
||||||
|
off = 'off',
|
||||||
|
messages = 'messages',
|
||||||
|
verbose = 'verbose',
|
||||||
|
}
|
||||||
|
|
||||||
|
local config = self.config
|
||||||
|
|
||||||
|
local workspace_folders --- @type lsp.WorkspaceFolder[]?
|
||||||
|
local root_uri --- @type string?
|
||||||
|
local root_path --- @type string?
|
||||||
|
if config.workspace_folders or config.root_dir then
|
||||||
|
if config.root_dir and not config.workspace_folders then
|
||||||
|
workspace_folders = {
|
||||||
|
{
|
||||||
|
uri = vim.uri_from_fname(config.root_dir),
|
||||||
|
name = string.format('%s', config.root_dir),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
else
|
||||||
|
workspace_folders = config.workspace_folders
|
||||||
|
end
|
||||||
|
root_uri = workspace_folders[1].uri
|
||||||
|
root_path = vim.uri_to_fname(root_uri)
|
||||||
|
else
|
||||||
|
workspace_folders = nil
|
||||||
|
root_uri = nil
|
||||||
|
root_path = nil
|
||||||
|
end
|
||||||
|
|
||||||
|
local initialize_params = {
|
||||||
|
-- The process Id of the parent process that started the server. Is null if
|
||||||
|
-- the process has not been started by another process. If the parent
|
||||||
|
-- process is not alive then the server should exit (see exit notification)
|
||||||
|
-- its process.
|
||||||
|
processId = uv.os_getpid(),
|
||||||
|
-- Information about the client
|
||||||
|
-- since 3.15.0
|
||||||
|
clientInfo = {
|
||||||
|
name = 'Neovim',
|
||||||
|
version = tostring(vim.version()),
|
||||||
|
},
|
||||||
|
-- The rootPath of the workspace. Is null if no folder is open.
|
||||||
|
--
|
||||||
|
-- @deprecated in favour of rootUri.
|
||||||
|
rootPath = root_path or vim.NIL,
|
||||||
|
-- The rootUri of the workspace. Is null if no folder is open. If both
|
||||||
|
-- `rootPath` and `rootUri` are set `rootUri` wins.
|
||||||
|
rootUri = root_uri or vim.NIL,
|
||||||
|
-- The workspace folders configured in the client when the server starts.
|
||||||
|
-- This property is only available if the client supports workspace folders.
|
||||||
|
-- It can be `null` if the client supports workspace folders but none are
|
||||||
|
-- configured.
|
||||||
|
workspaceFolders = workspace_folders or vim.NIL,
|
||||||
|
-- User provided initialization options.
|
||||||
|
initializationOptions = config.init_options,
|
||||||
|
-- The capabilities provided by the client (editor or tool)
|
||||||
|
capabilities = config.capabilities,
|
||||||
|
-- The initial trace setting. If omitted trace is disabled ("off").
|
||||||
|
-- trace = "off" | "messages" | "verbose";
|
||||||
|
trace = valid_traces[config.trace] or 'off',
|
||||||
|
}
|
||||||
|
if config.before_init then
|
||||||
|
--- @type boolean, string?
|
||||||
|
local status, err = pcall(config.before_init, initialize_params, config)
|
||||||
|
if not status then
|
||||||
|
self:write_error(lsp.client_errors.BEFORE_INIT_CALLBACK_ERROR, err)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if log.trace() then
|
||||||
|
log.trace(self._log_prefix, 'initialize_params', initialize_params)
|
||||||
|
end
|
||||||
|
|
||||||
|
local rpc = self.rpc
|
||||||
|
|
||||||
|
rpc.request('initialize', initialize_params, function(init_err, result)
|
||||||
|
assert(not init_err, tostring(init_err))
|
||||||
|
assert(result, 'server sent empty result')
|
||||||
|
rpc.notify('initialized', vim.empty_dict())
|
||||||
|
self.initialized = true
|
||||||
|
self.workspace_folders = workspace_folders
|
||||||
|
|
||||||
|
-- These are the cleaned up capabilities we use for dynamically deciding
|
||||||
|
-- when to send certain events to clients.
|
||||||
|
self.server_capabilities =
|
||||||
|
assert(result.capabilities, "initialize result doesn't contain capabilities")
|
||||||
|
self.server_capabilities = assert(lsp.protocol.resolve_capabilities(self.server_capabilities))
|
||||||
|
|
||||||
|
if self.server_capabilities.positionEncoding then
|
||||||
|
self.offset_encoding = self.server_capabilities.positionEncoding
|
||||||
|
end
|
||||||
|
|
||||||
|
if next(config.settings) then
|
||||||
|
self:_notify(ms.workspace_didChangeConfiguration, { settings = config.settings })
|
||||||
|
end
|
||||||
|
|
||||||
|
if config.on_init then
|
||||||
|
--- @type boolean, string?
|
||||||
|
local status, err = pcall(config.on_init, self, result)
|
||||||
|
if not status then
|
||||||
|
self:write_error(lsp.client_errors.ON_INIT_CALLBACK_ERROR, err)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if log.info() then
|
||||||
|
log.info(
|
||||||
|
self._log_prefix,
|
||||||
|
'server_capabilities',
|
||||||
|
{ server_capabilities = self.server_capabilities }
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
cb()
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
--- @private
|
||||||
|
--- 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
|
||||||
|
--- @return lsp.Handler|nil handler for the given method, if defined, or the default from |vim.lsp.handlers|
|
||||||
|
function Client:_resolve_handler(method)
|
||||||
|
return self.handlers[method] or lsp.handlers[method]
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Returns the buffer number for the given {bufnr}.
|
||||||
|
---
|
||||||
|
--- @param bufnr (integer|nil) Buffer number to resolve. Defaults to current buffer
|
||||||
|
--- @return integer bufnr
|
||||||
|
local function resolve_bufnr(bufnr)
|
||||||
|
vim.validate({ bufnr = { bufnr, 'n', true } })
|
||||||
|
if bufnr == nil or bufnr == 0 then
|
||||||
|
return api.nvim_get_current_buf()
|
||||||
|
end
|
||||||
|
return bufnr
|
||||||
|
end
|
||||||
|
|
||||||
|
--- @private
|
||||||
|
--- Sends a request to the server.
|
||||||
|
---
|
||||||
|
--- This is a thin wrapper around {client.rpc.request} with some additional
|
||||||
|
--- checks for capabilities and handler availability.
|
||||||
|
---
|
||||||
|
--- @param method string LSP method name.
|
||||||
|
--- @param params table|nil LSP request params.
|
||||||
|
--- @param handler lsp.Handler|nil Response |lsp-handler| for this method.
|
||||||
|
--- @param bufnr integer Buffer handle (0 for current).
|
||||||
|
--- @return boolean status, integer|nil request_id {status} is a bool indicating
|
||||||
|
--- whether the request was successful. If it is `false`, then it will
|
||||||
|
--- always be `false` (the client has shutdown). If it was
|
||||||
|
--- successful, then it will return {request_id} as the
|
||||||
|
--- second result. You can use this with `client.cancel_request(request_id)`
|
||||||
|
--- to cancel the-request.
|
||||||
|
--- @see |vim.lsp.buf_request_all()|
|
||||||
|
function Client:_request(method, params, handler, bufnr)
|
||||||
|
if not handler then
|
||||||
|
handler = assert(
|
||||||
|
self:_resolve_handler(method),
|
||||||
|
string.format('not found: %q request handler for client %q.', method, self.name)
|
||||||
|
)
|
||||||
|
end
|
||||||
|
-- Ensure pending didChange notifications are sent so that the server doesn't operate on a stale state
|
||||||
|
changetracking.flush(self, bufnr)
|
||||||
|
local version = lsp.util.buf_versions[bufnr]
|
||||||
|
bufnr = resolve_bufnr(bufnr)
|
||||||
|
if log.debug() then
|
||||||
|
log.debug(self._log_prefix, 'client.request', self.id, method, params, handler, bufnr)
|
||||||
|
end
|
||||||
|
local success, request_id = self.rpc.request(method, params, function(err, result)
|
||||||
|
local context = {
|
||||||
|
method = method,
|
||||||
|
client_id = self.id,
|
||||||
|
bufnr = bufnr,
|
||||||
|
params = params,
|
||||||
|
version = version,
|
||||||
|
}
|
||||||
|
handler(err, result, context)
|
||||||
|
end, function(request_id)
|
||||||
|
local request = self.requests[request_id]
|
||||||
|
request.type = 'complete'
|
||||||
|
api.nvim_exec_autocmds('LspRequest', {
|
||||||
|
buffer = api.nvim_buf_is_valid(bufnr) and bufnr or nil,
|
||||||
|
modeline = false,
|
||||||
|
data = { client_id = self.id, request_id = request_id, request = request },
|
||||||
|
})
|
||||||
|
self.requests[request_id] = nil
|
||||||
|
end)
|
||||||
|
|
||||||
|
if success and request_id then
|
||||||
|
local request = { type = 'pending', bufnr = bufnr, method = method }
|
||||||
|
self.requests[request_id] = request
|
||||||
|
api.nvim_exec_autocmds('LspRequest', {
|
||||||
|
buffer = bufnr,
|
||||||
|
modeline = false,
|
||||||
|
data = { client_id = self.id, request_id = request_id, request = request },
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
return success, request_id
|
||||||
|
end
|
||||||
|
|
||||||
|
-- TODO(lewis6991): duplicated from lsp.lua
|
||||||
|
local wait_result_reason = { [-1] = 'timeout', [-2] = 'interrupted', [-3] = 'error' }
|
||||||
|
|
||||||
|
-- TODO(lewis6991): duplicated from lsp.lua
|
||||||
|
--- Concatenates and writes a list of strings to the Vim error buffer.
|
||||||
|
---
|
||||||
|
---@param ... string List to write to the buffer
|
||||||
|
local function err_message(...)
|
||||||
|
api.nvim_err_writeln(table.concat(vim.tbl_flatten({ ... })))
|
||||||
|
api.nvim_command('redraw')
|
||||||
|
end
|
||||||
|
|
||||||
|
--- @private
|
||||||
|
--- Sends a request to the server and synchronously waits for the response.
|
||||||
|
---
|
||||||
|
--- This is a wrapper around {client.request}
|
||||||
|
---
|
||||||
|
--- @param method (string) LSP method name.
|
||||||
|
--- @param params (table) LSP request params.
|
||||||
|
--- @param timeout_ms (integer|nil) Maximum time in milliseconds to wait for
|
||||||
|
--- a result. Defaults to 1000
|
||||||
|
--- @param bufnr (integer) Buffer handle (0 for current).
|
||||||
|
--- @return {err: lsp.ResponseError|nil, result:any}|nil, string|nil err # a dictionary, where
|
||||||
|
--- `err` and `result` come from the |lsp-handler|.
|
||||||
|
--- On timeout, cancel or error, returns `(nil, err)` where `err` is a
|
||||||
|
--- string describing the failure reason. If the request was unsuccessful
|
||||||
|
--- returns `nil`.
|
||||||
|
--- @see |vim.lsp.buf_request_sync()|
|
||||||
|
function Client:_request_sync(method, params, timeout_ms, bufnr)
|
||||||
|
local request_result = nil
|
||||||
|
local function _sync_handler(err, result)
|
||||||
|
request_result = { err = err, result = result }
|
||||||
|
end
|
||||||
|
|
||||||
|
local success, request_id = self:_request(method, params, _sync_handler, bufnr)
|
||||||
|
if not success then
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
|
||||||
|
local wait_result, reason = vim.wait(timeout_ms or 1000, function()
|
||||||
|
return request_result ~= nil
|
||||||
|
end, 10)
|
||||||
|
|
||||||
|
if not wait_result then
|
||||||
|
if request_id then
|
||||||
|
self:_cancel_request(request_id)
|
||||||
|
end
|
||||||
|
return nil, wait_result_reason[reason]
|
||||||
|
end
|
||||||
|
return request_result
|
||||||
|
end
|
||||||
|
|
||||||
|
--- @private
|
||||||
|
--- Sends a notification to an LSP server.
|
||||||
|
---
|
||||||
|
--- @param method string LSP method name.
|
||||||
|
--- @param params table|nil LSP request params.
|
||||||
|
--- @return boolean status true if the notification was successful.
|
||||||
|
--- If it is false, then it will always be false
|
||||||
|
--- (the client has shutdown).
|
||||||
|
function Client:_notify(method, params)
|
||||||
|
if method ~= ms.textDocument_didChange then
|
||||||
|
changetracking.flush(self)
|
||||||
|
end
|
||||||
|
|
||||||
|
local client_active = self.rpc.notify(method, params)
|
||||||
|
|
||||||
|
if client_active then
|
||||||
|
vim.schedule(function()
|
||||||
|
api.nvim_exec_autocmds('LspNotify', {
|
||||||
|
modeline = false,
|
||||||
|
data = {
|
||||||
|
client_id = self.id,
|
||||||
|
method = method,
|
||||||
|
params = params,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
return client_active
|
||||||
|
end
|
||||||
|
|
||||||
|
--- @private
|
||||||
|
--- Cancels a request with a given request id.
|
||||||
|
---
|
||||||
|
--- @param id (integer) id of request to cancel
|
||||||
|
--- @return boolean status true if notification was successful. false otherwise
|
||||||
|
--- @see |vim.lsp.client.notify()|
|
||||||
|
function Client:_cancel_request(id)
|
||||||
|
vim.validate({ id = { id, 'n' } })
|
||||||
|
local request = self.requests[id]
|
||||||
|
if request and request.type == 'pending' then
|
||||||
|
request.type = 'cancel'
|
||||||
|
api.nvim_exec_autocmds('LspRequest', {
|
||||||
|
buffer = request.bufnr,
|
||||||
|
modeline = false,
|
||||||
|
data = { client_id = self.id, request_id = id, request = request },
|
||||||
|
})
|
||||||
|
end
|
||||||
|
return self.rpc.notify(ms.dollar_cancelRequest, { id = id })
|
||||||
|
end
|
||||||
|
|
||||||
|
--- @nodoc
|
||||||
|
--- Stops a client, optionally with force.
|
||||||
|
---
|
||||||
|
--- By default, it will just ask the - server to shutdown without force. If
|
||||||
|
--- you request to stop a client which has previously been requested to
|
||||||
|
--- shutdown, it will automatically escalate and force shutdown.
|
||||||
|
---
|
||||||
|
--- @param force boolean|nil
|
||||||
|
function Client:_stop(force)
|
||||||
|
local rpc = self.rpc
|
||||||
|
|
||||||
|
if rpc.is_closing() then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
if force or not self.initialized or self._graceful_shutdown_failed then
|
||||||
|
rpc.terminate()
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Sending a signal after a process has exited is acceptable.
|
||||||
|
rpc.request(ms.shutdown, nil, function(err, _)
|
||||||
|
if err == nil then
|
||||||
|
rpc.notify(ms.exit)
|
||||||
|
else
|
||||||
|
-- If there was an error in the shutdown request, then term to be safe.
|
||||||
|
rpc.terminate()
|
||||||
|
self._graceful_shutdown_failed = true
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
--- @private
|
||||||
|
--- Checks whether a client is stopped.
|
||||||
|
---
|
||||||
|
--- @return boolean # true if client is stopped or in the process of being
|
||||||
|
--- stopped; false otherwise
|
||||||
|
function Client:_is_stopped()
|
||||||
|
return self.rpc.is_closing()
|
||||||
|
end
|
||||||
|
|
||||||
|
--- @private
|
||||||
|
--- Execute a lsp command, either via client command function (if available)
|
||||||
|
--- or via workspace/executeCommand (if supported by the server)
|
||||||
|
---
|
||||||
|
--- @param command lsp.Command
|
||||||
|
--- @param context? {bufnr: integer}
|
||||||
|
--- @param handler? lsp.Handler only called if a server command
|
||||||
|
function Client:_exec_cmd(command, context, handler)
|
||||||
|
context = vim.deepcopy(context or {}, true) --[[@as lsp.HandlerContext]]
|
||||||
|
context.bufnr = context.bufnr or api.nvim_get_current_buf()
|
||||||
|
context.client_id = self.id
|
||||||
|
local cmdname = command.command
|
||||||
|
local fn = self.commands[cmdname] or lsp.commands[cmdname]
|
||||||
|
if fn then
|
||||||
|
fn(command, context)
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
local command_provider = self.server_capabilities.executeCommandProvider
|
||||||
|
local commands = type(command_provider) == 'table' and command_provider.commands or {}
|
||||||
|
if not vim.list_contains(commands, cmdname) then
|
||||||
|
vim.notify_once(
|
||||||
|
string.format(
|
||||||
|
'Language server `%s` does not support command `%s`. This command may require a client extension.',
|
||||||
|
self.name,
|
||||||
|
cmdname
|
||||||
|
),
|
||||||
|
vim.log.levels.WARN
|
||||||
|
)
|
||||||
|
return
|
||||||
|
end
|
||||||
|
-- Not using command directly to exclude extra properties,
|
||||||
|
-- see https://github.com/python-lsp/python-lsp-server/issues/146
|
||||||
|
local params = {
|
||||||
|
command = command.command,
|
||||||
|
arguments = command.arguments,
|
||||||
|
}
|
||||||
|
self.request(ms.workspace_executeCommand, params, handler, context.bufnr)
|
||||||
|
end
|
||||||
|
|
||||||
|
--- @package
|
||||||
|
--- Default handler for the 'textDocument/didOpen' LSP notification.
|
||||||
|
---
|
||||||
|
--- @param bufnr integer Number of the buffer, or 0 for current
|
||||||
|
function Client:_text_document_did_open_handler(bufnr)
|
||||||
|
changetracking.init(self, bufnr)
|
||||||
|
if not vim.tbl_get(self.server_capabilities, 'textDocumentSync', 'openClose') then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
if not api.nvim_buf_is_loaded(bufnr) then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
local filetype = vim.bo[bufnr].filetype
|
||||||
|
|
||||||
|
local params = {
|
||||||
|
textDocument = {
|
||||||
|
version = 0,
|
||||||
|
uri = vim.uri_from_bufnr(bufnr),
|
||||||
|
languageId = self.config.get_language_id(bufnr, filetype),
|
||||||
|
text = lsp._buf_get_full_text(bufnr),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
self.notify(ms.textDocument_didOpen, params)
|
||||||
|
lsp.util.buf_versions[bufnr] = params.textDocument.version
|
||||||
|
|
||||||
|
-- Next chance we get, we should re-do the diagnostics
|
||||||
|
vim.schedule(function()
|
||||||
|
-- Protect against a race where the buffer disappears
|
||||||
|
-- between `did_open_handler` and the scheduled function firing.
|
||||||
|
if api.nvim_buf_is_valid(bufnr) then
|
||||||
|
local namespace = vim.lsp.diagnostic.get_namespace(self.id)
|
||||||
|
vim.diagnostic.show(namespace, bufnr)
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
--- @private
|
||||||
|
--- Runs the on_attach function from the client's config if it was defined.
|
||||||
|
--- @param bufnr integer Buffer number
|
||||||
|
function Client:_on_attach(bufnr)
|
||||||
|
self:_text_document_did_open_handler(bufnr)
|
||||||
|
|
||||||
|
lsp._set_defaults(self, bufnr)
|
||||||
|
|
||||||
|
api.nvim_exec_autocmds('LspAttach', {
|
||||||
|
buffer = bufnr,
|
||||||
|
modeline = false,
|
||||||
|
data = { client_id = self.id },
|
||||||
|
})
|
||||||
|
|
||||||
|
if self.config.on_attach then
|
||||||
|
--- @type boolean, string?
|
||||||
|
local status, err = pcall(self.config.on_attach, self, bufnr)
|
||||||
|
if not status then
|
||||||
|
self:write_error(lsp.client_errors.ON_ATTACH_ERROR, err)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- schedule the initialization of semantic tokens to give the above
|
||||||
|
-- on_attach and LspAttach callbacks the ability to schedule wrap the
|
||||||
|
-- opt-out (deleting the semanticTokensProvider from capabilities)
|
||||||
|
vim.schedule(function()
|
||||||
|
if vim.tbl_get(self.server_capabilities, 'semanticTokensProvider', 'full') then
|
||||||
|
lsp.semantic_tokens.start(bufnr, self.id)
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
|
||||||
|
self.attached_buffers[bufnr] = true
|
||||||
|
end
|
||||||
|
|
||||||
|
--- @private
|
||||||
|
--- Logs the given error to the LSP log and to the error buffer.
|
||||||
|
--- @param code integer Error code
|
||||||
|
--- @param err any Error arguments
|
||||||
|
function Client:write_error(code, err)
|
||||||
|
if log.error() then
|
||||||
|
log.error(self._log_prefix, 'on_error', { code = lsp.client_errors[code], err = err })
|
||||||
|
end
|
||||||
|
err_message(self._log_prefix, ': Error ', lsp.client_errors[code], ': ', vim.inspect(err))
|
||||||
|
end
|
||||||
|
|
||||||
|
--- @param method string
|
||||||
|
--- @param opts? {bufnr: integer?}
|
||||||
|
function Client:_supports_method(method, opts)
|
||||||
|
opts = opts or {}
|
||||||
|
local required_capability = lsp._request_name_to_capability[method]
|
||||||
|
-- if we don't know about the method, assume that the client supports it.
|
||||||
|
if not required_capability then
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
if vim.tbl_get(self.server_capabilities, unpack(required_capability)) then
|
||||||
|
return true
|
||||||
|
else
|
||||||
|
if self.dynamic_capabilities:supports_registration(method) then
|
||||||
|
return self.dynamic_capabilities:supports(method, opts)
|
||||||
|
end
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return Client
|
@@ -48,7 +48,7 @@ local function execute_lens(lens, bufnr, client_id)
|
|||||||
|
|
||||||
local client = vim.lsp.get_client_by_id(client_id)
|
local client = vim.lsp.get_client_by_id(client_id)
|
||||||
assert(client, 'Client is required to execute lens, client_id=' .. client_id)
|
assert(client, 'Client is required to execute lens, client_id=' .. client_id)
|
||||||
client._exec_cmd(lens.command, { bufnr = bufnr }, function(...)
|
client:_exec_cmd(lens.command, { bufnr = bufnr }, function(...)
|
||||||
vim.lsp.handlers[ms.workspace_executeCommand](...)
|
vim.lsp.handlers[ms.workspace_executeCommand](...)
|
||||||
M.refresh()
|
M.refresh()
|
||||||
end)
|
end)
|
||||||
|
@@ -895,6 +895,7 @@ do
|
|||||||
---@field private _idx_read integer
|
---@field private _idx_read integer
|
||||||
---@field private _idx_write integer
|
---@field private _idx_write integer
|
||||||
---@field private _size integer
|
---@field private _size integer
|
||||||
|
---@overload fun(self): table?
|
||||||
local Ringbuf = {}
|
local Ringbuf = {}
|
||||||
|
|
||||||
--- Clear all items
|
--- Clear all items
|
||||||
|
Reference in New Issue
Block a user