semantic_tokens.lua (35407B)
1 local api = vim.api 2 local bit = require('bit') 3 local util = require('vim.lsp.util') 4 local Range = require('vim.treesitter._range') 5 local uv = vim.uv 6 7 local Capability = require('vim.lsp._capability') 8 9 local M = {} 10 11 --- @class (private) STTokenRange 12 --- @field line integer line number 0-based 13 --- @field start_col integer start column 0-based 14 --- @field end_line integer end line number 0-based 15 --- @field end_col integer end column 0-based 16 --- @field type string token type as string 17 --- @field modifiers table<string,boolean> token modifiers as a set. E.g., { static = true, readonly = true } 18 --- @field marked boolean whether this token has had extmarks applied 19 20 --- @class (private) STCurrentResult 21 --- @field version? integer document version associated with this result 22 --- @field result_id? string resultId from the server; used with delta requests 23 --- @field highlights? STTokenRange[] cache of highlight ranges for this document version 24 --- @field tokens? integer[] raw token array as received by the server. used for calculating delta responses 25 --- @field namespace_cleared? boolean whether the namespace was cleared for this result yet 26 27 --- @class (private) STActiveRequest 28 --- @field request_id? integer the LSP request ID of the most recent request sent to the server 29 --- @field version? integer the document version associated with the most recent request 30 31 --- @class (private) STClientState 32 --- @field namespace integer 33 --- @field supports_range boolean 34 --- @field supports_delta boolean 35 --- @field active_request STActiveRequest 36 --- @field active_range_request STActiveRequest 37 --- @field current_result STCurrentResult 38 --- @field has_full_result boolean 39 40 ---@class (private) STHighlighter : vim.lsp.Capability 41 ---@field active table<integer, STHighlighter> 42 ---@field bufnr integer 43 ---@field augroup integer augroup for buffer events 44 ---@field debounce integer milliseconds to debounce requests for new tokens 45 ---@field timer table uv_timer for debouncing requests for new tokens 46 ---@field client_state table<integer, STClientState> 47 local STHighlighter = { 48 name = 'semantic_tokens', 49 method = 'textDocument/semanticTokens', 50 active = {}, 51 } 52 STHighlighter.__index = STHighlighter 53 setmetatable(STHighlighter, Capability) 54 Capability.all[STHighlighter.name] = STHighlighter 55 56 --- Extracts modifier strings from the encoded number in the token array 57 --- 58 ---@param x integer 59 ---@param modifiers_table table<integer,string> 60 ---@return table<string, boolean> 61 local function modifiers_from_number(x, modifiers_table) 62 local modifiers = {} ---@type table<string,boolean> 63 local idx = 1 64 while x > 0 do 65 if bit.band(x, 1) == 1 then 66 modifiers[modifiers_table[idx]] = true 67 end 68 x = bit.rshift(x, 1) 69 idx = idx + 1 70 end 71 72 return modifiers 73 end 74 75 --- Converts a raw token list to a list of highlight ranges used by the on_win callback 76 --- 77 ---@async 78 ---@param data integer[] 79 ---@param bufnr integer 80 ---@param client vim.lsp.Client 81 ---@param request STActiveRequest 82 ---@param ranges STTokenRange[] 83 ---@return STTokenRange[] 84 local function tokens_to_ranges(data, bufnr, client, request, ranges) 85 local legend = client.server_capabilities.semanticTokensProvider.legend 86 local token_types = legend.tokenTypes 87 local token_modifiers = legend.tokenModifiers 88 local encoding = client.offset_encoding 89 local lines = api.nvim_buf_get_lines(bufnr, 0, -1, false) 90 -- For all encodings, \r\n takes up two code points, and \n (or \r) takes up one. 91 local eol_offset = vim.bo.fileformat[bufnr] == 'dos' and 2 or 1 92 local version = request.version 93 local request_id = request.request_id 94 local last_insert_idx = 1 95 96 local start = uv.hrtime() 97 local ms_to_ns = 1e6 98 local yield_interval_ns = 5 * ms_to_ns 99 local co, is_main = coroutine.running() 100 101 local line ---@type integer? 102 local start_char = 0 103 for i = 1, #data, 5 do 104 -- if this function is called from the main coroutine, let it run to completion with no yield 105 if not is_main then 106 local elapsed_ns = uv.hrtime() - start 107 108 if elapsed_ns > yield_interval_ns then 109 vim.schedule(function() 110 -- Ensure the request hasn't become stale since the last time the coroutine ran. 111 -- If it's stale, we don't resume the coroutine so it'll be garbage collected. 112 if 113 version == util.buf_versions[bufnr] 114 and request_id == request.request_id 115 and api.nvim_buf_is_valid(bufnr) 116 then 117 coroutine.resume(co) 118 end 119 end) 120 121 coroutine.yield() 122 start = uv.hrtime() 123 end 124 end 125 126 local delta_line = data[i] 127 line = line and line + delta_line or delta_line 128 local delta_start = data[i + 1] 129 start_char = delta_line == 0 and start_char + delta_start or delta_start 130 131 -- data[i+3] +1 because Lua tables are 1-indexed 132 local token_type = token_types[data[i + 3] + 1] 133 134 if token_type then 135 local modifiers = modifiers_from_number(data[i + 4], token_modifiers) 136 local end_char = start_char + data[i + 2] --- @type integer LuaLS bug 137 local buf_line = lines[line + 1] or '' 138 local end_line = line ---@type integer 139 local start_col = vim.str_byteindex(buf_line, encoding, start_char, false) 140 141 ---@type integer LuaLS bug, type must be marked explicitly here 142 local new_end_char = end_char - vim.str_utfindex(buf_line, encoding) - eol_offset 143 -- While end_char goes past the given line, extend the token range to the next line 144 while new_end_char > 0 do 145 end_char = new_end_char 146 end_line = end_line + 1 147 buf_line = lines[end_line + 1] or '' 148 new_end_char = new_end_char - vim.str_utfindex(buf_line, encoding) - eol_offset 149 end 150 151 local end_col = vim.str_byteindex(buf_line, encoding, end_char, false) 152 153 ---@type STTokenRange 154 local range = { 155 line = line, 156 end_line = end_line, 157 start_col = start_col, 158 end_col = end_col, 159 type = token_type, 160 modifiers = modifiers, 161 marked = false, 162 } 163 164 if last_insert_idx < #ranges then 165 local needs_insert = true 166 local idx = vim.list.bisect(ranges, { line = range.line }, { 167 lo = last_insert_idx, 168 key = function(highlight) 169 return highlight.line 170 end, 171 }) 172 while idx <= #ranges do 173 local token = ranges[idx] 174 175 if 176 token.line > range.line 177 or (token.line == range.line and token.start_col > range.start_col) 178 then 179 break 180 end 181 182 if 183 range.line == token.line 184 and range.start_col == token.start_col 185 and range.end_line == token.end_line 186 and range.end_col == token.end_col 187 and range.type == token.type 188 then 189 needs_insert = false 190 break 191 end 192 193 idx = idx + 1 194 end 195 196 last_insert_idx = idx 197 if needs_insert then 198 table.insert(ranges, last_insert_idx, range) 199 end 200 else 201 last_insert_idx = #ranges + 1 202 ranges[last_insert_idx] = range 203 end 204 end 205 end 206 207 return ranges 208 end 209 210 --- Construct a new STHighlighter for the buffer 211 --- 212 ---@private 213 ---@param bufnr integer 214 ---@return STHighlighter 215 function STHighlighter:new(bufnr) 216 self.debounce = 200 217 self = Capability.new(self, bufnr) 218 219 api.nvim_buf_attach(bufnr, false, { 220 on_lines = function(_, buf) 221 local highlighter = STHighlighter.active[buf] 222 if not highlighter then 223 return true 224 end 225 highlighter:on_change() 226 end, 227 on_reload = function(_, buf) 228 local highlighter = STHighlighter.active[buf] 229 if highlighter then 230 highlighter:reset() 231 highlighter:send_request() 232 end 233 end, 234 }) 235 236 return self 237 end 238 239 ---@package 240 function STHighlighter:on_attach(client_id) 241 local client = vim.lsp.get_client_by_id(client_id) 242 local state = self.client_state[client_id] 243 if not state then 244 state = { 245 namespace = api.nvim_create_namespace('nvim.lsp.semantic_tokens:' .. client_id), 246 supports_range = client 247 and client:supports_method('textDocument/semanticTokens/range', self.bufnr) 248 or false, 249 supports_delta = client 250 and client:supports_method('textDocument/semanticTokens/full/delta', self.bufnr) 251 or false, 252 active_request = {}, 253 active_range_request = {}, 254 current_result = {}, 255 has_full_result = false, 256 } 257 self.client_state[client_id] = state 258 end 259 260 api.nvim_create_autocmd({ 'BufWinEnter', 'InsertLeave' }, { 261 buffer = self.bufnr, 262 group = self.augroup, 263 callback = function() 264 self:send_request() 265 end, 266 }) 267 268 if state.supports_range then 269 api.nvim_create_autocmd('WinScrolled', { 270 buffer = self.bufnr, 271 group = self.augroup, 272 callback = function() 273 self:on_change() 274 end, 275 }) 276 end 277 278 self:send_request() 279 end 280 281 ---@package 282 function STHighlighter:on_detach(client_id) 283 local state = self.client_state[client_id] 284 if state then 285 --TODO: delete namespace if/when that becomes possible 286 api.nvim_buf_clear_namespace(self.bufnr, state.namespace, 0, -1) 287 api.nvim_clear_autocmds({ group = self.augroup }) 288 self.client_state[client_id] = nil 289 end 290 end 291 292 --- This is the entry point for getting all the tokens in a buffer. 293 --- 294 --- For the given clients (or all attached, if not provided), this sends semantic token requests to 295 --- ask for semantic tokens. If the server supports range requests and a full result has not been 296 --- processed yet, it will send a range request for the current visible range. Additionally, if a 297 --- result for the current document version hasn't been processed yet, it sends either a full or 298 --- delta request, depending on what the server supports and whether there's a current full result 299 --- for the previous document version. 300 --- 301 --- This function will skip full/delta requests on servers where there is an already an active 302 --- full/delta request in flight for the same version. If there is a stale request in flight, that 303 --- is cancelled prior to sending a new one. 304 --- 305 --- Finally, for successful requests, the requestId (full/delta) and document version are saved to 306 --- facilitate document synchronization in the response. 307 --- 308 ---@package 309 function STHighlighter:send_request() 310 local version = util.buf_versions[self.bufnr] 311 312 self:reset_timer() 313 314 for client_id, state in pairs(self.client_state) do 315 local client = vim.lsp.get_client_by_id(client_id) 316 if client then 317 -- If the server supports range and there's no full result yet, then start with a range 318 -- request 319 if state.supports_range and not state.has_full_result then 320 self:send_range_request(client, state, version) 321 end 322 323 if 324 (not state.has_full_result or state.current_result.version ~= version) 325 and state.active_request.version ~= version 326 then 327 self:send_full_delta_request(client, state, version) 328 end 329 end 330 end 331 end 332 333 --- Send a range request for the visible area 334 --- 335 ---@private 336 ---@param client vim.lsp.Client 337 ---@param state STClientState 338 ---@param version integer 339 function STHighlighter:send_range_request(client, state, version) 340 local active_request = state.active_range_request 341 342 -- cancel stale in-flight request 343 if active_request and active_request.request_id then 344 client:cancel_request(active_request.request_id) 345 active_request.request_id = nil 346 active_request.version = nil 347 end 348 349 ---@type lsp.SemanticTokensRangeParams 350 local params = { 351 textDocument = util.make_text_document_params(self.bufnr), 352 range = self:get_visible_range(), 353 } 354 355 ---@type vim.lsp.protocol.Method.ClientToServer.Request 356 local method = 'textDocument/semanticTokens/range' 357 358 ---@param response? lsp.SemanticTokens 359 local success, request_id = client:request(method, params, function(err, response, ctx) 360 local bufnr = assert(ctx.bufnr) 361 local highlighter = STHighlighter.active[bufnr] 362 if not highlighter then 363 return 364 end 365 366 -- Only process range response if we got a valid response and don't have a full result yet 367 if err or not response or state.has_full_result then 368 active_request.request_id = nil 369 active_request.version = nil 370 return 371 end 372 373 coroutine.wrap(STHighlighter.process_response)( 374 highlighter, 375 response, 376 client, 377 ctx.request_id, 378 version, 379 true 380 ) 381 end, self.bufnr) 382 383 if success then 384 active_request.request_id = request_id 385 active_request.version = version 386 end 387 end 388 389 --- Send a full or delta request 390 --- 391 ---@private 392 ---@param client vim.lsp.Client 393 ---@param state STClientState 394 ---@param version integer 395 function STHighlighter:send_full_delta_request(client, state, version) 396 local current_result = state.current_result 397 local active_request = state.active_request 398 399 -- cancel stale in-flight request 400 if active_request.request_id then 401 client:cancel_request(active_request.request_id) 402 active_request.request_id = nil 403 active_request.version = nil 404 end 405 406 ---@type lsp.SemanticTokensParams|lsp.SemanticTokensDeltaParams 407 local params = { textDocument = util.make_text_document_params(self.bufnr) } 408 409 ---@type vim.lsp.protocol.Method.ClientToServer.Request 410 local method = 'textDocument/semanticTokens/full' 411 412 if state.supports_delta and current_result.result_id then 413 method = 'textDocument/semanticTokens/full/delta' 414 params.previousResultId = current_result.result_id 415 end 416 417 ---@param response? lsp.SemanticTokens|lsp.SemanticTokensDelta 418 local success, request_id = client:request(method, params, function(err, response, ctx) 419 local bufnr = assert(ctx.bufnr) 420 local highlighter = STHighlighter.active[bufnr] 421 if not highlighter then 422 return 423 end 424 425 if err or not response then 426 active_request.request_id = nil 427 active_request.version = nil 428 return 429 end 430 431 coroutine.wrap(STHighlighter.process_response)( 432 highlighter, 433 response, 434 client, 435 ctx.request_id, 436 version, 437 false 438 ) 439 end, self.bufnr) 440 441 if success then 442 active_request.request_id = request_id 443 active_request.version = version 444 end 445 end 446 447 ---@private 448 function STHighlighter:cancel_active_request(client_id) 449 local state = self.client_state[client_id] 450 local client = vim.lsp.get_client_by_id(client_id) 451 452 ---@param request STActiveRequest 453 local function clear(request) 454 if client and request.request_id then 455 client:cancel_request(request.request_id) 456 request.request_id = nil 457 request.version = nil 458 end 459 end 460 461 clear(state.active_range_request) 462 clear(state.active_request) 463 end 464 465 --- Gets a range that encompasses all visible lines across all windows 466 --- @private 467 --- @return lsp.Range 468 function STHighlighter:get_visible_range() 469 local wins = vim.fn.win_findbuf(self.bufnr) 470 local min_start, max_end = nil, nil 471 472 for _, win in ipairs(wins) do 473 local wininfo = vim.fn.getwininfo(win)[1] 474 if wininfo then 475 local start_line = wininfo.topline - 1 476 local end_line = wininfo.botline 477 if not min_start or start_line < min_start then 478 min_start = start_line 479 end 480 if not max_end or end_line > max_end then 481 max_end = end_line 482 end 483 end 484 end 485 486 ---@type lsp.Range 487 return { 488 ['start'] = { line = min_start or 0, character = 0 }, 489 ['end'] = { line = max_end or 0, character = 0 }, 490 } 491 end 492 493 --- This function will parse the semantic token responses and set up the cache 494 --- (current_result). It also performs document synchronization by checking the 495 --- version of the document associated with the resulting request_id and only 496 --- performing work if the response is not out-of-date. 497 --- 498 --- Delta edits are applied if necessary, and new highlight ranges are calculated 499 --- and stored in the buffer state. 500 --- 501 --- Finally, a redraw command is issued to force nvim to redraw the screen to 502 --- pick up changed highlight tokens. 503 --- 504 ---@async 505 ---@param response lsp.SemanticTokens|lsp.SemanticTokensDelta 506 ---@param client vim.lsp.Client 507 ---@param request_id integer 508 ---@param version integer 509 ---@param is_range_request boolean 510 ---@private 511 function STHighlighter:process_response(response, client, request_id, version, is_range_request) 512 local state = self.client_state[client.id] 513 if not state then 514 return 515 end 516 517 ---@type STActiveRequest 518 local active_request 519 if is_range_request then 520 active_request = state.active_range_request 521 else 522 active_request = state.active_request 523 end 524 525 -- ignore stale responses 526 if active_request.request_id and request_id ~= active_request.request_id then 527 return 528 end 529 530 if not api.nvim_buf_is_valid(self.bufnr) then 531 return 532 end 533 534 -- if we have a response to a delta request, update the state of our tokens 535 -- appropriately. if it's a full response, just use that 536 local tokens ---@type integer[] 537 local token_edits = response.edits 538 if token_edits then 539 table.sort(token_edits, function(a, b) 540 return a.start < b.start 541 end) 542 543 tokens = {} --- @type integer[] 544 local old_tokens = assert(state.current_result.tokens) 545 local idx = 1 546 for _, token_edit in ipairs(token_edits) do 547 vim.list_extend(tokens, old_tokens, idx, token_edit.start) 548 if token_edit.data then 549 vim.list_extend(tokens, token_edit.data) 550 end 551 idx = token_edit.start + token_edit.deleteCount + 1 552 end 553 vim.list_extend(tokens, old_tokens, idx) 554 else 555 tokens = response.data 556 end 557 558 local current_result = state.current_result 559 local version_changed = version ~= current_result.version 560 local highlights = {} --- @type STTokenRange[] 561 if current_result.highlights and not version_changed then 562 highlights = assert(current_result.highlights) 563 end 564 565 -- convert token list to highlight ranges 566 -- this could yield and run over multiple event loop iterations 567 highlights = tokens_to_ranges(tokens, self.bufnr, client, active_request, highlights) 568 569 -- if this was a full result, mark the state as having processed it 570 if not is_range_request then 571 state.has_full_result = true 572 end 573 574 -- reset active request 575 active_request.request_id = nil 576 active_request.version = nil 577 578 -- update the state with the new results 579 current_result.version = version 580 current_result.result_id = not is_range_request and response.resultId or nil 581 current_result.tokens = tokens 582 current_result.highlights = highlights 583 if version_changed then 584 current_result.namespace_cleared = false 585 end 586 587 -- redraw all windows displaying buffer 588 api.nvim__redraw({ buf = self.bufnr, valid = true }) 589 end 590 591 --- @param bufnr integer 592 --- @param ns integer 593 --- @param token STTokenRange 594 --- @param hl_group string 595 --- @param priority integer 596 local function set_mark(bufnr, ns, token, hl_group, priority) 597 api.nvim_buf_set_extmark(bufnr, ns, token.line, token.start_col, { 598 hl_group = hl_group, 599 end_line = token.end_line, 600 end_col = token.end_col, 601 priority = priority, 602 strict = false, 603 }) 604 end 605 606 --- @param lnum integer 607 --- @param foldend integer? 608 --- @return boolean, integer? 609 local function check_fold(lnum, foldend) 610 if foldend and lnum <= foldend then 611 return true, foldend 612 end 613 614 local folded = vim.fn.foldclosed(lnum) 615 616 if folded == -1 then 617 return false, nil 618 end 619 620 return folded ~= lnum, vim.fn.foldclosedend(lnum) 621 end 622 623 --- on_win handler for the decoration provider (see |nvim_set_decoration_provider|) 624 --- 625 --- If there is a current result for the buffer and the version matches the 626 --- current document version, then the tokens are valid and can be applied. As 627 --- the buffer is drawn, this function will add extmark highlights for every 628 --- token in the range of visible lines. Once a highlight has been added, it 629 --- sticks around until the document changes and there's a new set of matching 630 --- highlight tokens available. 631 --- 632 --- If this is the first time a buffer is being drawn with a new set of 633 --- highlights for the current document version, the namespace is cleared to 634 --- remove extmarks from the last version. It's done here instead of the response 635 --- handler to avoid the "blink" that occurs due to the timing between the 636 --- response handler and the actual redraw. 637 --- 638 ---@package 639 ---@param topline integer 640 ---@param botline integer 641 function STHighlighter:on_win(topline, botline) 642 for client_id, state in pairs(self.client_state) do 643 local current_result = state.current_result 644 if current_result.version == util.buf_versions[self.bufnr] then 645 if not current_result.namespace_cleared then 646 api.nvim_buf_clear_namespace(self.bufnr, state.namespace, 0, -1) 647 current_result.namespace_cleared = true 648 end 649 650 -- We can't use ephemeral extmarks because the buffer updates are not in 651 -- sync with the list of semantic tokens. There's a delay between the 652 -- buffer changing and when the LSP server can respond with updated 653 -- tokens, and we don't want to "blink" the token highlights while 654 -- updates are in flight, and we don't want to use stale tokens because 655 -- they likely won't line up right with the actual buffer. 656 -- 657 -- Instead, we have to use normal extmarks that can attach to locations 658 -- in the buffer and are persisted between redraws. 659 -- 660 -- `strict = false` is necessary here for the 1% of cases where the 661 -- current result doesn't actually match the buffer contents. Some 662 -- LSP servers can respond with stale tokens on requests if they are 663 -- still processing changes from a didChange notification. 664 -- 665 -- LSP servers that do this _should_ follow up known stale responses 666 -- with a refresh notification once they've finished processing the 667 -- didChange notification, which would re-synchronize the tokens from 668 -- our end. 669 -- 670 -- The server I know of that does this is clangd when the preamble of 671 -- a file changes and the token request is processed with a stale 672 -- preamble while the new one is still being built. Once the preamble 673 -- finishes, clangd sends a refresh request which lets the client 674 -- re-synchronize the tokens. 675 676 local function set_mark0(token, hl_group, delta) 677 set_mark( 678 self.bufnr, 679 state.namespace, 680 token, 681 hl_group, 682 vim.hl.priorities.semantic_tokens + delta 683 ) 684 end 685 686 local ft = vim.bo[self.bufnr].filetype 687 local highlights = assert(current_result.highlights) 688 local first = vim.list.bisect(highlights, { end_line = topline }, { 689 key = function(highlight) 690 return highlight.end_line 691 end, 692 }) 693 local last = vim.list.bisect(highlights, { line = botline }, { 694 lo = first, 695 bound = 'upper', 696 key = function(highlight) 697 return highlight.line 698 end, 699 }) - 1 700 701 --- @type boolean?, integer? 702 local is_folded, foldend 703 704 for i = first, last do 705 local token = assert(highlights[i]) 706 707 is_folded, foldend = check_fold(token.line + 1, foldend) 708 709 if not is_folded and not token.marked then 710 set_mark0(token, string.format('@lsp.type.%s.%s', token.type, ft), 0) 711 for modifier in pairs(token.modifiers) do 712 set_mark0(token, string.format('@lsp.mod.%s.%s', modifier, ft), 1) 713 set_mark0(token, string.format('@lsp.typemod.%s.%s.%s', token.type, modifier, ft), 2) 714 end 715 token.marked = true 716 717 api.nvim_exec_autocmds('LspTokenUpdate', { 718 buffer = self.bufnr, 719 modeline = false, 720 data = { 721 token = token, 722 client_id = client_id, 723 }, 724 }) 725 end 726 end 727 end 728 end 729 end 730 731 --- Reset the buffer's highlighting state and clears the extmark highlights. 732 --- 733 ---@package 734 function STHighlighter:reset() 735 for client_id, state in pairs(self.client_state) do 736 api.nvim_buf_clear_namespace(self.bufnr, state.namespace, 0, -1) 737 state.current_result = {} 738 state.has_full_result = false 739 self:cancel_active_request(client_id) 740 end 741 end 742 743 --- Mark a client's results as dirty. This method will cancel any active 744 --- requests to the server and pause new highlights from being added 745 --- in the on_win callback. The rest of the current results are saved 746 --- in case the server supports delta requests. 747 --- 748 ---@package 749 ---@param client_id integer 750 function STHighlighter:mark_dirty(client_id) 751 local state = assert(self.client_state[client_id]) 752 753 -- if we clear the version from current_result, it'll cause the next 754 -- full/delta request to be sent and will also pause new highlights 755 -- from being added in on_win until a new result comes from the server 756 if state.current_result then 757 state.current_result.version = nil 758 end 759 760 -- clearing this flag will also allow range requests to fire to 761 -- potentially get a faster result 762 state.has_full_result = false 763 764 self:cancel_active_request(client_id) 765 end 766 767 ---@package 768 function STHighlighter:on_change() 769 self:reset_timer() 770 if self.debounce > 0 then 771 self.timer = vim.defer_fn(function() 772 self:send_request() 773 end, self.debounce) 774 else 775 self:send_request() 776 end 777 end 778 779 ---@private 780 function STHighlighter:reset_timer() 781 local timer = self.timer 782 if timer then 783 self.timer = nil 784 if not timer:is_closing() then 785 timer:stop() 786 timer:close() 787 end 788 end 789 end 790 791 ---@param bufnr (integer) Buffer number, or `0` for current buffer 792 ---@param client_id (integer) The ID of the |vim.lsp.Client| 793 ---@param debounce? (integer) (default: 200): Debounce token requests 794 --- to the server by the given number in milliseconds 795 function M._start(bufnr, client_id, debounce) 796 local highlighter = STHighlighter.active[bufnr] 797 798 if not highlighter then 799 highlighter = STHighlighter:new(bufnr) 800 highlighter.debounce = debounce or 200 801 else 802 highlighter.debounce = debounce or highlighter.debounce 803 end 804 805 highlighter:on_attach(client_id) 806 end 807 808 --- Start the semantic token highlighting engine for the given buffer with the 809 --- given client. The client must already be attached to the buffer. 810 --- 811 --- NOTE: This is currently called automatically by |vim.lsp.buf_attach_client()|. To 812 --- opt-out of semantic highlighting with a server that supports it, you can 813 --- delete the semanticTokensProvider table from the {server_capabilities} of 814 --- your client in your |LspAttach| callback or your configuration's 815 --- `on_attach` callback: 816 --- 817 --- ```lua 818 --- client.server_capabilities.semanticTokensProvider = nil 819 --- ``` 820 --- 821 ---@deprecated 822 ---@param bufnr (integer) Buffer number, or `0` for current buffer 823 ---@param client_id (integer) The ID of the |vim.lsp.Client| 824 ---@param opts? (table) Optional keyword arguments 825 --- - debounce (integer, default: 200): Debounce token requests 826 --- to the server by the given number in milliseconds 827 function M.start(bufnr, client_id, opts) 828 vim.deprecate('vim.lsp.semantic_tokens.start', 'vim.lsp.semantic_tokens.enable(true)', '0.13.0') 829 vim.validate('bufnr', bufnr, 'number') 830 vim.validate('client_id', client_id, 'number') 831 832 bufnr = vim._resolve_bufnr(bufnr) 833 834 opts = opts or {} 835 assert( 836 (not opts.debounce or type(opts.debounce) == 'number'), 837 'opts.debounce must be a number with the debounce time in milliseconds' 838 ) 839 840 local client = vim.lsp.get_client_by_id(client_id) 841 if not client then 842 vim.notify('[LSP] No client with id ' .. client_id, vim.log.levels.ERROR) 843 return 844 end 845 846 if not vim.lsp.buf_is_attached(bufnr, client_id) then 847 vim.notify( 848 '[LSP] Client with id ' .. client_id .. ' not attached to buffer ' .. bufnr, 849 vim.log.levels.WARN 850 ) 851 return 852 end 853 854 if 855 not client:supports_method('textDocument/semanticTokens/full', bufnr) 856 and not client:supports_method('textDocument/semanticTokens/range', bufnr) 857 then 858 vim.notify('[LSP] Server does not support semantic tokens', vim.log.levels.WARN) 859 return 860 end 861 862 M._start(bufnr, client_id, opts.debounce) 863 end 864 865 --- Stop the semantic token highlighting engine for the given buffer with the 866 --- given client. 867 --- 868 --- NOTE: This is automatically called by a |LspDetach| autocmd that is set up as part 869 --- of `start()`, so you should only need this function to manually disengage the semantic 870 --- token engine without fully detaching the LSP client from the buffer. 871 --- 872 ---@deprecated 873 ---@param bufnr (integer) Buffer number, or `0` for current buffer 874 ---@param client_id (integer) The ID of the |vim.lsp.Client| 875 function M.stop(bufnr, client_id) 876 vim.deprecate('vim.lsp.semantic_tokens.stop', 'vim.lsp.semantic_tokens.enable(false)', '0.13.0') 877 vim.validate('bufnr', bufnr, 'number') 878 vim.validate('client_id', client_id, 'number') 879 880 bufnr = vim._resolve_bufnr(bufnr) 881 882 local highlighter = STHighlighter.active[bufnr] 883 if not highlighter then 884 return 885 end 886 887 highlighter:on_detach(client_id) 888 889 if vim.tbl_isempty(highlighter.client_state) then 890 highlighter:destroy() 891 end 892 end 893 894 --- Query whether semantic tokens is enabled in the {filter}ed scope 895 ---@param filter? vim.lsp.capability.enable.Filter 896 function M.is_enabled(filter) 897 return vim.lsp._capability.is_enabled('semantic_tokens', filter) 898 end 899 900 --- Enables or disables semantic tokens for the {filter}ed scope. 901 --- 902 --- To "toggle", pass the inverse of `is_enabled()`: 903 --- 904 --- ```lua 905 --- vim.lsp.semantic_tokens.enable(not vim.lsp.semantic_tokens.is_enabled()) 906 --- ``` 907 --- 908 ---@param enable? boolean true/nil to enable, false to disable 909 ---@param filter? vim.lsp.capability.enable.Filter 910 function M.enable(enable, filter) 911 vim.lsp._capability.enable('semantic_tokens', enable, filter) 912 end 913 914 --- @nodoc 915 --- @class STTokenRangeInspect : STTokenRange 916 --- @field client_id integer 917 918 --- Return the semantic token(s) at the given position. 919 --- If called without arguments, returns the token under the cursor. 920 --- 921 ---@param bufnr integer|nil Buffer number (0 for current buffer, default) 922 ---@param row integer|nil Position row (default cursor position) 923 ---@param col integer|nil Position column (default cursor position) 924 --- 925 ---@return STTokenRangeInspect[]|nil (table|nil) List of tokens at position. Each token has 926 --- the following fields: 927 --- - line (integer) line number, 0-based 928 --- - start_col (integer) start column, 0-based 929 --- - end_line (integer) end line number, 0-based 930 --- - end_col (integer) end column, 0-based 931 --- - type (string) token type as string, e.g. "variable" 932 --- - modifiers (table) token modifiers as a set. E.g., { static = true, readonly = true } 933 --- - client_id (integer) 934 function M.get_at_pos(bufnr, row, col) 935 bufnr = vim._resolve_bufnr(bufnr) 936 937 local highlighter = STHighlighter.active[bufnr] 938 if not highlighter then 939 return 940 end 941 942 if row == nil or col == nil then 943 local cursor = api.nvim_win_get_cursor(0) 944 row, col = cursor[1] - 1, cursor[2] 945 end 946 947 local position = { row, col, row, col } 948 949 local tokens = {} --- @type STTokenRangeInspect[] 950 for client_id, client in pairs(highlighter.client_state) do 951 local highlights = client.current_result.highlights 952 if highlights then 953 local idx = vim.list.bisect(highlights, { end_line = row }, { 954 key = function(highlight) 955 return highlight.end_line 956 end, 957 }) 958 for i = idx, #highlights do 959 local token = highlights[i] 960 --- @cast token STTokenRangeInspect 961 962 if token.line > row then 963 break 964 end 965 966 if 967 Range.contains({ token.line, token.start_col, token.end_line, token.end_col }, position) 968 then 969 token.client_id = client_id 970 tokens[#tokens + 1] = token 971 end 972 end 973 end 974 end 975 return tokens 976 end 977 978 --- Force a refresh of all semantic tokens 979 --- 980 --- Only has an effect if the buffer is currently active for semantic token 981 --- highlighting (|vim.lsp.semantic_tokens.enable()| has been called for it) 982 --- 983 ---@param bufnr (integer|nil) filter by buffer. All buffers if nil, current 984 --- buffer if 0 985 function M.force_refresh(bufnr) 986 vim.validate('bufnr', bufnr, 'number', true) 987 988 local buffers = bufnr == nil and vim.tbl_keys(STHighlighter.active) 989 or { vim._resolve_bufnr(bufnr) } 990 991 for _, buffer in ipairs(buffers) do 992 local highlighter = STHighlighter.active[buffer] 993 if highlighter then 994 highlighter:reset() 995 highlighter:send_request() 996 end 997 end 998 end 999 1000 --- @class vim.lsp.semantic_tokens.highlight_token.Opts 1001 --- @inlinedoc 1002 --- 1003 --- Priority for the applied extmark. 1004 --- (Default: `vim.hl.priorities.semantic_tokens + 3`) 1005 --- @field priority? integer 1006 1007 --- Highlight a semantic token. 1008 --- 1009 --- Apply an extmark with a given highlight group for a semantic token. The 1010 --- mark will be deleted by the semantic token engine when appropriate; for 1011 --- example, when the LSP sends updated tokens. This function is intended for 1012 --- use inside |LspTokenUpdate| callbacks. 1013 ---@param token (table) A semantic token, found as `args.data.token` in |LspTokenUpdate| 1014 ---@param bufnr (integer) The buffer to highlight, or `0` for current buffer 1015 ---@param client_id (integer) The ID of the |vim.lsp.Client| 1016 ---@param hl_group (string) Highlight group name 1017 ---@param opts? vim.lsp.semantic_tokens.highlight_token.Opts Optional parameters: 1018 function M.highlight_token(token, bufnr, client_id, hl_group, opts) 1019 bufnr = vim._resolve_bufnr(bufnr) 1020 local highlighter = STHighlighter.active[bufnr] 1021 if not highlighter then 1022 return 1023 end 1024 1025 local state = highlighter.client_state[client_id] 1026 if not state then 1027 return 1028 end 1029 1030 local priority = opts and opts.priority or vim.hl.priorities.semantic_tokens + 3 1031 1032 set_mark(bufnr, state.namespace, token, hl_group, priority) 1033 end 1034 1035 --- |lsp-handler| for the method `workspace/semanticTokens/refresh` 1036 --- 1037 --- Refresh requests are sent by the server to indicate a project-wide change 1038 --- that requires all tokens to be re-requested by the client. This handler will 1039 --- invalidate the current results of all buffers and automatically kick off a 1040 --- new request for buffers that are displayed in a window. For those that aren't, a 1041 --- the BufWinEnter event should take care of it next time it's displayed. 1042 function M._refresh(err, _, ctx) 1043 if err then 1044 return vim.NIL 1045 end 1046 1047 for bufnr in pairs(vim.lsp.get_client_by_id(ctx.client_id).attached_buffers or {}) do 1048 local highlighter = STHighlighter.active[bufnr] 1049 if highlighter and highlighter.client_state[ctx.client_id] then 1050 highlighter:mark_dirty(ctx.client_id) 1051 1052 if not vim.tbl_isempty(vim.fn.win_findbuf(bufnr)) then 1053 -- some LSPs send rapid fire refresh notifications, so we'll debounce them with on_change() 1054 highlighter:on_change() 1055 end 1056 end 1057 end 1058 1059 return vim.NIL 1060 end 1061 1062 local namespace = api.nvim_create_namespace('nvim.lsp.semantic_tokens') 1063 api.nvim_set_decoration_provider(namespace, { 1064 on_win = function(_, _, bufnr, topline, botline) 1065 local highlighter = STHighlighter.active[bufnr] 1066 if highlighter then 1067 highlighter:on_win(topline, botline) 1068 end 1069 end, 1070 }) 1071 1072 --- for testing only! there is no guarantee of API stability with this! 1073 --- 1074 ---@private 1075 M.__STHighlighter = STHighlighter 1076 1077 -- Semantic tokens is enabled by default 1078 vim.lsp._capability.enable('semantic_tokens', true) 1079 1080 return M