From 7ac4cbcd2e612189d0b41d8e60e11981f23a17c7 Mon Sep 17 00:00:00 2001 From: Yi Ming Date: Thu, 10 Jul 2025 00:04:36 +0800 Subject: [PATCH 1/3] refactor(lsp): utility functions for `enable()/is_enabled()` --- runtime/lua/vim/lsp/_folding_range.lua | 10 +-- runtime/lua/vim/lsp/util.lua | 79 +++++++++++++++++++ .../plugin/lsp/folding_range_spec.lua | 8 +- 3 files changed, 88 insertions(+), 9 deletions(-) diff --git a/runtime/lua/vim/lsp/_folding_range.lua b/runtime/lua/vim/lsp/_folding_range.lua index ca4114f746..2037217eed 100644 --- a/runtime/lua/vim/lsp/_folding_range.lua +++ b/runtime/lua/vim/lsp/_folding_range.lua @@ -87,7 +87,7 @@ end --- Force `foldexpr()` to be re-evaluated, without opening folds. ---@param bufnr integer local function foldupdate(bufnr) - if not api.nvim_buf_is_loaded(bufnr) or not vim.b[bufnr]._lsp_folding_range_enabled then + if not api.nvim_buf_is_loaded(bufnr) or not vim.b[bufnr]._lsp_enable_folding_range then return end for _, winid in ipairs(vim.fn.win_findbuf(bufnr)) do @@ -159,7 +159,7 @@ end --- `foldupdate()` is scheduled once after the request is completed. ---@param client? vim.lsp.Client The client whose server supports `foldingRange`. function State:refresh(client) - if not vim.b._lsp_folding_range_enabled then + if not vim.b._lsp_enable_folding_range then return end @@ -252,7 +252,7 @@ function State:new(bufnr) pattern = 'foldexpr', callback = function() if vim.v.option_type == 'global' or vim.api.nvim_get_current_buf() == bufnr then - vim.b[bufnr]._lsp_folding_range_enabled = nil + vim.b[bufnr]._lsp_enable_folding_range = nil end end, }) @@ -349,8 +349,8 @@ end function M.foldexpr(lnum) local bufnr = api.nvim_get_current_buf() local state = State.active[bufnr] - if not vim.b[bufnr]._lsp_folding_range_enabled then - vim.b[bufnr]._lsp_folding_range_enabled = true + if not vim.b[bufnr]._lsp_enable_folding_range then + vim.b[bufnr]._lsp_enable_folding_range = true if state then state:refresh() end diff --git a/runtime/lua/vim/lsp/util.lua b/runtime/lua/vim/lsp/util.lua index d23b4816e0..ad1804db24 100644 --- a/runtime/lua/vim/lsp/util.lua +++ b/runtime/lua/vim/lsp/util.lua @@ -2340,6 +2340,85 @@ function M._refresh(method, opts) end end +---@param feature string +---@param client_id? integer +local function make_enable_var(feature, client_id) + return ('_lsp_enabled_%s%s'):format(feature, client_id and ('_client_%d'):format(client_id) or '') +end + +---@class vim.lsp.enable.Filter +---@inlinedoc +--- +--- Buffer number, or 0 for current buffer, or nil for all. +---@field bufnr? integer +--- +--- Client ID, or nil for all +---@field client_id? integer + +---@param feature string +---@param filter? vim.lsp.enable.Filter +function M._is_enabled(feature, filter) + vim.validate('feature', feature, 'string') + vim.validate('filter', filter, 'table', true) + + filter = filter or {} + local bufnr = filter.bufnr + local client_id = filter.client_id + + local var = make_enable_var(feature) + local client_var = make_enable_var(feature, client_id) + return vim.F.if_nil(client_id and vim.g[client_var], vim.g[var]) + and vim.F.if_nil(bufnr and vim.b[bufnr][var], vim.g[var]) +end + +---@param feature 'semantic_tokens' +---@param enable? boolean +---@param filter? vim.lsp.enable.Filter +function M._enable(feature, enable, filter) + vim.validate('feature', feature, 'string') + vim.validate('enable', enable, 'boolean', true) + vim.validate('filter', filter, 'table', true) + + enable = enable == nil or enable + filter = filter or {} + local bufnr = filter.bufnr + local client_id = filter.client_id + assert( + not (bufnr and client_id), + 'Only one of `bufnr` or `client_id` filters can be specified at a time.' + ) + + local var = make_enable_var(feature) + local client_var = make_enable_var(feature, client_id) + + if client_id then + if enable == vim.g[var] then + vim.g[client_var] = nil + else + vim.g[client_var] = enable + end + elseif bufnr then + if enable == vim.g[var] then + vim.b[bufnr][var] = nil + else + vim.b[bufnr][var] = enable + end + else + vim.g[var] = enable + for _, it_bufnr in ipairs(api.nvim_list_bufs()) do + if api.nvim_buf_is_loaded(it_bufnr) and vim.b[it_bufnr][var] == enable then + vim.b[it_bufnr][var] = nil + end + end + for _, it_client in ipairs(vim.lsp.get_clients()) do + local it_client_var = make_enable_var(feature, it_client.id) + if vim.g[it_client_var] and vim.g[it_client_var] == enable then + vim.g[it_client_var] = nil + end + end + end +end + M._get_line_byte_from_position = get_line_byte_from_position ---@nodoc diff --git a/test/functional/plugin/lsp/folding_range_spec.lua b/test/functional/plugin/lsp/folding_range_spec.lua index 277431c8c5..96e0a1e88d 100644 --- a/test/functional/plugin/lsp/folding_range_spec.lua +++ b/test/functional/plugin/lsp/folding_range_spec.lua @@ -135,25 +135,25 @@ static int foldLevel(linenr_T lnum) command([[split]]) end) - it('controls the value of `b:_lsp_folding_range_enabled`', function() + it('controls the value of `b:_lsp_enable_folding_range`', function() eq( true, exec_lua(function() - return vim.b._lsp_folding_range_enabled + return vim.b._lsp_enable_folding_range end) ) command [[setlocal foldexpr=]] eq( nil, exec_lua(function() - return vim.b._lsp_folding_range_enabled + return vim.b._lsp_enable_folding_range end) ) command([[set foldexpr=v:lua.vim.lsp.foldexpr()]]) eq( true, exec_lua(function() - return vim.b._lsp_folding_range_enabled + return vim.b._lsp_enable_folding_range end) ) end) From a8d9f3331ee4da11ae42fff67eb0450534a932a5 Mon Sep 17 00:00:00 2001 From: Yi Ming Date: Sun, 6 Jul 2025 22:30:04 +0800 Subject: [PATCH 2/3] test(lsp): remove the deprecated `feed_command` --- test/functional/plugin/lsp/semantic_tokens_spec.lua | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/test/functional/plugin/lsp/semantic_tokens_spec.lua b/test/functional/plugin/lsp/semantic_tokens_spec.lua index 4baedb3ece..ee8db0215c 100644 --- a/test/functional/plugin/lsp/semantic_tokens_spec.lua +++ b/test/functional/plugin/lsp/semantic_tokens_spec.lua @@ -8,7 +8,6 @@ local dedent = t.dedent local eq = t.eq local exec_lua = n.exec_lua local feed = n.feed -local feed_command = n.feed_command local insert = n.insert local matches = t.matches local api = n.api @@ -475,8 +474,8 @@ describe('semantic token highlighting', function() | ]], } - feed_command('%s/int x/int x()/') - feed_command('noh') + feed(':%s/int x/int x()/') + feed(':noh') screen:expect { grid = [[ #include | @@ -720,8 +719,8 @@ describe('semantic token highlighting', function() | ]], } - feed_command('%s/int x/int x()/') - feed_command('noh') + feed(':%s/int x/int x()/') + feed(':noh') -- the highlights don't change because our fake server sent the exact -- same result for the same method (the full request). "x" would have From 7e8aa0585ec6ebac2dc1d81bf6ead75c61564bc4 Mon Sep 17 00:00:00 2001 From: Yi Ming Date: Mon, 7 Jul 2025 11:58:44 +0800 Subject: [PATCH 3/3] refactor(lsp): rename vim.lsp.semantic_tokens start/stop to enable() --- runtime/doc/deprecated.txt | 2 + runtime/doc/lsp.txt | 52 +++++------ runtime/doc/news.txt | 2 + runtime/lua/vim/lsp/client.lua | 2 +- runtime/lua/vim/lsp/semantic_tokens.lua | 86 +++++++++++++++---- .../plugin/lsp/semantic_tokens_spec.lua | 61 ++----------- 6 files changed, 103 insertions(+), 102 deletions(-) diff --git a/runtime/doc/deprecated.txt b/runtime/doc/deprecated.txt index f235698cda..ffe7f166bd 100644 --- a/runtime/doc/deprecated.txt +++ b/runtime/doc/deprecated.txt @@ -35,6 +35,8 @@ LSP `vim.wo.conceallevel = 2`. • *vim.lsp.log.should_log()* Use |vim.lsp.log.set_format_func()| instead and return `nil` to omit entries from the logfile. +• *vim.lsp.semantic_tokens.start()* Use `vim.lsp.semantic_tokens.enable(true)` instead +• *vim.lsp.semantic_tokens.stop()* Use `vim.lsp.semantic_tokens.enable(false)` instead LUA diff --git a/runtime/doc/lsp.txt b/runtime/doc/lsp.txt index a9ba6b988f..aa398138f4 100644 --- a/runtime/doc/lsp.txt +++ b/runtime/doc/lsp.txt @@ -2162,11 +2162,25 @@ is_enabled({filter}) *vim.lsp.inlay_hint.is_enabled()* ============================================================================== Lua module: vim.lsp.semantic_tokens *lsp-semantic_tokens* +enable({enable}, {filter}) *vim.lsp.semantic_tokens.enable()* + Enables or disables semantic tokens for the {filter}ed scope. + + To "toggle", pass the inverse of `is_enabled()`: >lua + vim.lsp.semantic_tokens.enable(not vim.lsp.semantic_tokens.is_enabled()) +< + + Parameters: ~ + • {enable} (`boolean?`) true/nil to enable, false to disable + • {filter} (`table?`) A table with the following fields: + • {bufnr}? (`integer`) Buffer number, or 0 for current + buffer, or nil for all. + • {client_id}? (`integer`) Client ID, or nil for all + force_refresh({bufnr}) *vim.lsp.semantic_tokens.force_refresh()* Force a refresh of all semantic tokens Only has an effect if the buffer is currently active for semantic token - highlighting (|vim.lsp.semantic_tokens.start()| has been called for it) + highlighting (|vim.lsp.semantic_tokens.enable()| has been called for it) Parameters: ~ • {bufnr} (`integer?`) filter by buffer. All buffers if nil, current @@ -2215,38 +2229,14 @@ highlight_token({token}, {bufnr}, {client_id}, {hl_group}, {opts}) `vim.hl.priorities.semantic_tokens + 3`) Priority for the applied extmark. -start({bufnr}, {client_id}, {opts}) *vim.lsp.semantic_tokens.start()* - Start the semantic token highlighting engine for the given buffer with the - given client. The client must already be attached to the buffer. - - NOTE: This is currently called automatically by - |vim.lsp.buf_attach_client()|. To opt-out of semantic highlighting with a - server that supports it, you can delete the semanticTokensProvider table - from the {server_capabilities} of your client in your |LspAttach| callback - or your configuration's `on_attach` callback: >lua - client.server_capabilities.semanticTokensProvider = nil -< +is_enabled({filter}) *vim.lsp.semantic_tokens.is_enabled()* + Query whether semantic tokens is enabled in the {filter}ed scope Parameters: ~ - • {bufnr} (`integer`) Buffer number, or `0` for current buffer - • {client_id} (`integer`) The ID of the |vim.lsp.Client| - • {opts} (`table?`) Optional keyword arguments - • debounce (integer, default: 200): Debounce token - requests to the server by the given number in - milliseconds - -stop({bufnr}, {client_id}) *vim.lsp.semantic_tokens.stop()* - Stop the semantic token highlighting engine for the given buffer with the - given client. - - NOTE: This is automatically called by a |LspDetach| autocmd that is set up - as part of `start()`, so you should only need this function to manually - disengage the semantic token engine without fully detaching the LSP client - from the buffer. - - Parameters: ~ - • {bufnr} (`integer`) Buffer number, or `0` for current buffer - • {client_id} (`integer`) The ID of the |vim.lsp.Client| + • {filter} (`table?`) A table with the following fields: + • {bufnr}? (`integer`) Buffer number, or 0 for current + buffer, or nil for all. + • {client_id}? (`integer`) Client ID, or nil for all ============================================================================== diff --git a/runtime/doc/news.txt b/runtime/doc/news.txt index 9db0004143..5fe244270d 100644 --- a/runtime/doc/news.txt +++ b/runtime/doc/news.txt @@ -84,6 +84,8 @@ LSP • `root_markers` in |vim.lsp.Config| can now be ordered by priority. • The function set with |vim.lsp.log.set_format_func()| is now given all arguments corresponding to a log entry instead of the individual arguments. +• `vim.lsp.semantic_tokens.start/stop` now renamed to + `vim.lsp.semantic_tokens.enable` LUA diff --git a/runtime/lua/vim/lsp/client.lua b/runtime/lua/vim/lsp/client.lua index df3d688606..9a1b78c948 100644 --- a/runtime/lua/vim/lsp/client.lua +++ b/runtime/lua/vim/lsp/client.lua @@ -1077,7 +1077,7 @@ function Client:on_attach(bufnr) -- opt-out (deleting the semanticTokensProvider from capabilities) vim.schedule(function() if vim.tbl_get(self.server_capabilities, 'semanticTokensProvider', 'full') then - lsp.semantic_tokens.start(bufnr, self.id) + lsp.semantic_tokens._start(bufnr, self.id) end if vim.tbl_get(self.server_capabilities, 'foldingRangeProvider') then lsp._folding_range._setup(bufnr) diff --git a/runtime/lua/vim/lsp/semantic_tokens.lua b/runtime/lua/vim/lsp/semantic_tokens.lua index 8b34cf9d71..5246f93ef7 100644 --- a/runtime/lua/vim/lsp/semantic_tokens.lua +++ b/runtime/lua/vim/lsp/semantic_tokens.lua @@ -7,6 +7,8 @@ local uv = vim.uv local Capability = require('vim.lsp._capability') +local M = {} + --- @class (private) STTokenRange --- @field line integer line number 0-based --- @field start_col integer start column 0-based @@ -194,11 +196,13 @@ function STHighlighter:new(bufnr) if not highlighter then return true end - highlighter:on_change() + if M.is_enabled({ bufnr = buf }) then + highlighter:on_change() + end end, on_reload = function(_, buf) local highlighter = STHighlighter.active[buf] - if highlighter then + if highlighter and M.is_enabled({ bufnr = bufnr }) then highlighter:reset() highlighter:send_request() end @@ -209,7 +213,9 @@ function STHighlighter:new(bufnr) buffer = self.bufnr, group = self.augroup, callback = function() - self:send_request() + if M.is_enabled({ bufnr = bufnr }) then + self:send_request() + end end, }) @@ -582,7 +588,25 @@ function STHighlighter:reset_timer() end end -local M = {} +---@param bufnr (integer) Buffer number, or `0` for current buffer +---@param client_id (integer) The ID of the |vim.lsp.Client| +---@param debounce? (integer) (default: 200): Debounce token requests +--- to the server by the given number in milliseconds +function M._start(bufnr, client_id, debounce) + local highlighter = STHighlighter.active[bufnr] + + if not highlighter then + highlighter = STHighlighter:new(bufnr) + highlighter.debounce = debounce or 200 + else + highlighter.debounce = debounce or highlighter.debounce + end + + highlighter:on_attach(client_id) + if M.is_enabled({ bufnr = bufnr }) then + highlighter:send_request() + end +end --- Start the semantic token highlighting engine for the given buffer with the --- given client. The client must already be attached to the buffer. @@ -597,12 +621,14 @@ local M = {} --- client.server_capabilities.semanticTokensProvider = nil --- ``` --- +---@deprecated ---@param bufnr (integer) Buffer number, or `0` for current buffer ---@param client_id (integer) The ID of the |vim.lsp.Client| ---@param opts? (table) Optional keyword arguments --- - debounce (integer, default: 200): Debounce token requests --- to the server by the given number in milliseconds function M.start(bufnr, client_id, opts) + vim.deprecate('vim.lsp.semantic_tokens.start', 'vim.lsp.semantic_tokens.enable(true)', '0.13.0') vim.validate('bufnr', bufnr, 'number') vim.validate('client_id', client_id, 'number') @@ -633,17 +659,7 @@ function M.start(bufnr, client_id, opts) return end - local highlighter = STHighlighter.active[bufnr] - - if not highlighter then - highlighter = STHighlighter:new(bufnr) - highlighter.debounce = opts.debounce or 200 - else - highlighter.debounce = math.max(highlighter.debounce, opts.debounce or 200) - end - - highlighter:on_attach(client_id) - highlighter:send_request() + M._start(bufnr, client_id, opts.debounce) end --- Stop the semantic token highlighting engine for the given buffer with the @@ -653,9 +669,11 @@ end --- of `start()`, so you should only need this function to manually disengage the semantic --- token engine without fully detaching the LSP client from the buffer. --- +---@deprecated ---@param bufnr (integer) Buffer number, or `0` for current buffer ---@param client_id (integer) The ID of the |vim.lsp.Client| function M.stop(bufnr, client_id) + vim.deprecate('vim.lsp.semantic_tokens.stop', 'vim.lsp.semantic_tokens.enable(false)', '0.13.0') vim.validate('bufnr', bufnr, 'number') vim.validate('client_id', client_id, 'number') @@ -673,6 +691,37 @@ function M.stop(bufnr, client_id) end end +--- Query whether semantic tokens is enabled in the {filter}ed scope +---@param filter? vim.lsp.enable.Filter +function M.is_enabled(filter) + return util._is_enabled('semantic_tokens', filter) +end + +--- Enables or disables semantic tokens for the {filter}ed scope. +--- +--- To "toggle", pass the inverse of `is_enabled()`: +--- +--- ```lua +--- vim.lsp.semantic_tokens.enable(not vim.lsp.semantic_tokens.is_enabled()) +--- ``` +--- +---@param enable? boolean true/nil to enable, false to disable +---@param filter? vim.lsp.enable.Filter +function M.enable(enable, filter) + util._enable('semantic_tokens', enable, filter) + + for _, bufnr in ipairs(api.nvim_list_bufs()) do + local highlighter = STHighlighter.active[bufnr] + if highlighter then + if M.is_enabled({ bufnr = bufnr }) then + highlighter:send_request() + else + highlighter:reset() + end + end + end +end + --- @nodoc --- @class STTokenRangeInspect : STTokenRange --- @field client_id integer @@ -736,7 +785,7 @@ end --- Force a refresh of all semantic tokens --- --- Only has an effect if the buffer is currently active for semantic token ---- highlighting (|vim.lsp.semantic_tokens.start()| has been called for it) +--- highlighting (|vim.lsp.semantic_tokens.enable()| has been called for it) --- ---@param bufnr (integer|nil) filter by buffer. All buffers if nil, current --- buffer if 0 @@ -748,7 +797,7 @@ function M.force_refresh(bufnr) for _, buffer in ipairs(buffers) do local highlighter = STHighlighter.active[buffer] - if highlighter then + if highlighter and M.is_enabled({ bufnr = bufnr }) then highlighter:reset() highlighter:send_request() end @@ -831,4 +880,7 @@ api.nvim_set_decoration_provider(namespace, { ---@private M.__STHighlighter = STHighlighter +-- Semantic tokens is enabled by default +util._enable('semantic_tokens', true) + return M diff --git a/test/functional/plugin/lsp/semantic_tokens_spec.lua b/test/functional/plugin/lsp/semantic_tokens_spec.lua index ee8db0215c..928ad3571b 100644 --- a/test/functional/plugin/lsp/semantic_tokens_spec.lua +++ b/test/functional/plugin/lsp/semantic_tokens_spec.lua @@ -9,7 +9,6 @@ local eq = t.eq local exec_lua = n.exec_lua local feed = n.feed local insert = n.insert -local matches = t.matches local api = n.api local clear_notrace = t_lsp.clear_notrace @@ -254,10 +253,10 @@ describe('semantic token highlighting', function() end) it( - 'buffer is highlighted and unhighlighted when semantic token highlighting is started and stopped', + 'buffer is highlighted and unhighlighted when semantic token highlighting is enabled and disabled', function() local bufnr = n.api.nvim_get_current_buf() - local client_id = exec_lua(function() + exec_lua(function() vim.api.nvim_win_set_buf(0, bufnr) return vim.lsp.start({ name = 'dummy', cmd = _G.server.cmd }) end) @@ -267,7 +266,7 @@ describe('semantic token highlighting', function() exec_lua(function() --- @diagnostic disable-next-line:duplicate-set-field vim.notify = function() end - vim.lsp.semantic_tokens.stop(bufnr, client_id) + vim.lsp.semantic_tokens.enable(false) end) screen:expect { @@ -290,7 +289,7 @@ describe('semantic token highlighting', function() } exec_lua(function() - vim.lsp.semantic_tokens.start(bufnr, client_id) + vim.lsp.semantic_tokens.enable(true) end) screen:expect { @@ -315,7 +314,7 @@ describe('semantic token highlighting', function() ) it('highlights start and stop when using "0" for current buffer', function() - local client_id = exec_lua(function() + exec_lua(function() return vim.lsp.start({ name = 'dummy', cmd = _G.server.cmd }) end) @@ -324,7 +323,7 @@ describe('semantic token highlighting', function() exec_lua(function() --- @diagnostic disable-next-line:duplicate-set-field vim.notify = function() end - vim.lsp.semantic_tokens.stop(0, client_id) + vim.lsp.semantic_tokens.enable(false, { bufnr = 0 }) end) screen:expect { @@ -347,7 +346,7 @@ describe('semantic token highlighting', function() } exec_lua(function() - vim.lsp.semantic_tokens.start(0, client_id) + vim.lsp.semantic_tokens.enable(true, { bufnr = 0 }) end) screen:expect { @@ -495,36 +494,6 @@ describe('semantic token highlighting', function() } end) - it('prevents starting semantic token highlighting with invalid conditions', function() - local client_id = exec_lua(function() - _G.notifications = {} - --- @diagnostic disable-next-line:duplicate-set-field - vim.notify = function(...) - table.insert(_G.notifications, 1, { ... }) - end - return vim.lsp.start({ name = 'dummy', cmd = _G.server.cmd }, { attach = false }) - end) - eq(false, exec_lua('return vim.lsp.buf_is_attached(0, ...)', client_id)) - - insert(text) - - matches( - '%[LSP%] Client with id %d not attached to buffer %d', - exec_lua(function() - vim.lsp.semantic_tokens.start(0, client_id) - return _G.notifications[1][1] - end) - ) - - matches( - '%[LSP%] No client with id %d', - exec_lua(function() - vim.lsp.semantic_tokens.start(0, client_id + 1) - return _G.notifications[1][1] - end) - ) - end) - it( 'opt-out: does not activate semantic token highlighting if disabled in client attach', function() @@ -561,19 +530,6 @@ describe('semantic token highlighting', function() ]], } - eq( - '[LSP] Server does not support semantic tokens', - exec_lua(function() - local notifications = {} - --- @diagnostic disable-next-line:duplicate-set-field - vim.notify = function(...) - table.insert(notifications, 1, { ... }) - end - vim.lsp.semantic_tokens.start(0, client_id) - return notifications[1][1] - end) - ) - screen:expect { grid = [[ #include | @@ -1598,8 +1554,7 @@ int main() -- speed up vim.api.nvim_buf_set_lines calls by changing debounce to 10 for these tests vim.schedule(function() - vim.lsp.semantic_tokens.stop(bufnr, client_id) - vim.lsp.semantic_tokens.start(bufnr, client_id, { debounce = 10 }) + vim.lsp.semantic_tokens._start(bufnr, client_id, 10) end) return client_id end, test.legend, test.response1, test.response2)