neovim

Neovim text editor
git clone https://git.dasho.dev/neovim.git
Log | Files | Refs | README

_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