mirror of
https://github.com/neovim/neovim.git
synced 2025-02-25 18:55:25 -06:00
lsp: Add a flag to debounce didChange notifications
Would help with cases as reported in https://github.com/neovim/neovim/issues/14087
This commit is contained in:
parent
c9817603cf
commit
720c6353b5
@ -232,6 +232,12 @@ local function validate_client_config(config)
|
|||||||
flags = { config.flags, "t", true };
|
flags = { config.flags, "t", true };
|
||||||
get_language_id = { config.get_language_id, "f", true };
|
get_language_id = { config.get_language_id, "f", true };
|
||||||
}
|
}
|
||||||
|
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 cmd, cmd_args = lsp._cmd_parts(config.cmd)
|
||||||
local offset_encoding = valid_encodings.UTF16
|
local offset_encoding = valid_encodings.UTF16
|
||||||
@ -259,22 +265,166 @@ local function buf_get_full_text(bufnr)
|
|||||||
return text
|
return text
|
||||||
end
|
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
|
||||||
|
|
||||||
|
|
||||||
|
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
|
--@private
|
||||||
--- Default handler for the 'textDocument/didOpen' LSP notification.
|
--- Default handler for the 'textDocument/didOpen' LSP notification.
|
||||||
---
|
---
|
||||||
--@param bufnr (Number) Number of the buffer, or 0 for current
|
--@param bufnr (Number) Number of the buffer, or 0 for current
|
||||||
--@param client Client object
|
--@param client Client object
|
||||||
local function text_document_did_open_handler(bufnr, client)
|
local function text_document_did_open_handler(bufnr, client)
|
||||||
local use_incremental_sync = (
|
changetracking.init(client, bufnr)
|
||||||
if_nil(client.config.flags.allow_incremental_sync, true)
|
|
||||||
and client.resolved_capabilities.text_document_did_change == protocol.TextDocumentSyncKind.Incremental
|
|
||||||
)
|
|
||||||
if use_incremental_sync then
|
|
||||||
if not client._cached_buffers then
|
|
||||||
client._cached_buffers = {}
|
|
||||||
end
|
|
||||||
client._cached_buffers[bufnr] = nvim_buf_get_lines(bufnr, 0, -1, true)
|
|
||||||
end
|
|
||||||
if not client.resolved_capabilities.text_document_open_close then
|
if not client.resolved_capabilities.text_document_open_close then
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
@ -469,6 +619,9 @@ end
|
|||||||
--- server in the initialize request. Invalid/empty values will default to "off"
|
--- 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:
|
--@param flags: A table with flags for the client. The current (experimental) flags are:
|
||||||
--- - allow_incremental_sync (bool, default true): Allow using incremental sync for buffer edits
|
--- - 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
|
--@returns Client id. |vim.lsp.get_client_by_id()| Note: client may not be
|
||||||
--- fully initialized. Use `on_init` to do any actions once
|
--- fully initialized. Use `on_init` to do any actions once
|
||||||
@ -563,6 +716,7 @@ function lsp.start_client(config)
|
|||||||
uninitialized_clients[client_id] = nil
|
uninitialized_clients[client_id] = nil
|
||||||
|
|
||||||
lsp.diagnostic.reset(client_id, all_buffer_active_clients)
|
lsp.diagnostic.reset(client_id, all_buffer_active_clients)
|
||||||
|
changetracking.reset(client_id)
|
||||||
all_client_active_buffers[client_id] = nil
|
all_client_active_buffers[client_id] = nil
|
||||||
for _, client_ids in pairs(all_buffer_active_clients) do
|
for _, client_ids in pairs(all_buffer_active_clients) do
|
||||||
client_ids[client_id] = nil
|
client_ids[client_id] = nil
|
||||||
@ -721,6 +875,9 @@ function lsp.start_client(config)
|
|||||||
handler = resolve_handler(method)
|
handler = resolve_handler(method)
|
||||||
or error(string.format("not found: %q request handler for client %q.", method, client.name))
|
or error(string.format("not found: %q request handler for client %q.", method, client.name))
|
||||||
end
|
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)
|
local _ = log.debug() and log.debug(log_prefix, "client.request", client_id, method, params, handler, bufnr)
|
||||||
return rpc.request(method, params, function(err, result)
|
return rpc.request(method, params, function(err, result)
|
||||||
handler(err, method, result, client_id, bufnr)
|
handler(err, method, result, client_id, bufnr)
|
||||||
@ -765,6 +922,7 @@ function lsp.start_client(config)
|
|||||||
function client.stop(force)
|
function client.stop(force)
|
||||||
|
|
||||||
lsp.diagnostic.reset(client_id, all_buffer_active_clients)
|
lsp.diagnostic.reset(client_id, all_buffer_active_clients)
|
||||||
|
changetracking.reset(client_id)
|
||||||
all_client_active_buffers[client_id] = nil
|
all_client_active_buffers[client_id] = nil
|
||||||
for _, client_ids in pairs(all_buffer_active_clients) do
|
for _, client_ids in pairs(all_buffer_active_clients) do
|
||||||
client_ids[client_id] = nil
|
client_ids[client_id] = nil
|
||||||
@ -815,20 +973,6 @@ function lsp.start_client(config)
|
|||||||
return client_id
|
return client_id
|
||||||
end
|
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
|
--@private
|
||||||
--@fn text_document_did_change_handler(_, bufnr, changedtick, firstline, lastline, new_lastline, old_byte_size, old_utf32_size, old_utf16_size)
|
--@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.
|
--- Notify all attached clients that a buffer has changed.
|
||||||
@ -848,45 +992,9 @@ do
|
|||||||
if tbl_isempty(all_buffer_active_clients[bufnr] or {}) then
|
if tbl_isempty(all_buffer_active_clients[bufnr] or {}) then
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
util.buf_versions[bufnr] = changedtick
|
util.buf_versions[bufnr] = changedtick
|
||||||
|
local compute_change_and_notify = changetracking.prepare(bufnr, firstline, new_lastline, changedtick)
|
||||||
local incremental_changes = function(client)
|
for_each_buffer_client(bufnr, compute_change_and_notify)
|
||||||
local lines = nvim_buf_get_lines(bufnr, 0, -1, true)
|
|
||||||
local startline = math.min(firstline + 1, math.min(#client._cached_buffers[bufnr], #lines))
|
|
||||||
local endline = math.min(-(#lines - new_lastline), -1)
|
|
||||||
local incremental_change = vim.lsp.util.compute_diff(
|
|
||||||
client._cached_buffers[bufnr], lines, startline, endline, client.offset_encoding or "utf-16")
|
|
||||||
client._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)
|
|
||||||
for_each_buffer_client(bufnr, function(client)
|
|
||||||
local allow_incremental_sync = if_nil(client.config.flags.allow_incremental_sync, true)
|
|
||||||
local text_document_did_change = client.resolved_capabilities.text_document_did_change
|
|
||||||
local changes
|
|
||||||
if text_document_did_change == protocol.TextDocumentSyncKind.None then
|
|
||||||
return
|
|
||||||
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)
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -956,9 +1064,7 @@ function lsp.buf_attach_client(bufnr, client_id)
|
|||||||
if client.resolved_capabilities.text_document_open_close then
|
if client.resolved_capabilities.text_document_open_close then
|
||||||
client.notify('textDocument/didClose', params)
|
client.notify('textDocument/didClose', params)
|
||||||
end
|
end
|
||||||
if client._cached_buffers then
|
changetracking.reset_buf(client, bufnr)
|
||||||
client._cached_buffers[bufnr] = nil
|
|
||||||
end
|
|
||||||
end)
|
end)
|
||||||
util.buf_versions[bufnr] = nil
|
util.buf_versions[bufnr] = nil
|
||||||
all_buffer_active_clients[bufnr] = nil
|
all_buffer_active_clients[bufnr] = nil
|
||||||
|
Loading…
Reference in New Issue
Block a user