Skip to content

Commit 55187de

Browse files
authored
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
1 parent d14d308 commit 55187de

File tree

4 files changed

+123
-43
lines changed

4 files changed

+123
-43
lines changed

runtime/doc/lsp.txt

+10-2
Original file line numberDiff line numberDiff line change
@@ -1216,13 +1216,21 @@ remove_workspace_folder({workspace_folder})
12161216
{path} is not provided, the user will be prompted for a path
12171217
using |input()|.
12181218

1219-
rename({new_name}) *vim.lsp.buf.rename()*
1219+
rename({new_name}, {options}) *vim.lsp.buf.rename()*
12201220
Renames all references to the symbol under the cursor.
12211221

12221222
Parameters: ~
1223-
{new_name} (string) If not provided, the user will be
1223+
{new_name} string|nil If not provided, the user will be
12241224
prompted for a new name using
12251225
|vim.ui.input()|.
1226+
{options} table|nil additional options
1227+
• filter (function|nil): Predicate to filter
1228+
clients used for rename. Receives the
1229+
attached clients as argument and must return
1230+
a list of clients.
1231+
• name (string|nil): Restrict clients used for
1232+
rename to ones where client.name matches
1233+
this field.
12261234

12271235
server_ready() *vim.lsp.buf.server_ready()*
12281236
Checks whether the language servers attached to the current

runtime/lua/vim/lsp/buf.lua

+113-35
Original file line numberDiff line numberDiff line change
@@ -359,50 +359,128 @@ end
359359

360360
--- Renames all references to the symbol under the cursor.
361361
---
362-
---@param new_name (string) If not provided, the user will be prompted for a new
363-
---name using |vim.ui.input()|.
364-
function M.rename(new_name)
365-
local opts = {
366-
prompt = "New Name: "
367-
}
362+
---@param new_name string|nil If not provided, the user will be prompted for a new
363+
--- name using |vim.ui.input()|.
364+
---@param options table|nil additional options
365+
--- - filter (function|nil):
366+
--- Predicate to filter clients used for rename.
367+
--- Receives the attached clients as argument and must return a list of
368+
--- clients.
369+
--- - name (string|nil):
370+
--- Restrict clients used for rename to ones where client.name matches
371+
--- this field.
372+
function M.rename(new_name, options)
373+
options = options or {}
374+
local bufnr = options.bufnr or vim.api.nvim_get_current_buf()
375+
local clients = vim.lsp.buf_get_clients(bufnr)
368376

369-
---@private
370-
local function on_confirm(input)
371-
if not (input and #input > 0) then return end
372-
local params = util.make_position_params()
373-
params.newName = input
374-
request('textDocument/rename', params)
377+
if options.filter then
378+
clients = options.filter(clients)
379+
elseif options.name then
380+
clients = vim.tbl_filter(
381+
function(client) return client.name == options.name end,
382+
clients
383+
)
384+
end
385+
386+
if #clients == 0 then
387+
vim.notify("[LSP] Rename request failed, no matching language servers.")
375388
end
376389

390+
local win = vim.api.nvim_get_current_win()
391+
392+
-- Compute early to account for cursor movements after going async
393+
local cword = vfn.expand('<cword>')
394+
377395
---@private
378-
local function prepare_rename(err, result)
379-
if err == nil and result == nil then
380-
vim.notify('nothing to rename', vim.log.levels.INFO)
396+
local function get_text_at_range(range)
397+
return vim.api.nvim_buf_get_text(
398+
bufnr,
399+
range.start.line,
400+
range.start.character,
401+
range['end'].line,
402+
range['end'].character,
403+
{}
404+
)[1]
405+
end
406+
407+
local try_use_client
408+
try_use_client = function(idx, client)
409+
if not client then
381410
return
382411
end
383-
if result and result.placeholder then
384-
opts.default = result.placeholder
385-
if not new_name then npcall(vim.ui.input, opts, on_confirm) end
386-
elseif result and result.start and result['end'] and
387-
result.start.line == result['end'].line then
388-
local line = vfn.getline(result.start.line+1)
389-
local start_char = result.start.character+1
390-
local end_char = result['end'].character
391-
opts.default = string.sub(line, start_char, end_char)
392-
if not new_name then npcall(vim.ui.input, opts, on_confirm) end
412+
413+
---@private
414+
local function rename(name)
415+
local params = util.make_position_params(win, client.offset_encoding)
416+
params.newName = name
417+
local handler = client.handlers['textDocument/rename'] or vim.lsp.handlers['textDocument/rename']
418+
client.request('textDocument/rename', params, function(...)
419+
handler(...)
420+
try_use_client(next(clients, idx))
421+
end, bufnr)
422+
end
423+
424+
if client.supports_method("textDocument/prepareRename") then
425+
local params = util.make_position_params(win, client.offset_encoding)
426+
client.request('textDocument/prepareRename', params, function(err, result)
427+
if err or result == nil then
428+
if next(clients, idx) then
429+
try_use_client(next(clients, idx))
430+
else
431+
local msg = err and ('Error on prepareRename: ' .. (err.message or '')) or 'Nothing to rename'
432+
vim.notify(msg, vim.log.levels.INFO)
433+
end
434+
return
435+
end
436+
437+
if new_name then
438+
rename(new_name)
439+
return
440+
end
441+
442+
local prompt_opts = {
443+
prompt = "New Name: "
444+
}
445+
-- result: Range | { range: Range, placeholder: string }
446+
if result.placeholder then
447+
prompt_opts.default = result.placeholder
448+
elseif result.start then
449+
prompt_opts.default = get_text_at_range(result)
450+
elseif result.range then
451+
prompt_opts.default = get_text_at_range(result.range)
452+
else
453+
prompt_opts.default = cword
454+
end
455+
vim.ui.input(prompt_opts, function(input)
456+
if not input or #input == 0 then
457+
return
458+
end
459+
rename(input)
460+
end)
461+
end, bufnr)
462+
elseif client.supports_method("textDocument/rename") then
463+
if new_name then
464+
rename(new_name)
465+
return
466+
end
467+
468+
local prompt_opts = {
469+
prompt = "New Name: ",
470+
default = cword
471+
}
472+
vim.ui.input(prompt_opts, function(input)
473+
if not input or #input == 0 then
474+
return
475+
end
476+
rename(input)
477+
end)
393478
else
394-
-- fallback to guessing symbol using <cword>
395-
--
396-
-- this can happen if the language server does not support prepareRename,
397-
-- returns an unexpected response, or requests for "default behavior"
398-
--
399-
-- see https://microsoft.github.io/language-server-protocol/specification#textDocument_prepareRename
400-
opts.default = vfn.expand('<cword>')
401-
if not new_name then npcall(vim.ui.input, opts, on_confirm) end
479+
vim.notify('Client ' .. client.id .. '/' .. client.name .. ' has no rename capability')
402480
end
403-
if new_name then on_confirm(new_name) end
404481
end
405-
request('textDocument/prepareRename', util.make_position_params(), prepare_rename)
482+
483+
try_use_client(next(clients))
406484
end
407485

408486
--- Lists all the references to the symbol under the cursor in the quickfix window.

test/functional/fixtures/fake-lsp-server.lua

-4
Original file line numberDiff line numberDiff line change
@@ -222,10 +222,6 @@ function tests.prepare_rename_error()
222222
expect_request('textDocument/prepareRename', function()
223223
return {}, nil
224224
end)
225-
expect_request('textDocument/rename', function(params)
226-
assert_eq(params.newName, 'renameto')
227-
return nil, nil
228-
end)
229225
notify('shutdown')
230226
end;
231227
}

test/functional/plugin/lsp_spec.lua

-2
Original file line numberDiff line numberDiff line change
@@ -2636,10 +2636,8 @@ describe('LSP', function()
26362636
name = "prepare_rename_error",
26372637
expected_handlers = {
26382638
{NIL, {}, {method="shutdown", client_id=1}};
2639-
{NIL, NIL, {method="textDocument/rename", client_id=1, bufnr=1}};
26402639
{NIL, {}, {method="start", client_id=1}};
26412640
},
2642-
expected_text = "two", -- see test case
26432641
},
26442642
}) do
26452643
it(test.it, function()

0 commit comments

Comments
 (0)