aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMichael Strobel <71396679+Kibadda@users.noreply.github.com>2025-03-30 20:07:53 +0200
committerGitHub <noreply@github.com>2025-03-30 11:07:53 -0700
commit90d15227c55c9ae6e4d52884817db75e4329792b (patch)
tree495e6455629eb5dafd880409bc3e58ee076d319c
parentde96063bda43273f94478af2b02b5f5487b4f3f2 (diff)
downloadrneovim-90d15227c55c9ae6e4d52884817db75e4329792b.tar.gz
rneovim-90d15227c55c9ae6e4d52884817db75e4329792b.tar.bz2
rneovim-90d15227c55c9ae6e4d52884817db75e4329792b.zip
feat(lsp): workspace_required #31824
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: Justin M. Keyes <justinkz@gmail.com>
-rw-r--r--runtime/doc/lsp.txt241
-rw-r--r--runtime/doc/news.txt2
-rw-r--r--runtime/lua/vim/lsp.lua11
-rw-r--r--runtime/lua/vim/lsp/client.lua3
-rw-r--r--test/functional/plugin/lsp_spec.lua35
5 files changed, 173 insertions, 119 deletions
diff --git a/runtime/doc/lsp.txt b/runtime/doc/lsp.txt
index be1d209ac8..d96899dd74 100644
--- a/runtime/doc/lsp.txt
+++ b/runtime/doc/lsp.txt
@@ -1238,125 +1238,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()*
diff --git a/runtime/doc/news.txt b/runtime/doc/news.txt
index 5f47c474a7..5c676e228c 100644
--- a/runtime/doc/news.txt
+++ b/runtime/doc/news.txt
@@ -124,7 +124,7 @@ EVENTS
LSP
-• todo
+• |vim.lsp.Config| gained `workspace_required`.
LUA
diff --git a/runtime/lua/vim/lsp.lua b/runtime/lua/vim/lsp.lua
index 8c590ab6c8..3dcf692d24 100644
--- a/runtime/lua/vim/lsp.lua
+++ b/runtime/lua/vim/lsp.lua
@@ -615,6 +615,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
diff --git a/runtime/lua/vim/lsp/client.lua b/runtime/lua/vim/lsp/client.lua
index 8c75ee321d..b256eab1a6 100644
--- a/runtime/lua/vim/lsp/client.lua
+++ b/runtime/lua/vim/lsp/client.lua
@@ -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.
diff --git a/test/functional/plugin/lsp_spec.lua b/test/functional/plugin/lsp_spec.lua
index 614c49a41f..856c086add 100644
--- a/test/functional/plugin/lsp_spec.lua
+++ b/test/functional/plugin/lsp_spec.lua
@@ -6414,5 +6414,40 @@ describe('LSP', function()
filetypes = true,
}, 'cannot start foo due to config error: .* filetypes: expected table, got boolean')
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)