diff options
Diffstat (limited to 'runtime/lua/vim/lsp.lua')
-rw-r--r-- | runtime/lua/vim/lsp.lua | 425 |
1 files changed, 291 insertions, 134 deletions
diff --git a/runtime/lua/vim/lsp.lua b/runtime/lua/vim/lsp.lua index 1db9fd43ac..d45827b0d7 100644 --- a/runtime/lua/vim/lsp.lua +++ b/runtime/lua/vim/lsp.lua @@ -16,10 +16,7 @@ local validate = vim.validate local lsp = { protocol = protocol; - -- TODO(tjdevries): Add in the warning that `callbacks` is no longer supported. - -- util.warn_once("vim.lsp.callbacks is deprecated. Use vim.lsp.handlers instead.") handlers = default_handlers; - callbacks = default_handlers; buf = require'vim.lsp.buf'; diagnostic = require'vim.lsp.diagnostic'; @@ -41,11 +38,13 @@ lsp._request_name_to_capability = { ['textDocument/declaration'] = 'declaration'; ['textDocument/typeDefinition'] = 'type_definition'; ['textDocument/documentSymbol'] = 'document_symbol'; - ['textDocument/workspaceSymbol'] = 'workspace_symbol'; ['textDocument/prepareCallHierarchy'] = 'call_hierarchy'; ['textDocument/rename'] = 'rename'; ['textDocument/codeAction'] = 'code_action'; + ['textDocument/codeLens'] = 'code_lens'; + ['codeLens/resolve'] = 'code_lens_resolve'; ['workspace/executeCommand'] = 'execute_command'; + ['workspace/symbol'] = 'workspace_symbol'; ['textDocument/references'] = 'find_references'; ['textDocument/rangeFormatting'] = 'document_range_formatting'; ['textDocument/formatting'] = 'document_formatting'; @@ -219,8 +218,6 @@ local function validate_client_config(config) } validate { root_dir = { config.root_dir, is_dir, "directory" }; - -- TODO(remove-callbacks) - callbacks = { config.callbacks, "t", true }; handlers = { config.handlers, "t", true }; capabilities = { config.capabilities, "t", true }; cmd_cwd = { config.cmd_cwd, optional_validator(is_dir), "directory" }; @@ -233,14 +230,14 @@ local function validate_client_config(config) before_init = { config.before_init, "f", true }; offset_encoding = { config.offset_encoding, "s", true }; flags = { config.flags, "t", true }; + get_language_id = { config.get_language_id, "f", true }; } - - -- TODO(remove-callbacks) - if config.handlers and config.callbacks then - error(debug.traceback( - "Unable to configure LSP with both 'config.handlers' and 'config.callbacks'. Use 'config.handlers' exclusively." - )) - end + assert( + (not config.flags + or not config.flags.debounce_text_changes + or type(config.flags.debounce_text_changes) == 'number'), + "flags.debounce_text_changes must be nil or a number with the debounce time in milliseconds" + ) local cmd, cmd_args = lsp._cmd_parts(config.cmd) local offset_encoding = valid_encodings.UTF16 @@ -269,28 +266,202 @@ local function buf_get_full_text(bufnr) end --@private +--- Memoizes a function. On first run, the function return value is saved and +--- immediately returned on subsequent runs. If the function returns a multival, +--- only the first returned value will be memoized and returned. The function will only be run once, +--- even if it has side-effects. +--- +--@param fn (function) Function to run +--@returns (function) Memoized function +local function once(fn) + local value + local ran = false + return function(...) + if not ran then + value = fn(...) + ran = true + end + return value + end +end + + +local changetracking = {} +do + --- client_id → state + --- + --- state + --- pending_change?: function that the timer starts to trigger didChange + --- pending_changes: list of tables with the pending changesets; for incremental_sync only + --- use_incremental_sync: bool + --- buffers?: table (bufnr → lines); for incremental sync only + --- timer?: uv_timer + local state_by_client = {} + + function changetracking.init(client, bufnr) + local state = state_by_client[client.id] + if not state then + state = { + pending_changes = {}; + use_incremental_sync = ( + if_nil(client.config.flags.allow_incremental_sync, true) + and client.resolved_capabilities.text_document_did_change == protocol.TextDocumentSyncKind.Incremental + ); + } + state_by_client[client.id] = state + end + if not state.use_incremental_sync then + return + end + if not state.buffers then + state.buffers = {} + end + state.buffers[bufnr] = nvim_buf_get_lines(bufnr, 0, -1, true) + end + + function changetracking.reset_buf(client, bufnr) + local state = state_by_client[client.id] + if state then + changetracking._reset_timer(state) + if state.buffers then + state.buffers[bufnr] = nil + end + end + end + + function changetracking.reset(client_id) + local state = state_by_client[client_id] + if state then + state_by_client[client_id] = nil + changetracking._reset_timer(state) + end + end + + function changetracking.prepare(bufnr, firstline, new_lastline, changedtick) + local incremental_changes = function(client) + local cached_buffers = state_by_client[client.id].buffers + local lines = nvim_buf_get_lines(bufnr, 0, -1, true) + local startline = math.min(firstline + 1, math.min(#cached_buffers[bufnr], #lines)) + local endline = math.min(-(#lines - new_lastline), -1) + local incremental_change = vim.lsp.util.compute_diff( + cached_buffers[bufnr], lines, startline, endline, client.offset_encoding or 'utf-16') + cached_buffers[bufnr] = lines + return incremental_change + end + local full_changes = once(function() + return { + text = buf_get_full_text(bufnr); + }; + end) + local uri = vim.uri_from_bufnr(bufnr) + return function(client) + if client.resolved_capabilities.text_document_did_change == protocol.TextDocumentSyncKind.None then + return + end + local state = state_by_client[client.id] + local debounce = client.config.flags.debounce_text_changes + if not debounce then + local changes = state.use_incremental_sync and incremental_changes(client) or full_changes() + client.notify("textDocument/didChange", { + textDocument = { + uri = uri; + version = changedtick; + }; + contentChanges = { changes, } + }) + return + end + changetracking._reset_timer(state) + if state.use_incremental_sync then + -- This must be done immediately and cannot be delayed + -- The contents would further change and startline/endline may no longer fit + table.insert(state.pending_changes, incremental_changes(client)) + end + state.pending_change = function() + state.pending_change = nil + if client.is_stopped() then + return + end + local contentChanges + if state.use_incremental_sync then + contentChanges = state.pending_changes + state.pending_changes = {} + else + contentChanges = { full_changes(), } + end + client.notify("textDocument/didChange", { + textDocument = { + uri = uri; + version = changedtick; + }; + contentChanges = contentChanges + }) + end + state.timer = vim.loop.new_timer() + -- Must use schedule_wrap because `full_changes()` calls nvim_buf_get_lines + state.timer:start(debounce, 0, vim.schedule_wrap(state.pending_change)) + end + end + + function changetracking._reset_timer(state) + if state.timer then + state.timer:stop() + state.timer:close() + state.timer = nil + end + end + + --- Flushes any outstanding change notification. + function changetracking.flush(client) + local state = state_by_client[client.id] + if state then + changetracking._reset_timer(state) + if state.pending_change then + state.pending_change() + end + end + end +end + + +--@private --- Default handler for the 'textDocument/didOpen' LSP notification. --- --@param bufnr (Number) Number of the buffer, or 0 for current --@param client Client object local function text_document_did_open_handler(bufnr, client) + changetracking.init(client, bufnr) if not client.resolved_capabilities.text_document_open_close then return end if not vim.api.nvim_buf_is_loaded(bufnr) then return end + local filetype = nvim_buf_get_option(bufnr, 'filetype') + local params = { textDocument = { version = 0; uri = vim.uri_from_bufnr(bufnr); - -- TODO make sure our filetypes are compatible with languageId names. - languageId = nvim_buf_get_option(bufnr, 'filetype'); + languageId = client.config.get_language_id(bufnr, filetype); text = buf_get_full_text(bufnr); } } client.notify('textDocument/didOpen', params) util.buf_versions[bufnr] = params.textDocument.version + + -- Next chance we get, we should re-do the diagnostics + vim.schedule(function() + vim.lsp.handlers["textDocument/publishDiagnostics"]( + nil, + "textDocument/publishDiagnostics", + { + diagnostics = vim.lsp.diagnostic.get(bufnr, client.id), + uri = vim.uri_from_bufnr(bufnr), + }, + client.id + ) + end) end -- FIXME: DOC: Shouldn't need to use a dummy function @@ -412,6 +583,9 @@ end --- --@param name (string, default=client-id) Name in log messages. --- +--@param get_language_id function(bufnr, filetype) -> language ID as string. +--- Defaults to the filetype. +--- --@param offset_encoding (default="utf-16") One of "utf-8", "utf-16", --- or "utf-32" which is the encoding that the LSP server expects. Client does --- not verify this is correct. @@ -450,16 +624,10 @@ end --@param trace: "off" | "messages" | "verbose" | nil passed directly to the language --- server in the initialize request. Invalid/empty values will default to "off" --@param flags: A table with flags for the client. The current (experimental) flags are: ---- - allow_incremental_sync (bool, default false): Allow using on_line callbacks for lsp ---- ---- <pre> ---- -- In attach function for the client, you can do: ---- local custom_attach = function(client) ---- if client.config.flags then ---- client.config.flags.allow_incremental_sync = true ---- end ---- end ---- </pre> +--- - allow_incremental_sync (bool, default true): Allow using incremental sync for buffer edits +--- - debounce_text_changes (number, default nil): Debounce didChange +--- notifications to the server by the given number in milliseconds. No debounce +--- occurs if nil --- --@returns Client id. |vim.lsp.get_client_by_id()| Note: client may not be --- fully initialized. Use `on_init` to do any actions once @@ -471,10 +639,14 @@ function lsp.start_client(config) config.flags = config.flags or {} config.settings = config.settings or {} + -- By default, get_language_id just returns the exact filetype it is passed. + -- It is possible to pass in something that will calculate a different filetype, + -- to be sent by the client. + config.get_language_id = config.get_language_id or function(_, filetype) return filetype end + local client_id = next_client_id() - -- TODO(remove-callbacks) - local handlers = config.handlers or config.callbacks or {} + local handlers = config.handlers or {} local name = config.name or tostring(client_id) local log_prefix = string.format("LSP[%s]", name) @@ -548,19 +720,21 @@ function lsp.start_client(config) function dispatch.on_exit(code, signal) active_clients[client_id] = nil uninitialized_clients[client_id] = nil - local active_buffers = {} - for bufnr, client_ids in pairs(all_buffer_active_clients) do - if client_ids[client_id] then - table.insert(active_buffers, bufnr) - end + + 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 - -- Buffer level cleanup - vim.schedule(function() - for _, bufnr in ipairs(active_buffers) do - lsp.diagnostic.clear(bufnr) - end - 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", client_id, code, signal) + vim.schedule(function() + vim.notify(msg, vim.log.levels.WARN) + end) + end + if config.on_exit then pcall(config.on_exit, code, signal, client_id) end @@ -579,8 +753,6 @@ function lsp.start_client(config) offset_encoding = offset_encoding; config = config; - -- TODO(remove-callbacks) - callbacks = handlers; handlers = handlers; -- for $/progress report messages = { name = name, messages = {}, progress = {}, status = {} } @@ -709,6 +881,9 @@ function lsp.start_client(config) handler = resolve_handler(method) or error(string.format("not found: %q request handler for client %q.", method, client.name)) end + -- Ensure pending didChange notifications are sent so that the server doesn't operate on a stale state + changetracking.flush(client) + 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, method, result, client_id, bufnr) @@ -751,6 +926,14 @@ function lsp.start_client(config) --- --@param force (bool, optional) function client.stop(force) + + 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 + local handle = rpc.handle if handle:is_closing() then return @@ -797,25 +980,10 @@ function lsp.start_client(config) end --@private ---- Memoizes a function. On first run, the function return value is saved and ---- immediately returned on subsequent runs. ---- ---@param fn (function) Function to run ---@returns (function) Memoized function -local function once(fn) - local value - return function(...) - if not value then value = fn(...) end - return value - end -end - ---@private --@fn text_document_did_change_handler(_, bufnr, changedtick, firstline, lastline, new_lastline, old_byte_size, old_utf32_size, old_utf16_size) --- Notify all attached clients that a buffer has changed. local text_document_did_change_handler do - local encoding_index = { ["utf-8"] = 1; ["utf-16"] = 2; ["utf-32"] = 3; } text_document_did_change_handler = function(_, bufnr, changedtick, firstline, lastline, new_lastline, old_byte_size, old_utf32_size, old_utf16_size) @@ -830,60 +998,9 @@ do if tbl_isempty(all_buffer_active_clients[bufnr] or {}) then return end - util.buf_versions[bufnr] = changedtick - -- Lazy initialize these because clients may not even need them. - local incremental_changes = once(function(client) - local size_index = encoding_index[client.offset_encoding] - local length = select(size_index, old_byte_size, old_utf16_size, old_utf32_size) - local lines = nvim_buf_get_lines(bufnr, firstline, new_lastline, true) - - -- This is necessary because we are specifying the full line including the - -- newline in range. Therefore, we must replace the newline as well. - if #lines > 0 then - table.insert(lines, '') - end - return { - range = { - start = { line = firstline, character = 0 }; - ["end"] = { line = lastline, character = 0 }; - }; - rangeLength = length; - text = table.concat(lines, '\n'); - }; - end) - local full_changes = once(function() - return { - text = buf_get_full_text(bufnr); - }; - end) - local uri = vim.uri_from_bufnr(bufnr) - for_each_buffer_client(bufnr, function(client, _client_id) - local allow_incremental_sync = if_nil(client.config.flags.allow_incremental_sync, false) - - local text_document_did_change = client.resolved_capabilities.text_document_did_change - local changes - if text_document_did_change == protocol.TextDocumentSyncKind.None then - return - --[=[ TODO(ashkan) there seem to be problems with the byte_sizes sent by - -- neovim right now so only send the full content for now. In general, we - -- can assume that servers *will* support both versions anyway, as there - -- is no way to specify the sync capability by the client. - -- See https://github.com/palantir/python-language-server/commit/cfd6675bc10d5e8dbc50fc50f90e4a37b7178821#diff-f68667852a14e9f761f6ebf07ba02fc8 for an example of pyls handling both. - --]=] - elseif not allow_incremental_sync or text_document_did_change == protocol.TextDocumentSyncKind.Full then - changes = full_changes(client) - elseif text_document_did_change == protocol.TextDocumentSyncKind.Incremental then - changes = incremental_changes(client) - end - client.notify("textDocument/didChange", { - textDocument = { - uri = uri; - version = changedtick; - }; - contentChanges = { changes; } - }) - end) + local compute_change_and_notify = changetracking.prepare(bufnr, firstline, new_lastline, changedtick) + for_each_buffer_client(bufnr, compute_change_and_notify) end end @@ -928,16 +1045,32 @@ function lsp.buf_attach_client(bufnr, client_id) all_buffer_active_clients[bufnr] = buffer_client_ids local uri = vim.uri_from_bufnr(bufnr) - nvim_command(string.format("autocmd BufWritePost <buffer=%d> lua vim.lsp._text_document_did_save_handler(0)", bufnr)) + local buf_did_save_autocommand = [=[ + augroup lsp_c_%d_b_%d_did_save + au! + au BufWritePost <buffer=%d> lua vim.lsp._text_document_did_save_handler(0) + augroup END + ]=] + vim.api.nvim_exec(string.format(buf_did_save_autocommand, client_id, bufnr, bufnr), false) -- First time, so attach and set up stuff. vim.api.nvim_buf_attach(bufnr, false, { on_lines = text_document_did_change_handler; + on_reload = function() + local params = { textDocument = { uri = uri; } } + for_each_buffer_client(bufnr, function(client, _) + if client.resolved_capabilities.text_document_open_close then + client.notify('textDocument/didClose', params) + end + text_document_did_open_handler(bufnr, client) + end) + end; on_detach = function() local params = { textDocument = { uri = uri; } } for_each_buffer_client(bufnr, function(client, _) if client.resolved_capabilities.text_document_open_close then client.notify('textDocument/didClose', params) end + changetracking.reset_buf(client, bufnr) end) util.buf_versions[bufnr] = nil all_buffer_active_clients[bufnr] = nil @@ -1016,23 +1149,12 @@ end function lsp.stop_client(client_id, force) local ids = type(client_id) == 'table' and client_id or {client_id} for _, id in ipairs(ids) do - local resolved_client_id if type(id) == 'table' and id.stop ~= nil then id.stop(force) - resolved_client_id = id.id elseif active_clients[id] then active_clients[id].stop(force) - resolved_client_id = id elseif uninitialized_clients[id] then uninitialized_clients[id].stop(true) - resolved_client_id = id - end - if resolved_client_id then - local client_buffers = lsp.get_buffers_by_client_id(resolved_client_id) - for idx = 1, #client_buffers do - lsp.diagnostic.clear(client_buffers[idx], resolved_client_id) - end - all_client_active_buffers[resolved_client_id] = nil end end end @@ -1150,9 +1272,49 @@ function lsp.buf_request(bufnr, method, params, handler) return client_request_ids, _cancel_all_requests end ---- Sends a request to a server and waits for the response. +---Sends an async request for all active clients attached to the buffer. +---Executes the callback on the combined result. +---Parameters are the same as |vim.lsp.buf_request()| but the return result and callback are +---different. +--- +--@param bufnr (number) Buffer handle, or 0 for current. +--@param method (string) LSP method name +--@param params (optional, table) Parameters to send to the server +--@param callback (function) The callback to call when all requests are finished. +-- Unlike `buf_request`, this will collect all the responses from each server instead of handling them. +-- A map of client_id:request_result will be provided to the callback +-- +--@returns (function) A function that will cancel all requests which is the same as the one returned from `buf_request`. +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 + end) + + local function _sync_handler(err, _, result, client_id) + request_results[client_id] = { error = err, result = result } + result_count = result_count + 1 + set_expected_result_count() + + if result_count >= expected_result_count then + callback(request_results) + end + end + + client_request_ids, cancel = lsp.buf_request(bufnr, method, params, _sync_handler) + + return cancel +end + +--- Sends a request to all server and waits for the response of all of them. --- ---- Calls |vim.lsp.buf_request()| but blocks Nvim while awaiting the result. +--- Calls |vim.lsp.buf_request_all()| but blocks Nvim while awaiting the result. --- Parameters are the same as |vim.lsp.buf_request()| but the return result is --- different. Wait maximum of {timeout_ms} (default 100) ms. --- @@ -1166,26 +1328,21 @@ end --- returns `(nil, err)` where `err` is a string describing the failure --- reason. function lsp.buf_request_sync(bufnr, method, params, timeout_ms) - local request_results = {} - local result_count = 0 - local function _sync_handler(err, _, result, client_id) - request_results[client_id] = { error = err, result = result } - result_count = result_count + 1 - end - local client_request_ids, cancel = lsp.buf_request(bufnr, method, params, _sync_handler) - local expected_result_count = 0 - for _ in pairs(client_request_ids) do - expected_result_count = expected_result_count + 1 - end + local request_results + + local cancel = lsp.buf_request_all(bufnr, method, params, function(it) + request_results = it + end) local wait_result, reason = vim.wait(timeout_ms or 100, function() - return result_count >= expected_result_count + return request_results ~= nil end, 10) if not wait_result then cancel() return nil, wait_result_reason[reason] end + return request_results end |