From 00dc12c5d8454a2d3c6806710f63bbb446076e96 Mon Sep 17 00:00:00 2001 From: Ashkan Kiani Date: Wed, 13 Nov 2019 12:55:26 -0800 Subject: lua LSP client: initial implementation (#11336) Mainly configuration and RPC infrastructure can be considered "done". Specific requests and their callbacks will be improved later (and also served by plugins). There are also some TODO:s for the client itself, like incremental updates. Co-authored by at-tjdevries and at-h-michael, with many review/suggestion contributions. --- runtime/lua/vim/lsp.lua | 1055 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 1055 insertions(+) create mode 100644 runtime/lua/vim/lsp.lua (limited to 'runtime/lua/vim/lsp.lua') diff --git a/runtime/lua/vim/lsp.lua b/runtime/lua/vim/lsp.lua new file mode 100644 index 0000000000..9dbe03dace --- /dev/null +++ b/runtime/lua/vim/lsp.lua @@ -0,0 +1,1055 @@ +local builtin_callbacks = require 'vim.lsp.builtin_callbacks' +local log = require 'vim.lsp.log' +local lsp_rpc = require 'vim.lsp.rpc' +local protocol = require 'vim.lsp.protocol' +local util = require 'vim.lsp.util' + +local nvim_err_writeln, nvim_buf_get_lines, nvim_command, nvim_buf_get_option + = vim.api.nvim_err_writeln, vim.api.nvim_buf_get_lines, vim.api.nvim_command, vim.api.nvim_buf_get_option +local uv = vim.loop +local tbl_isempty, tbl_extend = vim.tbl_isempty, vim.tbl_extend +local validate = vim.validate + +local lsp = { + protocol = protocol; + builtin_callbacks = builtin_callbacks; + util = util; + -- Allow raw RPC access. + rpc = lsp_rpc; + -- Export these directly from rpc. + rpc_response_error = lsp_rpc.rpc_response_error; + -- You probably won't need this directly, since __tostring is set for errors + -- by the RPC. + -- format_rpc_error = lsp_rpc.format_rpc_error; +} + +-- TODO consider whether 'eol' or 'fixeol' should change the nvim_buf_get_lines that send. +-- TODO improve handling of scratch buffers with LSP attached. + +local function resolve_bufnr(bufnr) + validate { bufnr = { bufnr, 'n', true } } + if bufnr == nil or bufnr == 0 then + return vim.api.nvim_get_current_buf() + end + return bufnr +end + +local function is_dir(filename) + validate{filename={filename,'s'}} + local stat = uv.fs_stat(filename) + return stat and stat.type == 'directory' or false +end + +-- TODO Use vim.wait when that is available, but provide an alternative for now. +local wait = vim.wait or function(timeout_ms, condition, interval) + validate { + timeout_ms = { timeout_ms, 'n' }; + condition = { condition, 'f' }; + interval = { interval, 'n', true }; + } + assert(timeout_ms > 0, "timeout_ms must be > 0") + local _ = log.debug() and log.debug("wait.fallback", timeout_ms) + interval = interval or 200 + local interval_cmd = "sleep "..interval.."m" + local timeout = timeout_ms + uv.now() + -- TODO is there a better way to sync this? + while true do + uv.update_time() + if condition() then + return 0 + end + if uv.now() >= timeout then + return -1 + end + nvim_command(interval_cmd) + -- vim.loop.sleep(10) + end +end +local wait_result_reason = { [-1] = "timeout"; [-2] = "interrupted"; [-3] = "error" } + +local valid_encodings = { + ["utf-8"] = 'utf-8'; ["utf-16"] = 'utf-16'; ["utf-32"] = 'utf-32'; + ["utf8"] = 'utf-8'; ["utf16"] = 'utf-16'; ["utf32"] = 'utf-32'; + UTF8 = 'utf-8'; UTF16 = 'utf-16'; UTF32 = 'utf-32'; +} + +local client_index = 0 +local function next_client_id() + client_index = client_index + 1 + return client_index +end +-- Tracks all clients created via lsp.start_client +local active_clients = {} +local all_buffer_active_clients = {} +local uninitialized_clients = {} + +local function for_each_buffer_client(bufnr, callback) + validate { + callback = { callback, 'f' }; + } + bufnr = resolve_bufnr(bufnr) + local client_ids = all_buffer_active_clients[bufnr] + if not client_ids or tbl_isempty(client_ids) then + return + end + for client_id in pairs(client_ids) do + -- This is unlikely to happen. Could only potentially happen in a race + -- condition between literally a single statement. + -- We could skip this error, but let's error for now. + local client = active_clients[client_id] + -- or error(string.format("Client %d has already shut down.", client_id)) + if client then + callback(client, client_id) + end + end +end + +-- Error codes to be used with `on_error` from |vim.lsp.start_client|. +-- Can be used to look up the string from a the number or the number +-- from the string. +lsp.client_errors = tbl_extend("error", lsp_rpc.client_errors, vim.tbl_add_reverse_lookup { + ON_INIT_CALLBACK_ERROR = table.maxn(lsp_rpc.client_errors) + 1; +}) + +local function validate_encoding(encoding) + validate { + encoding = { encoding, 's' }; + } + return valid_encodings[encoding:lower()] + or error(string.format("Invalid offset encoding %q. Must be one of: 'utf-8', 'utf-16', 'utf-32'", encoding)) +end + +local function validate_command(input) + local cmd, cmd_args + if type(input) == 'string' then + -- Use a shell to execute the command if it is a string. + cmd = vim.api.nvim_get_option('shell') + cmd_args = {vim.api.nvim_get_option('shellcmdflag'), input} + elseif vim.tbl_islist(input) then + cmd = input[1] + cmd_args = {} + -- Don't mutate our input. + for i, v in ipairs(input) do + assert(type(v) == 'string', "input arguments must be strings") + if i > 1 then + table.insert(cmd_args, v) + end + end + else + error("cmd type must be string or list.") + end + return cmd, cmd_args +end + +local function optional_validator(fn) + return function(v) + return v == nil or fn(v) + end +end + +local function validate_client_config(config) + validate { + config = { config, 't' }; + } + validate { + root_dir = { config.root_dir, is_dir, "directory" }; + callbacks = { config.callbacks, "t", true }; + capabilities = { config.capabilities, "t", true }; + -- cmd = { config.cmd, "s", false }; + cmd_cwd = { config.cmd_cwd, optional_validator(is_dir), "directory" }; + cmd_env = { config.cmd_env, "f", true }; + name = { config.name, 's', true }; + on_error = { config.on_error, "f", true }; + on_exit = { config.on_exit, "f", true }; + on_init = { config.on_init, "f", true }; + offset_encoding = { config.offset_encoding, "s", true }; + } + local cmd, cmd_args = validate_command(config.cmd) + local offset_encoding = valid_encodings.UTF16 + if config.offset_encoding then + offset_encoding = validate_encoding(config.offset_encoding) + end + return { + cmd = cmd; cmd_args = cmd_args; + offset_encoding = offset_encoding; + } +end + +local function text_document_did_open_handler(bufnr, client) + 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 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'); + text = table.concat(nvim_buf_get_lines(bufnr, 0, -1, false), '\n'); + } + } + client.notify('textDocument/didOpen', params) +end + + +--- Start a client and initialize it. +-- Its arguments are passed via a configuration object. +-- +-- Mandatory parameters: +-- +-- root_dir: {string} specifying the directory where the LSP server will base +-- as its rootUri on initialization. +-- +-- cmd: {string} or {list} which is the base command to execute for the LSP. A +-- string will be run using |'shell'| and a list will be interpreted as a bare +-- command with arguments passed. This is the same as |jobstart()|. +-- +-- Optional parameters: + +-- cmd_cwd: {string} specifying the directory to launch the `cmd` process. This +-- is not related to `root_dir`. By default, |getcwd()| is used. +-- +-- cmd_env: {table} specifying the environment flags to pass to the LSP on +-- spawn. This can be specified using keys like a map or as a list with `k=v` +-- pairs or both. Non-string values are coerced to a string. +-- For example: `{ "PRODUCTION=true"; "TEST=123"; PORT = 8080; HOST = "0.0.0.0"; }`. +-- +-- capabilities: A {table} which will be used instead of +-- `vim.lsp.protocol.make_client_capabilities()` which contains neovim's +-- default capabilities and passed to the language server on initialization. +-- You'll probably want to use make_client_capabilities() and modify the +-- result. +-- NOTE: +-- To send an empty dictionary, you should use +-- `{[vim.type_idx]=vim.types.dictionary}` Otherwise, it will be encoded as +-- an array. +-- +-- callbacks: A {table} of whose keys are language server method names and the +-- values are `function(err, method, params, client_id)`. +-- This will be called for: +-- - notifications from the server, where `err` will always be `nil` +-- - requests initiated by the server. For these, you can respond by returning +-- two values: `result, err`. The err must be in the format of an RPC error, +-- which is `{ code, message, data? }`. You can use |vim.lsp.rpc_response_error()| +-- to help with this. +-- - as a callback for requests initiated by the client if the request doesn't +-- explicitly specify a callback. +-- +-- init_options: A {table} of values to pass in the initialization request +-- as `initializationOptions`. See the `initialize` in the LSP spec. +-- +-- name: A {string} used in log messages. Defaults to {client_id} +-- +-- offset_encoding: One of 'utf-8', 'utf-16', or 'utf-32' which is the +-- encoding that the LSP server expects. By default, it is 'utf-16' as +-- specified in the LSP specification. The client does not verify this +-- is correct. +-- +-- on_error(code, ...): A function for handling errors thrown by client +-- operation. {code} is a number describing the error. Other arguments may be +-- passed depending on the error kind. @see |vim.lsp.client_errors| for +-- possible errors. `vim.lsp.client_errors[code]` can be used to retrieve a +-- human understandable string. +-- +-- on_init(client, initialize_result): A function which is called after the +-- request `initialize` is completed. `initialize_result` contains +-- `capabilities` and anything else the server may send. For example, `clangd` +-- sends `result.offsetEncoding` if `capabilities.offsetEncoding` was sent to +-- it. +-- +-- on_exit(code, signal, client_id): A function which is called after the +-- client has exited. code is the exit code of the process, and signal is a +-- number describing the signal used to terminate (if any). +-- +-- on_attach(client, bufnr): A function which is called after the client is +-- attached to a buffer. +-- +-- trace: 'off' | 'messages' | 'verbose' | nil passed directly to the language +-- server in the initialize request. Invalid/empty values will default to 'off' +-- +-- @returns client_id You can use |vim.lsp.get_client_by_id()| to get the +-- actual client. +-- +-- NOTE: The client is only available *after* it has been initialized, which +-- may happen after a small delay (or never if there is an error). +-- For this reason, you may want to use `on_init` to do any actions once the +-- client has been initialized. +function lsp.start_client(config) + local cleaned_config = validate_client_config(config) + local cmd, cmd_args, offset_encoding = cleaned_config.cmd, cleaned_config.cmd_args, cleaned_config.offset_encoding + + local client_id = next_client_id() + + local callbacks = tbl_extend("keep", config.callbacks or {}, builtin_callbacks) + -- Copy metatable if it has one. + if config.callbacks and config.callbacks.__metatable then + setmetatable(callbacks, getmetatable(config.callbacks)) + end + local name = config.name or tostring(client_id) + local log_prefix = string.format("LSP[%s]", name) + + local handlers = {} + + function handlers.notification(method, params) + local _ = log.debug() and log.debug('notification', method, params) + local callback = callbacks[method] + if callback then + -- Method name is provided here for convenience. + callback(nil, method, params, client_id) + end + end + + function handlers.server_request(method, params) + local _ = log.debug() and log.debug('server_request', method, params) + local callback = callbacks[method] + if callback then + local _ = log.debug() and log.debug("server_request: found callback for", method) + return callback(nil, method, params, client_id) + end + local _ = log.debug() and log.debug("server_request: no callback found for", method) + return nil, lsp.rpc_response_error(protocol.ErrorCodes.MethodNotFound) + end + + function handlers.on_error(code, err) + local _ = log.error() and log.error(log_prefix, "on_error", { code = lsp.client_errors[code], err = err }) + nvim_err_writeln(string.format('%s: Error %s: %q', log_prefix, lsp.client_errors[code], vim.inspect(err))) + if config.on_error then + local status, usererr = pcall(config.on_error, code, err) + if not status then + local _ = log.error() and log.error(log_prefix, "user on_error failed", { err = usererr }) + nvim_err_writeln(log_prefix.." user on_error failed: "..tostring(usererr)) + end + end + end + + function handlers.on_exit(code, signal) + active_clients[client_id] = nil + uninitialized_clients[client_id] = nil + for _, client_ids in pairs(all_buffer_active_clients) do + client_ids[client_id] = nil + end + if config.on_exit then + pcall(config.on_exit, code, signal, client_id) + end + end + + -- Start the RPC client. + local rpc = lsp_rpc.start(cmd, cmd_args, handlers, { + cwd = config.cmd_cwd; + env = config.cmd_env; + }) + + local client = { + id = client_id; + name = name; + rpc = rpc; + offset_encoding = offset_encoding; + callbacks = callbacks; + config = config; + } + + -- Store the uninitialized_clients for cleanup in case we exit before + -- initialize finishes. + uninitialized_clients[client_id] = client; + + local function initialize() + local valid_traces = { + off = 'off'; messages = 'messages'; verbose = 'verbose'; + } + local initialize_params = { + -- The process Id of the parent process that started the server. Is null if + -- the process has not been started by another process. If the parent + -- process is not alive then the server should exit (see exit notification) + -- its process. + processId = uv.getpid(); + -- The rootPath of the workspace. Is null if no folder is open. + -- + -- @deprecated in favour of rootUri. + rootPath = nil; + -- The rootUri of the workspace. Is null if no folder is open. If both + -- `rootPath` and `rootUri` are set `rootUri` wins. + rootUri = vim.uri_from_fname(config.root_dir); +-- rootUri = vim.uri_from_fname(vim.fn.expand("%:p:h")); + -- User provided initialization options. + initializationOptions = config.init_options; + -- The capabilities provided by the client (editor or tool) + capabilities = config.capabilities or protocol.make_client_capabilities(); + -- The initial trace setting. If omitted trace is disabled ('off'). + -- trace = 'off' | 'messages' | 'verbose'; + trace = valid_traces[config.trace] or 'off'; + -- The workspace folders configured in the client when the server starts. + -- This property is only available if the client supports workspace folders. + -- It can be `null` if the client supports workspace folders but none are + -- configured. + -- + -- Since 3.6.0 + -- workspaceFolders?: WorkspaceFolder[] | null; + -- export interface WorkspaceFolder { + -- -- The associated URI for this workspace folder. + -- uri + -- -- The name of the workspace folder. Used to refer to this + -- -- workspace folder in the user interface. + -- name + -- } + workspaceFolders = nil; + } + local _ = log.debug() and log.debug(log_prefix, "initialize_params", initialize_params) + 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', {}) + client.initialized = true + uninitialized_clients[client_id] = nil + client.server_capabilities = assert(result.capabilities, "initialize result doesn't contain capabilities") + -- These are the cleaned up capabilities we use for dynamically deciding + -- when to send certain events to clients. + client.resolved_capabilities = protocol.resolve_capabilities(client.server_capabilities) + if config.on_init then + local status, err = pcall(config.on_init, client, result) + if not status then + pcall(handlers.on_error, lsp.client_errors.ON_INIT_CALLBACK_ERROR, err) + end + end + local _ = log.debug() and log.debug(log_prefix, "server_capabilities", client.server_capabilities) + local _ = log.info() and log.info(log_prefix, "initialized", { resolved_capabilities = client.resolved_capabilities }) + + -- Only assign after initialized. + active_clients[client_id] = client + -- If we had been registered before we start, then send didOpen This can + -- happen if we attach to buffers before initialize finishes or if + -- someone restarts a client. + for bufnr, client_ids in pairs(all_buffer_active_clients) do + if client_ids[client_id] then + client._on_attach(bufnr) + end + end + end) + end + + local function unsupported_method(method) + local msg = "server doesn't support "..method + local _ = log.warn() and log.warn(msg) + nvim_err_writeln(msg) + return lsp.rpc_response_error(protocol.ErrorCodes.MethodNotFound, msg) + end + + --- Checks capabilities before rpc.request-ing. + function client.request(method, params, callback) + if not callback then + callback = client.callbacks[method] + or error(string.format("request callback is empty and no default was found for client %s", client.name)) + end + local _ = log.debug() and log.debug(log_prefix, "client.request", client_id, method, params, callback) + -- TODO keep these checks or just let it go anyway? + if (not client.resolved_capabilities.hover and method == 'textDocument/hover') + or (not client.resolved_capabilities.signature_help and method == 'textDocument/signatureHelp') + or (not client.resolved_capabilities.goto_definition and method == 'textDocument/definition') + or (not client.resolved_capabilities.implementation and method == 'textDocument/implementation') + then + callback(unsupported_method(method), method, nil, client_id) + return + end + return rpc.request(method, params, function(err, result) + callback(err, method, result, client_id) + end) + end + + function client.notify(...) + return rpc.notify(...) + end + + function client.cancel_request(id) + validate{id = {id, 'n'}} + return rpc.notify("$/cancelRequest", { id = id }) + end + + -- Track this so that we can escalate automatically if we've alredy tried a + -- graceful shutdown + local tried_graceful_shutdown = false + function client.stop(force) + local handle = rpc.handle + if handle:is_closing() then + return + end + if force or (not client.initialized) or tried_graceful_shutdown then + handle:kill(15) + return + end + tried_graceful_shutdown = true + -- Sending a signal after a process has exited is acceptable. + rpc.request('shutdown', nil, function(err, _) + if err == nil then + rpc.notify('exit') + else + -- If there was an error in the shutdown request, then term to be safe. + handle:kill(15) + end + end) + end + + function client.is_stopped() + return rpc.handle:is_closing() + end + + function client._on_attach(bufnr) + text_document_did_open_handler(bufnr, client) + if config.on_attach then + -- TODO(ashkan) handle errors. + pcall(config.on_attach, client, bufnr) + end + end + + initialize() + + return client_id +end + +local function once(fn) + local value + return function(...) + if not value then value = fn(...) end + return value + end +end + +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) + local _ = log.debug() and log.debug("on_lines", bufnr, changedtick, firstline, + lastline, new_lastline, old_byte_size, old_utf32_size, old_utf16_size, nvim_buf_get_lines(bufnr, firstline, new_lastline, true)) + if old_byte_size == 0 then + return + end + -- Don't do anything if there are no clients attached. + if tbl_isempty(all_buffer_active_clients[bufnr] or {}) then + return + end + -- 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 = table.concat(nvim_buf_get_lines(bufnr, 0, -1, false), "\n"); + }; + end) + local uri = vim.uri_from_bufnr(bufnr) + for_each_buffer_client(bufnr, function(client, _client_id) + 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 true 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) + end +end + +-- Buffer lifecycle handler for textDocument/didSave +function lsp._text_document_did_save_handler(bufnr) + bufnr = resolve_bufnr(bufnr) + local uri = vim.uri_from_bufnr(bufnr) + local text = once(function() + return table.concat(nvim_buf_get_lines(bufnr, 0, -1, false), '\n') + end) + for_each_buffer_client(bufnr, function(client, _client_id) + if client.resolved_capabilities.text_document_save then + local included_text + if client.resolved_capabilities.text_document_save_include_text then + included_text = text() + end + client.notify('textDocument/didSave', { + textDocument = { + uri = uri; + text = included_text; + } + }) + end + end) +end + +-- Implements the textDocument/did* notifications required to track a buffer +-- for any language server. +-- @param bufnr [number] buffer handle or 0 for current +-- @param client_id [number] the client id +function lsp.buf_attach_client(bufnr, client_id) + validate { + bufnr = {bufnr, 'n', true}; + client_id = {client_id, 'n'}; + } + bufnr = resolve_bufnr(bufnr) + local buffer_client_ids = all_buffer_active_clients[bufnr] + -- This is our first time attaching to this buffer. + if not buffer_client_ids then + buffer_client_ids = {} + all_buffer_active_clients[bufnr] = buffer_client_ids + + local uri = vim.uri_from_bufnr(bufnr) + nvim_command(string.format("autocmd BufWritePost lua vim.lsp._text_document_did_save_handler(0)", bufnr)) + -- First time, so attach and set up stuff. + vim.api.nvim_buf_attach(bufnr, false, { + on_lines = text_document_did_change_handler; + on_detach = function() + local params = { textDocument = { uri = uri; } } + for_each_buffer_client(bufnr, function(client, _client_id) + if client.resolved_capabilities.text_document_open_close then + client.notify('textDocument/didClose', params) + end + end) + all_buffer_active_clients[bufnr] = nil + end; + -- TODO if we know all of the potential clients ahead of time, then we + -- could conditionally set this. + -- utf_sizes = size_index > 1; + utf_sizes = true; + }) + end + 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 + + local client = active_clients[client_id] + -- Send didOpen for the client if it is initialized. If it isn't initialized + -- then it will send didOpen on initialize. + if client then + client._on_attach(bufnr) + end + return true +end + +-- Check if a buffer is attached for a particular client. +-- @param bufnr [number] buffer handle or 0 for current +-- @param client_id [number] the client id +function lsp.buf_is_attached(bufnr, client_id) + return (all_buffer_active_clients[bufnr] or {})[client_id] == true +end + +-- Look up an active client by its id, returns nil if it is not yet initialized +-- or is not a valid id. +-- @param client_id number the client id. +function lsp.get_client_by_id(client_id) + return active_clients[client_id] +end + +-- Stop a client by its id, optionally with force. +-- You can also use the `stop()` function on a client if you already have +-- access to it. +-- By default, it will just ask the server to shutdown without force. +-- If you request to stop a client which has previously been requested to shutdown, +-- it will automatically force shutdown. +-- @param client_id number the client id. +-- @param force boolean (optional) whether to use force or request shutdown +function lsp.stop_client(client_id, force) + local client + client = active_clients[client_id] + if client then + client.stop(force) + return + end + client = uninitialized_clients[client_id] + if client then + client.stop(true) + end +end + +-- Returns a list of all the active clients. +function lsp.get_active_clients() + return vim.tbl_values(active_clients) +end + +-- Stop all the clients, optionally with force. +-- You can also use the `stop()` function on a client if you already have +-- access to it. +-- By default, it will just ask the server to shutdown without force. +-- If you request to stop a client which has previously been requested to shutdown, +-- it will automatically force shutdown. +-- @param force boolean (optional) whether to use force or request shutdown +function lsp.stop_all_clients(force) + for _, client in pairs(uninitialized_clients) do + client.stop(true) + end + for _, client in pairs(active_clients) do + client.stop(force) + end +end + +function lsp._vim_exit_handler() + log.info("exit_handler", active_clients) + for _, client in pairs(uninitialized_clients) do + client.stop(true) + end + -- TODO handle v:dying differently? + if tbl_isempty(active_clients) then + return + end + for _, client in pairs(active_clients) do + client.stop() + end + local wait_result = wait(500, function() return tbl_isempty(active_clients) end, 50) + if wait_result ~= 0 then + for _, client in pairs(active_clients) do + client.stop(true) + end + end +end + +nvim_command("autocmd VimLeavePre * lua vim.lsp._vim_exit_handler()") + +--- +--- Buffer level client functions. +--- + +--- Send a request to a server and return the response +-- @param bufnr [number] Buffer handle or 0 for current. +-- @param method [string] Request method name +-- @param params [table|nil] Parameters to send to the server +-- @param callback [function|nil] Request callback (or uses the client's callbacks) +-- +-- @returns: client_request_ids, cancel_all_requests +function lsp.buf_request(bufnr, method, params, callback) + validate { + bufnr = { bufnr, 'n', true }; + method = { method, 's' }; + callback = { callback, 'f', true }; + } + local client_request_ids = {} + for_each_buffer_client(bufnr, function(client, client_id) + local request_success, request_id = client.request(method, params, callback) + + -- 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) + + local function cancel_all_requests() + for client_id, request_id in pairs(client_request_ids) do + local client = active_clients[client_id] + client.cancel_request(request_id) + end + end + + return client_request_ids, cancel_all_requests +end + +--- Send a request to a server and wait for the response. +-- @param bufnr [number] Buffer handle or 0 for current. +-- @param method [string] Request method name +-- @param params [string] Parameters to send to the server +-- @param timeout_ms [number|100] Maximum ms to wait for a result +-- +-- @returns: The table of {[client_id] = request_result} +function lsp.buf_request_sync(bufnr, method, params, timeout_ms) + local request_results = {} + local result_count = 0 + local function callback(err, _method, 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, callback) + local expected_result_count = 0 + for _ in pairs(client_request_ids) do + expected_result_count = expected_result_count + 1 + end + local wait_result = wait(timeout_ms or 100, function() + return result_count >= expected_result_count + end, 10) + if wait_result ~= 0 then + cancel() + return nil, wait_result_reason[wait_result] + end + return request_results +end + +--- Send a notification to a server +-- @param bufnr [number] (optional): The number of the buffer +-- @param method [string]: Name of the request method +-- @param params [string]: Arguments to send to the server +-- +-- @returns nil +function lsp.buf_notify(bufnr, method, params) + validate { + bufnr = { bufnr, 'n', true }; + method = { method, 's' }; + } + for_each_buffer_client(bufnr, function(client, _client_id) + client.rpc.notify(method, params) + end) +end + +--- Function which can be called to generate omnifunc compatible completion. +function lsp.omnifunc(findstart, base) + local _ = log.debug() and log.debug("omnifunc.findstart", { findstart = findstart, base = base }) + + local bufnr = resolve_bufnr() + local has_buffer_clients = not tbl_isempty(all_buffer_active_clients[bufnr] or {}) + if not has_buffer_clients then + if findstart == 1 then + return -1 + else + return {} + end + end + + if findstart == 1 then + return vim.fn.col('.') + else + local pos = vim.api.nvim_win_get_cursor(0) + local line = assert(nvim_buf_get_lines(bufnr, pos[1]-1, pos[1], false)[1]) + local _ = log.trace() and log.trace("omnifunc.line", pos, line) + local line_to_cursor = line:sub(1, pos[2]+1) + local _ = log.trace() and log.trace("omnifunc.line_to_cursor", line_to_cursor) + local params = { + textDocument = { + uri = vim.uri_from_bufnr(bufnr); + }; + position = { + -- 0-indexed for both line and character + line = pos[1] - 1, + character = pos[2], + }; + -- The completion context. This is only available if the client specifies + -- to send this using `ClientCapabilities.textDocument.completion.contextSupport === true` + -- context = nil or { + -- triggerKind = protocol.CompletionTriggerKind.Invoked; + -- triggerCharacter = nil or ""; + -- }; + } + -- TODO handle timeout error differently? Like via an error? + local client_responses = lsp.buf_request_sync(bufnr, 'textDocument/completion', params) or {} + local matches = {} + for _, response in pairs(client_responses) do + -- TODO how to handle errors? + if not response.error then + local data = response.result + local completion_items = util.text_document_completion_list_to_complete_items(data or {}, line_to_cursor) + local _ = log.trace() and log.trace("omnifunc.completion_items", completion_items) + vim.list_extend(matches, completion_items) + end + end + return matches + end +end + +--- +--- FileType based configuration utility +--- + +local all_filetype_configs = {} + +-- Lookup a filetype config client by its name. +function lsp.get_filetype_client_by_name(name) + local config = all_filetype_configs[name] + if config.client_id then + return active_clients[config.client_id] + end +end + +local function start_filetype_config(config) + config.client_id = lsp.start_client(config) + nvim_command(string.format( + "autocmd FileType %s silent lua vim.lsp.buf_attach_client(0, %d)", + table.concat(config.filetypes, ','), + config.client_id)) + return config.client_id +end + +-- Easy configuration option for common LSP use-cases. +-- This will lazy initialize the client when the filetypes specified are +-- encountered and attach to those buffers. +-- +-- The configuration options are the same as |vim.lsp.start_client()|, but +-- with a few additions and distinctions: +-- +-- Additional parameters: +-- - filetype: {string} or {list} of filetypes to attach to. +-- - name: A unique string among all other servers configured with +-- |vim.lsp.add_filetype_config|. +-- +-- Differences: +-- - root_dir: will default to |getcwd()| +-- +function lsp.add_filetype_config(config) + -- Additional defaults. + -- Keep a copy of the user's input for debugging reasons. + local user_config = config + config = tbl_extend("force", {}, user_config) + config.root_dir = config.root_dir or uv.cwd() + -- Validate config. + validate_client_config(config) + validate { + name = { config.name, 's' }; + } + assert(config.filetype, "config must have 'filetype' key") + + local filetypes + if type(config.filetype) == 'string' then + filetypes = { config.filetype } + elseif type(config.filetype) == 'table' then + filetypes = config.filetype + assert(not tbl_isempty(filetypes), "config.filetype must not be an empty table") + else + error("config.filetype must be a string or a list of strings") + end + + if all_filetype_configs[config.name] then + -- If the client exists, then it is likely that they are doing some kind of + -- reload flow, so let's not throw an error here. + if all_filetype_configs[config.name].client_id then + -- TODO log here? It might be unnecessarily annoying. + return + end + error(string.format('A configuration with the name %q already exists. They must be unique', config.name)) + end + + all_filetype_configs[config.name] = tbl_extend("keep", config, { + client_id = nil; + filetypes = filetypes; + user_config = user_config; + }) + + nvim_command(string.format( + "autocmd FileType %s ++once silent lua vim.lsp._start_filetype_config_client(%q)", + table.concat(filetypes, ','), + config.name)) +end + +-- Create a copy of an existing configuration, and override config with values +-- from new_config. +-- This is useful if you wish you create multiple LSPs with different root_dirs +-- or other use cases. +-- +-- You can specify a new unique name, but if you do not, a unique name will be +-- created like `name-dup_count`. +-- +-- existing_name: the name of the existing config to copy. +-- new_config: the new configuration options. @see |vim.lsp.start_client()|. +-- @returns string the new name. +function lsp.copy_filetype_config(existing_name, new_config) + local config = all_filetype_configs[existing_name] + or error(string.format("Configuration with name %q doesn't exist", existing_name)) + config = tbl_extend("force", config, new_config or {}) + config.client_id = nil + config.original_config_name = existing_name + + -- If the user didn't rename it, we will. + if config.name == existing_name then + -- Create a new, unique name. + local duplicate_count = 0 + for _, conf in pairs(all_filetype_configs) do + if conf.original_config_name == existing_name then + duplicate_count = duplicate_count + 1 + end + end + config.name = string.format("%s-%d", existing_name, duplicate_count + 1) + end + print("New config name:", config.name) + lsp.add_filetype_config(config) + return config.name +end + +-- Autocmd handler to actually start the client when an applicable filetype is +-- encountered. +function lsp._start_filetype_config_client(name) + local config = all_filetype_configs[name] + -- If it exists and is running, don't make it again. + if config.client_id and active_clients[config.client_id] then + -- TODO log here? + return + end + lsp.buf_attach_client(0, start_filetype_config(config)) + return config.client_id +end + +--- +--- Miscellaneous utilities. +--- + +-- Retrieve a map from client_id to client of all active buffer clients. +-- @param bufnr [number] (optional): buffer handle or 0 for current +function lsp.buf_get_clients(bufnr) + bufnr = resolve_bufnr(bufnr) + local result = {} + for_each_buffer_client(bufnr, function(client, client_id) + result[client_id] = client + end) + return result +end + +-- Print some debug information about the current buffer clients. +-- The output of this function should not be relied upon and may change. +function lsp.buf_print_debug_info(bufnr) + print(vim.inspect(lsp.buf_get_clients(bufnr))) +end + +-- Print some debug information about all LSP related things. +-- The output of this function should not be relied upon and may change. +function lsp.print_debug_info() + print(vim.inspect({ clients = active_clients, filetype_configs = all_filetype_configs })) +end + +-- Log level dictionary with reverse lookup as well. +-- +-- Can be used to lookup the number from the name or the +-- name from the number. +-- Levels by name: 'trace', 'debug', 'info', 'warn', 'error' +-- Level numbers begin with 'trace' at 0 +lsp.log_levels = log.levels + +-- Set the log level for lsp logging. +-- Levels by name: 'trace', 'debug', 'info', 'warn', 'error' +-- Level numbers begin with 'trace' at 0 +-- @param level [number|string] the case insensitive level name or number @see |vim.lsp.log_levels| +function lsp.set_log_level(level) + if type(level) == 'string' or type(level) == 'number' then + log.set_level(level) + else + error(string.format("Invalid log level: %q", level)) + end +end + +-- Return the path of the logfile used by the LSP client. +function lsp.get_log_path() + return log.get_filename() +end + +return lsp +-- vim:sw=2 ts=2 et -- cgit From 0904ffe387f825404d8467f9787678c2251728dd Mon Sep 17 00:00:00 2001 From: Ashkan Kiani Date: Wed, 20 Nov 2019 11:36:50 -0800 Subject: Bugfixes. - Return after an error in RPC. - Use an empty vim table for serialization. --- runtime/lua/vim/lsp.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'runtime/lua/vim/lsp.lua') diff --git a/runtime/lua/vim/lsp.lua b/runtime/lua/vim/lsp.lua index 9dbe03dace..4724ff0281 100644 --- a/runtime/lua/vim/lsp.lua +++ b/runtime/lua/vim/lsp.lua @@ -400,7 +400,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', {}) + rpc.notify('initialized', {[vim.type_idx]=vim.types.dictionary}) client.initialized = true uninitialized_clients[client_id] = nil client.server_capabilities = assert(result.capabilities, "initialize result doesn't contain capabilities") -- cgit From a4b7004f489030d9ee7e3bbfc156ab540744279b Mon Sep 17 00:00:00 2001 From: Ashkan Kiani Date: Wed, 20 Nov 2019 16:03:32 -0800 Subject: Move everything to buf & default_callbacks - Rename builtin_callbacks to default_callbacks and slightly change its semantics: - No longer contains the default implementations. Instead, any default_callbacks will be used in preference for our .buf methods. - Add this to the docs. --- runtime/lua/vim/lsp.lua | 137 +++--------------------------------------------- 1 file changed, 6 insertions(+), 131 deletions(-) (limited to 'runtime/lua/vim/lsp.lua') diff --git a/runtime/lua/vim/lsp.lua b/runtime/lua/vim/lsp.lua index 4724ff0281..44909a85ee 100644 --- a/runtime/lua/vim/lsp.lua +++ b/runtime/lua/vim/lsp.lua @@ -1,4 +1,4 @@ -local builtin_callbacks = require 'vim.lsp.builtin_callbacks' +local default_callbacks = require 'vim.lsp.default_callbacks' local log = require 'vim.lsp.log' local lsp_rpc = require 'vim.lsp.rpc' local protocol = require 'vim.lsp.protocol' @@ -12,7 +12,8 @@ local validate = vim.validate local lsp = { protocol = protocol; - builtin_callbacks = builtin_callbacks; + default_callbacks = default_callbacks; + buf = require'vim.lsp.buf'; util = util; -- Allow raw RPC access. rpc = lsp_rpc; @@ -283,7 +284,7 @@ function lsp.start_client(config) local client_id = next_client_id() - local callbacks = tbl_extend("keep", config.callbacks or {}, builtin_callbacks) + local callbacks = tbl_extend("keep", config.callbacks or {}, default_callbacks) -- Copy metatable if it has one. if config.callbacks and config.callbacks.__metatable then setmetatable(callbacks, getmetatable(config.callbacks)) @@ -869,134 +870,8 @@ function lsp.omnifunc(findstart, base) end end ---- ---- FileType based configuration utility ---- - -local all_filetype_configs = {} - --- Lookup a filetype config client by its name. -function lsp.get_filetype_client_by_name(name) - local config = all_filetype_configs[name] - if config.client_id then - return active_clients[config.client_id] - end -end - -local function start_filetype_config(config) - config.client_id = lsp.start_client(config) - nvim_command(string.format( - "autocmd FileType %s silent lua vim.lsp.buf_attach_client(0, %d)", - table.concat(config.filetypes, ','), - config.client_id)) - return config.client_id -end - --- Easy configuration option for common LSP use-cases. --- This will lazy initialize the client when the filetypes specified are --- encountered and attach to those buffers. --- --- The configuration options are the same as |vim.lsp.start_client()|, but --- with a few additions and distinctions: --- --- Additional parameters: --- - filetype: {string} or {list} of filetypes to attach to. --- - name: A unique string among all other servers configured with --- |vim.lsp.add_filetype_config|. --- --- Differences: --- - root_dir: will default to |getcwd()| --- -function lsp.add_filetype_config(config) - -- Additional defaults. - -- Keep a copy of the user's input for debugging reasons. - local user_config = config - config = tbl_extend("force", {}, user_config) - config.root_dir = config.root_dir or uv.cwd() - -- Validate config. - validate_client_config(config) - validate { - name = { config.name, 's' }; - } - assert(config.filetype, "config must have 'filetype' key") - - local filetypes - if type(config.filetype) == 'string' then - filetypes = { config.filetype } - elseif type(config.filetype) == 'table' then - filetypes = config.filetype - assert(not tbl_isempty(filetypes), "config.filetype must not be an empty table") - else - error("config.filetype must be a string or a list of strings") - end - - if all_filetype_configs[config.name] then - -- If the client exists, then it is likely that they are doing some kind of - -- reload flow, so let's not throw an error here. - if all_filetype_configs[config.name].client_id then - -- TODO log here? It might be unnecessarily annoying. - return - end - error(string.format('A configuration with the name %q already exists. They must be unique', config.name)) - end - - all_filetype_configs[config.name] = tbl_extend("keep", config, { - client_id = nil; - filetypes = filetypes; - user_config = user_config; - }) - - nvim_command(string.format( - "autocmd FileType %s ++once silent lua vim.lsp._start_filetype_config_client(%q)", - table.concat(filetypes, ','), - config.name)) -end - --- Create a copy of an existing configuration, and override config with values --- from new_config. --- This is useful if you wish you create multiple LSPs with different root_dirs --- or other use cases. --- --- You can specify a new unique name, but if you do not, a unique name will be --- created like `name-dup_count`. --- --- existing_name: the name of the existing config to copy. --- new_config: the new configuration options. @see |vim.lsp.start_client()|. --- @returns string the new name. -function lsp.copy_filetype_config(existing_name, new_config) - local config = all_filetype_configs[existing_name] - or error(string.format("Configuration with name %q doesn't exist", existing_name)) - config = tbl_extend("force", config, new_config or {}) - config.client_id = nil - config.original_config_name = existing_name - - -- If the user didn't rename it, we will. - if config.name == existing_name then - -- Create a new, unique name. - local duplicate_count = 0 - for _, conf in pairs(all_filetype_configs) do - if conf.original_config_name == existing_name then - duplicate_count = duplicate_count + 1 - end - end - config.name = string.format("%s-%d", existing_name, duplicate_count + 1) - end - print("New config name:", config.name) - lsp.add_filetype_config(config) - return config.name -end - --- Autocmd handler to actually start the client when an applicable filetype is --- encountered. -function lsp._start_filetype_config_client(name) - local config = all_filetype_configs[name] - -- If it exists and is running, don't make it again. - if config.client_id and active_clients[config.client_id] then - -- TODO log here? - return - end - lsp.buf_attach_client(0, start_filetype_config(config)) - return config.client_id +function lsp.client_is_stopped(client_id) + return active_clients[client_id] == nil end --- -- cgit From 03eb88848c2bea6c0c1da7acc97754d6f47b5118 Mon Sep 17 00:00:00 2001 From: Ashkan Kiani Date: Wed, 20 Nov 2019 16:09:03 -0800 Subject: Change callback resolution to be dynamic. This allows default_callbacks to be specified after client creation to be considered. Also it simplifies the code. --- runtime/lua/vim/lsp.lua | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) (limited to 'runtime/lua/vim/lsp.lua') diff --git a/runtime/lua/vim/lsp.lua b/runtime/lua/vim/lsp.lua index 44909a85ee..a284dd1ee7 100644 --- a/runtime/lua/vim/lsp.lua +++ b/runtime/lua/vim/lsp.lua @@ -284,19 +284,19 @@ function lsp.start_client(config) local client_id = next_client_id() - local callbacks = tbl_extend("keep", config.callbacks or {}, default_callbacks) - -- Copy metatable if it has one. - if config.callbacks and config.callbacks.__metatable then - setmetatable(callbacks, getmetatable(config.callbacks)) - end + local callbacks = config.callbacks or {} local name = config.name or tostring(client_id) local log_prefix = string.format("LSP[%s]", name) local handlers = {} + local function resolve_callback(method) + return callbacks[method] or default_callbacks[method] + end + function handlers.notification(method, params) local _ = log.debug() and log.debug('notification', method, params) - local callback = callbacks[method] + local callback = resolve_callback(method) if callback then -- Method name is provided here for convenience. callback(nil, method, params, client_id) @@ -305,7 +305,7 @@ function lsp.start_client(config) function handlers.server_request(method, params) local _ = log.debug() and log.debug('server_request', method, params) - local callback = callbacks[method] + local callback = resolve_callback(method) if callback then local _ = log.debug() and log.debug("server_request: found callback for", method) return callback(nil, method, params, client_id) @@ -316,7 +316,8 @@ function lsp.start_client(config) function handlers.on_error(code, err) local _ = log.error() and log.error(log_prefix, "on_error", { code = lsp.client_errors[code], err = err }) - nvim_err_writeln(string.format('%s: Error %s: %q', log_prefix, lsp.client_errors[code], vim.inspect(err))) + print(string.format('%s: Error %s: %q', log_prefix, lsp.client_errors[code], vim.inspect(err))) +-- nvim_err_writeln(string.format('%s: Error %s: %q', log_prefix, lsp.client_errors[code], vim.inspect(err))) if config.on_error then local status, usererr = pcall(config.on_error, code, err) if not status then @@ -440,7 +441,7 @@ function lsp.start_client(config) --- Checks capabilities before rpc.request-ing. function client.request(method, params, callback) if not callback then - callback = client.callbacks[method] + callback = resolve_callback(method) or error(string.format("request callback is empty and no default was found for client %s", client.name)) end local _ = log.debug() and log.debug(log_prefix, "client.request", client_id, method, params, callback) -- cgit From c40f8600d2418dfdfeacbba3efe11ae7c6c70ad3 Mon Sep 17 00:00:00 2001 From: Ashkan Kiani Date: Wed, 20 Nov 2019 16:16:13 -0800 Subject: Change error writer to not be annoying. --- runtime/lua/vim/lsp.lua | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) (limited to 'runtime/lua/vim/lsp.lua') diff --git a/runtime/lua/vim/lsp.lua b/runtime/lua/vim/lsp.lua index a284dd1ee7..1b9d788c43 100644 --- a/runtime/lua/vim/lsp.lua +++ b/runtime/lua/vim/lsp.lua @@ -27,6 +27,11 @@ local lsp = { -- TODO consider whether 'eol' or 'fixeol' should change the nvim_buf_get_lines that send. -- TODO improve handling of scratch buffers with LSP attached. +local function err_message(...) + nvim_err_writeln(table.concat(vim.tbl_flatten{...})) + nvim_command("redraw") +end + local function resolve_bufnr(bufnr) validate { bufnr = { bufnr, 'n', true } } if bufnr == nil or bufnr == 0 then @@ -316,13 +321,12 @@ function lsp.start_client(config) function handlers.on_error(code, err) local _ = log.error() and log.error(log_prefix, "on_error", { code = lsp.client_errors[code], err = err }) - print(string.format('%s: Error %s: %q', log_prefix, lsp.client_errors[code], vim.inspect(err))) --- nvim_err_writeln(string.format('%s: Error %s: %q', log_prefix, lsp.client_errors[code], vim.inspect(err))) + err_message(log_prefix, ': Error ', lsp.client_errors[code], ': ', vim.inspect(err)) if config.on_error then local status, usererr = pcall(config.on_error, code, err) if not status then local _ = log.error() and log.error(log_prefix, "user on_error failed", { err = usererr }) - nvim_err_writeln(log_prefix.." user on_error failed: "..tostring(usererr)) + err_message(log_prefix, ' user on_error failed: ', tostring(usererr)) end end end @@ -434,7 +438,7 @@ function lsp.start_client(config) local function unsupported_method(method) local msg = "server doesn't support "..method local _ = log.warn() and log.warn(msg) - nvim_err_writeln(msg) + err_message(msg) return lsp.rpc_response_error(protocol.ErrorCodes.MethodNotFound, msg) end -- cgit From 4c7ef3754a7679a7f095fa9e739a665ead4c71b4 Mon Sep 17 00:00:00 2001 From: Ashkan Kiani Date: Wed, 20 Nov 2019 16:37:23 -0800 Subject: Satisfy lualint. --- runtime/lua/vim/lsp.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'runtime/lua/vim/lsp.lua') diff --git a/runtime/lua/vim/lsp.lua b/runtime/lua/vim/lsp.lua index 1b9d788c43..b2dc1315dd 100644 --- a/runtime/lua/vim/lsp.lua +++ b/runtime/lua/vim/lsp.lua @@ -903,7 +903,7 @@ end -- Print some debug information about all LSP related things. -- The output of this function should not be relied upon and may change. function lsp.print_debug_info() - print(vim.inspect({ clients = active_clients, filetype_configs = all_filetype_configs })) + print(vim.inspect({ clients = active_clients })) end -- Log level dictionary with reverse lookup as well. -- cgit From 76e0a8bd935b27270f8f6a594c3025bc494f4422 Mon Sep 17 00:00:00 2001 From: Björn Linse Date: Thu, 21 Nov 2019 10:29:54 +0100 Subject: lsp: transmit "\n" after last line when 'eol' is set Otherwise some servers like clangd will emit spurious "no newline at end of file" warnings. --- runtime/lua/vim/lsp.lua | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) (limited to 'runtime/lua/vim/lsp.lua') diff --git a/runtime/lua/vim/lsp.lua b/runtime/lua/vim/lsp.lua index 9dbe03dace..0c1a145c04 100644 --- a/runtime/lua/vim/lsp.lua +++ b/runtime/lua/vim/lsp.lua @@ -23,7 +23,6 @@ local lsp = { -- format_rpc_error = lsp_rpc.format_rpc_error; } --- TODO consider whether 'eol' or 'fixeol' should change the nvim_buf_get_lines that send. -- TODO improve handling of scratch buffers with LSP attached. local function resolve_bufnr(bufnr) @@ -175,6 +174,14 @@ local function validate_client_config(config) } end +local function buf_get_full_text(bufnr) + local text = table.concat(nvim_buf_get_lines(bufnr, 0, -1, true), '\n') + if nvim_buf_get_option(bufnr, 'eol') then + text = text .. '\n' + end + return text +end + local function text_document_did_open_handler(bufnr, client) if not client.resolved_capabilities.text_document_open_close then return @@ -188,7 +195,7 @@ local function text_document_did_open_handler(bufnr, client) uri = vim.uri_from_bufnr(bufnr); -- TODO make sure our filetypes are compatible with languageId names. languageId = nvim_buf_get_option(bufnr, 'filetype'); - text = table.concat(nvim_buf_get_lines(bufnr, 0, -1, false), '\n'); + text = buf_get_full_text(bufnr); } } client.notify('textDocument/didOpen', params) @@ -551,7 +558,7 @@ do end) local full_changes = once(function() return { - text = table.concat(nvim_buf_get_lines(bufnr, 0, -1, false), "\n"); + text = buf_get_full_text(bufnr); }; end) local uri = vim.uri_from_bufnr(bufnr) -- cgit From bcae04f6c62b23104e85bb08c54a16d0cdc33852 Mon Sep 17 00:00:00 2001 From: Ashkan Kiani Date: Thu, 21 Nov 2019 15:19:06 -0800 Subject: Updates - Use correct implementation of text_edits. - Send indent options to rangeFormatting and formatting. - Remove references to vim bindings and filetype from lsp.txt - Add more examples to docs. - Add before_init to allow changing initialize_params. --- runtime/lua/vim/lsp.lua | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) (limited to 'runtime/lua/vim/lsp.lua') diff --git a/runtime/lua/vim/lsp.lua b/runtime/lua/vim/lsp.lua index 5a0b320f4c..790f8f8a9d 100644 --- a/runtime/lua/vim/lsp.lua +++ b/runtime/lua/vim/lsp.lua @@ -160,13 +160,13 @@ local function validate_client_config(config) root_dir = { config.root_dir, is_dir, "directory" }; callbacks = { config.callbacks, "t", true }; capabilities = { config.capabilities, "t", true }; - -- cmd = { config.cmd, "s", false }; cmd_cwd = { config.cmd_cwd, optional_validator(is_dir), "directory" }; cmd_env = { config.cmd_env, "f", true }; name = { config.name, 's', true }; on_error = { config.on_error, "f", true }; on_exit = { config.on_exit, "f", true }; on_init = { config.on_init, "f", true }; + before_init = { config.before_init, "f", true }; offset_encoding = { config.offset_encoding, "s", true }; } local cmd, cmd_args = validate_command(config.cmd) @@ -267,6 +267,12 @@ end -- possible errors. `vim.lsp.client_errors[code]` can be used to retrieve a -- human understandable string. -- +-- before_init(initialize_params, config): A function which is called *before* +-- the request `initialize` is completed. `initialize_params` contains +-- the parameters we are sending to the server and `config` is the config that +-- was passed to `start_client()` for convenience. You can use this to modify +-- parameters before they are sent. +-- -- on_init(client, initialize_result): A function which is called after the -- request `initialize` is completed. `initialize_result` contains -- `capabilities` and anything else the server may send. For example, `clangd` @@ -385,7 +391,6 @@ function lsp.start_client(config) -- The rootUri of the workspace. Is null if no folder is open. If both -- `rootPath` and `rootUri` are set `rootUri` wins. rootUri = vim.uri_from_fname(config.root_dir); --- rootUri = vim.uri_from_fname(vim.fn.expand("%:p:h")); -- User provided initialization options. initializationOptions = config.init_options; -- The capabilities provided by the client (editor or tool) @@ -409,6 +414,10 @@ function lsp.start_client(config) -- } workspaceFolders = nil; } + if config.before_init then + -- TODO(ashkan) handle errors here. + pcall(config.before_init, initialize_params, config) + end local _ = log.debug() and log.debug(log_prefix, "initialize_params", initialize_params) rpc.request('initialize', initialize_params, function(init_err, result) assert(not init_err, tostring(init_err)) -- cgit From c055ca00ce61499e9161b8d2fa48568e833ee5b0 Mon Sep 17 00:00:00 2001 From: Ashkan Kiani Date: Thu, 21 Nov 2019 15:55:45 -0800 Subject: Remove comments. --- runtime/lua/vim/lsp.lua | 4 ---- 1 file changed, 4 deletions(-) (limited to 'runtime/lua/vim/lsp.lua') diff --git a/runtime/lua/vim/lsp.lua b/runtime/lua/vim/lsp.lua index 790f8f8a9d..17d6001496 100644 --- a/runtime/lua/vim/lsp.lua +++ b/runtime/lua/vim/lsp.lua @@ -98,11 +98,7 @@ local function for_each_buffer_client(bufnr, callback) return end for client_id in pairs(client_ids) do - -- This is unlikely to happen. Could only potentially happen in a race - -- condition between literally a single statement. - -- We could skip this error, but let's error for now. local client = active_clients[client_id] - -- or error(string.format("Client %d has already shut down.", client_id)) if client then callback(client, client_id) end -- cgit From a3d67dac5f9de6099890ae4b9fab2b70b2279a29 Mon Sep 17 00:00:00 2001 From: Ashkan Kiani Date: Thu, 21 Nov 2019 16:23:12 -0800 Subject: Fix encoding translation in other places. --- runtime/lua/vim/lsp.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'runtime/lua/vim/lsp.lua') diff --git a/runtime/lua/vim/lsp.lua b/runtime/lua/vim/lsp.lua index 17d6001496..880d811647 100644 --- a/runtime/lua/vim/lsp.lua +++ b/runtime/lua/vim/lsp.lua @@ -862,7 +862,7 @@ function lsp.omnifunc(findstart, base) position = { -- 0-indexed for both line and character line = pos[1] - 1, - character = pos[2], + character = vim.str_utfindex(line, pos[2]), }; -- The completion context. This is only available if the client specifies -- to send this using `ClientCapabilities.textDocument.completion.contextSupport === true` -- cgit From d410812311f7b462420690455914ea1316953b3a Mon Sep 17 00:00:00 2001 From: Ashkan Kiani Date: Sat, 23 Nov 2019 16:14:24 -0800 Subject: UI tweaks. - Hide diagnostics on client exit - Stop insert on popup focus. - Hide popup on insertchar (for signature_help) --- runtime/lua/vim/lsp.lua | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) (limited to 'runtime/lua/vim/lsp.lua') diff --git a/runtime/lua/vim/lsp.lua b/runtime/lua/vim/lsp.lua index 880d811647..7076db68d3 100644 --- a/runtime/lua/vim/lsp.lua +++ b/runtime/lua/vim/lsp.lua @@ -343,9 +343,19 @@ function lsp.start_client(config) function handlers.on_exit(code, signal) active_clients[client_id] = nil uninitialized_clients[client_id] = nil - for _, client_ids in pairs(all_buffer_active_clients) do + 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 client_ids[client_id] = nil end + -- Buffer level cleanup + vim.schedule(function() + for _, bufnr in ipairs(active_buffers) do + util.buf_clear_diagnostics(bufnr) + end + end) if config.on_exit then pcall(config.on_exit, code, signal, client_id) end -- cgit From a9036502dca7191c8ada70367e44c398d48dd94a Mon Sep 17 00:00:00 2001 From: Ashkan Kiani Date: Sun, 24 Nov 2019 03:14:03 -0800 Subject: Bring vim into local scope --- runtime/lua/vim/lsp.lua | 1 + 1 file changed, 1 insertion(+) (limited to 'runtime/lua/vim/lsp.lua') diff --git a/runtime/lua/vim/lsp.lua b/runtime/lua/vim/lsp.lua index 7076db68d3..928d19177a 100644 --- a/runtime/lua/vim/lsp.lua +++ b/runtime/lua/vim/lsp.lua @@ -4,6 +4,7 @@ local lsp_rpc = require 'vim.lsp.rpc' local protocol = require 'vim.lsp.protocol' local util = require 'vim.lsp.util' +local vim = vim local nvim_err_writeln, nvim_buf_get_lines, nvim_command, nvim_buf_get_option = vim.api.nvim_err_writeln, vim.api.nvim_buf_get_lines, vim.api.nvim_command, vim.api.nvim_buf_get_option local uv = vim.loop -- cgit From 6e8c5779cf960893850501e4871dc9be671db298 Mon Sep 17 00:00:00 2001 From: Ashkan Kiani Date: Tue, 26 Nov 2019 05:59:40 -0800 Subject: LSP: Move default buf callbacks to vim.lsp.callbacks (#11452) - In the process, refactored focusable_preview to a util function. - Add text for locations_to_items of the current line. - Improve location callback to handle multiple return values by using set_qflist. - Remove update_tagstack and leave note for future travelers. --- runtime/lua/vim/lsp.lua | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) (limited to 'runtime/lua/vim/lsp.lua') diff --git a/runtime/lua/vim/lsp.lua b/runtime/lua/vim/lsp.lua index 928d19177a..82f4fda66f 100644 --- a/runtime/lua/vim/lsp.lua +++ b/runtime/lua/vim/lsp.lua @@ -1,4 +1,4 @@ -local default_callbacks = require 'vim.lsp.default_callbacks' +local default_callbacks = require 'vim.lsp.callbacks' local log = require 'vim.lsp.log' local lsp_rpc = require 'vim.lsp.rpc' local protocol = require 'vim.lsp.protocol' @@ -13,7 +13,7 @@ local validate = vim.validate local lsp = { protocol = protocol; - default_callbacks = default_callbacks; + callbacks = default_callbacks; buf = require'vim.lsp.buf'; util = util; -- Allow raw RPC access. @@ -469,7 +469,7 @@ function lsp.start_client(config) function client.request(method, params, callback) if not callback then callback = resolve_callback(method) - or error(string.format("request callback is empty and no default was found for client %s", client.name)) + or error("not found: request callback for client "..client.name) end local _ = log.debug() and log.debug(log_prefix, "client.request", client_id, method, pa