mirror of
https://github.com/neovim/neovim
synced 2025-07-16 09:11:51 +00:00
feat(lsp): incremental selection via "textDocument/selectionRange" #34011
Select outwards with "an" and inwards with "in" in Visual mode. Ranges are reset when leaving Visual mode.
This commit is contained in:
@ -81,7 +81,7 @@ the options are empty or were set by the builtin runtime (ftplugin) files. The
|
||||
options are not restored when the LSP client is stopped or detached.
|
||||
|
||||
GLOBAL DEFAULTS
|
||||
*grr* *gra* *grn* *gri* *i_CTRL-S*
|
||||
*grr* *gra* *grn* *gri* *i_CTRL-S* *an* *in*
|
||||
These GLOBAL keymaps are created unconditionally when Nvim starts:
|
||||
- "grn" is mapped in Normal mode to |vim.lsp.buf.rename()|
|
||||
- "gra" is mapped in Normal and Visual mode to |vim.lsp.buf.code_action()|
|
||||
@ -89,6 +89,8 @@ These GLOBAL keymaps are created unconditionally when Nvim starts:
|
||||
- "gri" is mapped in Normal mode to |vim.lsp.buf.implementation()|
|
||||
- "gO" is mapped in Normal mode to |vim.lsp.buf.document_symbol()|
|
||||
- CTRL-S is mapped in Insert mode to |vim.lsp.buf.signature_help()|
|
||||
- "an" and "in" are mapped in Visual mode to outer and inner incremental
|
||||
selections, respectively, using |vim.lsp.buf.selection_range()|
|
||||
|
||||
BUFFER-LOCAL DEFAULTS
|
||||
- 'omnifunc' is set to |vim.lsp.omnifunc()|, use |i_CTRL-X_CTRL-O| to trigger
|
||||
@ -1833,6 +1835,14 @@ 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()*
|
||||
Perform an incremental selection at the cursor position based on ranges
|
||||
given by the LSP. The `direction` parameter specifies whether the
|
||||
selection should head inward or outward.
|
||||
|
||||
Parameters: ~
|
||||
• {direction} (`'inner'|'outer'`)
|
||||
|
||||
signature_help({config}) *vim.lsp.buf.signature_help()*
|
||||
Displays signature information about the symbol under the cursor in a
|
||||
floating window.
|
||||
|
@ -167,6 +167,8 @@ LSP
|
||||
"resolving" it).
|
||||
• Support for `workspace/diagnostic`: |vim.lsp.buf.workspace_diagnostics()|
|
||||
https://microsoft.github.io/language-server-protocol/specifications/specification-current/#workspace_dagnostics
|
||||
• Incremental selection is now supported via `textDocument/selectionRange`.
|
||||
`an` selects outwards and `in` selects inwards.
|
||||
|
||||
LUA
|
||||
|
||||
|
@ -209,6 +209,14 @@ do
|
||||
vim.lsp.buf.implementation()
|
||||
end, { desc = 'vim.lsp.buf.implementation()' })
|
||||
|
||||
vim.keymap.set('x', 'an', function()
|
||||
vim.lsp.buf.selection_range('outer')
|
||||
end, { desc = "vim.lsp.buf.selection_range('outer')" })
|
||||
|
||||
vim.keymap.set('x', 'in', function()
|
||||
vim.lsp.buf.selection_range('inner')
|
||||
end, { desc = "vim.lsp.buf.selection_range('inner')" })
|
||||
|
||||
vim.keymap.set('n', 'gO', function()
|
||||
vim.lsp.buf.document_symbol()
|
||||
end, { desc = 'vim.lsp.buf.document_symbol()' })
|
||||
|
@ -1327,4 +1327,121 @@ function M.execute_command(command_params)
|
||||
lsp.buf_request(0, ms.workspace_executeCommand, command_params)
|
||||
end
|
||||
|
||||
---@type { index: integer, ranges: lsp.Range[] }?
|
||||
local selection_ranges = nil
|
||||
|
||||
---@param range lsp.Range
|
||||
local function select_range(range)
|
||||
local start_line = range.start.line + 1
|
||||
local end_line = range['end'].line + 1
|
||||
|
||||
local start_col = range.start.character
|
||||
local end_col = range['end'].character
|
||||
|
||||
-- If the selection ends at column 0, adjust the position to the end of the previous line.
|
||||
if end_col == 0 then
|
||||
end_line = end_line - 1
|
||||
local end_line_text = api.nvim_buf_get_lines(0, end_line - 1, end_line, true)[1]
|
||||
end_col = #end_line_text
|
||||
end
|
||||
|
||||
vim.fn.setpos("'<", { 0, start_line, start_col + 1, 0 })
|
||||
vim.fn.setpos("'>", { 0, end_line, end_col, 0 })
|
||||
vim.cmd.normal({ 'gv', bang = true })
|
||||
end
|
||||
|
||||
---@param range lsp.Range
|
||||
local function is_empty(range)
|
||||
return range.start.line == range['end'].line and range.start.character == range['end'].character
|
||||
end
|
||||
|
||||
--- Perform an incremental selection at the cursor position based on ranges given by the LSP. The
|
||||
--- `direction` parameter specifies whether the selection should head inward or outward.
|
||||
---
|
||||
--- @param direction 'inner' | 'outer'
|
||||
function M.selection_range(direction)
|
||||
if selection_ranges then
|
||||
local offset = direction == 'outer' and 1 or -1
|
||||
local new_index = selection_ranges.index + offset
|
||||
if new_index <= #selection_ranges.ranges and new_index >= 1 then
|
||||
selection_ranges.index = new_index
|
||||
end
|
||||
|
||||
select_range(selection_ranges.ranges[selection_ranges.index])
|
||||
return
|
||||
end
|
||||
|
||||
local method = ms.textDocument_selectionRange
|
||||
local client = lsp.get_clients({ method = method, bufnr = 0 })[1]
|
||||
if not client then
|
||||
vim.notify(lsp._unsupported_method(method), vim.log.levels.WARN)
|
||||
return
|
||||
end
|
||||
|
||||
local position_params = util.make_position_params(0, client.offset_encoding)
|
||||
|
||||
---@type lsp.SelectionRangeParams
|
||||
local params = {
|
||||
textDocument = position_params.textDocument,
|
||||
positions = { position_params.position },
|
||||
}
|
||||
|
||||
lsp.buf_request(
|
||||
0,
|
||||
ms.textDocument_selectionRange,
|
||||
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 reponse 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,
|
||||
})
|
||||
|
||||
if #ranges > 0 then
|
||||
selection_ranges = { index = 1, ranges = ranges }
|
||||
select_range(ranges[1])
|
||||
end
|
||||
end
|
||||
)
|
||||
end
|
||||
|
||||
return M
|
||||
|
@ -539,6 +539,9 @@ function protocol.make_client_capabilities()
|
||||
colorProvider = {
|
||||
dynamicRegistration = true,
|
||||
},
|
||||
selectionRange = {
|
||||
dynamicRegistration = false,
|
||||
},
|
||||
},
|
||||
workspace = {
|
||||
symbol = {
|
||||
|
Reference in New Issue
Block a user