diff options
Diffstat (limited to 'runtime/lua/vim/lsp.lua')
-rw-r--r-- | runtime/lua/vim/lsp.lua | 364 |
1 files changed, 205 insertions, 159 deletions
diff --git a/runtime/lua/vim/lsp.lua b/runtime/lua/vim/lsp.lua index 6d29c9e4df..ebdc050405 100644 --- a/runtime/lua/vim/lsp.lua +++ b/runtime/lua/vim/lsp.lua @@ -3,6 +3,7 @@ local validate = vim.validate local lsp = vim._defer_require('vim.lsp', { _changetracking = ..., --- @module 'vim.lsp._changetracking' + _folding_range = ..., --- @module 'vim.lsp._folding_range' _snippet_grammar = ..., --- @module 'vim.lsp._snippet_grammar' _tagfunc = ..., --- @module 'vim.lsp._tagfunc' _watchfiles = ..., --- @module 'vim.lsp._watchfiles' @@ -57,6 +58,7 @@ lsp._request_name_to_capability = { [ms.textDocument_documentHighlight] = { 'documentHighlightProvider' }, [ms.textDocument_documentLink] = { 'documentLinkProvider' }, [ms.textDocument_documentSymbol] = { 'documentSymbolProvider' }, + [ms.textDocument_foldingRange] = { 'foldingRangeProvider' }, [ms.textDocument_formatting] = { 'documentFormattingProvider' }, [ms.textDocument_hover] = { 'hoverProvider' }, [ms.textDocument_implementation] = { 'implementationProvider' }, @@ -87,18 +89,6 @@ lsp._request_name_to_capability = { -- TODO improve handling of scratch buffers with LSP attached. ---- Returns the buffer number for the given {bufnr}. ---- ----@param bufnr (integer|nil) Buffer number to resolve. Defaults to current buffer ----@return integer bufnr -local function resolve_bufnr(bufnr) - validate('bufnr', bufnr, 'number', true) - if bufnr == nil or bufnr == 0 then - return api.nvim_get_current_buf() - end - return bufnr -end - ---@private --- Called by the client when trying to call a method that's not --- supported in any of the servers registered for the current buffer. @@ -112,6 +102,22 @@ function lsp._unsupported_method(method) return msg end +---@private +---@param workspace_folders string|lsp.WorkspaceFolder[]? +---@return lsp.WorkspaceFolder[]? +function lsp._get_workspace_folders(workspace_folders) + if type(workspace_folders) == 'table' then + return workspace_folders + elseif type(workspace_folders) == 'string' then + return { + { + uri = vim.uri_from_fname(workspace_folders), + name = workspace_folders, + }, + } + end +end + local wait_result_reason = { [-1] = 'timeout', [-2] = 'interrupted', [-3] = 'error' } local format_line_ending = { @@ -194,32 +200,138 @@ local function reuse_client_default(client, config) return false end - if config.root_dir then - local root = vim.uri_from_fname(config.root_dir) - for _, dir in ipairs(client.workspace_folders or {}) do - -- note: do not need to check client.root_dir since that should be client.workspace_folders[1] - if root == dir.uri then - return true + local config_folders = lsp._get_workspace_folders(config.workspace_folders or config.root_dir) + or {} + local config_folders_included = 0 + + if not next(config_folders) then + return false + end + + for _, config_folder in ipairs(config_folders) do + for _, client_folder in ipairs(client.workspace_folders) do + if config_folder.uri == client_folder.uri then + config_folders_included = config_folders_included + 1 + break end end end - -- TODO(lewis6991): also check config.workspace_folders + return config_folders_included == #config_folders +end - return false +--- Reset defaults set by `set_defaults`. +--- Must only be called if the last client attached to a buffer exits. +local function reset_defaults(bufnr) + if vim.bo[bufnr].tagfunc == 'v:lua.vim.lsp.tagfunc' then + vim.bo[bufnr].tagfunc = nil + end + if vim.bo[bufnr].omnifunc == 'v:lua.vim.lsp.omnifunc' then + vim.bo[bufnr].omnifunc = nil + end + if vim.bo[bufnr].formatexpr == 'v:lua.vim.lsp.formatexpr()' then + vim.bo[bufnr].formatexpr = nil + end + vim._with({ buf = bufnr }, function() + local keymap = vim.fn.maparg('K', 'n', false, true) + if keymap and keymap.callback == vim.lsp.buf.hover and keymap.buffer == 1 then + vim.keymap.del('n', 'K', { buffer = bufnr }) + end + end) +end + +--- @param code integer +--- @param signal integer +--- @param client_id integer +local function on_client_exit(code, signal, client_id) + local client = all_clients[client_id] + + vim.schedule(function() + for bufnr in pairs(client.attached_buffers) do + if client and client.attached_buffers[bufnr] and api.nvim_buf_is_valid(bufnr) then + api.nvim_exec_autocmds('LspDetach', { + buffer = bufnr, + modeline = false, + data = { client_id = client_id }, + }) + end + + client.attached_buffers[bufnr] = nil + + if #lsp.get_clients({ bufnr = bufnr, _uninitialized = true }) == 0 then + reset_defaults(bufnr) + end + end + + local namespace = vim.lsp.diagnostic.get_namespace(client_id) + vim.diagnostic.reset(namespace) + end) + + local name = client.name or 'unknown' + + -- Schedule the deletion of the client object so that it exists in the execution of LspDetach + -- autocommands + vim.schedule(function() + all_clients[client_id] = nil + + -- Client can be absent if executable starts, but initialize fails + -- init/attach won't have happened + if client then + changetracking.reset(client) + end + if code ~= 0 or (signal ~= 0 and signal ~= 15) then + local msg = string.format( + 'Client %s quit with exit code %s and signal %s. Check log for errors: %s', + name, + code, + signal, + lsp.get_log_path() + ) + vim.notify(msg, vim.log.levels.WARN) + end + end) +end + +--- Creates and initializes a client with the given configuration. +--- @param config vim.lsp.ClientConfig Configuration for the server. +--- @return integer? client_id |vim.lsp.get_client_by_id()| Note: client may not be +--- fully initialized. Use `on_init` to do any actions once +--- the client has been initialized. +--- @return string? # Error message, if any +local function create_and_initialize_client(config) + local ok, res = pcall(require('vim.lsp.client').create, config) + if not ok then + return nil, res --[[@as string]] + end + + local client = assert(res) + + --- @diagnostic disable-next-line: invisible + table.insert(client._on_exit_cbs, on_client_exit) + + all_clients[client.id] = client + + client:initialize() + + return client.id, nil end --- @class vim.lsp.start.Opts --- @inlinedoc --- --- Predicate used to decide if a client should be re-used. Used on all ---- running clients. The default implementation re-uses a client if name and ---- root_dir matches. +--- running clients. The default implementation re-uses a client if it has the +--- same name and if the given workspace folders (or root_dir) are all included +--- in the client's workspace folders. --- @field reuse_client? fun(client: vim.lsp.Client, config: vim.lsp.ClientConfig): boolean --- --- Buffer handle to attach to if starting or re-using a client (0 for current). --- @field bufnr? integer --- +--- Whether to attach the client to a buffer (default true). +--- If set to `false`, `reuse_client` and `bufnr` will be ignored. +--- @field attach? boolean +--- --- Suppress error reporting if the LSP server fails to start (default false). --- @field silent? boolean @@ -237,10 +349,10 @@ end --- }) --- ``` --- ---- See |vim.lsp.start_client()| for all available options. The most important are: +--- See |vim.lsp.ClientConfig| for all available options. The most important are: --- --- - `name` arbitrary name for the LSP client. Should be unique per language server. ---- - `cmd` command string[] or function, described at |vim.lsp.start_client()|. +--- - `cmd` command string[] or function. --- - `root_dir` path to the project root. By default this is used to decide if an existing client --- should be re-used. The example above uses |vim.fs.root()| to detect the root by traversing --- the file system upwards starting from the current directory until either a `pyproject.toml` @@ -260,15 +372,19 @@ end --- `ftplugin/<filetype_name>.lua` (See |ftplugin-name|) --- --- @param config vim.lsp.ClientConfig Configuration for the server. ---- @param opts vim.lsp.start.Opts? Optional keyword arguments +--- @param opts vim.lsp.start.Opts? Optional keyword arguments. --- @return integer? client_id function lsp.start(config, opts) opts = opts or {} local reuse_client = opts.reuse_client or reuse_client_default - local bufnr = resolve_bufnr(opts.bufnr) + local bufnr = vim._resolve_bufnr(opts.bufnr) for _, client in pairs(all_clients) do if reuse_client(client, config) then + if opts.attach == false then + return client.id + end + if lsp.buf_attach_client(bufnr, client.id) then return client.id else @@ -277,7 +393,7 @@ function lsp.start(config, opts) end end - local client_id, err = lsp.start_client(config) + local client_id, err = create_and_initialize_client(config) if err then if not opts.silent then vim.notify(err, vim.log.levels.WARN) @@ -285,6 +401,10 @@ function lsp.start(config, opts) return nil end + if opts.attach == false then + return client_id + end + if client_id and lsp.buf_attach_client(bufnr, client_id) then return client_id end @@ -381,78 +501,7 @@ function lsp._set_defaults(client, bufnr) end end ---- Reset defaults set by `set_defaults`. ---- Must only be called if the last client attached to a buffer exits. -local function reset_defaults(bufnr) - if vim.bo[bufnr].tagfunc == 'v:lua.vim.lsp.tagfunc' then - vim.bo[bufnr].tagfunc = nil - end - if vim.bo[bufnr].omnifunc == 'v:lua.vim.lsp.omnifunc' then - vim.bo[bufnr].omnifunc = nil - end - if vim.bo[bufnr].formatexpr == 'v:lua.vim.lsp.formatexpr()' then - vim.bo[bufnr].formatexpr = nil - end - vim._with({ buf = bufnr }, function() - local keymap = vim.fn.maparg('K', 'n', false, true) - if keymap and keymap.callback == vim.lsp.buf.hover and keymap.buffer == 1 then - vim.keymap.del('n', 'K', { buffer = bufnr }) - end - end) -end - ---- @param code integer ---- @param signal integer ---- @param client_id integer -local function on_client_exit(code, signal, client_id) - local client = all_clients[client_id] - - vim.schedule(function() - for bufnr in pairs(client.attached_buffers) do - if client and client.attached_buffers[bufnr] and api.nvim_buf_is_valid(bufnr) then - api.nvim_exec_autocmds('LspDetach', { - buffer = bufnr, - modeline = false, - data = { client_id = client_id }, - }) - end - - client.attached_buffers[bufnr] = nil - - if #lsp.get_clients({ bufnr = bufnr, _uninitialized = true }) == 0 then - reset_defaults(bufnr) - end - end - - local namespace = vim.lsp.diagnostic.get_namespace(client_id) - vim.diagnostic.reset(namespace) - end) - - local name = client.name or 'unknown' - - -- Schedule the deletion of the client object so that it exists in the execution of LspDetach - -- autocommands - vim.schedule(function() - all_clients[client_id] = nil - - -- Client can be absent if executable starts, but initialize fails - -- init/attach won't have happened - if client then - changetracking.reset(client) - end - if code ~= 0 or (signal ~= 0 and signal ~= 15) then - local msg = string.format( - 'Client %s quit with exit code %s and signal %s. Check log for errors: %s', - name, - code, - signal, - lsp.get_log_path() - ) - vim.notify(msg, vim.log.levels.WARN) - end - end) -end - +--- @deprecated --- Starts and initializes a client with the given configuration. --- @param config vim.lsp.ClientConfig Configuration for the server. --- @return integer? client_id |vim.lsp.get_client_by_id()| Note: client may not be @@ -460,27 +509,14 @@ end --- the client has been initialized. --- @return string? # Error message, if any function lsp.start_client(config) - local ok, res = pcall(require('vim.lsp.client').create, config) - if not ok then - return nil, res --[[@as string]] - end - - local client = assert(res) - - --- @diagnostic disable-next-line: invisible - table.insert(client._on_exit_cbs, on_client_exit) - - all_clients[client.id] = client - - client:initialize() - - return client.id, nil + vim.deprecate('vim.lsp.start_client()', 'vim.lsp.start()', '0.13') + return create_and_initialize_client(config) end ---Buffer lifecycle handler for textDocument/didSave --- @param bufnr integer local function text_document_did_save_handler(bufnr) - bufnr = resolve_bufnr(bufnr) + bufnr = vim._resolve_bufnr(bufnr) local uri = vim.uri_from_bufnr(bufnr) local text = once(lsp._buf_get_full_text) for _, client in ipairs(lsp.get_clients({ bufnr = bufnr })) do @@ -641,7 +677,7 @@ end function lsp.buf_attach_client(bufnr, client_id) validate('bufnr', bufnr, 'number', true) validate('client_id', client_id, 'number') - bufnr = resolve_bufnr(bufnr) + bufnr = vim._resolve_bufnr(bufnr) if not api.nvim_buf_is_loaded(bufnr) then log.warn(string.format('buf_attach_client called on unloaded buffer (id: %d): ', bufnr)) return false @@ -678,7 +714,7 @@ end function lsp.buf_detach_client(bufnr, client_id) validate('bufnr', bufnr, 'number', true) validate('client_id', client_id, 'number') - bufnr = resolve_bufnr(bufnr) + bufnr = vim._resolve_bufnr(bufnr) local client = all_clients[client_id] if not client or not client.attached_buffers[bufnr] then @@ -784,7 +820,7 @@ function lsp.get_clients(filter) local clients = {} --- @type vim.lsp.Client[] - local bufnr = filter.bufnr and resolve_bufnr(filter.bufnr) + local bufnr = filter.bufnr and vim._resolve_bufnr(filter.bufnr) for _, client in pairs(all_clients) do if @@ -880,7 +916,7 @@ function lsp.buf_request(bufnr, method, params, handler, on_unsupported) validate('handler', handler, 'function', true) validate('on_unsupported', on_unsupported, 'function', true) - bufnr = resolve_bufnr(bufnr) + bufnr = vim._resolve_bufnr(bufnr) local method_supported = false local clients = lsp.get_clients({ bufnr = bufnr }) local client_request_ids = {} --- @type table<integer,integer> @@ -1094,6 +1130,54 @@ function lsp.tagfunc(pattern, flags) return vim.lsp._tagfunc(pattern, flags) end +--- Provides an interface between the built-in client and a `foldexpr` function. +--- +--- To use, check for the "textDocument/foldingRange" capability in an +--- |LspAttach| autocommand. Example: +--- +--- ```lua +--- vim.api.nvim_create_autocommand('LspAttach', { +--- callback = function(args) +--- local client = vim.lsp.get_client_by_id(args.data.client_id) +--- if client:supports_method('textDocument/foldingRange') then +--- vim.wo.foldmethod = 'expr' +--- vim.wo.foldexpr = 'v:lua.vim.lsp.foldexpr()' +--- end +--- end, +--- }) +--- ``` +--- +---@param lnum integer line number +function lsp.foldexpr(lnum) + return vim.lsp._folding_range.foldexpr(lnum) +end + +--- Close all {kind} of folds in the the window with {winid}. +--- +--- To automatically fold imports when opening a file, you can use an autocmd: +--- +--- ```lua +--- vim.api.nvim_create_autocmd('LspNotify', { +--- callback = function(args) +--- if args.data.method == 'textDocument/didOpen' then +--- vim.lsp.foldclose('imports', vim.fn.bufwinid(args.buf)) +--- end +--- end, +--- }) +--- ``` +--- +---@param kind lsp.FoldingRangeKind Kind to close, one of "comment", "imports" or "region". +---@param winid? integer Defaults to the current window. +function lsp.foldclose(kind, winid) + return vim.lsp._folding_range.foldclose(kind, winid) +end + +--- Provides a `foldtext` function that shows the `collapsedText` retrieved, +--- defaults to the first folded line if `collapsedText` is not provided. +function lsp.foldtext() + return vim.lsp._folding_range.foldtext() +end + ---Checks whether a client is stopped. --- ---@param client_id (integer) @@ -1112,7 +1196,7 @@ end function lsp.buf_get_clients(bufnr) vim.deprecate('vim.lsp.buf_get_clients()', 'vim.lsp.get_clients()', '0.12') local result = {} --- @type table<integer,vim.lsp.Client> - for _, client in ipairs(lsp.get_clients({ bufnr = resolve_bufnr(bufnr) })) do + for _, client in ipairs(lsp.get_clients({ bufnr = vim._resolve_bufnr(bufnr) })) do result[client.id] = client end return result @@ -1166,7 +1250,7 @@ function lsp.for_each_buffer_client(bufnr, fn) 'lsp.get_clients({ bufnr = bufnr }) with regular loop', '0.12' ) - bufnr = resolve_bufnr(bufnr) + bufnr = vim._resolve_bufnr(bufnr) for _, client in pairs(lsp.get_clients({ bufnr = bufnr })) do fn(client, client.id, bufnr) @@ -1183,44 +1267,6 @@ function lsp.with(handler, override_config) end end ---- Helper function to use when implementing a handler. ---- This will check that all of the keys in the user configuration ---- are valid keys and make sense to include for this handler. ---- ---- Will error on invalid keys (i.e. keys that do not exist in the options) ---- @param name string ---- @param options table<string,any> ---- @param user_config table<string,any> -function lsp._with_extend(name, options, user_config) - user_config = user_config or {} - - local resulting_config = {} --- @type table<string,any> - for k, v in pairs(user_config) do - if options[k] == nil then - error( - debug.traceback( - string.format( - 'Invalid option for `%s`: %s. Valid options are:\n%s', - name, - k, - vim.inspect(vim.tbl_keys(options)) - ) - ) - ) - end - - resulting_config[k] = v - end - - for k, v in pairs(options) do - if resulting_config[k] == nil then - resulting_config[k] = v - end - end - - return resulting_config -end - --- Registry for client side commands. --- This is an extension point for plugins to handle custom commands which are --- not part of the core language server protocol specification. |