commit e82aef2e22a57688dcc19a978cbe083349ad8a2a
parent 60c35cc4c7b713c27e8bfdd196cbee46cf050bbb
Author: Branden Call <54908229+brandencall@users.noreply.github.com>
Date: Mon, 24 Nov 2025 18:10:50 -0700
feat(lsp): incremental-selection operator-pending mode #36575
Problem:
LSP incremental selection provides default visual-mode keymaps for `an`
and `in`. Operator-pending mode is not supported, so `dan` and `can` do
not apply the operation.
Solution:
Modify selection_range() to be synchronous.
Add operator-pending mappings.
Diffstat:
3 files changed, 70 insertions(+), 59 deletions(-)
diff --git a/runtime/doc/lsp.txt b/runtime/doc/lsp.txt
@@ -83,7 +83,7 @@ These GLOBAL keymaps are created unconditionally when Nvim starts:
- "grt" is mapped to |vim.lsp.buf.type_definition()|
- "gO" is mapped to |vim.lsp.buf.document_symbol()|
- CTRL-S (Insert mode) is mapped to |vim.lsp.buf.signature_help()|
-- "an" and "in" (Visual mode) are mapped to outer and inner incremental
+- "an" and "in" (Visual and Operator-pending mode) are mapped to outer and inner incremental
selections, respectively, using |vim.lsp.buf.selection_range()|
BUFFER-LOCAL DEFAULTS
@@ -1512,13 +1512,16 @@ rename({new_name}, {opts}) *vim.lsp.buf.rename()*
ones where client.name matches this field.
• {bufnr}? (`integer`) (default: current buffer)
-selection_range({direction}) *vim.lsp.buf.selection_range()*
+ *vim.lsp.buf.selection_range()*
+selection_range({direction}, {timeout_ms})
Perform an incremental selection at the cursor position based on ranges
given by the LSP. The `direction` parameter specifies the number of times
to expand the selection. Negative values will shrink the selection.
Parameters: ~
- • {direction} (`integer`)
+ • {direction} (`integer`)
+ • {timeout_ms} (`integer?`) (default: `1000`) Maximum time
+ (milliseconds) to wait for a result.
signature_help({config}) *vim.lsp.buf.signature_help()*
Displays signature information about the symbol under the cursor in a
diff --git a/runtime/lua/vim/_defaults.lua b/runtime/lua/vim/_defaults.lua
@@ -227,6 +227,14 @@ do
vim.lsp.buf.selection_range(-vim.v.count1)
end, { desc = 'vim.lsp.buf.selection_range(-vim.v.count1)' })
+ vim.keymap.set('o', 'an', function()
+ vim.lsp.buf.selection_range(vim.v.count1, 1000)
+ end, { desc = 'vim.lsp.buf.selection_range(vim.v.count1, timeout_ms)' })
+
+ vim.keymap.set('o', 'in', function()
+ vim.lsp.buf.selection_range(-vim.v.count1, 1000)
+ end, { desc = 'vim.lsp.buf.selection_range(-vim.v.count1, timeout_ms)' })
+
vim.keymap.set('n', 'gO', function()
vim.lsp.buf.document_symbol()
end, { desc = 'vim.lsp.buf.document_symbol()' })
diff --git a/runtime/lua/vim/lsp/buf.lua b/runtime/lua/vim/lsp/buf.lua
@@ -1408,8 +1408,10 @@ end
--- will shrink the selection.
---
--- @param direction integer
-function M.selection_range(direction)
+--- @param timeout_ms integer? (default: `1000`) Maximum time (milliseconds) to wait for a result.
+function M.selection_range(direction, timeout_ms)
validate('direction', direction, 'number')
+ validate('timeout_ms', timeout_ms, 'number', true)
if selection_ranges then
local new_index = selection_ranges.index + direction
@@ -1434,63 +1436,61 @@ function M.selection_range(direction)
positions = { position_params.position },
}
- lsp.buf_request(
- 0,
- method,
- params,
- ---@param response lsp.SelectionRange[]?
- function(err, response)
- if err then
- lsp.log.error(err.code, err.message)
- return
- end
- if not response then
- return
- end
- -- We only requested one range, thus we get the first and only response here.
- response = response[1]
- local ranges = {} ---@type lsp.Range[]
- local lines = api.nvim_buf_get_lines(0, 0, -1, false)
-
- -- Populate the list of ranges from the given request.
- while response do
- local range = response.range
- if not is_empty(range) then
- local start_line = range.start.line
- local end_line = range['end'].line
- range.start.character = vim.str_byteindex(
- lines[start_line + 1] or '',
- client.offset_encoding,
- range.start.character,
- false
- )
- range['end'].character = vim.str_byteindex(
- lines[end_line + 1] or '',
- client.offset_encoding,
- range['end'].character,
- false
- )
- ranges[#ranges + 1] = range
- end
- response = response.parent
- end
-
- -- Clear selection ranges when leaving visual mode.
- api.nvim_create_autocmd('ModeChanged', {
- once = true,
- pattern = 'v*:*',
- callback = function()
- selection_ranges = nil
- end,
- })
+ timeout_ms = timeout_ms or 1000
+ local result, err = lsp.buf_request_sync(0, method, params, timeout_ms)
+ if err then
+ lsp.log.error('selectionRange request failed: ' .. err)
+ return
+ end
+ if not result or not result[client.id] or not result[client.id].result then
+ return
+ end
+ if result[client.id].error then
+ lsp.log.error(result[client.id].error.code, result[client.id].error.message)
+ end
- if #ranges > 0 then
- local index = math.min(#ranges, math.max(1, direction))
- selection_ranges = { index = index, ranges = ranges }
- select_range(ranges[index])
- end
+ -- We only requested one range, thus we get the first and only reponse here.
+ local response = assert(result[client.id].result[1]) ---@type lsp.SelectionRange
+ local ranges = {} ---@type lsp.Range[]
+ local lines = api.nvim_buf_get_lines(0, 0, -1, false)
+
+ -- Populate the list of ranges from the given request.
+ while response do
+ local range = response.range
+ if not is_empty(range) then
+ local start_line = range.start.line
+ local end_line = range['end'].line
+ range.start.character = vim.str_byteindex(
+ lines[start_line + 1] or '',
+ client.offset_encoding,
+ range.start.character,
+ false
+ )
+ range['end'].character = vim.str_byteindex(
+ lines[end_line + 1] or '',
+ client.offset_encoding,
+ range['end'].character,
+ false
+ )
+ ranges[#ranges + 1] = range
end
- )
+ response = response.parent
+ end
+
+ -- Clear selection ranges when leaving visual mode.
+ api.nvim_create_autocmd('ModeChanged', {
+ once = true,
+ pattern = 'v*:*',
+ callback = function()
+ selection_ranges = nil
+ end,
+ })
+
+ if #ranges > 0 then
+ local index = math.min(#ranges, math.max(1, direction))
+ selection_ranges = { index = index, ranges = ranges }
+ select_range(ranges[index])
+ end
end
return M