Skip to content

Commit

Permalink
fix(lsp): fix rename capability checks and multi client support (neov…
Browse files Browse the repository at this point in the history
…im#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
  • Loading branch information
mfussenegger authored and dmitmel committed May 18, 2022
1 parent 23899bc commit 74494a1
Show file tree
Hide file tree
Showing 4 changed files with 123 additions and 43 deletions.
12 changes: 10 additions & 2 deletions runtime/doc/lsp.txt
Original file line number Diff line number Diff line change
Expand Up @@ -1216,13 +1216,21 @@ remove_workspace_folder({workspace_folder})
{path} is not provided, the user will be prompted for a path
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.

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
|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()*
Checks whether the language servers attached to the current
Expand Down
148 changes: 113 additions & 35 deletions runtime/lua/vim/lsp/buf.lua
Original file line number Diff line number Diff line change
Expand Up @@ -359,50 +359,128 @@ end

--- Renames all references to the symbol under the cursor.
---
---@param new_name (string) If not provided, the user will be prompted for a new
---name using |vim.ui.input()|.
function M.rename(new_name)
local opts = {
prompt = "New Name: "
}
---@param new_name string|nil If not provided, the user will be prompted for a new
--- name using |vim.ui.input()|.
---@param 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.
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
local function on_confirm(input)
if not (input and #input > 0) then return end
local params = util.make_position_params()
params.newName = input
request('textDocument/rename', params)
if options.filter then
clients = options.filter(clients)
elseif options.name then
clients = vim.tbl_filter(
function(client) return client.name == options.name end,
clients
)
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
local function prepare_rename(err, result)
if err == nil and result == nil then
vim.notify('nothing to rename', vim.log.levels.INFO)
local function get_text_at_range(range)
return vim.api.nvim_buf_get_text(
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
end
if result and result.placeholder then
opts.default = result.placeholder
if not new_name then npcall(vim.ui.input, opts, on_confirm) end
elseif result and result.start and result['end'] and
result.start.line == result['end'].line then
local line = vfn.getline(result.start.line+1)
local start_char = result.start.character+1
local end_char = result['end'].character
opts.default = string.sub(line, start_char, end_char)
if not new_name then npcall(vim.ui.input, opts, on_confirm) end

---@private
local function rename(name)
local params = util.make_position_params(win, client.offset_encoding)
params.newName = name
local handler = client.handlers['textDocument/rename'] or vim.lsp.handlers['textDocument/rename']
client.request('textDocument/rename', params, function(...)
handler(...)
try_use_client(next(clients, idx))
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
local msg = err and ('Error on prepareRename: ' .. (err.message or '')) or 'Nothing to rename'
vim.notify(msg, vim.log.levels.INFO)
end
return
end

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
-- fallback to guessing symbol using <cword>
--
-- 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
vim.notify('Client ' .. client.id .. '/' .. client.name .. ' has no rename capability')
end
if new_name then on_confirm(new_name) end
end
request('textDocument/prepareRename', util.make_position_params(), prepare_rename)

try_use_client(next(clients))
end

--- Lists all the references to the symbol under the cursor in the quickfix window.
Expand Down
4 changes: 0 additions & 4 deletions test/functional/fixtures/fake-lsp-server.lua
Original file line number Diff line number Diff line change
Expand Up @@ -222,10 +222,6 @@ function tests.prepare_rename_error()
expect_request('textDocument/prepareRename', function()
return {}, nil
end)
expect_request('textDocument/rename', function(params)
assert_eq(params.newName, 'renameto')
return nil, nil
end)
notify('shutdown')
end;
}
Expand Down
2 changes: 0 additions & 2 deletions test/functional/plugin/lsp_spec.lua
Original file line number Diff line number Diff line change
Expand Up @@ -2636,10 +2636,8 @@ describe('LSP', function()
name = "prepare_rename_error",
expected_handlers = {
{NIL, {}, {method="shutdown", client_id=1}};
{NIL, NIL, {method="textDocument/rename", client_id=1, bufnr=1}};
{NIL, {}, {method="start", client_id=1}};
},
expected_text = "two", -- see test case
},
}) do
it(test.it, function()
Expand Down

0 comments on commit 74494a1

Please sign in to comment.