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

View File

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

View File

@ -633,6 +633,17 @@ function lsp.start(config, opts)
config.root_dir = vim.fs.root(bufnr, opts._root_markers)
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
if reuse_client(client, config) 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.
--- @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()|,
--- passed to the language server on initialization. Hint: use make_client_capabilities() and modify
--- its result.

View File

@ -6456,5 +6456,40 @@ describe('LSP', function()
vim.lsp.config('*', {})
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)