aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--runtime/lua/vim/lsp.lua543
-rw-r--r--runtime/lua/vim/lsp/_dynamic.lua5
-rw-r--r--runtime/lua/vim/lsp/buf.lua2
-rw-r--r--runtime/lua/vim/lsp/client.lua663
-rw-r--r--runtime/lua/vim/lsp/codelens.lua2
-rw-r--r--runtime/lua/vim/shared.lua1
6 files changed, 709 insertions, 507 deletions
diff --git a/runtime/lua/vim/lsp.lua b/runtime/lua/vim/lsp.lua
index d8d47a8464..dc50ab0267 100644
--- a/runtime/lua/vim/lsp.lua
+++ b/runtime/lua/vim/lsp.lua
@@ -15,6 +15,7 @@ local lsp = vim._defer_require('vim.lsp', {
_tagfunc = ..., --- @module 'vim.lsp._tagfunc'
_watchfiles = ..., --- @module 'vim.lsp._watchfiles'
buf = ..., --- @module 'vim.lsp.buf'
+ client = ..., --- @module 'vim.lsp.client'
codelens = ..., --- @module 'vim.lsp.codelens'
diagnostic = ..., --- @module 'vim.lsp.diagnostic'
handlers = ..., --- @module 'vim.lsp.handlers'
@@ -259,7 +260,7 @@ end
--- Validates a client configuration as given to |vim.lsp.start_client()|.
---
---@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 Encoding.
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'
)
- 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
if type(config_cmd) == 'function' then
cmd = config_cmd
@@ -341,42 +342,6 @@ local function once(fn)
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
--
--- LSP client object. You can get an active client object via
@@ -556,7 +521,9 @@ function lsp.status()
local percentage = nil
local messages = {} --- @type string[]
for _, client in ipairs(vim.lsp.get_clients()) do
+ --- @diagnostic disable-next-line:no-unknown
for progress in client.progress do
+ --- @cast progress {token: lsp.ProgressToken, value: lsp.LSPAny}
local value = progress.value
if type(value) == 'table' and value.kind then
local message = value.message and (value.title .. ': ' .. value.message) or value.title
@@ -655,6 +622,26 @@ end
--- @field flags table
--- @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
-- documented twice: Here, and on the methods themselves (e.g.
-- `client.request()`). This is a workaround for the vimdoc generator script
@@ -875,26 +862,6 @@ function lsp.start_client(config)
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
--- Invoked on client exit.
---
@@ -971,456 +938,26 @@ function lsp.start_client(config)
return
end
- ---@class lsp.Client
- local client = {
- id = client_id,
- name = name,
- rpc = rpc,
- offset_encoding = offset_encoding,
- config = config,
- attached_buffers = {}, --- @type table<integer,true>
-
- handlers = handlers,
- --- @type table<string,function>
- commands = config.commands or {},
+ config.capabilities = config.capabilities or protocol.make_client_capabilities()
- --- @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()
+ local client = require('vim.lsp.client').new(client_id, rpc, handlers, offset_encoding, config)
-- Store the uninitialized_clients for cleanup in case we exit before initialize finishes.
uninitialized_clients[client_id] = client
- local function initialize()
- local valid_traces = {
- off = 'off',
- messages = 'messages',
- verbose = 'verbose',
- }
-
- 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
- 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
-
- --- @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)
+ client:initialize(function()
+ uninitialized_clients[client_id] = nil
+ -- 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
-
- -- 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()
+ end)
return client_id
end
@@ -1564,7 +1101,7 @@ function lsp.buf_attach_client(bufnr, client_id)
if vim.tbl_get(client.server_capabilities, 'textDocumentSync', 'openClose') then
client.notify(ms.textDocument_didClose, params)
end
- text_document_did_open_handler(bufnr, client)
+ client:_text_document_did_open_handler(bufnr)
end
end,
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
-- then it will send didOpen on initialize.
if client then
- client._on_attach(bufnr)
+ client:_on_attach(bufnr)
end
return true
end
diff --git a/runtime/lua/vim/lsp/_dynamic.lua b/runtime/lua/vim/lsp/_dynamic.lua
index 3c9dee2c69..8b8f3bdc38 100644
--- a/runtime/lua/vim/lsp/_dynamic.lua
+++ b/runtime/lua/vim/lsp/_dynamic.lua
@@ -6,6 +6,7 @@ local glob = vim.glob
local M = {}
--- @param client_id number
+--- @return lsp.DynamicCapabilities
function M.new(client_id)
return setmetatable({
capabilities = {},
@@ -37,7 +38,7 @@ function M:register(registrations)
end
--- @param unregisterations lsp.Unregistration[]
---- @private
+--- @package
function M:unregister(unregisterations)
for _, unreg in ipairs(unregisterations) do
local method = unreg.method
@@ -77,7 +78,7 @@ end
--- @param method string
--- @param opts? {bufnr: integer?}
---- @private
+--- @package
function M:supports(method, opts)
return self:get(method, opts) ~= nil
end
diff --git a/runtime/lua/vim/lsp/buf.lua b/runtime/lua/vim/lsp/buf.lua
index d67b2ac8ea..7fc5286a78 100644
--- a/runtime/lua/vim/lsp/buf.lua
+++ b/runtime/lua/vim/lsp/buf.lua
@@ -652,7 +652,7 @@ local function on_code_action_results(results, opts)
end
if action.command then
local command = type(action.command) == 'table' and action.command or action
- client._exec_cmd(command, ctx)
+ client:_exec_cmd(command, ctx)
end
end
diff --git a/runtime/lua/vim/lsp/client.lua b/runtime/lua/vim/lsp/client.lua
new file mode 100644
index 0000000000..2daf564f63
--- /dev/null
+++ b/runtime/lua/vim/lsp/client.lua
@@ -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
diff --git a/runtime/lua/vim/lsp/codelens.lua b/runtime/lua/vim/lsp/codelens.lua
index 199da288f4..a045a6bad4 100644
--- a/runtime/lua/vim/lsp/codelens.lua
+++ b/runtime/lua/vim/lsp/codelens.lua
@@ -48,7 +48,7 @@ local function execute_lens(lens, bufnr, client_id)
local client = vim.lsp.get_client_by_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](...)
M.refresh()
end)
diff --git a/runtime/lua/vim/shared.lua b/runtime/lua/vim/shared.lua
index fa7690e41e..dd0f7c2e1e 100644
--- a/runtime/lua/vim/shared.lua
+++ b/runtime/lua/vim/shared.lua
@@ -895,6 +895,7 @@ do
---@field private _idx_read integer
---@field private _idx_write integer
---@field private _size integer
+ ---@overload fun(self): table?
local Ringbuf = {}
--- Clear all items