_changetracking.lua 12 KB

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