_changetracking.lua (11794B)
1 local protocol = require('vim.lsp.protocol') 2 local sync = require('vim.lsp.sync') 3 local util = require('vim.lsp.util') 4 5 local api = vim.api 6 local uv = vim.uv 7 8 local M = {} 9 10 --- LSP has 3 different sync modes: 11 --- - None (Servers will read the files themselves when needed) 12 --- - Full (Client sends the full buffer content on updates) 13 --- - Incremental (Client sends only the changed parts) 14 --- 15 --- Changes are tracked per buffer. 16 --- A buffer can have multiple clients attached and each client needs to send the changes 17 --- To minimize the amount of changesets to compute, computation is grouped: 18 --- 19 --- None: One group for all clients 20 --- Full: One group for all clients 21 --- Incremental: One group per `position_encoding` 22 --- 23 --- Sending changes can be debounced per buffer. To simplify the implementation the 24 --- smallest debounce interval is used and we don't group clients by different intervals. 25 --- 26 --- @class vim.lsp.CTGroup 27 --- @field sync_kind integer TextDocumentSyncKind, considers config.flags.allow_incremental_sync 28 --- @field position_encoding "utf-8"|"utf-16"|"utf-32" 29 --- 30 --- @class vim.lsp.CTBufferState 31 --- @field name string name of the buffer 32 --- @field lines string[] snapshot of buffer lines from last didChange 33 --- @field lines_tmp string[] 34 --- @field pending_changes table[] List of debounced changes in incremental sync mode 35 --- @field timer uv.uv_timer_t? uv_timer 36 --- @field last_flush nil|number uv.hrtime of the last flush/didChange-notification 37 --- @field needs_flush boolean true if buffer updates haven't been sent to clients/servers yet 38 --- @field refs integer how many clients are using this group 39 --- 40 --- @class vim.lsp.CTGroupState 41 --- @field buffers table<integer,vim.lsp.CTBufferState> 42 --- @field debounce integer debounce duration in ms 43 --- @field clients table<integer, vim.lsp.Client> clients using this state. {client_id, client} 44 45 ---@param group vim.lsp.CTGroup 46 ---@return string 47 local function group_key(group) 48 if group.sync_kind == protocol.TextDocumentSyncKind.Incremental then 49 return tostring(group.sync_kind) .. '\0' .. group.position_encoding 50 end 51 return tostring(group.sync_kind) 52 end 53 54 ---@type table<vim.lsp.CTGroup,vim.lsp.CTGroupState> 55 local state_by_group = setmetatable({}, { 56 __index = function(tbl, k) 57 return rawget(tbl, group_key(k)) 58 end, 59 __newindex = function(tbl, k, v) 60 rawset(tbl, group_key(k), v) 61 end, 62 }) 63 64 ---@param client vim.lsp.Client 65 ---@return vim.lsp.CTGroup 66 local function get_group(client) 67 local allow_inc_sync = vim.F.if_nil(client.flags.allow_incremental_sync, true) 68 local change_capability = vim.tbl_get(client.server_capabilities, 'textDocumentSync', 'change') 69 local sync_kind = change_capability or protocol.TextDocumentSyncKind.None 70 if not allow_inc_sync and change_capability == protocol.TextDocumentSyncKind.Incremental then 71 sync_kind = protocol.TextDocumentSyncKind.Full --[[@as integer]] 72 end 73 return { 74 sync_kind = sync_kind, 75 position_encoding = client.offset_encoding, 76 } 77 end 78 79 ---@param state vim.lsp.CTBufferState 80 ---@param encoding string 81 ---@param bufnr integer 82 ---@param firstline integer 83 ---@param lastline integer 84 ---@param new_lastline integer 85 ---@return lsp.TextDocumentContentChangeEvent 86 local function incremental_changes(state, encoding, bufnr, firstline, lastline, new_lastline) 87 local prev_lines = state.lines 88 local curr_lines = state.lines_tmp 89 90 local changed_lines = api.nvim_buf_get_lines(bufnr, firstline, new_lastline, true) 91 for i = 1, firstline do 92 curr_lines[i] = prev_lines[i] 93 end 94 for i = firstline + 1, new_lastline do 95 curr_lines[i] = changed_lines[i - firstline] 96 end 97 for i = lastline + 1, #prev_lines do 98 curr_lines[i - lastline + new_lastline] = prev_lines[i] 99 end 100 if vim.tbl_isempty(curr_lines) then 101 -- Can happen when deleting the entire contents of a buffer, see https://github.com/neovim/neovim/issues/16259. 102 curr_lines[1] = '' 103 end 104 105 local line_ending = vim.lsp._buf_get_line_ending(bufnr) 106 local incremental_change = sync.compute_diff( 107 prev_lines, 108 curr_lines, 109 firstline, 110 lastline, 111 new_lastline, 112 encoding, 113 line_ending 114 ) 115 116 -- Double-buffering of lines tables is used to reduce the load on the garbage collector. 117 -- At this point the prev_lines table is useless, but its internal storage has already been allocated, 118 -- so let's keep it around for the next didChange event, in which it will become the next 119 -- curr_lines table. Note that setting elements to nil doesn't actually deallocate slots in the 120 -- internal storage - it merely marks them as free, for the GC to deallocate them. 121 for i in ipairs(prev_lines) do 122 prev_lines[i] = nil 123 end 124 state.lines = curr_lines 125 state.lines_tmp = prev_lines 126 127 return incremental_change 128 end 129 130 ---@param client vim.lsp.Client 131 ---@param bufnr integer 132 function M.init(client, bufnr) 133 assert(client.offset_encoding, 'lsp client must have an offset_encoding') 134 local group = get_group(client) 135 local state = state_by_group[group] 136 if state then 137 state.debounce = math.min(state.debounce, client.flags.debounce_text_changes or 150) 138 state.clients[client.id] = client 139 else 140 state = { 141 buffers = {}, 142 debounce = client.flags.debounce_text_changes or 150, 143 clients = { 144 [client.id] = client, 145 }, 146 } 147 state_by_group[group] = state 148 end 149 local buf_state = state.buffers[bufnr] 150 if buf_state then 151 buf_state.refs = buf_state.refs + 1 152 else 153 buf_state = { 154 name = api.nvim_buf_get_name(bufnr), 155 lines = {}, 156 lines_tmp = {}, 157 pending_changes = {}, 158 needs_flush = false, 159 refs = 1, 160 } 161 state.buffers[bufnr] = buf_state 162 if group.sync_kind == protocol.TextDocumentSyncKind.Incremental then 163 buf_state.lines = api.nvim_buf_get_lines(bufnr, 0, -1, true) 164 end 165 end 166 end 167 168 --- @param client vim.lsp.Client 169 --- @param bufnr integer 170 --- @param name string 171 --- @return string 172 function M._get_and_set_name(client, bufnr, name) 173 local state = state_by_group[get_group(client)] or {} 174 local buf_state = (state.buffers or {})[bufnr] 175 local old_name = buf_state.name 176 buf_state.name = name 177 return old_name 178 end 179 180 ---@param buf_state vim.lsp.CTBufferState 181 local function reset_timer(buf_state) 182 local timer = buf_state.timer 183 if timer then 184 buf_state.timer = nil 185 if not timer:is_closing() then 186 timer:stop() 187 timer:close() 188 end 189 end 190 end 191 192 --- @param client vim.lsp.Client 193 --- @param bufnr integer 194 function M.reset_buf(client, bufnr) 195 M.flush(client, bufnr) 196 local state = state_by_group[get_group(client)] 197 if not state then 198 return 199 end 200 assert(state.buffers, 'CTGroupState must have buffers') 201 local buf_state = state.buffers[bufnr] 202 if not buf_state then 203 return 204 end 205 buf_state.refs = buf_state.refs - 1 206 assert(buf_state.refs >= 0, 'refcount on buffer state must not get negative') 207 if buf_state.refs == 0 then 208 state.buffers[bufnr] = nil 209 reset_timer(buf_state) 210 end 211 end 212 213 --- @param client vim.lsp.Client 214 function M.reset(client) 215 local state = state_by_group[get_group(client)] 216 if not state then 217 return 218 end 219 state.clients[client.id] = nil 220 if vim.tbl_count(state.clients) == 0 then 221 for _, buf_state in pairs(state.buffers) do 222 reset_timer(buf_state) 223 end 224 state.buffers = {} 225 end 226 end 227 228 -- Adjust debounce time by taking time of last didChange notification into 229 -- consideration. If the last didChange happened more than `debounce` time ago, 230 -- debounce can be skipped and otherwise maybe reduced. 231 -- 232 -- This turns the debounce into a kind of client rate limiting 233 -- 234 ---@param debounce integer 235 ---@param buf_state vim.lsp.CTBufferState 236 ---@return number 237 local function next_debounce(debounce, buf_state) 238 if debounce == 0 then 239 return 0 240 end 241 local ns_to_ms = 0.000001 242 if not buf_state.last_flush then 243 return debounce 244 end 245 local now = uv.hrtime() 246 local ms_since_last_flush = (now - buf_state.last_flush) * ns_to_ms 247 return math.max(debounce - ms_since_last_flush, 0) 248 end 249 250 ---@param bufnr integer 251 ---@param sync_kind integer protocol.TextDocumentSyncKind 252 ---@param state vim.lsp.CTGroupState 253 ---@param buf_state vim.lsp.CTBufferState 254 local function send_changes(bufnr, sync_kind, state, buf_state) 255 if not buf_state.needs_flush then 256 return 257 end 258 buf_state.last_flush = uv.hrtime() 259 buf_state.needs_flush = false 260 261 if not api.nvim_buf_is_valid(bufnr) then 262 buf_state.pending_changes = {} 263 return 264 end 265 266 local changes --- @type lsp.TextDocumentContentChangeEvent[] 267 if sync_kind == protocol.TextDocumentSyncKind.None then 268 return 269 elseif sync_kind == protocol.TextDocumentSyncKind.Incremental then 270 changes = buf_state.pending_changes 271 buf_state.pending_changes = {} 272 else 273 changes = { 274 { text = vim.lsp._buf_get_full_text(bufnr) }, 275 } 276 end 277 local uri = vim.uri_from_bufnr(bufnr) 278 for _, client in pairs(state.clients) do 279 if not client:is_stopped() and vim.lsp.buf_is_attached(bufnr, client.id) then 280 client:notify('textDocument/didChange', { 281 textDocument = { 282 uri = uri, 283 version = util.buf_versions[bufnr], 284 }, 285 contentChanges = changes, 286 }) 287 end 288 end 289 end 290 291 --- @param bufnr integer 292 --- @param firstline integer 293 --- @param lastline integer 294 --- @param new_lastline integer 295 --- @param group vim.lsp.CTGroup 296 local function send_changes_for_group(bufnr, firstline, lastline, new_lastline, group) 297 local state = state_by_group[group] 298 if not state then 299 error( 300 string.format( 301 'changetracking.init must have been called for all LSP clients. group=%s states=%s', 302 vim.inspect(group), 303 vim.inspect(vim.tbl_keys(state_by_group)) 304 ) 305 ) 306 end 307 local buf_state = state.buffers[bufnr] 308 buf_state.needs_flush = true 309 reset_timer(buf_state) 310 local debounce = next_debounce(state.debounce, buf_state) 311 if group.sync_kind == protocol.TextDocumentSyncKind.Incremental then 312 -- This must be done immediately and cannot be delayed 313 -- The contents would further change and startline/endline may no longer fit 314 local changes = incremental_changes( 315 buf_state, 316 group.position_encoding, 317 bufnr, 318 firstline, 319 lastline, 320 new_lastline 321 ) 322 table.insert(buf_state.pending_changes, changes) 323 end 324 if debounce == 0 then 325 send_changes(bufnr, group.sync_kind, state, buf_state) 326 else 327 local timer = assert(uv.new_timer(), 'Must be able to create timer') 328 buf_state.timer = timer 329 timer:start( 330 debounce, 331 0, 332 vim.schedule_wrap(function() 333 reset_timer(buf_state) 334 send_changes(bufnr, group.sync_kind, state, buf_state) 335 end) 336 ) 337 end 338 end 339 340 --- @param bufnr integer 341 --- @param firstline integer 342 --- @param lastline integer 343 --- @param new_lastline integer 344 function M.send_changes(bufnr, firstline, lastline, new_lastline) 345 local groups = {} ---@type table<string,vim.lsp.CTGroup> 346 for _, client in pairs(vim.lsp.get_clients({ bufnr = bufnr })) do 347 local group = get_group(client) 348 groups[group_key(group)] = group 349 end 350 for _, group in pairs(groups) do 351 send_changes_for_group(bufnr, firstline, lastline, new_lastline, group) 352 end 353 end 354 355 --- Flushes any outstanding change notification. 356 ---@param client vim.lsp.Client 357 ---@param bufnr? integer 358 function M.flush(client, bufnr) 359 local group = get_group(client) 360 local state = state_by_group[group] 361 if not state then 362 return 363 end 364 if bufnr then 365 local buf_state = state.buffers[bufnr] or {} 366 reset_timer(buf_state) 367 send_changes(bufnr, group.sync_kind, state, buf_state) 368 else 369 for buf, buf_state in pairs(state.buffers) do 370 reset_timer(buf_state) 371 send_changes(buf, group.sync_kind, state, buf_state) 372 end 373 end 374 end 375 376 return M