_folding_range.lua 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374
  1. local util = require('vim.lsp.util')
  2. local log = require('vim.lsp.log')
  3. local ms = require('vim.lsp.protocol').Methods
  4. local api = vim.api
  5. local M = {}
  6. ---@class (private) vim.lsp.folding_range.BufState
  7. ---
  8. ---@field version? integer
  9. ---
  10. --- Never use this directly, `renew()` the cached foldinfo
  11. --- then use on demand via `row_*` fields.
  12. ---
  13. --- Index In the form of client_id -> ranges
  14. ---@field client_ranges table<integer, lsp.FoldingRange[]?>
  15. ---
  16. --- Index in the form of row -> [foldlevel, mark]
  17. ---@field row_level table<integer, [integer, ">" | "<"?]?>
  18. ---
  19. --- Index in the form of start_row -> kinds
  20. ---@field row_kinds table<integer, table<lsp.FoldingRangeKind, true?>?>>
  21. ---
  22. --- Index in the form of start_row -> collapsed_text
  23. ---@field row_text table<integer, string?>
  24. ---@type table<integer, vim.lsp.folding_range.BufState?>
  25. local bufstates = {}
  26. --- Renew the cached foldinfo in the buffer.
  27. ---@param bufnr integer
  28. local function renew(bufnr)
  29. local bufstate = assert(bufstates[bufnr])
  30. ---@type table<integer, [integer, ">" | "<"?]?>
  31. local row_level = {}
  32. ---@type table<integer, table<lsp.FoldingRangeKind, true?>?>>
  33. local row_kinds = {}
  34. ---@type table<integer, string?>
  35. local row_text = {}
  36. for _, ranges in pairs(bufstate.client_ranges) do
  37. for _, range in ipairs(ranges) do
  38. local start_row = range.startLine
  39. local end_row = range.endLine
  40. -- Adding folds within a single line is not supported by Nvim.
  41. if start_row ~= end_row then
  42. row_text[start_row] = range.collapsedText
  43. local kind = range.kind
  44. if kind then
  45. local kinds = row_kinds[start_row] or {}
  46. kinds[kind] = true
  47. row_kinds[start_row] = kinds
  48. end
  49. for row = start_row, end_row do
  50. local level = row_level[row] or { 0 }
  51. level[1] = level[1] + 1
  52. row_level[row] = level
  53. end
  54. row_level[start_row][2] = '>'
  55. row_level[end_row][2] = '<'
  56. end
  57. end
  58. end
  59. bufstate.row_level = row_level
  60. bufstate.row_kinds = row_kinds
  61. bufstate.row_text = row_text
  62. end
  63. --- Renew the cached foldinfo then force `foldexpr()` to be re-evaluated,
  64. --- without opening folds.
  65. ---@param bufnr integer
  66. local function foldupdate(bufnr)
  67. renew(bufnr)
  68. for _, winid in ipairs(vim.fn.win_findbuf(bufnr)) do
  69. local wininfo = vim.fn.getwininfo(winid)[1]
  70. if wininfo and wininfo.tabnr == vim.fn.tabpagenr() then
  71. if vim.wo[winid].foldmethod == 'expr' then
  72. vim._foldupdate(winid, 0, api.nvim_buf_line_count(bufnr))
  73. end
  74. end
  75. end
  76. end
  77. --- Whether `foldupdate()` is scheduled for the buffer with `bufnr`.
  78. ---
  79. --- Index in the form of bufnr -> true?
  80. ---@type table<integer, true?>
  81. local scheduled_foldupdate = {}
  82. --- Schedule `foldupdate()` after leaving insert mode.
  83. ---@param bufnr integer
  84. local function schedule_foldupdate(bufnr)
  85. if not scheduled_foldupdate[bufnr] then
  86. scheduled_foldupdate[bufnr] = true
  87. api.nvim_create_autocmd('InsertLeave', {
  88. buffer = bufnr,
  89. once = true,
  90. callback = function()
  91. foldupdate(bufnr)
  92. scheduled_foldupdate[bufnr] = nil
  93. end,
  94. })
  95. end
  96. end
  97. ---@param results table<integer,{err: lsp.ResponseError?, result: lsp.FoldingRange[]?}>
  98. ---@type lsp.MultiHandler
  99. local function multi_handler(results, ctx)
  100. local bufnr = assert(ctx.bufnr)
  101. -- Handling responses from outdated buffer only causes performance overhead.
  102. if util.buf_versions[bufnr] ~= ctx.version then
  103. return
  104. end
  105. local bufstate = assert(bufstates[bufnr])
  106. for client_id, result in pairs(results) do
  107. if result.err then
  108. log.error(result.err)
  109. else
  110. bufstate.client_ranges[client_id] = result.result
  111. end
  112. end
  113. bufstate.version = ctx.version
  114. if api.nvim_get_mode().mode:match('^i') then
  115. -- `foldUpdate()` is guarded in insert mode.
  116. schedule_foldupdate(bufnr)
  117. else
  118. foldupdate(bufnr)
  119. end
  120. end
  121. ---@param result lsp.FoldingRange[]?
  122. ---@type lsp.Handler
  123. local function handler(err, result, ctx)
  124. multi_handler({ [ctx.client_id] = { err = err, result = result } }, ctx)
  125. end
  126. --- Request `textDocument/foldingRange` from the server.
  127. --- `foldupdate()` is scheduled once after the request is completed.
  128. ---@param bufnr integer
  129. ---@param client? vim.lsp.Client The client whose server supports `foldingRange`.
  130. local function request(bufnr, client)
  131. ---@type lsp.FoldingRangeParams
  132. local params = { textDocument = util.make_text_document_params(bufnr) }
  133. if client then
  134. client:request(ms.textDocument_foldingRange, params, handler, bufnr)
  135. return
  136. end
  137. if not next(vim.lsp.get_clients({ bufnr = bufnr, method = ms.textDocument_foldingRange })) then
  138. return
  139. end
  140. vim.lsp.buf_request_all(bufnr, ms.textDocument_foldingRange, params, multi_handler)
  141. end
  142. -- NOTE:
  143. -- `bufstate` and event hooks are interdependent:
  144. -- * `bufstate` needs event hooks for correctness.
  145. -- * event hooks require the previous `bufstate` for updates.
  146. -- Since they are manually created and destroyed,
  147. -- we ensure their lifecycles are always synchronized.
  148. --
  149. -- TODO(ofseed):
  150. -- 1. Implement clearing `bufstate` and event hooks
  151. -- when no clients in the buffer support the corresponding method.
  152. -- 2. Then generalize this state management to other LSP modules.
  153. local augroup_setup = api.nvim_create_augroup('nvim.lsp.folding_range.setup', {})
  154. --- Initialize `bufstate` and event hooks, then request folding ranges.
  155. --- Manage their lifecycle within this function.
  156. ---@param bufnr integer
  157. ---@return vim.lsp.folding_range.BufState?
  158. local function setup(bufnr)
  159. if not api.nvim_buf_is_loaded(bufnr) then
  160. return
  161. end
  162. -- Register the new `bufstate`.
  163. bufstates[bufnr] = {
  164. client_ranges = {},
  165. row_level = {},
  166. row_kinds = {},
  167. row_text = {},
  168. }
  169. -- Event hooks from `buf_attach` can't be removed externally.
  170. -- Hooks and `bufstate` share the same lifecycle;
  171. -- they should self-destroy if `bufstate == nil`.
  172. api.nvim_buf_attach(bufnr, false, {
  173. -- `on_detach` also runs on buffer reload (`:e`).
  174. -- Ensure `bufstate` and hooks are cleared to avoid duplication or leftover states.
  175. on_detach = function()
  176. bufstates[bufnr] = nil
  177. api.nvim_clear_autocmds({ buffer = bufnr, group = augroup_setup })
  178. end,
  179. -- Reset `bufstate` and request folding ranges.
  180. on_reload = function()
  181. bufstates[bufnr] = {
  182. client_ranges = {},
  183. row_level = {},
  184. row_kinds = {},
  185. row_text = {},
  186. }
  187. request(bufnr)
  188. end,
  189. --- Sync changed rows with their previous foldlevels before applying new ones.
  190. on_bytes = function(_, _, _, start_row, _, _, old_row, _, _, new_row, _, _)
  191. if bufstates[bufnr] == nil then
  192. return true
  193. end
  194. local row_level = bufstates[bufnr].row_level
  195. if next(row_level) == nil then
  196. return
  197. end
  198. local row = new_row - old_row
  199. if row > 0 then
  200. vim._list_insert(row_level, start_row, start_row + math.abs(row) - 1, { -1 })
  201. -- If the previous row ends a fold,
  202. -- Nvim treats the first row after consecutive `-1`s as a new fold start,
  203. -- which is not the desired behavior.
  204. local prev_level = row_level[start_row - 1]
  205. if prev_level and prev_level[2] == '<' then
  206. row_level[start_row] = { prev_level[1] - 1 }
  207. end
  208. elseif row < 0 then
  209. vim._list_remove(row_level, start_row, start_row + math.abs(row) - 1)
  210. end
  211. end,
  212. })
  213. api.nvim_create_autocmd('LspDetach', {
  214. group = augroup_setup,
  215. buffer = bufnr,
  216. callback = function(args)
  217. if not api.nvim_buf_is_loaded(bufnr) then
  218. return
  219. end
  220. ---@type integer
  221. local client_id = args.data.client_id
  222. bufstates[bufnr].client_ranges[client_id] = nil
  223. ---@type vim.lsp.Client[]
  224. local clients = vim
  225. .iter(vim.lsp.get_clients({ bufnr = bufnr, method = ms.textDocument_foldingRange }))
  226. ---@param client vim.lsp.Client
  227. :filter(function(client)
  228. return client.id ~= client_id
  229. end)
  230. :totable()
  231. if #clients == 0 then
  232. bufstates[bufnr] = {
  233. client_ranges = {},
  234. row_level = {},
  235. row_kinds = {},
  236. row_text = {},
  237. }
  238. end
  239. foldupdate(bufnr)
  240. end,
  241. })
  242. api.nvim_create_autocmd('LspAttach', {
  243. group = augroup_setup,
  244. buffer = bufnr,
  245. callback = function(args)
  246. local client = assert(vim.lsp.get_client_by_id(args.data.client_id))
  247. if client:supports_method(vim.lsp.protocol.Methods.textDocument_foldingRange, bufnr) then
  248. request(bufnr, client)
  249. end
  250. end,
  251. })
  252. api.nvim_create_autocmd('LspNotify', {
  253. group = augroup_setup,
  254. buffer = bufnr,
  255. callback = function(args)
  256. local client = assert(vim.lsp.get_client_by_id(args.data.client_id))
  257. if
  258. client:supports_method(ms.textDocument_foldingRange, bufnr)
  259. and (
  260. args.data.method == ms.textDocument_didChange
  261. or args.data.method == ms.textDocument_didOpen
  262. )
  263. then
  264. request(bufnr, client)
  265. end
  266. end,
  267. })
  268. request(bufnr)
  269. return bufstates[bufnr]
  270. end
  271. ---@param kind lsp.FoldingRangeKind
  272. ---@param winid integer
  273. local function foldclose(kind, winid)
  274. vim._with({ win = winid }, function()
  275. local bufnr = api.nvim_win_get_buf(winid)
  276. local row_kinds = bufstates[bufnr].row_kinds
  277. -- Reverse traverse to ensure that the smallest ranges are closed first.
  278. for row = api.nvim_buf_line_count(bufnr) - 1, 0, -1 do
  279. local kinds = row_kinds[row]
  280. if kinds and kinds[kind] then
  281. vim.cmd(row + 1 .. 'foldclose')
  282. end
  283. end
  284. end)
  285. end
  286. ---@param kind lsp.FoldingRangeKind
  287. ---@param winid? integer
  288. function M.foldclose(kind, winid)
  289. vim.validate('kind', kind, 'string')
  290. vim.validate('winid', winid, 'number', true)
  291. winid = winid or api.nvim_get_current_win()
  292. local bufnr = api.nvim_win_get_buf(winid)
  293. local bufstate = bufstates[bufnr]
  294. if not bufstate then
  295. return
  296. end
  297. if bufstate.version == util.buf_versions[bufnr] then
  298. foldclose(kind, winid)
  299. return
  300. end
  301. -- Schedule `foldclose()` if the buffer is not up-to-date.
  302. if not next(vim.lsp.get_clients({ bufnr = bufnr, method = ms.textDocument_foldingRange })) then
  303. return
  304. end
  305. ---@type lsp.FoldingRangeParams
  306. local params = { textDocument = util.make_text_document_params(bufnr) }
  307. vim.lsp.buf_request_all(bufnr, ms.textDocument_foldingRange, params, function(...)
  308. multi_handler(...)
  309. foldclose(kind, winid)
  310. end)
  311. end
  312. ---@return string
  313. function M.foldtext()
  314. local bufnr = api.nvim_get_current_buf()
  315. local lnum = vim.v.foldstart
  316. local row = lnum - 1
  317. local bufstate = bufstates[bufnr]
  318. if bufstate and bufstate.row_text[row] then
  319. return bufstate.row_text[row]
  320. end
  321. return vim.fn.getline(lnum)
  322. end
  323. ---@param lnum? integer
  324. ---@return string level
  325. function M.foldexpr(lnum)
  326. local bufnr = api.nvim_get_current_buf()
  327. local bufstate = bufstates[bufnr] or setup(bufnr)
  328. if not bufstate then
  329. return '0'
  330. end
  331. local row = (lnum or vim.v.lnum) - 1
  332. local level = bufstate.row_level[row]
  333. return level and (level[2] or '') .. (level[1] or '0') or '0'
  334. end
  335. return M