diff options
author | Michael Strobel <71396679+Kibadda@users.noreply.github.com> | 2025-03-30 20:07:53 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2025-03-30 11:07:53 -0700 |
commit | 90d15227c55c9ae6e4d52884817db75e4329792b (patch) | |
tree | 495e6455629eb5dafd880409bc3e58ee076d319c | |
parent | de96063bda43273f94478af2b02b5f5487b4f3f2 (diff) | |
download | rneovim-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.txt | 241 | ||||
-rw-r--r-- | runtime/doc/news.txt | 2 | ||||
-rw-r--r-- | runtime/lua/vim/lsp.lua | 11 | ||||
-rw-r--r-- | runtime/lua/vim/lsp/client.lua | 3 | ||||
-rw-r--r-- | test/functional/plugin/lsp_spec.lua | 35 |
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) |