semantic_tokens.lua 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832
  1. local api = vim.api
  2. local bit = require('bit')
  3. local ms = require('vim.lsp.protocol').Methods
  4. local util = require('vim.lsp.util')
  5. local uv = vim.uv
  6. --- @class (private) STTokenRange
  7. --- @field line integer line number 0-based
  8. --- @field start_col integer start column 0-based
  9. --- @field end_col integer end column 0-based
  10. --- @field type string token type as string
  11. --- @field modifiers table<string,boolean> token modifiers as a set. E.g., { static = true, readonly = true }
  12. --- @field marked boolean whether this token has had extmarks applied
  13. ---
  14. --- @class (private) STCurrentResult
  15. --- @field version? integer document version associated with this result
  16. --- @field result_id? string resultId from the server; used with delta requests
  17. --- @field highlights? STTokenRange[] cache of highlight ranges for this document version
  18. --- @field tokens? integer[] raw token array as received by the server. used for calculating delta responses
  19. --- @field namespace_cleared? boolean whether the namespace was cleared for this result yet
  20. ---
  21. --- @class (private) STActiveRequest
  22. --- @field request_id? integer the LSP request ID of the most recent request sent to the server
  23. --- @field version? integer the document version associated with the most recent request
  24. ---
  25. --- @class (private) STClientState
  26. --- @field namespace integer
  27. --- @field active_request STActiveRequest
  28. --- @field current_result STCurrentResult
  29. ---@class (private) STHighlighter
  30. ---@field active table<integer, STHighlighter>
  31. ---@field bufnr integer
  32. ---@field augroup integer augroup for buffer events
  33. ---@field debounce integer milliseconds to debounce requests for new tokens
  34. ---@field timer table uv_timer for debouncing requests for new tokens
  35. ---@field client_state table<integer, STClientState>
  36. local STHighlighter = { active = {} }
  37. --- Do a binary search of the tokens in the half-open range [lo, hi).
  38. ---
  39. --- Return the index i in range such that tokens[j].line < line for all j < i, and
  40. --- tokens[j].line >= line for all j >= i, or return hi if no such index is found.
  41. local function lower_bound(tokens, line, lo, hi)
  42. while lo < hi do
  43. local mid = bit.rshift(lo + hi, 1) -- Equivalent to floor((lo + hi) / 2).
  44. if tokens[mid].line < line then
  45. lo = mid + 1
  46. else
  47. hi = mid
  48. end
  49. end
  50. return lo
  51. end
  52. --- Do a binary search of the tokens in the half-open range [lo, hi).
  53. ---
  54. --- Return the index i in range such that tokens[j].line <= line for all j < i, and
  55. --- tokens[j].line > line for all j >= i, or return hi if no such index is found.
  56. local function upper_bound(tokens, line, lo, hi)
  57. while lo < hi do
  58. local mid = bit.rshift(lo + hi, 1) -- Equivalent to floor((lo + hi) / 2).
  59. if line < tokens[mid].line then
  60. hi = mid
  61. else
  62. lo = mid + 1
  63. end
  64. end
  65. return lo
  66. end
  67. --- Extracts modifier strings from the encoded number in the token array
  68. ---
  69. ---@param x integer
  70. ---@param modifiers_table table<integer,string>
  71. ---@return table<string, boolean>
  72. local function modifiers_from_number(x, modifiers_table)
  73. local modifiers = {} ---@type table<string,boolean>
  74. local idx = 1
  75. while x > 0 do
  76. if bit.band(x, 1) == 1 then
  77. modifiers[modifiers_table[idx]] = true
  78. end
  79. x = bit.rshift(x, 1)
  80. idx = idx + 1
  81. end
  82. return modifiers
  83. end
  84. --- Converts a raw token list to a list of highlight ranges used by the on_win callback
  85. ---
  86. ---@param data integer[]
  87. ---@param bufnr integer
  88. ---@param client vim.lsp.Client
  89. ---@param request STActiveRequest
  90. ---@return STTokenRange[]
  91. local function tokens_to_ranges(data, bufnr, client, request)
  92. local legend = client.server_capabilities.semanticTokensProvider.legend
  93. local token_types = legend.tokenTypes
  94. local token_modifiers = legend.tokenModifiers
  95. local encoding = client.offset_encoding
  96. local lines = api.nvim_buf_get_lines(bufnr, 0, -1, false)
  97. local ranges = {} ---@type STTokenRange[]
  98. local start = uv.hrtime()
  99. local ms_to_ns = 1e6
  100. local yield_interval_ns = 5 * ms_to_ns
  101. local co, is_main = coroutine.running()
  102. local line ---@type integer?
  103. local start_char = 0
  104. for i = 1, #data, 5 do
  105. -- if this function is called from the main coroutine, let it run to completion with no yield
  106. if not is_main then
  107. local elapsed_ns = uv.hrtime() - start
  108. if elapsed_ns > yield_interval_ns then
  109. vim.schedule(function()
  110. coroutine.resume(co, util.buf_versions[bufnr])
  111. end)
  112. if request.version ~= coroutine.yield() then
  113. -- request became stale since the last time the coroutine ran.
  114. -- abandon it by yielding without a way to resume
  115. coroutine.yield()
  116. end
  117. start = uv.hrtime()
  118. end
  119. end
  120. local delta_line = data[i]
  121. line = line and line + delta_line or delta_line
  122. local delta_start = data[i + 1]
  123. start_char = delta_line == 0 and start_char + delta_start or delta_start
  124. -- data[i+3] +1 because Lua tables are 1-indexed
  125. local token_type = token_types[data[i + 3] + 1]
  126. if token_type then
  127. local modifiers = modifiers_from_number(data[i + 4], token_modifiers)
  128. local end_char = start_char + data[i + 2] --- @type integer LuaLS bug
  129. local buf_line = lines and lines[line + 1] or ''
  130. local start_col = vim.str_byteindex(buf_line, encoding, start_char, false)
  131. local end_col = vim.str_byteindex(buf_line, encoding, end_char, false)
  132. ranges[#ranges + 1] = {
  133. line = line,
  134. start_col = start_col,
  135. end_col = end_col,
  136. type = token_type,
  137. modifiers = modifiers,
  138. marked = false,
  139. }
  140. end
  141. end
  142. return ranges
  143. end
  144. --- Construct a new STHighlighter for the buffer
  145. ---
  146. ---@private
  147. ---@param bufnr integer
  148. ---@return STHighlighter
  149. function STHighlighter.new(bufnr)
  150. local self = setmetatable({}, { __index = STHighlighter })
  151. self.bufnr = bufnr
  152. self.augroup = api.nvim_create_augroup('nvim.lsp.semantic_tokens:' .. bufnr, { clear = true })
  153. self.client_state = {}
  154. STHighlighter.active[bufnr] = self
  155. api.nvim_buf_attach(bufnr, false, {
  156. on_lines = function(_, buf)
  157. local highlighter = STHighlighter.active[buf]
  158. if not highlighter then
  159. return true
  160. end
  161. highlighter:on_change()
  162. end,
  163. on_reload = function(_, buf)
  164. local highlighter = STHighlighter.active[buf]
  165. if highlighter then
  166. highlighter:reset()
  167. highlighter:send_request()
  168. end
  169. end,
  170. })
  171. api.nvim_create_autocmd({ 'BufWinEnter', 'InsertLeave' }, {
  172. buffer = self.bufnr,
  173. group = self.augroup,
  174. callback = function()
  175. self:send_request()
  176. end,
  177. })
  178. api.nvim_create_autocmd('LspDetach', {
  179. buffer = self.bufnr,
  180. group = self.augroup,
  181. callback = function(args)
  182. self:detach(args.data.client_id)
  183. if vim.tbl_isempty(self.client_state) then
  184. self:destroy()
  185. end
  186. end,
  187. })
  188. return self
  189. end
  190. ---@package
  191. function STHighlighter:destroy()
  192. for client_id, _ in pairs(self.client_state) do
  193. self:detach(client_id)
  194. end
  195. api.nvim_del_augroup_by_id(self.augroup)
  196. STHighlighter.active[self.bufnr] = nil
  197. end
  198. ---@package
  199. function STHighlighter:attach(client_id)
  200. local state = self.client_state[client_id]
  201. if not state then
  202. state = {
  203. namespace = api.nvim_create_namespace('nvim.lsp.semantic_tokens:' .. client_id),
  204. active_request = {},
  205. current_result = {},
  206. }
  207. self.client_state[client_id] = state
  208. end
  209. end
  210. ---@package
  211. function STHighlighter:detach(client_id)
  212. local state = self.client_state[client_id]
  213. if state then
  214. --TODO: delete namespace if/when that becomes possible
  215. api.nvim_buf_clear_namespace(self.bufnr, state.namespace, 0, -1)
  216. self.client_state[client_id] = nil
  217. end
  218. end
  219. --- This is the entry point for getting all the tokens in a buffer.
  220. ---
  221. --- For the given clients (or all attached, if not provided), this sends a request
  222. --- to ask for semantic tokens. If the server supports delta requests, that will
  223. --- be prioritized if we have a previous requestId and token array.
  224. ---
  225. --- This function will skip servers where there is an already an active request in
  226. --- flight for the same version. If there is a stale request in flight, that is
  227. --- cancelled prior to sending a new one.
  228. ---
  229. --- Finally, if the request was successful, the requestId and document version
  230. --- are saved to facilitate document synchronization in the response.
  231. ---
  232. ---@package
  233. function STHighlighter:send_request()
  234. local version = util.buf_versions[self.bufnr]
  235. self:reset_timer()
  236. for client_id, state in pairs(self.client_state) do
  237. local client = vim.lsp.get_client_by_id(client_id)
  238. local current_result = state.current_result
  239. local active_request = state.active_request
  240. -- Only send a request for this client if the current result is out of date and
  241. -- there isn't a current a request in flight for this version
  242. if client and current_result.version ~= version and active_request.version ~= version then
  243. -- cancel stale in-flight request
  244. if active_request.request_id then
  245. client:cancel_request(active_request.request_id)
  246. active_request = {}
  247. state.active_request = active_request
  248. end
  249. local spec = client.server_capabilities.semanticTokensProvider.full
  250. local hasEditProvider = type(spec) == 'table' and spec.delta
  251. local params = { textDocument = util.make_text_document_params(self.bufnr) }
  252. local method = ms.textDocument_semanticTokens_full
  253. if hasEditProvider and current_result.result_id then
  254. method = method .. '/delta'
  255. params.previousResultId = current_result.result_id
  256. end
  257. ---@param response? lsp.SemanticTokens|lsp.SemanticTokensDelta
  258. local success, request_id = client:request(method, params, function(err, response, ctx)
  259. -- look client up again using ctx.client_id instead of using a captured
  260. -- client object
  261. local c = vim.lsp.get_client_by_id(ctx.client_id)
  262. local bufnr = assert(ctx.bufnr)
  263. local highlighter = STHighlighter.active[bufnr]
  264. if not (c and highlighter) then
  265. return
  266. end
  267. if err or not response then
  268. highlighter.client_state[c.id].active_request = {}
  269. return
  270. end
  271. coroutine.wrap(STHighlighter.process_response)(highlighter, response, c, version)
  272. end, self.bufnr)
  273. if success then
  274. active_request.request_id = request_id
  275. active_request.version = version
  276. end
  277. end
  278. end
  279. end
  280. --- This function will parse the semantic token responses and set up the cache
  281. --- (current_result). It also performs document synchronization by checking the
  282. --- version of the document associated with the resulting request_id and only
  283. --- performing work if the response is not out-of-date.
  284. ---
  285. --- Delta edits are applied if necessary, and new highlight ranges are calculated
  286. --- and stored in the buffer state.
  287. ---
  288. --- Finally, a redraw command is issued to force nvim to redraw the screen to
  289. --- pick up changed highlight tokens.
  290. ---
  291. ---@param response lsp.SemanticTokens|lsp.SemanticTokensDelta
  292. ---@private
  293. function STHighlighter:process_response(response, client, version)
  294. local state = self.client_state[client.id]
  295. if not state then
  296. return
  297. end
  298. -- ignore stale responses
  299. if state.active_request.version and version ~= state.active_request.version then
  300. return
  301. end
  302. if not api.nvim_buf_is_valid(self.bufnr) then
  303. return
  304. end
  305. -- if we have a response to a delta request, update the state of our tokens
  306. -- appropriately. if it's a full response, just use that
  307. local tokens ---@type integer[]
  308. local token_edits = response.edits
  309. if token_edits then
  310. table.sort(token_edits, function(a, b)
  311. return a.start < b.start
  312. end)
  313. tokens = {} --- @type integer[]
  314. local old_tokens = assert(state.current_result.tokens)
  315. local idx = 1
  316. for _, token_edit in ipairs(token_edits) do
  317. vim.list_extend(tokens, old_tokens, idx, token_edit.start)
  318. if token_edit.data then
  319. vim.list_extend(tokens, token_edit.data)
  320. end
  321. idx = token_edit.start + token_edit.deleteCount + 1
  322. end
  323. vim.list_extend(tokens, old_tokens, idx)
  324. else
  325. tokens = response.data
  326. end
  327. -- convert token list to highlight ranges
  328. -- this could yield and run over multiple event loop iterations
  329. local highlights = tokens_to_ranges(tokens, self.bufnr, client, state.active_request)
  330. -- reset active request
  331. state.active_request = {}
  332. -- update the state with the new results
  333. local current_result = state.current_result
  334. current_result.version = version
  335. current_result.result_id = response.resultId
  336. current_result.tokens = tokens
  337. current_result.highlights = highlights
  338. current_result.namespace_cleared = false
  339. -- redraw all windows displaying buffer (if still valid)
  340. if api.nvim_buf_is_valid(self.bufnr) then
  341. api.nvim__redraw({ buf = self.bufnr, valid = true })
  342. end
  343. end
  344. --- @param bufnr integer
  345. --- @param ns integer
  346. --- @param token STTokenRange
  347. --- @param hl_group string
  348. --- @param priority integer
  349. local function set_mark(bufnr, ns, token, hl_group, priority)
  350. vim.api.nvim_buf_set_extmark(bufnr, ns, token.line, token.start_col, {
  351. hl_group = hl_group,
  352. end_col = token.end_col,
  353. priority = priority,
  354. strict = false,
  355. })
  356. end
  357. --- @param lnum integer
  358. --- @param foldend integer?
  359. --- @return boolean, integer?
  360. local function check_fold(lnum, foldend)
  361. if foldend and lnum <= foldend then
  362. return true, foldend
  363. end
  364. local folded = vim.fn.foldclosed(lnum)
  365. if folded == -1 then
  366. return false, nil
  367. end
  368. return folded ~= lnum, vim.fn.foldclosedend(lnum)
  369. end
  370. --- on_win handler for the decoration provider (see |nvim_set_decoration_provider|)
  371. ---
  372. --- If there is a current result for the buffer and the version matches the
  373. --- current document version, then the tokens are valid and can be applied. As
  374. --- the buffer is drawn, this function will add extmark highlights for every
  375. --- token in the range of visible lines. Once a highlight has been added, it
  376. --- sticks around until the document changes and there's a new set of matching
  377. --- highlight tokens available.
  378. ---
  379. --- If this is the first time a buffer is being drawn with a new set of
  380. --- highlights for the current document version, the namespace is cleared to
  381. --- remove extmarks from the last version. It's done here instead of the response
  382. --- handler to avoid the "blink" that occurs due to the timing between the
  383. --- response handler and the actual redraw.
  384. ---
  385. ---@package
  386. ---@param topline integer
  387. ---@param botline integer
  388. function STHighlighter:on_win(topline, botline)
  389. for client_id, state in pairs(self.client_state) do
  390. local current_result = state.current_result
  391. if current_result.version == util.buf_versions[self.bufnr] then
  392. if not current_result.namespace_cleared then
  393. api.nvim_buf_clear_namespace(self.bufnr, state.namespace, 0, -1)
  394. current_result.namespace_cleared = true
  395. end
  396. -- We can't use ephemeral extmarks because the buffer updates are not in
  397. -- sync with the list of semantic tokens. There's a delay between the
  398. -- buffer changing and when the LSP server can respond with updated
  399. -- tokens, and we don't want to "blink" the token highlights while
  400. -- updates are in flight, and we don't want to use stale tokens because
  401. -- they likely won't line up right with the actual buffer.
  402. --
  403. -- Instead, we have to use normal extmarks that can attach to locations
  404. -- in the buffer and are persisted between redraws.
  405. --
  406. -- `strict = false` is necessary here for the 1% of cases where the
  407. -- current result doesn't actually match the buffer contents. Some
  408. -- LSP servers can respond with stale tokens on requests if they are
  409. -- still processing changes from a didChange notification.
  410. --
  411. -- LSP servers that do this _should_ follow up known stale responses
  412. -- with a refresh notification once they've finished processing the
  413. -- didChange notification, which would re-synchronize the tokens from
  414. -- our end.
  415. --
  416. -- The server I know of that does this is clangd when the preamble of
  417. -- a file changes and the token request is processed with a stale
  418. -- preamble while the new one is still being built. Once the preamble
  419. -- finishes, clangd sends a refresh request which lets the client
  420. -- re-synchronize the tokens.
  421. local function set_mark0(token, hl_group, delta)
  422. set_mark(
  423. self.bufnr,
  424. state.namespace,
  425. token,
  426. hl_group,
  427. vim.hl.priorities.semantic_tokens + delta
  428. )
  429. end
  430. local ft = vim.bo[self.bufnr].filetype
  431. local highlights = assert(current_result.highlights)
  432. local first = lower_bound(highlights, topline, 1, #highlights + 1)
  433. local last = upper_bound(highlights, botline, first, #highlights + 1) - 1
  434. --- @type boolean?, integer?
  435. local is_folded, foldend
  436. for i = first, last do
  437. local token = highlights[i]
  438. is_folded, foldend = check_fold(token.line + 1, foldend)
  439. if not is_folded and not token.marked then
  440. set_mark0(token, string.format('@lsp.type.%s.%s', token.type, ft), 0)
  441. for modifier in pairs(token.modifiers) do
  442. set_mark0(token, string.format('@lsp.mod.%s.%s', modifier, ft), 1)
  443. set_mark0(token, string.format('@lsp.typemod.%s.%s.%s', token.type, modifier, ft), 2)
  444. end
  445. token.marked = true
  446. api.nvim_exec_autocmds('LspTokenUpdate', {
  447. buffer = self.bufnr,
  448. modeline = false,
  449. data = {
  450. token = token,
  451. client_id = client_id,
  452. },
  453. })
  454. end
  455. end
  456. end
  457. end
  458. end
  459. --- Reset the buffer's highlighting state and clears the extmark highlights.
  460. ---
  461. ---@package
  462. function STHighlighter:reset()
  463. for client_id, state in pairs(self.client_state) do
  464. api.nvim_buf_clear_namespace(self.bufnr, state.namespace, 0, -1)
  465. state.current_result = {}
  466. if state.active_request.request_id then
  467. local client = vim.lsp.get_client_by_id(client_id)
  468. assert(client)
  469. client:cancel_request(state.active_request.request_id)
  470. state.active_request = {}
  471. end
  472. end
  473. end
  474. --- Mark a client's results as dirty. This method will cancel any active
  475. --- requests to the server and pause new highlights from being added
  476. --- in the on_win callback. The rest of the current results are saved
  477. --- in case the server supports delta requests.
  478. ---
  479. ---@package
  480. ---@param client_id integer
  481. function STHighlighter:mark_dirty(client_id)
  482. local state = self.client_state[client_id]
  483. assert(state)
  484. -- if we clear the version from current_result, it'll cause the
  485. -- next request to be sent and will also pause new highlights
  486. -- from being added in on_win until a new result comes from
  487. -- the server
  488. if state.current_result then
  489. state.current_result.version = nil
  490. end
  491. if state.active_request.request_id then
  492. local client = vim.lsp.get_client_by_id(client_id)
  493. assert(client)
  494. client:cancel_request(state.active_request.request_id)
  495. state.active_request = {}
  496. end
  497. end
  498. ---@package
  499. function STHighlighter:on_change()
  500. self:reset_timer()
  501. if self.debounce > 0 then
  502. self.timer = vim.defer_fn(function()
  503. self:send_request()
  504. end, self.debounce)
  505. else
  506. self:send_request()
  507. end
  508. end
  509. ---@private
  510. function STHighlighter:reset_timer()
  511. local timer = self.timer
  512. if timer then
  513. self.timer = nil
  514. if not timer:is_closing() then
  515. timer:stop()
  516. timer:close()
  517. end
  518. end
  519. end
  520. local M = {}
  521. --- Start the semantic token highlighting engine for the given buffer with the
  522. --- given client. The client must already be attached to the buffer.
  523. ---
  524. --- NOTE: This is currently called automatically by |vim.lsp.buf_attach_client()|. To
  525. --- opt-out of semantic highlighting with a server that supports it, you can
  526. --- delete the semanticTokensProvider table from the {server_capabilities} of
  527. --- your client in your |LspAttach| callback or your configuration's
  528. --- `on_attach` callback:
  529. ---
  530. --- ```lua
  531. --- client.server_capabilities.semanticTokensProvider = nil
  532. --- ```
  533. ---
  534. ---@param bufnr (integer) Buffer number, or `0` for current buffer
  535. ---@param client_id (integer) The ID of the |vim.lsp.Client|
  536. ---@param opts? (table) Optional keyword arguments
  537. --- - debounce (integer, default: 200): Debounce token requests
  538. --- to the server by the given number in milliseconds
  539. function M.start(bufnr, client_id, opts)
  540. vim.validate('bufnr', bufnr, 'number')
  541. vim.validate('client_id', client_id, 'number')
  542. bufnr = vim._resolve_bufnr(bufnr)
  543. opts = opts or {}
  544. assert(
  545. (not opts.debounce or type(opts.debounce) == 'number'),
  546. 'opts.debounce must be a number with the debounce time in milliseconds'
  547. )
  548. local client = vim.lsp.get_client_by_id(client_id)
  549. if not client then
  550. vim.notify('[LSP] No client with id ' .. client_id, vim.log.levels.ERROR)
  551. return
  552. end
  553. if not vim.lsp.buf_is_attached(bufnr, client_id) then
  554. vim.notify(
  555. '[LSP] Client with id ' .. client_id .. ' not attached to buffer ' .. bufnr,
  556. vim.log.levels.WARN
  557. )
  558. return
  559. end
  560. if not vim.tbl_get(client.server_capabilities, 'semanticTokensProvider', 'full') then
  561. vim.notify('[LSP] Server does not support semantic tokens', vim.log.levels.WARN)
  562. return
  563. end
  564. local highlighter = STHighlighter.active[bufnr]
  565. if not highlighter then
  566. highlighter = STHighlighter.new(bufnr)
  567. highlighter.debounce = opts.debounce or 200
  568. else
  569. highlighter.debounce = math.max(highlighter.debounce, opts.debounce or 200)
  570. end
  571. highlighter:attach(client_id)
  572. highlighter:send_request()
  573. end
  574. --- Stop the semantic token highlighting engine for the given buffer with the
  575. --- given client.
  576. ---
  577. --- NOTE: This is automatically called by a |LspDetach| autocmd that is set up as part
  578. --- of `start()`, so you should only need this function to manually disengage the semantic
  579. --- token engine without fully detaching the LSP client from the buffer.
  580. ---
  581. ---@param bufnr (integer) Buffer number, or `0` for current buffer
  582. ---@param client_id (integer) The ID of the |vim.lsp.Client|
  583. function M.stop(bufnr, client_id)
  584. vim.validate('bufnr', bufnr, 'number')
  585. vim.validate('client_id', client_id, 'number')
  586. bufnr = vim._resolve_bufnr(bufnr)
  587. local highlighter = STHighlighter.active[bufnr]
  588. if not highlighter then
  589. return
  590. end
  591. highlighter:detach(client_id)
  592. if vim.tbl_isempty(highlighter.client_state) then
  593. highlighter:destroy()
  594. end
  595. end
  596. --- @nodoc
  597. --- @class STTokenRangeInspect : STTokenRange
  598. --- @field client_id integer
  599. --- Return the semantic token(s) at the given position.
  600. --- If called without arguments, returns the token under the cursor.
  601. ---
  602. ---@param bufnr integer|nil Buffer number (0 for current buffer, default)
  603. ---@param row integer|nil Position row (default cursor position)
  604. ---@param col integer|nil Position column (default cursor position)
  605. ---
  606. ---@return STTokenRangeInspect[]|nil (table|nil) List of tokens at position. Each token has
  607. --- the following fields:
  608. --- - line (integer) line number, 0-based
  609. --- - start_col (integer) start column, 0-based
  610. --- - end_col (integer) end column, 0-based
  611. --- - type (string) token type as string, e.g. "variable"
  612. --- - modifiers (table) token modifiers as a set. E.g., { static = true, readonly = true }
  613. --- - client_id (integer)
  614. function M.get_at_pos(bufnr, row, col)
  615. bufnr = vim._resolve_bufnr(bufnr)
  616. local highlighter = STHighlighter.active[bufnr]
  617. if not highlighter then
  618. return
  619. end
  620. if row == nil or col == nil then
  621. local cursor = api.nvim_win_get_cursor(0)
  622. row, col = cursor[1] - 1, cursor[2]
  623. end
  624. local tokens = {} --- @type STTokenRangeInspect[]
  625. for client_id, client in pairs(highlighter.client_state) do
  626. local highlights = client.current_result.highlights
  627. if highlights then
  628. local idx = lower_bound(highlights, row, 1, #highlights + 1)
  629. for i = idx, #highlights do
  630. local token = highlights[i]
  631. --- @cast token STTokenRangeInspect
  632. if token.line > row then
  633. break
  634. end
  635. if token.start_col <= col and token.end_col > col then
  636. token.client_id = client_id
  637. tokens[#tokens + 1] = token
  638. end
  639. end
  640. end
  641. end
  642. return tokens
  643. end
  644. --- Force a refresh of all semantic tokens
  645. ---
  646. --- Only has an effect if the buffer is currently active for semantic token
  647. --- highlighting (|vim.lsp.semantic_tokens.start()| has been called for it)
  648. ---
  649. ---@param bufnr (integer|nil) filter by buffer. All buffers if nil, current
  650. --- buffer if 0
  651. function M.force_refresh(bufnr)
  652. vim.validate('bufnr', bufnr, 'number', true)
  653. local buffers = bufnr == nil and vim.tbl_keys(STHighlighter.active)
  654. or { vim._resolve_bufnr(bufnr) }
  655. for _, buffer in ipairs(buffers) do
  656. local highlighter = STHighlighter.active[buffer]
  657. if highlighter then
  658. highlighter:reset()
  659. highlighter:send_request()
  660. end
  661. end
  662. end
  663. --- @class vim.lsp.semantic_tokens.highlight_token.Opts
  664. --- @inlinedoc
  665. ---
  666. --- Priority for the applied extmark.
  667. --- (Default: `vim.hl.priorities.semantic_tokens + 3`)
  668. --- @field priority? integer
  669. --- Highlight a semantic token.
  670. ---
  671. --- Apply an extmark with a given highlight group for a semantic token. The
  672. --- mark will be deleted by the semantic token engine when appropriate; for
  673. --- example, when the LSP sends updated tokens. This function is intended for
  674. --- use inside |LspTokenUpdate| callbacks.
  675. ---@param token (table) A semantic token, found as `args.data.token` in |LspTokenUpdate|
  676. ---@param bufnr (integer) The buffer to highlight, or `0` for current buffer
  677. ---@param client_id (integer) The ID of the |vim.lsp.Client|
  678. ---@param hl_group (string) Highlight group name
  679. ---@param opts? vim.lsp.semantic_tokens.highlight_token.Opts Optional parameters:
  680. function M.highlight_token(token, bufnr, client_id, hl_group, opts)
  681. bufnr = vim._resolve_bufnr(bufnr)
  682. local highlighter = STHighlighter.active[bufnr]
  683. if not highlighter then
  684. return
  685. end
  686. local state = highlighter.client_state[client_id]
  687. if not state then
  688. return
  689. end
  690. local priority = opts and opts.priority or vim.hl.priorities.semantic_tokens + 3
  691. set_mark(bufnr, state.namespace, token, hl_group, priority)
  692. end
  693. --- |lsp-handler| for the method `workspace/semanticTokens/refresh`
  694. ---
  695. --- Refresh requests are sent by the server to indicate a project-wide change
  696. --- that requires all tokens to be re-requested by the client. This handler will
  697. --- invalidate the current results of all buffers and automatically kick off a
  698. --- new request for buffers that are displayed in a window. For those that aren't, a
  699. --- the BufWinEnter event should take care of it next time it's displayed.
  700. function M._refresh(err, _, ctx)
  701. if err then
  702. return vim.NIL
  703. end
  704. for _, bufnr in ipairs(vim.lsp.get_buffers_by_client_id(ctx.client_id)) do
  705. local highlighter = STHighlighter.active[bufnr]
  706. if highlighter and highlighter.client_state[ctx.client_id] then
  707. highlighter:mark_dirty(ctx.client_id)
  708. if not vim.tbl_isempty(vim.fn.win_findbuf(bufnr)) then
  709. highlighter:send_request()
  710. end
  711. end
  712. end
  713. return vim.NIL
  714. end
  715. local namespace = api.nvim_create_namespace('nvim.lsp.semantic_tokens')
  716. api.nvim_set_decoration_provider(namespace, {
  717. on_win = function(_, _, bufnr, topline, botline)
  718. local highlighter = STHighlighter.active[bufnr]
  719. if highlighter then
  720. highlighter:on_win(topline, botline)
  721. end
  722. end,
  723. })
  724. --- for testing only! there is no guarantee of API stability with this!
  725. ---
  726. ---@private
  727. M.__STHighlighter = STHighlighter
  728. return M