aboutsummaryrefslogtreecommitdiff
path: root/runtime/lua/vim/lsp.lua
diff options
context:
space:
mode:
Diffstat (limited to 'runtime/lua/vim/lsp.lua')
-rw-r--r--runtime/lua/vim/lsp.lua185
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)