mirror of
https://github.com/neovim/neovim.git
synced 2024-12-19 18:55:14 -07:00
refactor(lsp): move changetracking to separate file (#26577)
* refactor(lsp): move changetracking to separate file - Prefixed changetracking types with `vim.lsp.` * fixup!: make _reset_timer a local function * fixup!: remove @private annotations * fixup!: changetracking.lua -> _changetracking.lua * fixup! types * fixup! add send_changes_for_group
This commit is contained in:
parent
8f08b1efbd
commit
5a2536de0c
@ -5,7 +5,7 @@ local lsp_rpc = require('vim.lsp.rpc')
|
||||
local protocol = require('vim.lsp.protocol')
|
||||
local ms = protocol.Methods
|
||||
local util = require('vim.lsp.util')
|
||||
local sync = require('vim.lsp.sync')
|
||||
local changetracking = require('vim.lsp._changetracking')
|
||||
local semantic_tokens = require('vim.lsp.semantic_tokens')
|
||||
|
||||
local api = vim.api
|
||||
@ -132,9 +132,10 @@ local format_line_ending = {
|
||||
['mac'] = '\r',
|
||||
}
|
||||
|
||||
---@private
|
||||
---@param bufnr (number)
|
||||
---@return string
|
||||
local function buf_get_line_ending(bufnr)
|
||||
function lsp._buf_get_line_ending(bufnr)
|
||||
return format_line_ending[vim.bo[bufnr].fileformat] or '\n'
|
||||
end
|
||||
|
||||
@ -305,12 +306,13 @@ local function validate_client_config(config)
|
||||
return cmd, cmd_args, offset_encoding
|
||||
end
|
||||
|
||||
---@private
|
||||
--- Returns full text of buffer {bufnr} as a string.
|
||||
---
|
||||
---@param bufnr (number) Buffer handle, or 0 for current.
|
||||
---@return string # Buffer text as string.
|
||||
local function buf_get_full_text(bufnr)
|
||||
local line_ending = buf_get_line_ending(bufnr)
|
||||
function lsp._buf_get_full_text(bufnr)
|
||||
local line_ending = lsp._buf_get_line_ending(bufnr)
|
||||
local text = table.concat(nvim_buf_get_lines(bufnr, 0, -1, true), line_ending)
|
||||
if vim.bo[bufnr].eol then
|
||||
text = text .. line_ending
|
||||
@ -338,354 +340,6 @@ local function once(fn)
|
||||
end
|
||||
end
|
||||
|
||||
local changetracking = {}
|
||||
do
|
||||
---@private
|
||||
---
|
||||
--- LSP has 3 different sync modes:
|
||||
--- - None (Servers will read the files themselves when needed)
|
||||
--- - Full (Client sends the full buffer content on updates)
|
||||
--- - Incremental (Client sends only the changed parts)
|
||||
---
|
||||
--- Changes are tracked per buffer.
|
||||
--- A buffer can have multiple clients attached and each client needs to send the changes
|
||||
--- To minimize the amount of changesets to compute, computation is grouped:
|
||||
---
|
||||
--- None: One group for all clients
|
||||
--- Full: One group for all clients
|
||||
--- Incremental: One group per `offset_encoding`
|
||||
---
|
||||
--- Sending changes can be debounced per buffer. To simplify the implementation the
|
||||
--- smallest debounce interval is used and we don't group clients by different intervals.
|
||||
---
|
||||
--- @class CTGroup
|
||||
--- @field sync_kind integer TextDocumentSyncKind, considers config.flags.allow_incremental_sync
|
||||
--- @field offset_encoding "utf-8"|"utf-16"|"utf-32"
|
||||
---
|
||||
--- @class CTBufferState
|
||||
--- @field name string name of the buffer
|
||||
--- @field lines string[] snapshot of buffer lines from last didChange
|
||||
--- @field lines_tmp string[]
|
||||
--- @field pending_changes table[] List of debounced changes in incremental sync mode
|
||||
--- @field timer nil|uv.uv_timer_t uv_timer
|
||||
--- @field last_flush nil|number uv.hrtime of the last flush/didChange-notification
|
||||
--- @field needs_flush boolean true if buffer updates haven't been sent to clients/servers yet
|
||||
--- @field refs integer how many clients are using this group
|
||||
---
|
||||
--- @class CTGroupState
|
||||
--- @field buffers table<integer, CTBufferState>
|
||||
--- @field debounce integer debounce duration in ms
|
||||
--- @field clients table<integer, table> clients using this state. {client_id, client}
|
||||
|
||||
---@param group CTGroup
|
||||
---@return string
|
||||
local function group_key(group)
|
||||
if group.sync_kind == protocol.TextDocumentSyncKind.Incremental then
|
||||
return tostring(group.sync_kind) .. '\0' .. group.offset_encoding
|
||||
end
|
||||
return tostring(group.sync_kind)
|
||||
end
|
||||
|
||||
---@private
|
||||
---@type table<CTGroup, CTGroupState>
|
||||
local state_by_group = setmetatable({}, {
|
||||
__index = function(tbl, k)
|
||||
return rawget(tbl, group_key(k))
|
||||
end,
|
||||
__newindex = function(tbl, k, v)
|
||||
rawset(tbl, group_key(k), v)
|
||||
end,
|
||||
})
|
||||
|
||||
---@param client lsp.Client
|
||||
---@return CTGroup
|
||||
local function get_group(client)
|
||||
local allow_inc_sync = if_nil(client.config.flags.allow_incremental_sync, true)
|
||||
local change_capability = vim.tbl_get(client.server_capabilities, 'textDocumentSync', 'change')
|
||||
local sync_kind = change_capability or protocol.TextDocumentSyncKind.None
|
||||
if not allow_inc_sync and change_capability == protocol.TextDocumentSyncKind.Incremental then
|
||||
sync_kind = protocol.TextDocumentSyncKind.Full --[[@as integer]]
|
||||
end
|
||||
return {
|
||||
sync_kind = sync_kind,
|
||||
offset_encoding = client.offset_encoding,
|
||||
}
|
||||
end
|
||||
|
||||
---@param state CTBufferState
|
||||
local function incremental_changes(state, encoding, bufnr, firstline, lastline, new_lastline)
|
||||
local prev_lines = state.lines
|
||||
local curr_lines = state.lines_tmp
|
||||
|
||||
local changed_lines = nvim_buf_get_lines(bufnr, firstline, new_lastline, true)
|
||||
for i = 1, firstline do
|
||||
curr_lines[i] = prev_lines[i]
|
||||
end
|
||||
for i = firstline + 1, new_lastline do
|
||||
curr_lines[i] = changed_lines[i - firstline]
|
||||
end
|
||||
for i = lastline + 1, #prev_lines do
|
||||
curr_lines[i - lastline + new_lastline] = prev_lines[i]
|
||||
end
|
||||
if tbl_isempty(curr_lines) then
|
||||
-- Can happen when deleting the entire contents of a buffer, see https://github.com/neovim/neovim/issues/16259.
|
||||
curr_lines[1] = ''
|
||||
end
|
||||
|
||||
local line_ending = buf_get_line_ending(bufnr)
|
||||
local incremental_change = sync.compute_diff(
|
||||
state.lines,
|
||||
curr_lines,
|
||||
firstline,
|
||||
lastline,
|
||||
new_lastline,
|
||||
encoding,
|
||||
line_ending
|
||||
)
|
||||
|
||||
-- Double-buffering of lines tables is used to reduce the load on the garbage collector.
|
||||
-- At this point the prev_lines table is useless, but its internal storage has already been allocated,
|
||||
-- so let's keep it around for the next didChange event, in which it will become the next
|
||||
-- curr_lines table. Note that setting elements to nil doesn't actually deallocate slots in the
|
||||
-- internal storage - it merely marks them as free, for the GC to deallocate them.
|
||||
for i in ipairs(prev_lines) do
|
||||
prev_lines[i] = nil
|
||||
end
|
||||
state.lines = curr_lines
|
||||
state.lines_tmp = prev_lines
|
||||
|
||||
return incremental_change
|
||||
end
|
||||
|
||||
---@private
|
||||
function changetracking.init(client, bufnr)
|
||||
assert(client.offset_encoding, 'lsp client must have an offset_encoding')
|
||||
local group = get_group(client)
|
||||
local state = state_by_group[group]
|
||||
if state then
|
||||
state.debounce = math.min(state.debounce, client.config.flags.debounce_text_changes or 150)
|
||||
state.clients[client.id] = client
|
||||
else
|
||||
state = {
|
||||
buffers = {},
|
||||
debounce = client.config.flags.debounce_text_changes or 150,
|
||||
clients = {
|
||||
[client.id] = client,
|
||||
},
|
||||
}
|
||||
state_by_group[group] = state
|
||||
end
|
||||
local buf_state = state.buffers[bufnr]
|
||||
if buf_state then
|
||||
buf_state.refs = buf_state.refs + 1
|
||||
else
|
||||
buf_state = {
|
||||
name = api.nvim_buf_get_name(bufnr),
|
||||
lines = {},
|
||||
lines_tmp = {},
|
||||
pending_changes = {},
|
||||
needs_flush = false,
|
||||
refs = 1,
|
||||
}
|
||||
state.buffers[bufnr] = buf_state
|
||||
if group.sync_kind == protocol.TextDocumentSyncKind.Incremental then
|
||||
buf_state.lines = nvim_buf_get_lines(bufnr, 0, -1, true)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
---@private
|
||||
function changetracking._get_and_set_name(client, bufnr, name)
|
||||
local state = state_by_group[get_group(client)] or {}
|
||||
local buf_state = (state.buffers or {})[bufnr]
|
||||
local old_name = buf_state.name
|
||||
buf_state.name = name
|
||||
return old_name
|
||||
end
|
||||
|
||||
---@private
|
||||
function changetracking.reset_buf(client, bufnr)
|
||||
changetracking.flush(client, bufnr)
|
||||
local state = state_by_group[get_group(client)]
|
||||
if not state then
|
||||
return
|
||||
end
|
||||
assert(state.buffers, 'CTGroupState must have buffers')
|
||||
local buf_state = state.buffers[bufnr]
|
||||
buf_state.refs = buf_state.refs - 1
|
||||
assert(buf_state.refs >= 0, 'refcount on buffer state must not get negative')
|
||||
if buf_state.refs == 0 then
|
||||
state.buffers[bufnr] = nil
|
||||
changetracking._reset_timer(buf_state)
|
||||
end
|
||||
end
|
||||
|
||||
---@private
|
||||
function changetracking.reset(client)
|
||||
local state = state_by_group[get_group(client)]
|
||||
if not state then
|
||||
return
|
||||
end
|
||||
state.clients[client.id] = nil
|
||||
if vim.tbl_count(state.clients) == 0 then
|
||||
for _, buf_state in pairs(state.buffers) do
|
||||
changetracking._reset_timer(buf_state)
|
||||
end
|
||||
state.buffers = {}
|
||||
end
|
||||
end
|
||||
|
||||
-- Adjust debounce time by taking time of last didChange notification into
|
||||
-- consideration. If the last didChange happened more than `debounce` time ago,
|
||||
-- debounce can be skipped and otherwise maybe reduced.
|
||||
--
|
||||
-- This turns the debounce into a kind of client rate limiting
|
||||
--
|
||||
---@param debounce integer
|
||||
---@param buf_state CTBufferState
|
||||
---@return number
|
||||
local function next_debounce(debounce, buf_state)
|
||||
if debounce == 0 then
|
||||
return 0
|
||||
end
|
||||
local ns_to_ms = 0.000001
|
||||
if not buf_state.last_flush then
|
||||
return debounce
|
||||
end
|
||||
local now = uv.hrtime()
|
||||
local ms_since_last_flush = (now - buf_state.last_flush) * ns_to_ms
|
||||
return math.max(debounce - ms_since_last_flush, 0)
|
||||
end
|
||||
|
||||
---@param bufnr integer
|
||||
---@param sync_kind integer protocol.TextDocumentSyncKind
|
||||
---@param state CTGroupState
|
||||
---@param buf_state CTBufferState
|
||||
local function send_changes(bufnr, sync_kind, state, buf_state)
|
||||
if not buf_state.needs_flush then
|
||||
return
|
||||
end
|
||||
buf_state.last_flush = uv.hrtime()
|
||||
buf_state.needs_flush = false
|
||||
|
||||
if not api.nvim_buf_is_valid(bufnr) then
|
||||
buf_state.pending_changes = {}
|
||||
return
|
||||
end
|
||||
|
||||
local changes --- @type lsp.TextDocumentContentChangeEvent[]
|
||||
if sync_kind == protocol.TextDocumentSyncKind.None then
|
||||
return
|
||||
elseif sync_kind == protocol.TextDocumentSyncKind.Incremental then
|
||||
changes = buf_state.pending_changes
|
||||
buf_state.pending_changes = {}
|
||||
else
|
||||
changes = {
|
||||
{ text = buf_get_full_text(bufnr) },
|
||||
}
|
||||
end
|
||||
local uri = vim.uri_from_bufnr(bufnr)
|
||||
for _, client in pairs(state.clients) do
|
||||
if not client.is_stopped() and lsp.buf_is_attached(bufnr, client.id) then
|
||||
client.notify(ms.textDocument_didChange, {
|
||||
textDocument = {
|
||||
uri = uri,
|
||||
version = util.buf_versions[bufnr],
|
||||
},
|
||||
contentChanges = changes,
|
||||
})
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
---@private
|
||||
function changetracking.send_changes(bufnr, firstline, lastline, new_lastline)
|
||||
local groups = {} ---@type table<string,CTGroup>
|
||||
for _, client in pairs(lsp.get_clients({ bufnr = bufnr })) do
|
||||
local group = get_group(client)
|
||||
groups[group_key(group)] = group
|
||||
end
|
||||
for _, group in pairs(groups) do
|
||||
local state = state_by_group[group]
|
||||
if not state then
|
||||
error(
|
||||
string.format(
|
||||
'changetracking.init must have been called for all LSP clients. group=%s states=%s',
|
||||
vim.inspect(group),
|
||||
vim.inspect(vim.tbl_keys(state_by_group))
|
||||
)
|
||||
)
|
||||
end
|
||||
local buf_state = state.buffers[bufnr]
|
||||
buf_state.needs_flush = true
|
||||
changetracking._reset_timer(buf_state)
|
||||
local debounce = next_debounce(state.debounce, buf_state)
|
||||
if group.sync_kind == protocol.TextDocumentSyncKind.Incremental then
|
||||
-- This must be done immediately and cannot be delayed
|
||||
-- The contents would further change and startline/endline may no longer fit
|
||||
local changes = incremental_changes(
|
||||
buf_state,
|
||||
group.offset_encoding,
|
||||
bufnr,
|
||||
firstline,
|
||||
lastline,
|
||||
new_lastline
|
||||
)
|
||||
table.insert(buf_state.pending_changes, changes)
|
||||
end
|
||||
if debounce == 0 then
|
||||
send_changes(bufnr, group.sync_kind, state, buf_state)
|
||||
else
|
||||
local timer = assert(uv.new_timer(), 'Must be able to create timer')
|
||||
buf_state.timer = timer
|
||||
timer:start(
|
||||
debounce,
|
||||
0,
|
||||
vim.schedule_wrap(function()
|
||||
changetracking._reset_timer(buf_state)
|
||||
send_changes(bufnr, group.sync_kind, state, buf_state)
|
||||
end)
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
---@private
|
||||
---@param buf_state CTBufferState
|
||||
function changetracking._reset_timer(buf_state)
|
||||
local timer = buf_state.timer
|
||||
if timer then
|
||||
buf_state.timer = nil
|
||||
if not timer:is_closing() then
|
||||
timer:stop()
|
||||
timer:close()
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--- Flushes any outstanding change notification.
|
||||
---@private
|
||||
---@param client lsp.Client
|
||||
---@param bufnr? integer
|
||||
function changetracking.flush(client, bufnr)
|
||||
local group = get_group(client)
|
||||
local state = state_by_group[group]
|
||||
if not state then
|
||||
return
|
||||
end
|
||||
if bufnr then
|
||||
local buf_state = state.buffers[bufnr] or {}
|
||||
changetracking._reset_timer(buf_state)
|
||||
send_changes(bufnr, group.sync_kind, state, buf_state)
|
||||
else
|
||||
for buf, buf_state in pairs(state.buffers) do
|
||||
changetracking._reset_timer(buf_state)
|
||||
send_changes(buf, group.sync_kind, state, buf_state)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--- Default handler for the 'textDocument/didOpen' LSP notification.
|
||||
---
|
||||
---@param bufnr integer Number of the buffer, or 0 for current
|
||||
@ -705,7 +359,7 @@ local function text_document_did_open_handler(bufnr, client)
|
||||
version = 0,
|
||||
uri = vim.uri_from_bufnr(bufnr),
|
||||
languageId = client.config.get_language_id(bufnr, filetype),
|
||||
text = buf_get_full_text(bufnr),
|
||||
text = lsp._buf_get_full_text(bufnr),
|
||||
},
|
||||
}
|
||||
client.notify(ms.textDocument_didOpen, params)
|
||||
@ -1792,7 +1446,7 @@ end
|
||||
local function text_document_did_save_handler(bufnr)
|
||||
bufnr = resolve_bufnr(bufnr)
|
||||
local uri = vim.uri_from_bufnr(bufnr)
|
||||
local text = once(buf_get_full_text)
|
||||
local text = once(lsp._buf_get_full_text)
|
||||
for _, client in ipairs(lsp.get_clients({ bufnr = bufnr })) do
|
||||
local name = api.nvim_buf_get_name(bufnr)
|
||||
local old_name = changetracking._get_and_set_name(client, bufnr, name)
|
||||
@ -1807,7 +1461,7 @@ local function text_document_did_save_handler(bufnr)
|
||||
version = 0,
|
||||
uri = uri,
|
||||
languageId = client.config.get_language_id(bufnr, vim.bo[bufnr].filetype),
|
||||
text = buf_get_full_text(bufnr),
|
||||
text = lsp._buf_get_full_text(bufnr),
|
||||
},
|
||||
})
|
||||
util.buf_versions[bufnr] = 0
|
||||
|
373
runtime/lua/vim/lsp/_changetracking.lua
Normal file
373
runtime/lua/vim/lsp/_changetracking.lua
Normal file
@ -0,0 +1,373 @@
|
||||
local protocol = require('vim.lsp.protocol')
|
||||
local sync = require('vim.lsp.sync')
|
||||
local util = require('vim.lsp.util')
|
||||
|
||||
local api = vim.api
|
||||
local uv = vim.uv
|
||||
|
||||
local M = {}
|
||||
|
||||
--- LSP has 3 different sync modes:
|
||||
--- - None (Servers will read the files themselves when needed)
|
||||
--- - Full (Client sends the full buffer content on updates)
|
||||
--- - Incremental (Client sends only the changed parts)
|
||||
---
|
||||
--- Changes are tracked per buffer.
|
||||
--- A buffer can have multiple clients attached and each client needs to send the changes
|
||||
--- To minimize the amount of changesets to compute, computation is grouped:
|
||||
---
|
||||
--- None: One group for all clients
|
||||
--- Full: One group for all clients
|
||||
--- Incremental: One group per `offset_encoding`
|
||||
---
|
||||
--- Sending changes can be debounced per buffer. To simplify the implementation the
|
||||
--- smallest debounce interval is used and we don't group clients by different intervals.
|
||||
---
|
||||
--- @class vim.lsp.CTGroup
|
||||
--- @field sync_kind integer TextDocumentSyncKind, considers config.flags.allow_incremental_sync
|
||||
--- @field offset_encoding "utf-8"|"utf-16"|"utf-32"
|
||||
---
|
||||
--- @class vim.lsp.CTBufferState
|
||||
--- @field name string name of the buffer
|
||||
--- @field lines string[] snapshot of buffer lines from last didChange
|
||||
--- @field lines_tmp string[]
|
||||
--- @field pending_changes table[] List of debounced changes in incremental sync mode
|
||||
--- @field timer uv.uv_timer_t? uv_timer
|
||||
--- @field last_flush nil|number uv.hrtime of the last flush/didChange-notification
|
||||
--- @field needs_flush boolean true if buffer updates haven't been sent to clients/servers yet
|
||||
--- @field refs integer how many clients are using this group
|
||||
---
|
||||
--- @class vim.lsp.CTGroupState
|
||||
--- @field buffers table<integer,vim.lsp.CTBufferState>
|
||||
--- @field debounce integer debounce duration in ms
|
||||
--- @field clients table<integer, table> clients using this state. {client_id, client}
|
||||
|
||||
---@param group vim.lsp.CTGroup
|
||||
---@return string
|
||||
local function group_key(group)
|
||||
if group.sync_kind == protocol.TextDocumentSyncKind.Incremental then
|
||||
return tostring(group.sync_kind) .. '\0' .. group.offset_encoding
|
||||
end
|
||||
return tostring(group.sync_kind)
|
||||
end
|
||||
|
||||
---@type table<vim.lsp.CTGroup,vim.lsp.CTGroupState>
|
||||
local state_by_group = setmetatable({}, {
|
||||
__index = function(tbl, k)
|
||||
return rawget(tbl, group_key(k))
|
||||
end,
|
||||
__newindex = function(tbl, k, v)
|
||||
rawset(tbl, group_key(k), v)
|
||||
end,
|
||||
})
|
||||
|
||||
---@param client lsp.Client
|
||||
---@return vim.lsp.CTGroup
|
||||
local function get_group(client)
|
||||
local allow_inc_sync = vim.F.if_nil(client.config.flags.allow_incremental_sync, true)
|
||||
local change_capability = vim.tbl_get(client.server_capabilities, 'textDocumentSync', 'change')
|
||||
local sync_kind = change_capability or protocol.TextDocumentSyncKind.None
|
||||
if not allow_inc_sync and change_capability == protocol.TextDocumentSyncKind.Incremental then
|
||||
sync_kind = protocol.TextDocumentSyncKind.Full --[[@as integer]]
|
||||
end
|
||||
return {
|
||||
sync_kind = sync_kind,
|
||||
offset_encoding = client.offset_encoding,
|
||||
}
|
||||
end
|
||||
|
||||
---@param state vim.lsp.CTBufferState
|
||||
---@param encoding string
|
||||
---@param bufnr integer
|
||||
---@param firstline integer
|
||||
---@param lastline integer
|
||||
---@param new_lastline integer
|
||||
---@return lsp.TextDocumentContentChangeEvent
|
||||
local function incremental_changes(state, encoding, bufnr, firstline, lastline, new_lastline)
|
||||
local prev_lines = state.lines
|
||||
local curr_lines = state.lines_tmp
|
||||
|
||||
local changed_lines = api.nvim_buf_get_lines(bufnr, firstline, new_lastline, true)
|
||||
for i = 1, firstline do
|
||||
curr_lines[i] = prev_lines[i]
|
||||
end
|
||||
for i = firstline + 1, new_lastline do
|
||||
curr_lines[i] = changed_lines[i - firstline]
|
||||
end
|
||||
for i = lastline + 1, #prev_lines do
|
||||
curr_lines[i - lastline + new_lastline] = prev_lines[i]
|
||||
end
|
||||
if vim.tbl_isempty(curr_lines) then
|
||||
-- Can happen when deleting the entire contents of a buffer, see https://github.com/neovim/neovim/issues/16259.
|
||||
curr_lines[1] = ''
|
||||
end
|
||||
|
||||
local line_ending = vim.lsp._buf_get_line_ending(bufnr)
|
||||
local incremental_change = sync.compute_diff(
|
||||
state.lines,
|
||||
curr_lines,
|
||||
firstline,
|
||||
lastline,
|
||||
new_lastline,
|
||||
encoding,
|
||||
line_ending
|
||||
)
|
||||
|
||||
-- Double-buffering of lines tables is used to reduce the load on the garbage collector.
|
||||
-- At this point the prev_lines table is useless, but its internal storage has already been allocated,
|
||||
-- so let's keep it around for the next didChange event, in which it will become the next
|
||||
-- curr_lines table. Note that setting elements to nil doesn't actually deallocate slots in the
|
||||
-- internal storage - it merely marks them as free, for the GC to deallocate them.
|
||||
for i in ipairs(prev_lines) do
|
||||
prev_lines[i] = nil
|
||||
end
|
||||
state.lines = curr_lines
|
||||
state.lines_tmp = prev_lines
|
||||
|
||||
return incremental_change
|
||||
end
|
||||
|
||||
---@param client lsp.Client
|
||||
---@param bufnr integer
|
||||
function M.init(client, bufnr)
|
||||
assert(client.offset_encoding, 'lsp client must have an offset_encoding')
|
||||
local group = get_group(client)
|
||||
local state = state_by_group[group]
|
||||
if state then
|
||||
state.debounce = math.min(state.debounce, client.config.flags.debounce_text_changes or 150)
|
||||
state.clients[client.id] = client
|
||||
else
|
||||
state = {
|
||||
buffers = {},
|
||||
debounce = client.config.flags.debounce_text_changes or 150,
|
||||
clients = {
|
||||
[client.id] = client,
|
||||
},
|
||||
}
|
||||
state_by_group[group] = state
|
||||
end
|
||||
local buf_state = state.buffers[bufnr]
|
||||
if buf_state then
|
||||
buf_state.refs = buf_state.refs + 1
|
||||
else
|
||||
buf_state = {
|
||||
name = api.nvim_buf_get_name(bufnr),
|
||||
lines = {},
|
||||
lines_tmp = {},
|
||||
pending_changes = {},
|
||||
needs_flush = false,
|
||||
refs = 1,
|
||||
}
|
||||
state.buffers[bufnr] = buf_state
|
||||
if group.sync_kind == protocol.TextDocumentSyncKind.Incremental then
|
||||
buf_state.lines = api.nvim_buf_get_lines(bufnr, 0, -1, true)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--- @param client lsp.Client
|
||||
--- @param bufnr integer
|
||||
--- @param name string
|
||||
--- @return string
|
||||
function M._get_and_set_name(client, bufnr, name)
|
||||
local state = state_by_group[get_group(client)] or {}
|
||||
local buf_state = (state.buffers or {})[bufnr]
|
||||
local old_name = buf_state.name
|
||||
buf_state.name = name
|
||||
return old_name
|
||||
end
|
||||
|
||||
---@param buf_state vim.lsp.CTBufferState
|
||||
local function reset_timer(buf_state)
|
||||
local timer = buf_state.timer
|
||||
if timer then
|
||||
buf_state.timer = nil
|
||||
if not timer:is_closing() then
|
||||
timer:stop()
|
||||
timer:close()
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--- @param client lsp.Client
|
||||
--- @param bufnr integer
|
||||
function M.reset_buf(client, bufnr)
|
||||
M.flush(client, bufnr)
|
||||
local state = state_by_group[get_group(client)]
|
||||
if not state then
|
||||
return
|
||||
end
|
||||
assert(state.buffers, 'CTGroupState must have buffers')
|
||||
local buf_state = state.buffers[bufnr]
|
||||
buf_state.refs = buf_state.refs - 1
|
||||
assert(buf_state.refs >= 0, 'refcount on buffer state must not get negative')
|
||||
if buf_state.refs == 0 then
|
||||
state.buffers[bufnr] = nil
|
||||
reset_timer(buf_state)
|
||||
end
|
||||
end
|
||||
|
||||
--- @param client lsp.Client
|
||||
function M.reset(client)
|
||||
local state = state_by_group[get_group(client)]
|
||||
if not state then
|
||||
return
|
||||
end
|
||||
state.clients[client.id] = nil
|
||||
if vim.tbl_count(state.clients) == 0 then
|
||||
for _, buf_state in pairs(state.buffers) do
|
||||
reset_timer(buf_state)
|
||||
end
|
||||
state.buffers = {}
|
||||
end
|
||||
end
|
||||
|
||||
-- Adjust debounce time by taking time of last didChange notification into
|
||||
-- consideration. If the last didChange happened more than `debounce` time ago,
|
||||
-- debounce can be skipped and otherwise maybe reduced.
|
||||
--
|
||||
-- This turns the debounce into a kind of client rate limiting
|
||||
--
|
||||
---@param debounce integer
|
||||
---@param buf_state vim.lsp.CTBufferState
|
||||
---@return number
|
||||
local function next_debounce(debounce, buf_state)
|
||||
if debounce == 0 then
|
||||
return 0
|
||||
end
|
||||
local ns_to_ms = 0.000001
|
||||
if not buf_state.last_flush then
|
||||
return debounce
|
||||
end
|
||||
local now = uv.hrtime()
|
||||
local ms_since_last_flush = (now - buf_state.last_flush) * ns_to_ms
|
||||
return math.max(debounce - ms_since_last_flush, 0)
|
||||
end
|
||||
|
||||
---@param bufnr integer
|
||||
---@param sync_kind integer protocol.TextDocumentSyncKind
|
||||
---@param state vim.lsp.CTGroupState
|
||||
---@param buf_state vim.lsp.CTBufferState
|
||||
local function send_changes(bufnr, sync_kind, state, buf_state)
|
||||
if not buf_state.needs_flush then
|
||||
return
|
||||
end
|
||||
buf_state.last_flush = uv.hrtime()
|
||||
buf_state.needs_flush = false
|
||||
|
||||
if not api.nvim_buf_is_valid(bufnr) then
|
||||
buf_state.pending_changes = {}
|
||||
return
|
||||
end
|
||||
|
||||
local changes --- @type lsp.TextDocumentContentChangeEvent[]
|
||||
if sync_kind == protocol.TextDocumentSyncKind.None then
|
||||
return
|
||||
elseif sync_kind == protocol.TextDocumentSyncKind.Incremental then
|
||||
changes = buf_state.pending_changes
|
||||
buf_state.pending_changes = {}
|
||||
else
|
||||
changes = {
|
||||
{ text = vim.lsp._buf_get_full_text(bufnr) },
|
||||
}
|
||||
end
|
||||
local uri = vim.uri_from_bufnr(bufnr)
|
||||
for _, client in pairs(state.clients) do
|
||||
if not client.is_stopped() and vim.lsp.buf_is_attached(bufnr, client.id) then
|
||||
client.notify(protocol.Methods.textDocument_didChange, {
|
||||
textDocument = {
|
||||
uri = uri,
|
||||
version = util.buf_versions[bufnr],
|
||||
},
|
||||
contentChanges = changes,
|
||||
})
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--- @param bufnr integer
|
||||
--- @param firstline integer
|
||||
--- @param lastline integer
|
||||
--- @param new_lastline integer
|
||||
--- @param group vim.lsp.CTGroup
|
||||
local function send_changes_for_group(bufnr, firstline, lastline, new_lastline, group)
|
||||
local state = state_by_group[group]
|
||||
if not state then
|
||||
error(
|
||||
string.format(
|
||||
'changetracking.init must have been called for all LSP clients. group=%s states=%s',
|
||||
vim.inspect(group),
|
||||
vim.inspect(vim.tbl_keys(state_by_group))
|
||||
)
|
||||
)
|
||||
end
|
||||
local buf_state = state.buffers[bufnr]
|
||||
buf_state.needs_flush = true
|
||||
reset_timer(buf_state)
|
||||
local debounce = next_debounce(state.debounce, buf_state)
|
||||
if group.sync_kind == protocol.TextDocumentSyncKind.Incremental then
|
||||
-- This must be done immediately and cannot be delayed
|
||||
-- The contents would further change and startline/endline may no longer fit
|
||||
local changes = incremental_changes(
|
||||
buf_state,
|
||||
group.offset_encoding,
|
||||
bufnr,
|
||||
firstline,
|
||||
lastline,
|
||||
new_lastline
|
||||
)
|
||||
table.insert(buf_state.pending_changes, changes)
|
||||
end
|
||||
if debounce == 0 then
|
||||
send_changes(bufnr, group.sync_kind, state, buf_state)
|
||||
else
|
||||
local timer = assert(uv.new_timer(), 'Must be able to create timer')
|
||||
buf_state.timer = timer
|
||||
timer:start(
|
||||
debounce,
|
||||
0,
|
||||
vim.schedule_wrap(function()
|
||||
reset_timer(buf_state)
|
||||
send_changes(bufnr, group.sync_kind, state, buf_state)
|
||||
end)
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
--- @param bufnr integer
|
||||
--- @param firstline integer
|
||||
--- @param lastline integer
|
||||
--- @param new_lastline integer
|
||||
function M.send_changes(bufnr, firstline, lastline, new_lastline)
|
||||
local groups = {} ---@type table<string,vim.lsp.CTGroup>
|
||||
for _, client in pairs(vim.lsp.get_clients({ bufnr = bufnr })) do
|
||||
local group = get_group(client)
|
||||
groups[group_key(group)] = group
|
||||
end
|
||||
for _, group in pairs(groups) do
|
||||
send_changes_for_group(bufnr, firstline, lastline, new_lastline, group)
|
||||
end
|
||||
end
|
||||
|
||||
--- Flushes any outstanding change notification.
|
||||
---@param client lsp.Client
|
||||
---@param bufnr? integer
|
||||
function M.flush(client, bufnr)
|
||||
local group = get_group(client)
|
||||
local state = state_by_group[group]
|
||||
if not state then
|
||||
return
|
||||
end
|
||||
if bufnr then
|
||||
local buf_state = state.buffers[bufnr] or {}
|
||||
reset_timer(buf_state)
|
||||
send_changes(bufnr, group.sync_kind, state, buf_state)
|
||||
else
|
||||
for buf, buf_state in pairs(state.buffers) do
|
||||
reset_timer(buf_state)
|
||||
send_changes(buf, group.sync_kind, state, buf_state)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return M
|
@ -397,7 +397,7 @@ end
|
||||
---@param new_lastline integer line to begin search in new_lines for last difference
|
||||
---@param offset_encoding string encoding requested by language server
|
||||
---@param line_ending string
|
||||
---@return table TextDocumentContentChangeEvent see https://microsoft.github.io/language-server-protocol/specification/#textDocumentContentChangeEvent
|
||||
---@return lsp.TextDocumentContentChangeEvent : see https://microsoft.github.io/language-server-protocol/specification/#textDocumentContentChangeEvent
|
||||
function M.compute_diff(
|
||||
prev_lines,
|
||||
curr_lines,
|
||||
|
Loading…
Reference in New Issue
Block a user