fix(lsp): add "silent" option to vim.lsp.start (#28478)

vim.notify cannot be suppressed and it is not always necessary to
display a visible warning to the user if the RPC process fails to start.
For instance, a user may have the same LSP configuration across systems,
some of which may not have all of the LSP server executables installed.
In that case, the user receives a notification every time a file is
opened that they cannot suppress.

Instead of using vim.notify in vim.lsp.rpc, propagate a normal error up
through the call stack and use vim.notify in vim.lsp.start() only if
the "silent" option is not set.

This also updates lsp.start_client() to return an error message as its
second return value if an error occurred, rather than calling vim.notify
directly. Callers of lsp.start_client() will need to update call sites
appropriately if they wish to report errors to the user (or even better,
switch to vim.lsp.start).
This commit is contained in:
Gregory Anders
2024-04-26 08:15:44 -05:00
committed by GitHub
parent 567f8a300b
commit 37d8e50459
5 changed files with 42 additions and 35 deletions

View File

@@ -869,12 +869,14 @@ start({config}, {opts}) *vim.lsp.start()*
|vim.lsp.ClientConfig|. |vim.lsp.ClientConfig|.
• {opts} (`table?`) Optional keyword arguments • {opts} (`table?`) Optional keyword arguments
• {reuse_client} • {reuse_client}
(`fun(client: vim.lsp.Client, config: table): boolean`) (`fun(client: vim.lsp.Client, config: vim.lsp.ClientConfig): boolean`)
Predicate used to decide if a client should be re-used. Predicate used to decide if a client should be re-used.
Used on all running clients. The default implementation Used on all running clients. The default implementation
re-uses a client if name and root_dir matches. re-uses a client if name and root_dir matches.
• {bufnr} (`integer`) Buffer handle to attach to if starting • {bufnr} (`integer`) Buffer handle to attach to if starting
or re-using a client (0 for current). or re-using a client (0 for current).
• {silent} (`boolean`) Suppress error reporting if the LSP
server fails to start (default false).
Return: ~ Return: ~
(`integer?`) client_id (`integer?`) client_id
@@ -886,10 +888,11 @@ start_client({config}) *vim.lsp.start_client()*
• {config} (`vim.lsp.ClientConfig`) Configuration for the server. See • {config} (`vim.lsp.ClientConfig`) Configuration for the server. See
|vim.lsp.ClientConfig|. |vim.lsp.ClientConfig|.
Return: ~ Return (multiple): ~
(`integer?`) client_id |vim.lsp.get_client_by_id()| Note: client may (`integer?`) client_id |vim.lsp.get_client_by_id()| Note: client may
not be fully initialized. Use `on_init` to do any actions once the not be fully initialized. Use `on_init` to do any actions once the
client has been initialized. client has been initialized.
(`string?`) Error message, if any
status() *vim.lsp.status()* status() *vim.lsp.status()*
Consumes the latest progress messages from all clients and formats them as Consumes the latest progress messages from all clients and formats them as
@@ -1073,7 +1076,7 @@ Lua module: vim.lsp.client *lsp-client*
*vim.lsp.ClientConfig* *vim.lsp.ClientConfig*
Fields: ~ Fields: ~
• {cmd} (`string[]|fun(dispatchers: vim.lsp.rpc.Dispatchers): vim.lsp.rpc.PublicClient?`) • {cmd} (`string[]|fun(dispatchers: vim.lsp.rpc.Dispatchers): vim.lsp.rpc.PublicClient`)
command string[] that launches the language command string[] that launches the language
server (treated as in |jobstart()|, must be server (treated as in |jobstart()|, must be
absolute or on `$PATH`, shell constructs like absolute or on `$PATH`, shell constructs like
@@ -2340,7 +2343,7 @@ start({cmd}, {dispatchers}, {extra_spawn_params}) *vim.lsp.rpc.start()*
See |vim.system()| See |vim.system()|
Return: ~ Return: ~
(`vim.lsp.rpc.PublicClient?`) Client RPC object, with these methods: (`vim.lsp.rpc.PublicClient`) Client RPC object, with these methods:
• `notify()` |vim.lsp.rpc.notify()| • `notify()` |vim.lsp.rpc.notify()|
• `request()` |vim.lsp.rpc.request()| • `request()` |vim.lsp.rpc.request()|
• `is_closing()` returns a boolean indicating if the RPC is closing. • `is_closing()` returns a boolean indicating if the RPC is closing.

View File

@@ -243,6 +243,8 @@ The following new APIs and features were added.
(e.g. `commitCharacters`). Note that this might affect plugins and (e.g. `commitCharacters`). Note that this might affect plugins and
language servers that don't support the feature, and in such cases the language servers that don't support the feature, and in such cases the
respective capability can be unset. respective capability can be unset.
• |vim.lsp.start()| accepts a "silent" option for suppressing messages
if an LSP server failed to start.
• Treesitter • Treesitter
• Bundled parsers and queries (highlight, folds) for Markdown, Python, and • Bundled parsers and queries (highlight, folds) for Markdown, Python, and

View File

@@ -195,10 +195,13 @@ end
--- Predicate used to decide if a client should be re-used. Used on all --- Predicate used to decide if a client should be re-used. Used on all
--- running clients. The default implementation re-uses a client if name and --- running clients. The default implementation re-uses a client if name and
--- root_dir matches. --- root_dir matches.
--- @field reuse_client fun(client: vim.lsp.Client, config: table): boolean --- @field reuse_client fun(client: vim.lsp.Client, config: vim.lsp.ClientConfig): boolean
--- ---
--- Buffer handle to attach to if starting or re-using a client (0 for current). --- Buffer handle to attach to if starting or re-using a client (0 for current).
--- @field bufnr integer --- @field bufnr integer
---
--- Suppress error reporting if the LSP server fails to start (default false).
--- @field silent boolean
--- Create a new LSP client and start a language server or reuses an already --- Create a new LSP client and start a language server or reuses an already
--- running client if one is found matching `name` and `root_dir`. --- running client if one is found matching `name` and `root_dir`.
@@ -246,19 +249,25 @@ function lsp.start(config, opts)
for _, client in pairs(all_clients) do for _, client in pairs(all_clients) do
if reuse_client(client, config) then if reuse_client(client, config) then
lsp.buf_attach_client(bufnr, client.id) if lsp.buf_attach_client(bufnr, client.id) then
return client.id return client.id
end
end end
end end
local client_id = lsp.start_client(config) local client_id, err = lsp.start_client(config)
if err then
if not client_id then if not opts.silent then
return -- lsp.start_client will have printed an error vim.notify(err, vim.log.levels.WARN)
end
return nil
end end
lsp.buf_attach_client(bufnr, client_id) if client_id and lsp.buf_attach_client(bufnr, client_id) then
return client_id return client_id
end
return nil
end end
--- Consumes the latest progress messages from all clients and formats them as a string. --- Consumes the latest progress messages from all clients and formats them as a string.
@@ -420,16 +429,18 @@ end
--- Starts and initializes a client with the given configuration. --- Starts and initializes a client with the given configuration.
--- @param config vim.lsp.ClientConfig Configuration for the server. --- @param config vim.lsp.ClientConfig Configuration for the server.
--- @return integer|nil client_id |vim.lsp.get_client_by_id()| Note: client may not be --- @return integer? client_id |vim.lsp.get_client_by_id()| Note: client may not be
--- fully initialized. Use `on_init` to do any actions once --- fully initialized. Use `on_init` to do any actions once
--- the client has been initialized. --- the client has been initialized.
--- @return string? # Error message, if any
function lsp.start_client(config) function lsp.start_client(config)
local client = require('vim.lsp.client').create(config) local ok, res = pcall(require('vim.lsp.client').create, config)
if not ok then
if not client then return nil, res --[[@as string]]
return
end end
local client = assert(res)
--- @diagnostic disable-next-line: invisible --- @diagnostic disable-next-line: invisible
table.insert(client._on_exit_cbs, on_client_exit) table.insert(client._on_exit_cbs, on_client_exit)
@@ -437,7 +448,7 @@ function lsp.start_client(config)
client:initialize() client:initialize()
return client.id return client.id, nil
end end
--- Notify all attached clients that a buffer has changed. --- Notify all attached clients that a buffer has changed.

View File

@@ -37,7 +37,7 @@ local validate = vim.validate
--- `is_closing` and `terminate`. --- `is_closing` and `terminate`.
--- See |vim.lsp.rpc.request()|, |vim.lsp.rpc.notify()|. --- See |vim.lsp.rpc.request()|, |vim.lsp.rpc.notify()|.
--- For TCP there is a builtin RPC client factory: |vim.lsp.rpc.connect()| --- For TCP there is a builtin RPC client factory: |vim.lsp.rpc.connect()|
--- @field cmd string[]|fun(dispatchers: vim.lsp.rpc.Dispatchers): vim.lsp.rpc.PublicClient? --- @field cmd string[]|fun(dispatchers: vim.lsp.rpc.Dispatchers): vim.lsp.rpc.PublicClient
--- ---
--- Directory to launch the `cmd` process. Not related to `root_dir`. --- Directory to launch the `cmd` process. Not related to `root_dir`.
--- (default: cwd) --- (default: cwd)
@@ -506,25 +506,17 @@ function Client.create(config)
} }
-- Start the RPC client. -- Start the RPC client.
local rpc --- @type vim.lsp.rpc.PublicClient?
local config_cmd = config.cmd local config_cmd = config.cmd
if type(config_cmd) == 'function' then if type(config_cmd) == 'function' then
rpc = config_cmd(dispatchers) self.rpc = config_cmd(dispatchers)
else else
rpc = lsp.rpc.start(config_cmd, dispatchers, { self.rpc = lsp.rpc.start(config_cmd, dispatchers, {
cwd = config.cmd_cwd, cwd = config.cmd_cwd,
env = config.cmd_env, env = config.cmd_env,
detached = config.detached, detached = config.detached,
}) })
end end
-- Return nil if the rpc client fails to start
if not rpc then
return
end
self.rpc = rpc
setmetatable(self, Client) setmetatable(self, Client)
return self return self

View File

@@ -722,7 +722,7 @@ end
--- @param cmd string[] Command to start the LSP server. --- @param cmd string[] Command to start the LSP server.
--- @param dispatchers? vim.lsp.rpc.Dispatchers --- @param dispatchers? vim.lsp.rpc.Dispatchers
--- @param extra_spawn_params? vim.lsp.rpc.ExtraSpawnParams --- @param extra_spawn_params? vim.lsp.rpc.ExtraSpawnParams
--- @return vim.lsp.rpc.PublicClient? : Client RPC object, with these methods: --- @return vim.lsp.rpc.PublicClient : Client RPC object, with these methods:
--- - `notify()` |vim.lsp.rpc.notify()| --- - `notify()` |vim.lsp.rpc.notify()|
--- - `request()` |vim.lsp.rpc.request()| --- - `request()` |vim.lsp.rpc.request()|
--- - `is_closing()` returns a boolean indicating if the RPC is closing. --- - `is_closing()` returns a boolean indicating if the RPC is closing.
@@ -797,8 +797,7 @@ function M.start(cmd, dispatchers, extra_spawn_params)
end end
local msg = local msg =
string.format('Spawning language server with cmd: `%s` failed%s', vim.inspect(cmd), sfx) string.format('Spawning language server with cmd: `%s` failed%s', vim.inspect(cmd), sfx)
vim.notify(msg, vim.log.levels.WARN) error(msg)
return nil
end end
sysobj = sysobj_or_err --[[@as vim.SystemObj]] sysobj = sysobj_or_err --[[@as vim.SystemObj]]