feat(lsp): workspace_required (#33608)

Problem:
Some language servers do not work properly without a workspace folder.

Solution:
Add `workspace_required`, which skips starting the lsp client if no
workspace folder is found.

Co-authored-by: Michael Strobel <71396679+Kibadda@users.noreply.github.com>
This commit is contained in:
Justin M. Keyes
2025-04-24 07:06:30 -07:00
committed by GitHub
parent 448c2cec9d
commit 66953b16a2
5 changed files with 173 additions and 118 deletions

View File

@ -1240,15 +1240,16 @@ Lua module: vim.lsp.client *lsp-client*
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
"~" are not expanded), or function that creates "~" are not expanded), or function that creates
an RPC client. Function receives a `dispatchers` an RPC client. Function receives a
table and returns a table with member functions `dispatchers` table and returns a table with
`request`, `notify`, `is_closing` and member functions `request`, `notify`,
`terminate`. See |vim.lsp.rpc.request()|, `is_closing` and `terminate`. See
|vim.lsp.rpc.request()|,
|vim.lsp.rpc.notify()|. For TCP there is a |vim.lsp.rpc.notify()|. For TCP there is a
builtin RPC client factory: builtin RPC client factory:
|vim.lsp.rpc.connect()| |vim.lsp.rpc.connect()|
• {cmd_cwd}? (`string`, default: cwd) Directory to launch the • {cmd_cwd}? (`string`, default: cwd) Directory to launch
`cmd` process. Not related to `root_dir`. the `cmd` process. Not related to `root_dir`.
• {cmd_env}? (`table`) Environment flags to pass to the LSP • {cmd_env}? (`table`) Environment flags to pass to the LSP
on spawn. Must be specified using a table. on spawn. Must be specified using a table.
Non-string values are coerced to string. Non-string values are coerced to string.
@ -1266,23 +1267,26 @@ Lua module: vim.lsp.client *lsp-client*
will be derived from the first workspace folder will be derived from the first workspace folder
in this list. See `workspaceFolders` in the LSP in this list. See `workspaceFolders` in the LSP
spec. spec.
• {workspace_required}? (`boolean`) (default false) Server requires a
workspace (no "single file" support).
• {capabilities}? (`lsp.ClientCapabilities`) Map overriding the • {capabilities}? (`lsp.ClientCapabilities`) Map overriding the
default capabilities defined by default capabilities defined by
|vim.lsp.protocol.make_client_capabilities()|, |vim.lsp.protocol.make_client_capabilities()|,
passed to the language server on initialization. passed to the language server on
Hint: use make_client_capabilities() and modify initialization. Hint: use
its result. make_client_capabilities() and modify its
result.
• Note: To send an empty dictionary use • Note: To send an empty dictionary use
|vim.empty_dict()|, else it will be encoded as |vim.empty_dict()|, else it will be encoded
an array. as an array.
• {handlers}? (`table<string,function>`) Map of language • {handlers}? (`table<string,function>`) Map of language
server method names to |lsp-handler| server method names to |lsp-handler|
• {settings}? (`lsp.LSPObject`) Map with language server • {settings}? (`lsp.LSPObject`) Map with language server
specific settings. See the {settings} in specific settings. See the {settings} in
|vim.lsp.Client|. |vim.lsp.Client|.
• {commands}? (`table<string,fun(command: lsp.Command, ctx: table)>`) • {commands}? (`table<string,fun(command: lsp.Command, ctx: table)>`)
Table that maps string of clientside commands to Table that maps string of clientside commands
user-defined functions. Commands passed to to user-defined functions. Commands passed to
`start()` take precedence over the global `start()` take precedence over the global
command registry. Each key must be a unique command registry. Each key must be a unique
command name, and the value is a function which command name, and the value is a function which
@ -1290,17 +1294,17 @@ Lua module: vim.lsp.client *lsp-client*
lenses, ...) triggers the command. lenses, ...) triggers the command.
• {init_options}? (`lsp.LSPObject`) Values to pass in the • {init_options}? (`lsp.LSPObject`) Values to pass in the
initialization request as initialization request as
`initializationOptions`. See `initialize` in the `initializationOptions`. See `initialize` in
LSP spec. the LSP spec.
• {name}? (`string`, default: client-id) Name in log • {name}? (`string`, default: client-id) Name in log
messages. messages.
• {get_language_id}? (`fun(bufnr: integer, filetype: string): string`) • {get_language_id}? (`fun(bufnr: integer, filetype: string): string`)
Language ID as string. Defaults to the buffer Language ID as string. Defaults to the buffer
filetype. filetype.
• {offset_encoding}? (`'utf-8'|'utf-16'|'utf-32'`) Called "position • {offset_encoding}? (`'utf-8'|'utf-16'|'utf-32'`) Called "position
encoding" in LSP spec, the encoding that the LSP encoding" in LSP spec, the encoding that the
server expects. Client does not verify this is LSP server expects. Client does not verify this
correct. is correct.
• {on_error}? (`fun(code: integer, err: string)`) Callback • {on_error}? (`fun(code: integer, err: string)`) Callback
invoked when the client operation throws an invoked when the client operation throws an
error. `code` is a number describing the error. error. `code` is a number describing the error.
@ -1313,17 +1317,18 @@ Lua module: vim.lsp.client *lsp-client*
Callback invoked before the LSP "initialize" Callback invoked before the LSP "initialize"
phase, where `params` contains the parameters phase, where `params` contains the parameters
being sent to the server and `config` is the being sent to the server and `config` is the
config that was passed to |vim.lsp.start()|. You config that was passed to |vim.lsp.start()|.
can use this to modify parameters before they You can use this to modify parameters before
are sent. they are sent.
• {on_init}? (`elem_or_list<fun(client: vim.lsp.Client, init_result: lsp.InitializeResult)>`) • {on_init}? (`elem_or_list<fun(client: vim.lsp.Client, init_result: lsp.InitializeResult)>`)
Callback invoked after LSP "initialize", where Callback invoked after LSP "initialize", where
`result` is a table of `capabilities` and `result` is a table of `capabilities` and
anything else the server may send. For example, anything else the server may send. For example,
clangd sends `init_result.offsetEncoding` if clangd sends `init_result.offsetEncoding` if
`capabilities.offsetEncoding` was sent to it. `capabilities.offsetEncoding` was sent to it.
You can only modify the `client.offset_encoding` You can only modify the
here before any notifications are sent. `client.offset_encoding` here before any
notifications are sent.
• {on_exit}? (`elem_or_list<fun(code: integer, signal: integer, client_id: integer)>`) • {on_exit}? (`elem_or_list<fun(code: integer, signal: integer, client_id: integer)>`)
Callback invoked on client exit. Callback invoked on client exit.
• code: exit code of the process • code: exit code of the process
@ -1336,11 +1341,11 @@ Lua module: vim.lsp.client *lsp-client*
• {trace}? (`'off'|'messages'|'verbose'`, default: "off") • {trace}? (`'off'|'messages'|'verbose'`, default: "off")
Passed directly to the language server in the Passed directly to the language server in the
initialize request. Invalid/empty values will initialize request. Invalid/empty values will
• {flags}? (`table`) A table with flags for the client. The • {flags}? (`table`) A table with flags for the client.
current (experimental) flags are: The current (experimental) flags are:
• {allow_incremental_sync}? (`boolean`, default: • {allow_incremental_sync}? (`boolean`,
`true`) Allow using incremental sync for default: `true`) Allow using incremental sync
buffer edits for buffer edits
• {debounce_text_changes} (`integer`, default: • {debounce_text_changes} (`integer`, default:
`150`) Debounce `didChange` notifications to `150`) Debounce `didChange` notifications to
the server by the given number in the server by the given number in
@ -1352,8 +1357,8 @@ Lua module: vim.lsp.client *lsp-client*
false, nvim exits immediately after sending false, nvim exits immediately after sending
the "shutdown" request to the server. the "shutdown" request to the server.
• {root_dir}? (`string`) Directory where the LSP server will • {root_dir}? (`string`) Directory where the LSP server will
base its workspaceFolders, rootUri, and rootPath base its workspaceFolders, rootUri, and
on initialization. rootPath on initialization.
Client:cancel_request({id}) *Client:cancel_request()* Client:cancel_request({id}) *Client:cancel_request()*

View File

@ -276,6 +276,7 @@ LSP
`codeAction/resolve` request. `codeAction/resolve` request.
• The `textDocument/completion` request now includes the completion context in • The `textDocument/completion` request now includes the completion context in
its parameters. its parameters.
• |vim.lsp.Config| gained `workspace_required`.
LUA LUA

View File

@ -633,6 +633,17 @@ function lsp.start(config, opts)
config.root_dir = vim.fs.root(bufnr, opts._root_markers) config.root_dir = vim.fs.root(bufnr, opts._root_markers)
end end
if
not config.root_dir
and (not config.workspace_folders or #config.workspace_folders == 0)
and config.workspace_required
then
log.info(
('skipping config "%s": workspace_required=true, no workspace found'):format(config.name)
)
return
end
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
if opts.attach == false then if opts.attach == false then

View File

@ -63,6 +63,9 @@ local validate = vim.validate
--- folder in this list. See `workspaceFolders` in the LSP spec. --- folder in this list. See `workspaceFolders` in the LSP spec.
--- @field workspace_folders? lsp.WorkspaceFolder[] --- @field workspace_folders? lsp.WorkspaceFolder[]
--- ---
--- (default false) Server requires a workspace (no "single file" support).
--- @field workspace_required? boolean
---
--- Map overriding the default capabilities defined by |vim.lsp.protocol.make_client_capabilities()|, --- Map overriding the default capabilities defined by |vim.lsp.protocol.make_client_capabilities()|,
--- passed to the language server on initialization. Hint: use make_client_capabilities() and modify --- passed to the language server on initialization. Hint: use make_client_capabilities() and modify
--- its result. --- its result.

View File

@ -6456,5 +6456,40 @@ describe('LSP', function()
vim.lsp.config('*', {}) vim.lsp.config('*', {})
end) end)
end) end)
it('does not start without workspace if workspace_required=true', function()
exec_lua(create_server_definition)
local tmp1 = t.tmpname(true)
eq(
{ workspace_required = false },
exec_lua(function()
local server = _G._create_server({
handlers = {
initialize = function(_, _, callback)
callback(nil, { capabilities = {} })
end,
},
})
local ws_required = { cmd = server.cmd, workspace_required = true, filetypes = { 'foo' } }
local ws_not_required = vim.deepcopy(ws_required)
ws_not_required.workspace_required = false
vim.lsp.config('ws_required', ws_required)
vim.lsp.config('ws_not_required', ws_not_required)
vim.lsp.enable('ws_required')
vim.lsp.enable('ws_not_required')
vim.cmd.edit(assert(tmp1))
vim.bo.filetype = 'foo'
local clients = vim.lsp.get_clients({ bufnr = vim.api.nvim_get_current_buf() })
assert(1 == #clients)
return { workspace_required = clients[1].config.workspace_required }
end)
)
end)
end) end)
end) end)