aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--runtime/doc/lsp.txt9
-rw-r--r--runtime/lua/vim/lsp.lua475
-rw-r--r--runtime/lua/vim/lsp/client.lua262
-rw-r--r--runtime/lua/vim/lsp/rpc.lua17
-rw-r--r--test/functional/plugin/lsp_spec.lua24
5 files changed, 356 insertions, 431 deletions
diff --git a/runtime/doc/lsp.txt b/runtime/doc/lsp.txt
index 9bfe21054c..fc4c164ea0 100644
--- a/runtime/doc/lsp.txt
+++ b/runtime/doc/lsp.txt
@@ -932,7 +932,7 @@ start({config}, {opts}) *vim.lsp.start()*
`ftplugin/<filetype_name>.lua` (See |ftplugin-name|)
Parameters: ~
- • {config} (`table`) Same configuration as documented in
+ • {config} (`lsp.ClientConfig`) Same configuration as documented in
|vim.lsp.start_client()|
• {opts} (`lsp.StartOpts?`) Optional keyword arguments:
• reuse_client (fun(client: client, config: table): boolean)
@@ -2173,17 +2173,14 @@ rpc_response_error({code}, {message}, {data})
See also: ~
• lsp.ErrorCodes See `vim.lsp.protocol.ErrorCodes`
- *vim.lsp.rpc.start()*
-start({cmd}, {cmd_args}, {dispatchers}, {extra_spawn_params})
+start({cmd}, {dispatchers}, {extra_spawn_params}) *vim.lsp.rpc.start()*
Starts an LSP server process and create an LSP RPC client object to
interact with it. Communication with the spawned process happens via
stdio. For communication via TCP, spawn a process manually and use
|vim.lsp.rpc.connect()|
Parameters: ~
- • {cmd} (`string`) Command to start the LSP server.
- • {cmd_args} (`string[]`) List of additional string arguments
- to pass to {cmd}.
+ • {cmd} (`string[]`) Command to start the LSP server.
• {dispatchers} (`vim.lsp.rpc.Dispatchers?`) Dispatchers for LSP
message types. Valid dispatcher names are:
• `"notification"`
diff --git a/runtime/lua/vim/lsp.lua b/runtime/lua/vim/lsp.lua
index 7d8b7e50a3..13f2c92cc2 100644
--- a/runtime/lua/vim/lsp.lua
+++ b/runtime/lua/vim/lsp.lua
@@ -1,8 +1,4 @@
----@diagnostic disable: invisible
local api = vim.api
-local nvim_err_writeln, nvim_buf_get_lines, nvim_command, nvim_exec_autocmds =
- api.nvim_err_writeln, api.nvim_buf_get_lines, api.nvim_command, api.nvim_exec_autocmds
-local uv = vim.uv
local tbl_isempty, tbl_extend = vim.tbl_isempty, vim.tbl_extend
local validate = vim.validate
local if_nil = vim.F.if_nil
@@ -71,14 +67,6 @@ lsp._request_name_to_capability = {
-- TODO improve handling of scratch buffers with LSP attached.
---- Concatenates and writes a list of strings to the Vim error buffer.
----
----@param ... string List to write to the buffer
-local function err_message(...)
- nvim_err_writeln(table.concat(vim.tbl_flatten({ ... })))
- nvim_command('redraw')
-end
-
--- Returns the buffer number for the given {bufnr}.
---
---@param bufnr (integer|nil) Buffer number to resolve. Defaults to current buffer
@@ -104,30 +92,8 @@ function lsp._unsupported_method(method)
return msg
end
---- Checks whether a given path is a directory.
----
----@param filename (string) path to check
----@return boolean # true if {filename} exists and is a directory, false otherwise
-local function is_dir(filename)
- validate({ filename = { filename, 's' } })
- local stat = uv.fs_stat(filename)
- return stat and stat.type == 'directory' or false
-end
-
local wait_result_reason = { [-1] = 'timeout', [-2] = 'interrupted', [-3] = 'error' }
-local valid_encodings = {
- ['utf-8'] = 'utf-8',
- ['utf-16'] = 'utf-16',
- ['utf-32'] = 'utf-32',
- ['utf8'] = 'utf-8',
- ['utf16'] = 'utf-16',
- ['utf32'] = 'utf-32',
- UTF8 = 'utf-8',
- UTF16 = 'utf-16',
- UTF32 = 'utf-32',
-}
-
local format_line_ending = {
['unix'] = '\n',
['dos'] = '\r\n',
@@ -141,14 +107,6 @@ function lsp._buf_get_line_ending(bufnr)
return format_line_ending[vim.bo[bufnr].fileformat] or '\n'
end
-local client_index = 0
---- Returns a new, unused client id.
----
----@return integer client_id
-local function next_client_id()
- client_index = client_index + 1
- return client_index
-end
-- Tracks all clients created via lsp.start_client
local active_clients = {} --- @type table<integer,lsp.Client>
local all_buffer_active_clients = {} --- @type table<integer,table<integer,true>>
@@ -199,115 +157,6 @@ lsp.client_errors = tbl_extend(
})
)
---- Normalizes {encoding} to valid LSP encoding names.
----
----@param encoding (string) Encoding to normalize
----@return string # normalized encoding name
-local function validate_encoding(encoding)
- validate({
- encoding = { encoding, 's' },
- })
- return valid_encodings[encoding:lower()]
- or error(
- string.format(
- "Invalid offset encoding %q. Must be one of: 'utf-8', 'utf-16', 'utf-32'",
- encoding
- )
- )
-end
-
----@internal
---- Parses a command invocation into the command itself and its args. If there
---- are no arguments, an empty table is returned as the second argument.
----
----@param input string[]
----@return string command, string[] args #the command and arguments
-function lsp._cmd_parts(input)
- validate({
- cmd = {
- input,
- function()
- return vim.tbl_islist(input)
- end,
- 'list',
- },
- })
-
- local cmd = input[1]
- local cmd_args = {}
- -- Don't mutate our input.
- for i, v in ipairs(input) do
- validate({ ['cmd argument'] = { v, 's' } })
- if i > 1 then
- table.insert(cmd_args, v)
- end
- end
- return cmd, cmd_args
-end
-
---- Augments a validator function with support for optional (nil) values.
----
----@param fn (fun(v): boolean) The original validator function; should return a
----bool.
----@return fun(v): boolean # The augmented function. Also returns true if {v} is
----`nil`.
-local function optional_validator(fn)
- return function(v)
- return v == nil or fn(v)
- end
-end
-
---- Validates a client configuration as given to |vim.lsp.start_client()|.
----
----@param config (lsp.ClientConfig)
----@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)
- validate({
- config = { config, 't' },
- })
- validate({
- handlers = { config.handlers, 't', true },
- capabilities = { config.capabilities, 't', true },
- cmd_cwd = { config.cmd_cwd, optional_validator(is_dir), 'directory' },
- cmd_env = { config.cmd_env, 't', true },
- detached = { config.detached, 'b', true },
- name = { config.name, 's', true },
- on_error = { config.on_error, 'f', true },
- on_exit = { config.on_exit, 'f', true },
- on_init = { config.on_init, 'f', true },
- settings = { config.settings, 't', true },
- commands = { config.commands, 't', true },
- before_init = { config.before_init, 'f', true },
- offset_encoding = { config.offset_encoding, 's', true },
- flags = { config.flags, 't', true },
- get_language_id = { config.get_language_id, 'f', true },
- })
- assert(
- (
- not config.flags
- or not config.flags.debounce_text_changes
- or type(config.flags.debounce_text_changes) == 'number'
- ),
- 'flags.debounce_text_changes must be a number with the debounce time in milliseconds'
- )
-
- 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
- else
- cmd, cmd_args = lsp._cmd_parts(config_cmd)
- end
- local offset_encoding = valid_encodings.UTF16
- if config.offset_encoding then
- offset_encoding = validate_encoding(config.offset_encoding)
- end
-
- return cmd, cmd_args, offset_encoding
-end
-
---@private
--- Returns full text of buffer {bufnr} as a string.
---
@@ -315,7 +164,7 @@ end
---@return string # Buffer text as string.
function lsp._buf_get_full_text(bufnr)
local line_ending = lsp._buf_get_line_ending(bufnr)
- local text = table.concat(nvim_buf_get_lines(bufnr, 0, -1, true), line_ending)
+ local text = table.concat(api.nvim_buf_get_lines(bufnr, 0, -1, true), line_ending)
if vim.bo[bufnr].eol then
text = text .. line_ending
end
@@ -473,8 +322,8 @@ end
--- Either use |:au|, |nvim_create_autocmd()| or put the call in a
--- `ftplugin/<filetype_name>.lua` (See |ftplugin-name|)
---
----@param config table Same configuration as documented in |vim.lsp.start_client()|
----@param opts (nil|lsp.StartOpts) Optional keyword arguments:
+---@param config lsp.ClientConfig Same configuration as documented in |vim.lsp.start_client()|
+---@param opts lsp.StartOpts? Optional keyword arguments:
--- - reuse_client (fun(client: client, config: table): boolean)
--- Predicate used to decide if a client should be re-used.
--- Used on all running clients.
@@ -483,20 +332,16 @@ end
--- - bufnr (number)
--- Buffer handle to attach to if starting or re-using a
--- client (0 for current).
----@return integer|nil client_id
+---@return integer? client_id
function lsp.start(config, opts)
opts = opts or {}
local reuse_client = opts.reuse_client
or function(client, conf)
return client.config.root_dir == conf.root_dir and client.name == conf.name
end
- if not config.name and type(config.cmd) == 'table' then
- config.name = config.cmd[1] and vim.fs.basename(config.cmd[1]) or nil
- end
- local bufnr = opts.bufnr
- if bufnr == nil or bufnr == 0 then
- bufnr = api.nvim_get_current_buf()
- end
+
+ local bufnr = resolve_bufnr(opts.bufnr)
+
for _, clients in ipairs({ uninitialized_clients, lsp.get_clients() }) do
for _, client in pairs(clients) do
if reuse_client(client, config) then
@@ -505,10 +350,13 @@ function lsp.start(config, opts)
end
end
end
+
local client_id = lsp.start_client(config)
- if client_id == nil then
- return nil -- lsp.start_client will have printed an error
+
+ if not client_id then
+ return -- lsp.start_client will have printed an error
end
+
lsp.buf_attach_client(bufnr, client_id)
return client_id
end
@@ -599,29 +447,6 @@ function lsp._set_defaults(client, bufnr)
end
end
---- @class lsp.ClientConfig
---- @field cmd (string[]|fun(dispatchers: table):table)
---- @field cmd_cwd string
---- @field cmd_env (table)
---- @field detached boolean
---- @field workspace_folders (table)
---- @field capabilities lsp.ClientCapabilities
---- @field handlers table<string,function>
---- @field settings table
---- @field commands table
---- @field init_options table
---- @field name string
---- @field get_language_id fun(bufnr: integer, filetype: string): string
---- @field offset_encoding string
---- @field on_error fun(code: integer)
---- @field before_init function
---- @field on_init function
---- @field on_exit fun(code: integer, signal: integer, client_id: integer)
---- @field on_attach fun(client: lsp.Client, bufnr: integer)
---- @field trace 'off'|'messages'|'verbose'|nil
---- @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)
@@ -642,6 +467,90 @@ local function reset_defaults(bufnr)
end)
end
+--- @param client lsp.Client
+local function on_client_init(client)
+ local id = client.id
+ uninitialized_clients[id] = nil
+ -- Only assign after initialized.
+ active_clients[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[id] then
+ client.on_attach(bufnr)
+ end
+ end
+end
+
+--- @param code integer
+--- @param signal integer
+--- @param client_id integer
+local function on_client_exit(code, signal, client_id)
+ local client = active_clients[client_id] or uninitialized_clients[client_id]
+
+ for bufnr, client_ids in pairs(all_buffer_active_clients) do
+ if client_ids[client_id] then
+ vim.schedule(function()
+ if client and client.attached_buffers[bufnr] then
+ api.nvim_exec_autocmds('LspDetach', {
+ buffer = bufnr,
+ modeline = false,
+ data = { client_id = client_id },
+ })
+ end
+
+ local namespace = vim.lsp.diagnostic.get_namespace(client_id)
+ vim.diagnostic.reset(namespace, bufnr)
+
+ client_ids[client_id] = nil
+ if vim.tbl_isempty(client_ids) then
+ reset_defaults(bufnr)
+ end
+ end)
+ end
+ end
+
+ local name = client.name or 'unknown'
+
+ -- Schedule the deletion of the client object so that it exists in the execution of LspDetach
+ -- autocommands
+ vim.schedule(function()
+ active_clients[client_id] = nil
+ uninitialized_clients[client_id] = nil
+
+ -- Client can be absent if executable starts, but initialize fails
+ -- init/attach won't have happened
+ if client then
+ changetracking.reset(client)
+ end
+ if code ~= 0 or (signal ~= 0 and signal ~= 15) then
+ local msg = string.format(
+ 'Client %s quit with exit code %s and signal %s. Check log for errors: %s',
+ name,
+ code,
+ signal,
+ lsp.get_log_path()
+ )
+ vim.notify(msg, vim.log.levels.WARN)
+ end
+ end)
+end
+
+--- @generic F: function
+--- @param ... F
+--- @return F
+local function join_cbs(...)
+ local funcs = vim.F.pack_len(...)
+ return function(...)
+ for i = 1, funcs.n do
+ if funcs[i] ~= nil then
+ funcs[i](...)
+ end
+ 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
@@ -762,192 +671,22 @@ end
--- fully initialized. Use `on_init` to do any actions once
--- the client has been initialized.
function lsp.start_client(config)
- local cmd, cmd_args, offset_encoding = validate_client_config(config)
-
- config.flags = config.flags or {}
- config.settings = config.settings or {}
-
- -- By default, get_language_id just returns the exact filetype it is passed.
- -- It is possible to pass in something that will calculate a different filetype,
- -- to be sent by the client.
- config.get_language_id = config.get_language_id or function(_, filetype)
- return filetype
- end
-
- local client_id = next_client_id()
-
- local handlers = config.handlers or {}
- local name = config.name or tostring(client_id)
- local log_prefix = string.format('LSP[%s]', name)
-
- local dispatch = {}
-
- --- 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|
- local function resolve_handler(method)
- return handlers[method] or lsp.handlers[method]
- end
-
- ---@private
- --- Handles a notification sent by an LSP server by invoking the
- --- corresponding handler.
- ---
- ---@param method (string) LSP method name
- ---@param params (table) The parameters for that method.
- function dispatch.notification(method, params)
- log.trace('notification', method, params)
- local handler = resolve_handler(method)
- if handler then
- -- Method name is provided here for convenience.
- handler(nil, params, { method = method, client_id = client_id })
- end
- end
+ config = vim.deepcopy(config, false)
+ config.on_init = join_cbs(config.on_init, on_client_init)
+ config.on_exit = join_cbs(config.on_exit, on_client_exit)
- ---@private
- --- Handles a request from an LSP server by invoking the corresponding handler.
- ---
- ---@param method (string) LSP method name
- ---@param params (table) The parameters for that method
- ---@return any result
- ---@return lsp.ResponseError error code and message set in case an exception happens during the request.
- function dispatch.server_request(method, params)
- log.trace('server_request', method, params)
- local handler = resolve_handler(method)
- if handler then
- log.trace('server_request: found handler for', method)
- return handler(nil, params, { method = method, client_id = client_id })
- end
- log.warn('server_request: no handler found for', method)
- return nil, lsp.rpc_response_error(protocol.ErrorCodes.MethodNotFound)
- end
-
- --- Logs the given error to the LSP log and to the error buffer.
- --- @param code integer Error code
- --- @param err any Error arguments
- local function write_error(code, err)
- log.error(log_prefix, 'on_error', { code = lsp.client_errors[code], err = err })
- err_message(log_prefix, ': Error ', lsp.client_errors[code], ': ', vim.inspect(err))
- end
+ local client = require('vim.lsp.client').start(config)
- ---@private
- --- Invoked when the client operation throws an error.
- ---
- ---@param code (integer) Error code
- ---@param err any Other arguments may be passed depending on the error kind
- ---@see vim.lsp.rpc.client_errors for possible errors. Use
- ---`vim.lsp.rpc.client_errors[code]` to get a human-friendly name.
- function dispatch.on_error(code, err)
- write_error(code, err)
- if config.on_error then
- local status, usererr = pcall(config.on_error, code, err)
- if not status then
- log.error(log_prefix, 'user on_error failed', { err = usererr })
- err_message(log_prefix, ' user on_error failed: ', tostring(usererr))
- end
- end
- end
-
- ---@private
- --- Invoked on client exit.
- ---
- ---@param code (integer) exit code of the process
- ---@param signal (integer) the signal used to terminate (if any)
- function dispatch.on_exit(code, signal)
- if config.on_exit then
- pcall(config.on_exit, code, signal, client_id)
- end
-
- local client = active_clients[client_id] and active_clients[client_id]
- or uninitialized_clients[client_id]
-
- for bufnr, client_ids in pairs(all_buffer_active_clients) do
- if client_ids[client_id] then
- vim.schedule(function()
- if client and client.attached_buffers[bufnr] then
- nvim_exec_autocmds('LspDetach', {
- buffer = bufnr,
- modeline = false,
- data = { client_id = client_id },
- })
- end
-
- local namespace = vim.lsp.diagnostic.get_namespace(client_id)
- vim.diagnostic.reset(namespace, bufnr)
-
- client_ids[client_id] = nil
- if vim.tbl_isempty(client_ids) then
- reset_defaults(bufnr)
- end
- end)
- end
- end
-
- -- Schedule the deletion of the client object so that it exists in the execution of LspDetach
- -- autocommands
- vim.schedule(function()
- active_clients[client_id] = nil
- uninitialized_clients[client_id] = nil
-
- -- Client can be absent if executable starts, but initialize fails
- -- init/attach won't have happened
- if client then
- changetracking.reset(client)
- end
- if code ~= 0 or (signal ~= 0 and signal ~= 15) then
- local msg = string.format(
- 'Client %s quit with exit code %s and signal %s. Check log for errors: %s',
- name,
- code,
- signal,
- lsp.get_log_path()
- )
- vim.notify(msg, vim.log.levels.WARN)
- end
- end)
- end
-
- -- Start the RPC client.
- local rpc --- @type vim.lsp.rpc.PublicClient?
- if type(cmd) == 'function' then
- rpc = cmd(dispatch)
- else
- rpc = lsp.rpc.start(cmd, cmd_args, dispatch, {
- cwd = config.cmd_cwd,
- env = config.cmd_env,
- detached = config.detached,
- })
- end
-
- -- Return nil if client fails to start
- if not rpc then
+ if not client then
return
end
- 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
+ -- TODO(lewis6991): do this on before_init(). Requires API change to before_init() so it
+ -- can access the client_id.
+ uninitialized_clients[client.id] = client
- 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
- end)
-
- return client_id
+ return client.id
end
--- Notify all attached clients that a buffer has changed.
@@ -1149,7 +888,7 @@ function lsp.buf_detach_client(bufnr, client_id)
return
end
- nvim_exec_autocmds('LspDetach', {
+ api.nvim_exec_autocmds('LspDetach', {
buffer = bufnr,
modeline = false,
data = { client_id = client_id },
@@ -1372,7 +1111,7 @@ function lsp.buf_request(bufnr, method, params, handler)
-- if has client but no clients support the given method, notify the user
if next(clients) and not method_supported then
vim.notify(lsp._unsupported_method(method), vim.log.levels.ERROR)
- nvim_command('redraw')
+ vim.cmd.redraw()
return {}, function() end
end
diff --git a/runtime/lua/vim/lsp/client.lua b/runtime/lua/vim/lsp/client.lua
index 7bf83f4d2c..58db4387b6 100644
--- a/runtime/lua/vim/lsp/client.lua
+++ b/runtime/lua/vim/lsp/client.lua
@@ -4,6 +4,30 @@ local lsp = vim.lsp
local log = lsp.log
local ms = lsp.protocol.Methods
local changetracking = lsp._changetracking
+local validate = vim.validate
+
+--- @class lsp.ClientConfig
+--- @field cmd (string[]|fun(dispatchers: table):table)
+--- @field cmd_cwd string
+--- @field cmd_env (table)
+--- @field detached boolean
+--- @field workspace_folders (table)
+--- @field capabilities lsp.ClientCapabilities
+--- @field handlers table<string,function>
+--- @field settings table
+--- @field commands table
+--- @field init_options table
+--- @field name? string
+--- @field get_language_id fun(bufnr: integer, filetype: string): string
+--- @field offset_encoding string
+--- @field on_error fun(code: integer)
+--- @field before_init fun(params: lsp.InitializeParams, config: lsp.ClientConfig)
+--- @field on_init fun(client: lsp.Client, initialize_result: lsp.InitializeResult)
+--- @field on_exit fun(code: integer, signal: integer, client_id: integer)
+--- @field on_attach fun(client: lsp.Client, bufnr: integer)
+--- @field trace 'off'|'messages'|'verbose'|nil
+--- @field flags table
+--- @field root_dir string
--- @class lsp.Client.Progress: vim.Ringbuf<{token: integer|string, value: any}>
--- @field pending table<lsp.ProgressToken,lsp.LSPAny>
@@ -51,7 +75,6 @@ local changetracking = lsp._changetracking
--- @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
@@ -119,27 +142,131 @@ local function method_wrapper(cls, meth)
end
end
+local client_index = 0
+
+--- Checks whether a given path is a directory.
+--- @param filename (string) path to check
+--- @return boolean # true if {filename} exists and is a directory, false otherwise
+local function is_dir(filename)
+ validate({ filename = { filename, 's' } })
+ local stat = uv.fs_stat(filename)
+ return stat and stat.type == 'directory' or false
+end
+
+local valid_encodings = {
+ ['utf-8'] = 'utf-8',
+ ['utf-16'] = 'utf-16',
+ ['utf-32'] = 'utf-32',
+ ['utf8'] = 'utf-8',
+ ['utf16'] = 'utf-16',
+ ['utf32'] = 'utf-32',
+ UTF8 = 'utf-8',
+ UTF16 = 'utf-16',
+ UTF32 = 'utf-32',
+}
+
+--- Normalizes {encoding} to valid LSP encoding names.
+--- @param encoding string? Encoding to normalize
+--- @return string # normalized encoding name
+local function validate_encoding(encoding)
+ validate({
+ encoding = { encoding, 's', true },
+ })
+ if not encoding then
+ return valid_encodings.UTF16
+ end
+ return valid_encodings[encoding:lower()]
+ or error(
+ string.format(
+ "Invalid offset encoding %q. Must be one of: 'utf-8', 'utf-16', 'utf-32'",
+ encoding
+ )
+ )
+end
+
+--- Augments a validator function with support for optional (nil) values.
+--- @param fn (fun(v): boolean) The original validator function; should return a
+--- bool.
+--- @return fun(v): boolean # The augmented function. Also returns true if {v} is
+--- `nil`.
+local function optional_validator(fn)
+ return function(v)
+ return v == nil or fn(v)
+ end
+end
+
+--- Validates a client configuration as given to |vim.lsp.start_client()|.
+--- @param config lsp.ClientConfig
+local function process_client_config(config)
+ validate({
+ config = { config, 't' },
+ })
+ validate({
+ handlers = { config.handlers, 't', true },
+ capabilities = { config.capabilities, 't', true },
+ cmd_cwd = { config.cmd_cwd, optional_validator(is_dir), 'directory' },
+ cmd_env = { config.cmd_env, 't', true },
+ detached = { config.detached, 'b', true },
+ name = { config.name, 's', true },
+ on_error = { config.on_error, 'f', true },
+ on_exit = { config.on_exit, 'f', true },
+ on_init = { config.on_init, 'f', true },
+ settings = { config.settings, 't', true },
+ commands = { config.commands, 't', true },
+ before_init = { config.before_init, 'f', true },
+ offset_encoding = { config.offset_encoding, 's', true },
+ flags = { config.flags, 't', true },
+ get_language_id = { config.get_language_id, 'f', true },
+ })
+ assert(
+ (
+ not config.flags
+ or not config.flags.debounce_text_changes
+ or type(config.flags.debounce_text_changes) == 'number'
+ ),
+ 'flags.debounce_text_changes must be a number with the debounce time in milliseconds'
+ )
+
+ if not config.name and type(config.cmd) == 'table' then
+ config.name = config.cmd[1] and vim.fs.basename(config.cmd[1]) or nil
+ end
+
+ config.offset_encoding = validate_encoding(config.offset_encoding)
+ config.flags = config.flags or {}
+ config.settings = config.settings or {}
+ config.handlers = config.handlers or {}
+
+ -- By default, get_language_id just returns the exact filetype it is passed.
+ -- It is possible to pass in something that will calculate a different filetype,
+ -- to be sent by the client.
+ config.get_language_id = config.get_language_id or function(_, filetype)
+ return filetype
+ end
+
+ config.capabilities = config.capabilities or lsp.protocol.make_client_capabilities()
+ config.commands = config.commands or {}
+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)
+--- @return lsp.Client?
+function Client.start(config)
+ process_client_config(config)
+
+ client_index = client_index + 1
+ local id = client_index
+
local name = config.name or tostring(id)
--- @class lsp.Client
local self = {
id = id,
config = config,
- handlers = handlers,
- rpc = rpc,
- offset_encoding = offset_encoding,
+ handlers = config.handlers,
+ offset_encoding = config.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),
@@ -165,15 +292,46 @@ function Client.new(id, rpc, handlers, offset_encoding, config)
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
+ --- @type table<string|integer, string> title of unfinished progress sequences by token
self.progress.pending = {}
- return setmetatable(self, Client)
+ --- @type vim.lsp.rpc.Dispatchers
+ local dispatchers = {
+ notification = method_wrapper(self, Client._notification),
+ server_request = method_wrapper(self, Client._server_request),
+ on_error = method_wrapper(self, Client._on_error),
+ on_exit = method_wrapper(self, Client._on_exit),
+ }
+
+ -- Start the RPC client.
+ local rpc --- @type vim.lsp.rpc.PublicClient?
+ local config_cmd = config.cmd
+ if type(config_cmd) == 'function' then
+ rpc = config_cmd(dispatchers)
+ else
+ rpc = lsp.rpc.start(config_cmd, dispatchers, {
+ cwd = config.cmd_cwd,
+ env = config.cmd_env,
+ detached = config.detached,
+ })
+ end
+
+ -- Return nil if the rpc client fails to start
+ if not rpc then
+ return
+ end
+
+ self.rpc = rpc
+
+ setmetatable(self, Client)
+
+ self:initialize()
+
+ return self
end
--- @private
---- @param cb fun()
-function Client:initialize(cb)
+function Client:initialize()
local valid_traces = {
off = 'off',
messages = 'messages',
@@ -282,8 +440,6 @@ function Client:initialize(cb)
'server_capabilities',
{ server_capabilities = self.server_capabilities }
)
-
- cb()
end)
end
@@ -302,7 +458,7 @@ end
--- @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 } })
+ validate({ bufnr = { bufnr, 'n', true } })
if bufnr == nil or bufnr == 0 then
return api.nvim_get_current_buf()
end
@@ -374,10 +530,9 @@ 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
+--- @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')
@@ -461,7 +616,7 @@ end
--- @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' } })
+ validate({ id = { id, 'n' } })
local request = self.requests[id]
if request and request.type == 'pending' then
request.type = 'cancel'
@@ -527,7 +682,7 @@ function Client:_exec_cmd(command, context, handler)
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]
+ local fn = self.config.commands[cmdname] or lsp.commands[cmdname]
if fn then
fn(command, context)
return
@@ -654,4 +809,67 @@ function Client:_supports_method(method, opts)
end
end
+--- @private
+--- Handles a notification sent by an LSP server by invoking the
+--- corresponding handler.
+---
+--- @param method string LSP method name
+--- @param params table The parameters for that method.
+function Client:_notification(method, params)
+ log.trace('notification', method, params)
+ local handler = self:_resolve_handler(method)
+ if handler then
+ -- Method name is provided here for convenience.
+ handler(nil, params, { method = method, client_id = self.id })
+ end
+end
+
+--- @private
+--- Handles a request from an LSP server by invoking the corresponding handler.
+---
+--- @param method (string) LSP method name
+--- @param params (table) The parameters for that method
+--- @return any result
+--- @return lsp.ResponseError error code and message set in case an exception happens during the request.
+function Client:_server_request(method, params)
+ log.trace('server_request', method, params)
+ local handler = self:_resolve_handler(method)
+ if handler then
+ log.trace('server_request: found handler for', method)
+ return handler(nil, params, { method = method, client_id = self.id })
+ end
+ log.warn('server_request: no handler found for', method)
+ return nil, lsp.rpc_response_error(lsp.protocol.ErrorCodes.MethodNotFound)
+end
+
+--- @private
+--- Invoked when the client operation throws an error.
+---
+--- @param code integer Error code
+--- @param err any Other arguments may be passed depending on the error kind
+--- @see vim.lsp.rpc.client_errors for possible errors. Use
+--- `vim.lsp.rpc.client_errors[code]` to get a human-friendly name.
+function Client:_on_error(code, err)
+ self:write_error(code, err)
+ if self.config.on_error then
+ --- @type boolean, string
+ local status, usererr = pcall(self.config.on_error, code, err)
+ if not status then
+ log.error(self._log_prefix, 'user on_error failed', { err = usererr })
+ err_message(self._log_prefix, ' user on_error failed: ', tostring(usererr))
+ end
+ end
+end
+
+--- @private
+--- Invoked on client exit.
+---
+--- @param code integer) exit code of the process
+--- @param signal integer the signal used to terminate (if any)
+function Client:_on_exit(code, signal)
+ if self.config.on_exit then
+ pcall(self.config.on_exit, code, signal, self.id)
+ end
+end
+
return Client
diff --git a/runtime/lua/vim/lsp/rpc.lua b/runtime/lua/vim/lsp/rpc.lua
index 23f70826e5..e849bb4f2a 100644
--- a/runtime/lua/vim/lsp/rpc.lua
+++ b/runtime/lua/vim/lsp/rpc.lua
@@ -732,8 +732,7 @@ end
--- interact with it. Communication with the spawned process happens via stdio. For
--- communication via TCP, spawn a process manually and use |vim.lsp.rpc.connect()|
---
----@param cmd string Command to start the LSP server.
----@param cmd_args string[] List of additional string arguments to pass to {cmd}.
+---@param cmd string[] Command to start the LSP server.
---
---@param dispatchers? vim.lsp.rpc.Dispatchers Dispatchers for LSP message types.
--- Valid dispatcher names are:
@@ -754,12 +753,11 @@ end
--- - `request()` |vim.lsp.rpc.request()|
--- - `is_closing()` returns a boolean indicating if the RPC is closing.
--- - `terminate()` terminates the RPC client.
-function M.start(cmd, cmd_args, dispatchers, extra_spawn_params)
- log.info('Starting RPC client', { cmd = cmd, args = cmd_args, extra = extra_spawn_params })
+function M.start(cmd, dispatchers, extra_spawn_params)
+ log.info('Starting RPC client', { cmd = cmd, extra = extra_spawn_params })
validate({
- cmd = { cmd, 's' },
- cmd_args = { cmd_args, 't' },
+ cmd = { cmd, 't' },
dispatchers = { dispatchers, 't', true },
})
@@ -795,7 +793,7 @@ function M.start(cmd, cmd_args, dispatchers, extra_spawn_params)
local stderr_handler = function(_, chunk)
if chunk then
- log.error('rpc', cmd, 'stderr', chunk)
+ log.error('rpc', cmd[1], 'stderr', chunk)
end
end
@@ -804,10 +802,7 @@ function M.start(cmd, cmd_args, dispatchers, extra_spawn_params)
detached = extra_spawn_params.detached
end
- local cmd1 = { cmd }
- vim.list_extend(cmd1, cmd_args)
-
- local ok, sysobj_or_err = pcall(vim.system, cmd1, {
+ local ok, sysobj_or_err = pcall(vim.system, cmd, {
stdin = true,
stdout = stdout_handler,
stderr = stderr_handler,
diff --git a/test/functional/plugin/lsp_spec.lua b/test/functional/plugin/lsp_spec.lua
index ce76861b9a..fb153b83ca 100644
--- a/test/functional/plugin/lsp_spec.lua
+++ b/test/functional/plugin/lsp_spec.lua
@@ -139,30 +139,6 @@ describe('LSP', function()
end)
end)
end)
-
- describe('lsp._cmd_parts test', function()
- local function _cmd_parts(input)
- return exec_lua(
- [[
- lsp = require('vim.lsp')
- return lsp._cmd_parts(...)
- ]],
- input
- )
- end
- it('should valid cmd argument', function()
- eq(true, pcall(_cmd_parts, { 'nvim' }))
- eq(true, pcall(_cmd_parts, { 'nvim', '--head' }))
- end)
-
- it('should invalid cmd argument', function()
- eq('.../lsp.lua:0: cmd: expected list, got nvim', pcall_err(_cmd_parts, 'nvim'))
- eq(
- '.../lsp.lua:0: cmd argument: expected string, got number',
- pcall_err(_cmd_parts, { 'nvim', 1 })
- )
- end)
- end)
end)
describe('LSP', function()