fix(lsp): fix rename capability checks and multi client support (#18441)

Adds filter and id options to filter the client to use for rename.
Similar to the recently added `format` function.

rename will use all matching clients one after another and can handle a
mix of prepareRename/rename support. Also ensures the right
`offset_encoding` is used for the `make_position_params` calls
This commit is contained in:
Mathias Fußenegger
2022-05-05 23:56:00 +02:00
committed by GitHub
parent d14d308ce8
commit 55187de115
4 changed files with 124 additions and 44 deletions

View File

@@ -1216,13 +1216,21 @@ remove_workspace_folder({workspace_folder})
{path} is not provided, the user will be prompted for a path {path} is not provided, the user will be prompted for a path
using |input()|. using |input()|.
rename({new_name}) *vim.lsp.buf.rename()* rename({new_name}, {options}) *vim.lsp.buf.rename()*
Renames all references to the symbol under the cursor. Renames all references to the symbol under the cursor.
Parameters: ~ Parameters: ~
{new_name} (string) If not provided, the user will be {new_name} string|nil If not provided, the user will be
prompted for a new name using prompted for a new name using
|vim.ui.input()|. |vim.ui.input()|.
{options} table|nil additional options
• filter (function|nil): Predicate to filter
clients used for rename. Receives the
attached clients as argument and must return
a list of clients.
• name (string|nil): Restrict clients used for
rename to ones where client.name matches
this field.
server_ready() *vim.lsp.buf.server_ready()* server_ready() *vim.lsp.buf.server_ready()*
Checks whether the language servers attached to the current Checks whether the language servers attached to the current

View File

@@ -359,50 +359,128 @@ end
--- Renames all references to the symbol under the cursor. --- Renames all references to the symbol under the cursor.
--- ---
---@param new_name (string) If not provided, the user will be prompted for a new ---@param new_name string|nil If not provided, the user will be prompted for a new
--- name using |vim.ui.input()|. --- name using |vim.ui.input()|.
function M.rename(new_name) ---@param options table|nil additional options
local opts = { --- - filter (function|nil):
prompt = "New Name: " --- Predicate to filter clients used for rename.
} --- Receives the attached clients as argument and must return a list of
--- clients.
--- - name (string|nil):
--- Restrict clients used for rename to ones where client.name matches
--- this field.
function M.rename(new_name, options)
options = options or {}
local bufnr = options.bufnr or vim.api.nvim_get_current_buf()
local clients = vim.lsp.buf_get_clients(bufnr)
---@private if options.filter then
local function on_confirm(input) clients = options.filter(clients)
if not (input and #input > 0) then return end elseif options.name then
local params = util.make_position_params() clients = vim.tbl_filter(
params.newName = input function(client) return client.name == options.name end,
request('textDocument/rename', params) clients
)
end end
if #clients == 0 then
vim.notify("[LSP] Rename request failed, no matching language servers.")
end
local win = vim.api.nvim_get_current_win()
-- Compute early to account for cursor movements after going async
local cword = vfn.expand('<cword>')
---@private ---@private
local function prepare_rename(err, result) local function get_text_at_range(range)
if err == nil and result == nil then return vim.api.nvim_buf_get_text(
vim.notify('nothing to rename', vim.log.levels.INFO) bufnr,
range.start.line,
range.start.character,
range['end'].line,
range['end'].character,
{}
)[1]
end
local try_use_client
try_use_client = function(idx, client)
if not client then
return return
end end
if result and result.placeholder then
opts.default = result.placeholder ---@private
if not new_name then npcall(vim.ui.input, opts, on_confirm) end local function rename(name)
elseif result and result.start and result['end'] and local params = util.make_position_params(win, client.offset_encoding)
result.start.line == result['end'].line then params.newName = name
local line = vfn.getline(result.start.line+1) local handler = client.handlers['textDocument/rename'] or vim.lsp.handlers['textDocument/rename']
local start_char = result.start.character+1 client.request('textDocument/rename', params, function(...)
local end_char = result['end'].character handler(...)
opts.default = string.sub(line, start_char, end_char) try_use_client(next(clients, idx))
if not new_name then npcall(vim.ui.input, opts, on_confirm) end end, bufnr)
end
if client.supports_method("textDocument/prepareRename") then
local params = util.make_position_params(win, client.offset_encoding)
client.request('textDocument/prepareRename', params, function(err, result)
if err or result == nil then
if next(clients, idx) then
try_use_client(next(clients, idx))
else else
-- fallback to guessing symbol using <cword> local msg = err and ('Error on prepareRename: ' .. (err.message or '')) or 'Nothing to rename'
-- vim.notify(msg, vim.log.levels.INFO)
-- this can happen if the language server does not support prepareRename,
-- returns an unexpected response, or requests for "default behavior"
--
-- see https://microsoft.github.io/language-server-protocol/specification#textDocument_prepareRename
opts.default = vfn.expand('<cword>')
if not new_name then npcall(vim.ui.input, opts, on_confirm) end
end end
if new_name then on_confirm(new_name) end return
end end
request('textDocument/prepareRename', util.make_position_params(), prepare_rename)
if new_name then
rename(new_name)
return
end
local prompt_opts = {
prompt = "New Name: "
}
-- result: Range | { range: Range, placeholder: string }
if result.placeholder then
prompt_opts.default = result.placeholder
elseif result.start then
prompt_opts.default = get_text_at_range(result)
elseif result.range then
prompt_opts.default = get_text_at_range(result.range)
else
prompt_opts.default = cword
end
vim.ui.input(prompt_opts, function(input)
if not input or #input == 0 then
return
end
rename(input)
end)
end, bufnr)
elseif client.supports_method("textDocument/rename") then
if new_name then
rename(new_name)
return
end
local prompt_opts = {
prompt = "New Name: ",
default = cword
}
vim.ui.input(prompt_opts, function(input)
if not input or #input == 0 then
return
end
rename(input)
end)
else
vim.notify('Client ' .. client.id .. '/' .. client.name .. ' has no rename capability')
end
end
try_use_client(next(clients))
end end
--- Lists all the references to the symbol under the cursor in the quickfix window. --- Lists all the references to the symbol under the cursor in the quickfix window.

View File

@@ -222,10 +222,6 @@ function tests.prepare_rename_error()
expect_request('textDocument/prepareRename', function() expect_request('textDocument/prepareRename', function()
return {}, nil return {}, nil
end) end)
expect_request('textDocument/rename', function(params)
assert_eq(params.newName, 'renameto')
return nil, nil
end)
notify('shutdown') notify('shutdown')
end; end;
} }

View File

@@ -2636,10 +2636,8 @@ describe('LSP', function()
name = "prepare_rename_error", name = "prepare_rename_error",
expected_handlers = { expected_handlers = {
{NIL, {}, {method="shutdown", client_id=1}}; {NIL, {}, {method="shutdown", client_id=1}};
{NIL, NIL, {method="textDocument/rename", client_id=1, bufnr=1}};
{NIL, {}, {method="start", client_id=1}}; {NIL, {}, {method="start", client_id=1}};
}, },
expected_text = "two", -- see test case
}, },
}) do }) do
it(test.it, function() it(test.it, function()