diff options
Diffstat (limited to 'runtime/lua/vim/lsp.lua')
-rw-r--r-- | runtime/lua/vim/lsp.lua | 172 |
1 files changed, 123 insertions, 49 deletions
diff --git a/runtime/lua/vim/lsp.lua b/runtime/lua/vim/lsp.lua index 90c5872f11..9a008ac965 100644 --- a/runtime/lua/vim/lsp.lua +++ b/runtime/lua/vim/lsp.lua @@ -41,6 +41,7 @@ lsp._request_name_to_capability = { ['textDocument/documentSymbol'] = 'document_symbol'; ['textDocument/prepareCallHierarchy'] = 'call_hierarchy'; ['textDocument/rename'] = 'rename'; + ['textDocument/prepareRename'] = 'rename'; ['textDocument/codeAction'] = 'code_action'; ['textDocument/codeLens'] = 'code_lens'; ['codeLens/resolve'] = 'code_lens_resolve'; @@ -85,7 +86,7 @@ end function lsp._unsupported_method(method) local msg = string.format("method %s is not supported by any of the servers registered for the current buffer", method) log.warn(msg) - return lsp.rpc_response_error(protocol.ErrorCodes.MethodNotFound, msg) + return msg end ---@private @@ -121,24 +122,34 @@ local active_clients = {} local all_buffer_active_clients = {} local uninitialized_clients = {} --- Tracks all buffers attached to a client. -local all_client_active_buffers = {} - ---@private --- Invokes a function for each LSP client attached to the buffer {bufnr}. --- ---@param bufnr (Number) of buffer ---@param fn (function({client}, {client_id}, {bufnr}) Function to run on ---each client attached to that buffer. -local function for_each_buffer_client(bufnr, fn) +---@param restrict_client_ids table list of client ids on which to restrict function application. +local function for_each_buffer_client(bufnr, fn, restrict_client_ids) validate { fn = { fn, 'f' }; + restrict_client_ids = { restrict_client_ids, 't' , true}; } bufnr = resolve_bufnr(bufnr) local client_ids = all_buffer_active_clients[bufnr] if not client_ids or tbl_isempty(client_ids) then return end + + if restrict_client_ids and #restrict_client_ids > 0 then + local filtered_client_ids = {} + for client_id in pairs(client_ids) do + if vim.tbl_contains(restrict_client_ids, client_id) then + filtered_client_ids[client_id] = true + end + end + client_ids = filtered_client_ids + end + for client_id in pairs(client_ids) do local client = active_clients[client_id] if client then @@ -728,7 +739,6 @@ function lsp.start_client(config) lsp.diagnostic.reset(client_id, all_buffer_active_clients) changetracking.reset(client_id) - all_client_active_buffers[client_id] = nil for _, client_ids in pairs(all_buffer_active_clients) do client_ids[client_id] = nil end @@ -757,6 +767,7 @@ function lsp.start_client(config) rpc = rpc; offset_encoding = offset_encoding; config = config; + attached_buffers = {}; handlers = handlers; -- for $/progress report @@ -830,7 +841,7 @@ function lsp.start_client(config) rpc.request('initialize', initialize_params, function(init_err, result) assert(not init_err, tostring(init_err)) assert(result, "server sent empty result") - rpc.notify('initialized', {[vim.type_idx]=vim.types.dictionary}) + rpc.notify('initialized', vim.empty_dict()) client.initialized = true uninitialized_clients[client_id] = nil client.workspaceFolders = initialize_params.workspaceFolders @@ -896,7 +907,7 @@ function lsp.start_client(config) local _ = log.debug() and log.debug(log_prefix, "client.request", client_id, method, params, handler, bufnr) return rpc.request(method, params, function(err, result) - handler(err, result, {method=method, client_id=client_id, bufnr=bufnr}) + handler(err, result, {method=method, client_id=client_id, bufnr=bufnr, params=params}) end) end @@ -975,7 +986,6 @@ function lsp.start_client(config) lsp.diagnostic.reset(client_id, all_buffer_active_clients) changetracking.reset(client_id) - all_client_active_buffers[client_id] = nil for _, client_ids in pairs(all_buffer_active_clients) do client_ids[client_id] = nil end @@ -1018,6 +1028,7 @@ function lsp.start_client(config) -- TODO(ashkan) handle errors. pcall(config.on_attach, client, bufnr) end + client.attached_buffers[bufnr] = true end initialize() @@ -1128,12 +1139,6 @@ function lsp.buf_attach_client(bufnr, client_id) }) end - if not all_client_active_buffers[client_id] then - all_client_active_buffers[client_id] = {} - end - - table.insert(all_client_active_buffers[client_id], bufnr) - if buffer_client_ids[client_id] then return end -- This is our first time attaching this client to this buffer. buffer_client_ids[client_id] = true @@ -1158,7 +1163,7 @@ end --- Gets a client by id, or nil if the id is invalid. --- The returned client may not yet be fully initialized. -- ----@param client_id client id number +---@param client_id number client id --- ---@returns |vim.lsp.client| object, or nil function lsp.get_client_by_id(client_id) @@ -1167,15 +1172,11 @@ end --- Returns list of buffers attached to client_id. -- ----@param client_id client id +---@param client_id number client id ---@returns list of buffer ids function lsp.get_buffers_by_client_id(client_id) - local active_client_buffers = all_client_active_buffers[client_id] - if active_client_buffers then - return active_client_buffers - else - return {} - end + local client = lsp.get_client_by_id(client_id) + return client and vim.tbl_keys(client.attached_buffers) or {} end --- Stops a client(s). @@ -1254,33 +1255,33 @@ function lsp.buf_request(bufnr, method, params, handler) method = { method, 's' }; handler = { handler, 'f', true }; } - local client_request_ids = {} + local supported_clients = {} local method_supported = false - for_each_buffer_client(bufnr, function(client, client_id, resolved_bufnr) + for_each_buffer_client(bufnr, function(client, client_id) if client.supports_method(method) then method_supported = true - local request_success, request_id = client.request(method, params, handler, resolved_bufnr) - - -- This could only fail if the client shut down in the time since we looked - -- it up and we did the request, which should be rare. - if request_success then - client_request_ids[client_id] = request_id - end + table.insert(supported_clients, client_id) end end) - -- if has client but no clients support the given method, call the callback with the proper - -- error message. + -- if has client but no clients support the given method, notify the user if not tbl_isempty(all_buffer_active_clients[resolve_bufnr(bufnr)] or {}) and not method_supported then - local unsupported_err = lsp._unsupported_method(method) - handler = handler or lsp.handlers[method] - if handler then - handler(unsupported_err, nil, {method=method, bufnr=bufnr}) - end + vim.notify(lsp._unsupported_method(method), vim.log.levels.ERROR) + vim.api.nvim_command("redraw") return end + local client_request_ids = {} + for_each_buffer_client(bufnr, function(client, client_id, resolved_bufnr) + local request_success, request_id = client.request(method, params, handler, resolved_bufnr) + -- This could only fail if the client shut down in the time since we looked + -- it up and we did the request, which should be rare. + if request_success then + client_request_ids[client_id] = request_id + end + end, supported_clients) + local function _cancel_all_requests() for client_id, request_id in pairs(client_request_ids) do local client = active_clients[client_id] @@ -1308,12 +1309,13 @@ function lsp.buf_request_all(bufnr, method, params, callback) local request_results = {} local result_count = 0 local expected_result_count = 0 - local cancel, client_request_ids - local set_expected_result_count = once(function() - for _ in pairs(client_request_ids) do - expected_result_count = expected_result_count + 1 - end + local set_expected_result_count = once(function () + for_each_buffer_client(bufnr, function(client) + if client.supports_method(method) then + expected_result_count = expected_result_count + 1 + end + end) end) local function _sync_handler(err, result, ctx) @@ -1326,7 +1328,7 @@ function lsp.buf_request_all(bufnr, method, params, callback) end end - client_request_ids, cancel = lsp.buf_request(bufnr, method, params, _sync_handler) + local _, cancel = lsp.buf_request(bufnr, method, params, _sync_handler) return cancel end @@ -1383,6 +1385,29 @@ function lsp.buf_notify(bufnr, method, params) return resp end + +---@private +local function adjust_start_col(lnum, line, items, encoding) + local min_start_char = nil + for _, item in pairs(items) do + if item.textEdit and item.textEdit.range.start.line == lnum - 1 then + if min_start_char and min_start_char ~= item.textEdit.range.start.character then + return nil + end + min_start_char = item.textEdit.range.start.character + end + end + if min_start_char then + if encoding == 'utf-8' then + return min_start_char + else + return vim.str_byteindex(line, min_start_char, encoding == 'utf-16') + end + else + return nil + end +end + --- Implements 'omnifunc' compatible LSP completion. --- ---@see |complete-functions| @@ -1392,7 +1417,7 @@ end ---@param findstart 0 or 1, decides behavior ---@param base If findstart=0, text to match against --- ----@returns (number) Decided by `findstart`: +---@returns (number) Decided by {findstart}: --- - findstart=0: column where the completion starts, or -2 or -3 --- - findstart=1: list of matches (actually just calls |complete()|) function lsp.omnifunc(findstart, base) @@ -1418,17 +1443,37 @@ function lsp.omnifunc(findstart, base) -- Get the start position of the current keyword local textMatch = vim.fn.match(line_to_cursor, '\\k*$') - local prefix = line_to_cursor:sub(textMatch+1) local params = util.make_position_params() local items = {} - lsp.buf_request(bufnr, 'textDocument/completion', params, function(err, result) + lsp.buf_request(bufnr, 'textDocument/completion', params, function(err, result, ctx) if err or not result or vim.fn.mode() ~= "i" then return end + + -- Completion response items may be relative to a position different than `textMatch`. + -- Concrete example, with sumneko/lua-language-server: + -- + -- require('plenary.asy| + -- ▲ ▲ ▲ + -- │ │ └── cursor_pos: 20 + -- │ └────── textMatch: 17 + -- └────────────── textEdit.range.start.character: 9 + -- .newText = 'plenary.async' + -- ^^^ + -- prefix (We'd remove everything not starting with `asy`, + -- so we'd eliminate the `plenary.async` result + -- + -- `adjust_start_col` is used to prefer the language server boundary. + -- + local client = lsp.get_client_by_id(ctx.client_id) + local encoding = client and client.offset_encoding or 'utf-16' + local candidates = util.extract_completion_items(result) + local startbyte = adjust_start_col(pos[1], line, candidates, encoding) or textMatch + local prefix = line:sub(startbyte + 1, pos[2]) local matches = util.text_document_completion_list_to_complete_items(result, prefix) -- TODO(ashkan): is this the best way to do this? vim.list_extend(items, matches) - vim.fn.complete(textMatch+1, items) + vim.fn.complete(startbyte + 1, items) end) -- Return -2 to signal that we should continue completion so that we can @@ -1534,5 +1579,34 @@ function lsp._with_extend(name, options, user_config) 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. +--- +--- The registry is a table where the key is a unique command name, +--- and the value is a function which is called if any LSP action +--- (code action, code lenses, ...) triggers the command. +--- +--- If a LSP response contains a command for which no matching entry is +--- available in this registry, the command will be executed via the LSP server +--- using `workspace/executeCommand`. +--- +--- The first argument to the function will be the `Command`: +-- Command +-- title: String +-- command: String +-- arguments?: any[] +-- +--- The second argument is the `ctx` of |lsp-handler| +lsp.commands = setmetatable({}, { + __newindex = function(tbl, key, value) + assert(type(key) == 'string', "The key for commands in `vim.lsp.commands` must be a string") + assert(type(value) == 'function', "Command added to `vim.lsp.commands` must be a function") + rawset(tbl, key, value) + end; +}) + + return lsp -- vim:sw=2 ts=2 et |