commit 5c4f9b05fa4b73ff64a16535af2f0021f83ee0ad
parent 4778a4c201793141329cf58bb52a6fcc1d9c9eb1
Author: Justin M. Keyes <justinkz@gmail.com>
Date: Sat, 12 Jul 2025 23:44:21 -0400
Merge #34797 refactor(lsp): enable()/is_enabled()
Diffstat:
9 files changed, 195 insertions(+), 116 deletions(-)
diff --git 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
@@ -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
-<
-
- 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.
+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|
+ • {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
@@ -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/_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/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
@@ -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/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
@@ -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)
diff --git a/test/functional/plugin/lsp/semantic_tokens_spec.lua b/test/functional/plugin/lsp/semantic_tokens_spec.lua
@@ -8,9 +8,7 @@ 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
local clear_notrace = t_lsp.clear_notrace
@@ -255,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)
@@ -268,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 {
@@ -291,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 {
@@ -316,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)
@@ -325,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 {
@@ -348,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 {
@@ -475,8 +473,8 @@ describe('semantic token highlighting', function()
|
]],
}
- feed_command('%s/int x/int x()/')
- feed_command('noh')
+ feed(':%s/int x/int x()/<CR>')
+ feed(':noh<CR>')
screen:expect {
grid = [[
#include <iostream> |
@@ -496,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()
@@ -562,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 <iostream> |
@@ -720,8 +675,8 @@ describe('semantic token highlighting', function()
|
]],
}
- feed_command('%s/int x/int x()/')
- feed_command('noh')
+ feed(':%s/int x/int x()/<CR>')
+ feed(':noh<CR>')
-- the highlights don't change because our fake server sent the exact
-- same result for the same method (the full request). "x" would have
@@ -1599,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)