mirror of
				https://github.com/neovim/neovim.git
				synced 2025-11-04 01:34:25 +00:00 
			
		
		
		
	feat(lsp): support textDocument/prepareRename (#15514)
This commit is contained in:
		@@ -249,13 +249,34 @@ end
 | 
				
			|||||||
---@param new_name (string) If not provided, the user will be prompted for a new
 | 
					---@param new_name (string) If not provided, the user will be prompted for a new
 | 
				
			||||||
---name using |input()|.
 | 
					---name using |input()|.
 | 
				
			||||||
function M.rename(new_name)
 | 
					function M.rename(new_name)
 | 
				
			||||||
  -- TODO(ashkan) use prepareRename
 | 
					 | 
				
			||||||
  -- * result: [`Range`](#range) \| `{ range: Range, placeholder: string }` \| `null` describing the range of the string to rename and optionally a placeholder text of the string content to be renamed. If `null` is returned then it is deemed that a 'textDocument/rename' request is not valid at the given position.
 | 
					 | 
				
			||||||
  local params = util.make_position_params()
 | 
					  local params = util.make_position_params()
 | 
				
			||||||
  new_name = new_name or npcall(vfn.input, "New Name: ", vfn.expand('<cword>'))
 | 
					  local function prepare_rename(err, result)
 | 
				
			||||||
  if not (new_name and #new_name > 0) then return end
 | 
					    if err == nil and result == nil then
 | 
				
			||||||
  params.newName = new_name
 | 
					      vim.notify('nothing to rename', vim.log.levels.INFO)
 | 
				
			||||||
  request('textDocument/rename', params)
 | 
					      return
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					    if result and result.placeholder then
 | 
				
			||||||
 | 
					      new_name = new_name or npcall(vfn.input, "New Name: ", result.placeholder)
 | 
				
			||||||
 | 
					    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
 | 
				
			||||||
 | 
					      new_name = new_name or npcall(vfn.input, "New Name: ", string.sub(line, start_char, end_char))
 | 
				
			||||||
 | 
					    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
 | 
				
			||||||
 | 
					      new_name = new_name or npcall(vfn.input, "New Name: ", vfn.expand('<cword>'))
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					    if not (new_name and #new_name > 0) then return end
 | 
				
			||||||
 | 
					    params.newName = new_name
 | 
				
			||||||
 | 
					    request('textDocument/rename', params)
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					  request('textDocument/prepareRename', params, prepare_rename)
 | 
				
			||||||
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.
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -126,6 +126,89 @@ function tests.check_workspace_configuration()
 | 
				
			|||||||
  }
 | 
					  }
 | 
				
			||||||
end
 | 
					end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function tests.prepare_rename_nil()
 | 
				
			||||||
 | 
					  skeleton {
 | 
				
			||||||
 | 
					    on_init = function()
 | 
				
			||||||
 | 
					      return { capabilities = {
 | 
				
			||||||
 | 
					        renameProvider = true,
 | 
				
			||||||
 | 
					      } }
 | 
				
			||||||
 | 
					    end;
 | 
				
			||||||
 | 
					    body = function()
 | 
				
			||||||
 | 
					      notify('start')
 | 
				
			||||||
 | 
					      expect_request('textDocument/prepareRename', function()
 | 
				
			||||||
 | 
					        return nil, nil
 | 
				
			||||||
 | 
					      end)
 | 
				
			||||||
 | 
					      notify('shutdown')
 | 
				
			||||||
 | 
					    end;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function tests.prepare_rename_placeholder()
 | 
				
			||||||
 | 
					  skeleton {
 | 
				
			||||||
 | 
					    on_init = function()
 | 
				
			||||||
 | 
					      return { capabilities = {
 | 
				
			||||||
 | 
					        renameProvider = true,
 | 
				
			||||||
 | 
					      } }
 | 
				
			||||||
 | 
					    end;
 | 
				
			||||||
 | 
					    body = function()
 | 
				
			||||||
 | 
					      notify('start')
 | 
				
			||||||
 | 
					      expect_request('textDocument/prepareRename', function()
 | 
				
			||||||
 | 
					        return nil, {placeholder = 'placeholder'}
 | 
				
			||||||
 | 
					      end)
 | 
				
			||||||
 | 
					      expect_request('textDocument/rename', function(params)
 | 
				
			||||||
 | 
					        assert_eq(params.newName, 'renameto')
 | 
				
			||||||
 | 
					        return nil, nil
 | 
				
			||||||
 | 
					      end)
 | 
				
			||||||
 | 
					      notify('shutdown')
 | 
				
			||||||
 | 
					    end;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function tests.prepare_rename_range()
 | 
				
			||||||
 | 
					  skeleton {
 | 
				
			||||||
 | 
					    on_init = function()
 | 
				
			||||||
 | 
					      return { capabilities = {
 | 
				
			||||||
 | 
					        renameProvider = true,
 | 
				
			||||||
 | 
					      } }
 | 
				
			||||||
 | 
					    end;
 | 
				
			||||||
 | 
					    body = function()
 | 
				
			||||||
 | 
					      notify('start')
 | 
				
			||||||
 | 
					      expect_request('textDocument/prepareRename', function()
 | 
				
			||||||
 | 
					        return nil, {
 | 
				
			||||||
 | 
					          start = { line = 1, character = 8 },
 | 
				
			||||||
 | 
					          ['end'] = { line = 1, character = 12 },
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      end)
 | 
				
			||||||
 | 
					      expect_request('textDocument/rename', function(params)
 | 
				
			||||||
 | 
					        assert_eq(params.newName, 'renameto')
 | 
				
			||||||
 | 
					        return nil, nil
 | 
				
			||||||
 | 
					      end)
 | 
				
			||||||
 | 
					      notify('shutdown')
 | 
				
			||||||
 | 
					    end;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function tests.prepare_rename_error()
 | 
				
			||||||
 | 
					  skeleton {
 | 
				
			||||||
 | 
					    on_init = function()
 | 
				
			||||||
 | 
					      return { capabilities = {
 | 
				
			||||||
 | 
					        renameProvider = true,
 | 
				
			||||||
 | 
					      } }
 | 
				
			||||||
 | 
					    end;
 | 
				
			||||||
 | 
					    body = function()
 | 
				
			||||||
 | 
					      notify('start')
 | 
				
			||||||
 | 
					      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;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function tests.basic_check_capabilities()
 | 
					function tests.basic_check_capabilities()
 | 
				
			||||||
  skeleton {
 | 
					  skeleton {
 | 
				
			||||||
    on_init = function(params)
 | 
					    on_init = function(params)
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -2202,4 +2202,90 @@ describe('LSP', function()
 | 
				
			|||||||
      eq(expected, qflist)
 | 
					      eq(expected, qflist)
 | 
				
			||||||
    end)
 | 
					    end)
 | 
				
			||||||
  end)
 | 
					  end)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  describe('vim.lsp.buf.rename', function()
 | 
				
			||||||
 | 
					    for _, test in ipairs({
 | 
				
			||||||
 | 
					      {
 | 
				
			||||||
 | 
					        it = "does not attempt to rename on nil response",
 | 
				
			||||||
 | 
					        name = "prepare_rename_nil",
 | 
				
			||||||
 | 
					        expected_handlers = {
 | 
				
			||||||
 | 
					          {NIL, {}, {method="shutdown", client_id=1}};
 | 
				
			||||||
 | 
					          {NIL, {}, {method="start", client_id=1}};
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      {
 | 
				
			||||||
 | 
					        it = "handles prepareRename placeholder response",
 | 
				
			||||||
 | 
					        name = "prepare_rename_placeholder",
 | 
				
			||||||
 | 
					        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 = "placeholder", -- see fake lsp response
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      {
 | 
				
			||||||
 | 
					        it = "handles range response",
 | 
				
			||||||
 | 
					        name = "prepare_rename_range",
 | 
				
			||||||
 | 
					        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 = "line", -- see test case and fake lsp response
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      {
 | 
				
			||||||
 | 
					        it = "handles error",
 | 
				
			||||||
 | 
					        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()
 | 
				
			||||||
 | 
					      local client
 | 
				
			||||||
 | 
					      test_rpc_server {
 | 
				
			||||||
 | 
					        test_name = test.name;
 | 
				
			||||||
 | 
					        on_init = function(_client)
 | 
				
			||||||
 | 
					          client = _client
 | 
				
			||||||
 | 
					          eq(true, client.resolved_capabilities().rename)
 | 
				
			||||||
 | 
					        end;
 | 
				
			||||||
 | 
					        on_setup = function()
 | 
				
			||||||
 | 
					          exec_lua([=[
 | 
				
			||||||
 | 
					            local bufnr = vim.api.nvim_get_current_buf()
 | 
				
			||||||
 | 
					            lsp.buf_attach_client(bufnr, TEST_RPC_CLIENT_ID)
 | 
				
			||||||
 | 
					            vim.lsp._stubs = {}
 | 
				
			||||||
 | 
					            vim.fn.input = function(prompt, text)
 | 
				
			||||||
 | 
					              vim.lsp._stubs.input_prompt = prompt
 | 
				
			||||||
 | 
					              vim.lsp._stubs.input_text = text
 | 
				
			||||||
 | 
					              return 'renameto' -- expect this value in fake lsp
 | 
				
			||||||
 | 
					            end
 | 
				
			||||||
 | 
					            vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, {'', 'this is line two'})
 | 
				
			||||||
 | 
					            vim.fn.cursor(2, 13) -- the space between "line" and "two"
 | 
				
			||||||
 | 
					          ]=])
 | 
				
			||||||
 | 
					        end;
 | 
				
			||||||
 | 
					        on_exit = function(code, signal)
 | 
				
			||||||
 | 
					          eq(0, code, "exit code", fake_lsp_logfile)
 | 
				
			||||||
 | 
					          eq(0, signal, "exit signal", fake_lsp_logfile)
 | 
				
			||||||
 | 
					        end;
 | 
				
			||||||
 | 
					        on_handler = function(err, result, ctx)
 | 
				
			||||||
 | 
					          eq(table.remove(test.expected_handlers), {err, result, ctx}, "expected handler")
 | 
				
			||||||
 | 
					          if ctx.method == 'start' then
 | 
				
			||||||
 | 
					            exec_lua("vim.lsp.buf.rename()")
 | 
				
			||||||
 | 
					          end
 | 
				
			||||||
 | 
					          if ctx.method == 'shutdown' then
 | 
				
			||||||
 | 
					            if test.expected_text then
 | 
				
			||||||
 | 
					              eq("New Name: ", exec_lua("return vim.lsp._stubs.input_prompt"))
 | 
				
			||||||
 | 
					              eq(test.expected_text, exec_lua("return vim.lsp._stubs.input_text"))
 | 
				
			||||||
 | 
					            end
 | 
				
			||||||
 | 
					            client.stop()
 | 
				
			||||||
 | 
					          end
 | 
				
			||||||
 | 
					        end;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    end)
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					  end)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
end)
 | 
					end)
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user