mirror of
https://github.com/neovim/neovim
synced 2025-07-15 16:51:49 +00:00
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:
@ -1235,125 +1235,130 @@ Lua module: vim.lsp.client *lsp-client*
|
||||
*vim.lsp.ClientConfig*
|
||||
|
||||
Fields: ~
|
||||
• {cmd} (`string[]|fun(dispatchers: vim.lsp.rpc.Dispatchers): vim.lsp.rpc.PublicClient`)
|
||||
command string[] that launches the language
|
||||
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()|,
|
||||
|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_env}? (`table`) Environment flags to pass to the LSP
|
||||
on spawn. Must be specified using a table.
|
||||
Non-string values are coerced to string.
|
||||
Example: >lua
|
||||
{ PORT = 8080; HOST = "0.0.0.0"; }
|
||||
• {cmd} (`string[]|fun(dispatchers: vim.lsp.rpc.Dispatchers): vim.lsp.rpc.PublicClient`)
|
||||
command string[] that launches the language
|
||||
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()|,
|
||||
|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_env}? (`table`) Environment flags to pass to the LSP
|
||||
on spawn. Must be specified using a table.
|
||||
Non-string values are coerced to string.
|
||||
Example: >lua
|
||||
{ PORT = 8080; HOST = "0.0.0.0"; }
|
||||
<
|
||||
• {detached}? (`boolean`, default: true) Daemonize the server
|
||||
process so that it runs in a separate process
|
||||
group from Nvim. Nvim will shutdown the process
|
||||
on exit, but if Nvim fails to exit cleanly this
|
||||
could leave behind orphaned server processes.
|
||||
• {workspace_folders}? (`lsp.WorkspaceFolder[]`) List of workspace
|
||||
folders passed to the language server. For
|
||||
backwards compatibility rootUri and rootPath
|
||||
will be derived from the first workspace folder
|
||||
in this list. See `workspaceFolders` in the LSP
|
||||
spec.
|
||||
• {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.
|
||||
• Note: To send an empty dictionary use
|
||||
|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
|
||||
`start()` take precedence over the global
|
||||
command registry. Each key must be a unique
|
||||
command name, and the value is a function which
|
||||
is called if any LSP action (code action, code
|
||||
lenses, ...) triggers the command.
|
||||
• {init_options}? (`lsp.LSPObject`) Values to pass in the
|
||||
initialization request as
|
||||
`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.
|
||||
• {on_error}? (`fun(code: integer, err: string)`) Callback
|
||||
invoked when the client operation throws an
|
||||
error. `code` is a number describing the error.
|
||||
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
|
||||
human-friendly name.
|
||||
• {before_init}? (`fun(params: lsp.InitializeParams, config: vim.lsp.ClientConfig)`)
|
||||
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.
|
||||
• {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.
|
||||
• {on_exit}? (`elem_or_list<fun(code: integer, signal: integer, client_id: integer)>`)
|
||||
Callback invoked on client exit.
|
||||
• code: exit code of the process
|
||||
• signal: number describing the signal used to
|
||||
terminate (if any)
|
||||
• client_id: client handle
|
||||
• {on_attach}? (`elem_or_list<fun(client: vim.lsp.Client, bufnr: integer)>`)
|
||||
Callback invoked when client attaches to a
|
||||
buffer.
|
||||
• {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
|
||||
• {debounce_text_changes} (`integer`, default:
|
||||
`150`) Debounce `didChange` notifications to
|
||||
the server by the given number in
|
||||
milliseconds. No debounce occurs if `nil`.
|
||||
• {exit_timeout} (`integer|false`, default:
|
||||
`false`) Milliseconds to wait for server to
|
||||
exit cleanly after sending the "shutdown"
|
||||
request before sending kill -15. If set to
|
||||
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.
|
||||
• {detached}? (`boolean`, default: true) Daemonize the server
|
||||
process so that it runs in a separate process
|
||||
group from Nvim. Nvim will shutdown the process
|
||||
on exit, but if Nvim fails to exit cleanly this
|
||||
could leave behind orphaned server processes.
|
||||
• {workspace_folders}? (`lsp.WorkspaceFolder[]`) List of workspace
|
||||
folders passed to the language server. For
|
||||
backwards compatibility rootUri and rootPath
|
||||
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.
|
||||
• Note: To send an empty dictionary use
|
||||
|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
|
||||
`start()` take precedence over the global
|
||||
command registry. Each key must be a unique
|
||||
command name, and the value is a function which
|
||||
is called if any LSP action (code action, code
|
||||
lenses, ...) triggers the command.
|
||||
• {init_options}? (`lsp.LSPObject`) Values to pass in the
|
||||
initialization request as
|
||||
`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.
|
||||
• {on_error}? (`fun(code: integer, err: string)`) Callback
|
||||
invoked when the client operation throws an
|
||||
error. `code` is a number describing the error.
|
||||
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
|
||||
human-friendly name.
|
||||
• {before_init}? (`fun(params: lsp.InitializeParams, config: vim.lsp.ClientConfig)`)
|
||||
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.
|
||||
• {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.
|
||||
• {on_exit}? (`elem_or_list<fun(code: integer, signal: integer, client_id: integer)>`)
|
||||
Callback invoked on client exit.
|
||||
• code: exit code of the process
|
||||
• signal: number describing the signal used to
|
||||
terminate (if any)
|
||||
• client_id: client handle
|
||||
• {on_attach}? (`elem_or_list<fun(client: vim.lsp.Client, bufnr: integer)>`)
|
||||
Callback invoked when client attaches to a
|
||||
buffer.
|
||||
• {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
|
||||
• {debounce_text_changes} (`integer`, default:
|
||||
`150`) Debounce `didChange` notifications to
|
||||
the server by the given number in
|
||||
milliseconds. No debounce occurs if `nil`.
|
||||
• {exit_timeout} (`integer|false`, default:
|
||||
`false`) Milliseconds to wait for server to
|
||||
exit cleanly after sending the "shutdown"
|
||||
request before sending kill -15. If set to
|
||||
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.
|
||||
|
||||
|
||||
Client:cancel_request({id}) *Client:cancel_request()*
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
@ -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.
|
||||
|
@ -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)
|
||||
|
Reference in New Issue
Block a user