diff options
Diffstat (limited to 'runtime/lua/vim/lsp.lua')
| -rw-r--r-- | runtime/lua/vim/lsp.lua | 185 | 
1 files changed, 139 insertions, 46 deletions
| diff --git a/runtime/lua/vim/lsp.lua b/runtime/lua/vim/lsp.lua index 9c35351608..0fc0a7a7aa 100644 --- a/runtime/lua/vim/lsp.lua +++ b/runtime/lua/vim/lsp.lua @@ -5,6 +5,7 @@ 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 sync = require 'vim.lsp.sync'  local vim = vim  local nvim_err_writeln, nvim_buf_get_lines, nvim_command, nvim_buf_get_option @@ -108,6 +109,12 @@ local valid_encodings = {    UTF8      = 'utf-8'; UTF16      = 'utf-16'; UTF32      = 'utf-32';  } +local format_line_ending = { +  ["unix"] = '\n', +  ["dos"] = '\r\n', +  ["mac"] = '\r', +} +  local client_index = 0  ---@private  --- Returns a new, unused client id. @@ -122,9 +129,6 @@ 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}.  --- @@ -242,6 +246,7 @@ local function validate_client_config(config)      on_exit         = { config.on_exit, "f", true };      on_init         = { config.on_init, "f", true };      settings        = { config.settings, "t", true }; +    commands        = { config.commands, 't', true };      before_init     = { config.before_init, "f", true };      offset_encoding = { config.offset_encoding, "s", true };      flags           = { config.flags, "t", true }; @@ -353,15 +358,14 @@ do      end    end -  function changetracking.prepare(bufnr, firstline, new_lastline, changedtick) +  function changetracking.prepare(bufnr, firstline, lastline, 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 +      local curr_lines = nvim_buf_get_lines(bufnr, 0, -1, true) +      local line_ending = format_line_ending[vim.api.nvim_buf_get_option(0, 'fileformat')] +      local incremental_change = sync.compute_diff( +        cached_buffers[bufnr], curr_lines, firstline, lastline, new_lastline, client.offset_encoding or 'utf-16', line_ending or '\n') +      cached_buffers[bufnr] = curr_lines        return incremental_change      end      local full_changes = once(function() @@ -468,7 +472,11 @@ local function text_document_did_open_handler(bufnr, client)    -- Next chance we get, we should re-do the diagnostics    vim.schedule(function() -    vim.lsp.diagnostic.redraw(bufnr, client.id) +    -- Protect against a race where the buffer disappears +    -- between `did_open_handler` and the scheduled function firing. +    if vim.api.nvim_buf_is_valid(bufnr) then +      vim.lsp.diagnostic.redraw(bufnr, client.id) +    end    end)  end @@ -593,6 +601,11 @@ end  --- returned to the language server if requested via `workspace/configuration`.  --- Keys are case-sensitive.  --- +---@param commands table Table that maps string of clientside commands to user-defined functions. +--- Commands passed to start_client take precedence over the global command registry. Each key +--- must be a unique comand name, and the value is a function which is called if any LSP action +--- (code action, code lenses, ...) triggers the command. +---  ---@param init_options Values to pass in the initialization request  --- as `initializationOptions`. See `initialize` in the LSP spec.  --- @@ -647,7 +660,9 @@ end  --- - debounce_text_changes (number, default nil): Debounce didChange  ---       notifications to the server by the given number in milliseconds. No debounce  ---       occurs if nil ---- +--- - exit_timeout (number, default 500): Milliseconds to wait for server to +--        exit cleanly after sending the 'shutdown' request before sending kill -15. +--        If set to false, nvim exits immediately after sending the 'shutdown' request to the server.  ---@returns Client id. |vim.lsp.get_client_by_id()| Note: client may not be  --- fully initialized. Use `on_init` to do any actions once  --- the client has been initialized. @@ -742,7 +757,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 @@ -771,10 +785,14 @@ function lsp.start_client(config)      rpc = rpc;      offset_encoding = offset_encoding;      config = config; +    attached_buffers = {};      handlers = handlers; +    commands = config.commands or {}; + +    requests = {};      -- for $/progress report -    messages = { name = name, messages = {}, progress = {}, status = {} } +    messages = { name = name, messages = {}, progress = {}, status = {} };    }    -- Store the uninitialized_clients for cleanup in case we exit before initialize finishes. @@ -907,11 +925,21 @@ function lsp.start_client(config)      end      -- Ensure pending didChange notifications are sent so that the server doesn't operate on a stale state      changetracking.flush(client) - +    bufnr = resolve_bufnr(bufnr)      local _ = log.debug() and log.debug(log_prefix, "client.request", client_id, method, params, handler, bufnr) -    return rpc.request(method, params, function(err, result) +    local success, request_id = rpc.request(method, params, function(err, result)        handler(err, result, {method=method, client_id=client_id, bufnr=bufnr, params=params}) +    end, function(request_id) +      client.requests[request_id] = nil +      nvim_command("doautocmd <nomodeline> User LspRequest")      end) + +    if success then +      client.requests[request_id] = { type='pending', bufnr=bufnr, method=method } +      nvim_command("doautocmd <nomodeline> User LspRequest") +    end + +    return success, request_id    end    ---@private @@ -971,6 +999,11 @@ function lsp.start_client(config)    ---@see |vim.lsp.client.notify()|    function client.cancel_request(id)      validate{id = {id, 'n'}} +    local request = client.requests[id] +    if request and request.type == 'pending' then +      request.type = 'cancel' +      nvim_command("doautocmd <nomodeline> User LspRequest") +    end      return rpc.notify("$/cancelRequest", { id = id })    end @@ -989,7 +1022,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 @@ -1032,6 +1064,7 @@ function lsp.start_client(config)        -- TODO(ashkan) handle errors.        pcall(config.on_attach, client, bufnr)      end +    client.attached_buffers[bufnr] = true    end    initialize() @@ -1044,22 +1077,14 @@ end  --- Notify all attached clients that a buffer has changed.  local text_document_did_change_handler  do -  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( -      string.format("on_lines bufnr: %s, changedtick: %s, firstline: %s, lastline: %s, new_lastline: %s, old_byte_size: %s, old_utf32_size: %s, old_utf16_size: %s", -      bufnr, changedtick, firstline, lastline, new_lastline, old_byte_size, old_utf32_size, old_utf16_size), -      nvim_buf_get_lines(bufnr, firstline, new_lastline, true) -    ) +  text_document_did_change_handler = function(_, bufnr, changedtick, firstline, lastline, new_lastline)      -- Don't do anything if there are no clients attached.      if tbl_isempty(all_buffer_active_clients[bufnr] or {}) then        return      end      util.buf_versions[bufnr] = changedtick -    local compute_change_and_notify = changetracking.prepare(bufnr, firstline, new_lastline, changedtick) +    local compute_change_and_notify = changetracking.prepare(bufnr, firstline, lastline, new_lastline, changedtick)      for_each_buffer_client(bufnr, compute_change_and_notify)    end  end @@ -1142,12 +1167,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 @@ -1172,7 +1191,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) @@ -1181,15 +1200,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). @@ -1239,9 +1254,41 @@ function lsp._vim_exit_handler()      client.stop()    end -  if not vim.wait(500, function() return tbl_isempty(active_clients) end, 50) then -    for _, client in pairs(active_clients) do -      client.stop(true) +  local timeouts = {} +  local max_timeout = 0 +  local send_kill = false + +  for client_id, client in pairs(active_clients) do +    local timeout = if_nil(client.config.flags.exit_timeout, 500) +    if timeout then +      send_kill = true +      timeouts[client_id] = timeout +      max_timeout = math.max(timeout, max_timeout) +    end +  end + +  local poll_time = 50 + +  local function check_clients_closed() +    for client_id, timeout in pairs(timeouts) do +      timeouts[client_id] = timeout - poll_time +    end + +    for client_id, _ in pairs(active_clients) do +      if timeouts[client_id] ~= nil and timeouts[client_id] > 0 then +        return false +      end +    end +    return true +  end + +  if send_kill then +    if not vim.wait(max_timeout, check_clients_closed, poll_time) then +      for client_id, client in pairs(active_clients) do +        if timeouts[client_id] ~= nil then +          client.stop(true) +        end +      end      end    end  end @@ -1282,7 +1329,7 @@ function lsp.buf_request(bufnr, method, params, handler)    if not tbl_isempty(all_buffer_active_clients[resolve_bufnr(bufnr)] or {}) and not method_supported then      vim.notify(lsp._unsupported_method(method), vim.log.levels.ERROR)      vim.api.nvim_command("redraw") -    return +    return {}, function() end    end    local client_request_ids = {} @@ -1430,7 +1477,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) @@ -1494,6 +1541,52 @@ function lsp.omnifunc(findstart, base)    return -2  end +--- Provides an interface between the built-in client and a `formatexpr` function. +--- +--- Currently only supports a single client. This can be set via +--- `setlocal formatexpr=v:lua.vim.lsp.formatexpr()` but will typically or in `on_attach` +--- via `vim.api.nvim_buf_set_option(bufnr, 'formatexpr', 'v:lua.vim.lsp.formatexpr(#{timeout_ms:250})')`. +--- +---@param opts table options for customizing the formatting expression which takes the +---                   following optional keys: +---                   * timeout_ms (default 500ms). The timeout period for the formatting request. +function lsp.formatexpr(opts) +  opts = opts or {} +  local timeout_ms = opts.timeout_ms or 500 + +  if vim.tbl_contains({'i', 'R', 'ic', 'ix'}, vim.fn.mode()) then +    -- `formatexpr` is also called when exceeding `textwidth` in insert mode +    -- fall back to internal formatting +    return 1 +  end + +  local start_line = vim.v.lnum +  local end_line = start_line + vim.v.count - 1 + +  if start_line > 0 and end_line > 0 then +    local params = { +      textDocument = util.make_text_document_params(); +      range = { +        start = { line = start_line - 1; character = 0; }; +        ["end"] = { line = end_line - 1; character = 0; }; +      }; +    }; +    params.options = util.make_formatting_params().options +    local client_results = vim.lsp.buf_request_sync(0, "textDocument/rangeFormatting", params, timeout_ms) + +    -- Apply the text edits from one and only one of the clients. +    for _, response in pairs(client_results) do +      if response.result then +        vim.lsp.util.apply_text_edits(response.result, 0) +        return 0 +      end +    end +  end + +  -- do not run builtin formatter. +  return 0 +end +  ---Checks whether a client is stopped.  ---  ---@param client_id (Number) | 
