WIP: fix: make rename multi-token mapping aware

This commit is contained in:
Anatol Ulrich 2021-10-27 04:31:14 +02:00
parent a3830dfd3b
commit 6decfceae1
2 changed files with 108 additions and 66 deletions

View File

@ -3,6 +3,7 @@
//! This is mostly front-end for [`ide_db::rename`], but it also includes the
//! tests. This module also implements a couple of magic tricks, like renaming
//! `self` and to `self` (to switch between associated function and method).
use hir::{AsAssocItem, InFile, Semantics};
use ide_db::{
base_db::FileId,
@ -10,8 +11,12 @@ use ide_db::{
rename::{bail, format_err, source_edit_from_references, IdentifierKind},
RootDatabase,
};
use itertools::Itertools;
use stdx::{always, never};
use syntax::{ast, AstNode, SyntaxNode};
use syntax::{
ast::{self},
AstNode, SyntaxNode,
};
use text_edit::TextEdit;
@ -31,11 +36,12 @@ pub(crate) fn prepare_rename(
let source_file = sema.parse(position.file_id);
let syntax = source_file.syntax();
let (name_like, def) = find_definition(&sema, syntax, position)?;
if def.range_for_rename(&sema).is_none() {
bail!("No references found at position")
}
let mut defs = find_definitions(&sema, syntax, position)?;
// TODO:
// - empty case possible or already caught by `find_definitions`?
// - is "just take the first" correct? If not, what do?
let (name_like, _def) = defs.next().unwrap();
let frange = sema.original_range(name_like.syntax());
always!(frange.range.contains_inclusive(position.offset) && frange.file_id == position.file_id);
Ok(RangeInfo::new(frange.range, ()))
@ -61,20 +67,26 @@ pub(crate) fn rename(
let source_file = sema.parse(position.file_id);
let syntax = source_file.syntax();
let (_name_like, def) = find_definition(&sema, syntax, position)?;
let defs = find_definitions(&sema, syntax, position)?;
if let Definition::Local(local) = def {
if let Some(self_param) = local.as_self_param(sema.db) {
cov_mark::hit!(rename_self_to_param);
return rename_self_to_param(&sema, local, self_param, new_name);
}
if new_name == "self" {
cov_mark::hit!(rename_to_self);
return rename_to_self(&sema, local);
}
}
def.rename(&sema, new_name)
let ops: RenameResult<Vec<SourceChange>> = defs
.map(|(_namelike, def)| {
if let Definition::Local(local) = def {
if let Some(self_param) = local.as_self_param(sema.db) {
cov_mark::hit!(rename_self_to_param);
return rename_self_to_param(&sema, local, self_param, new_name);
}
if new_name == "self" {
cov_mark::hit!(rename_to_self);
return rename_to_self(&sema, local);
}
}
def.rename(&sema, new_name)
})
.collect();
ops?.into_iter()
.reduce(|acc, elem| acc.merge(elem))
.ok_or_else(|| format_err!("No references found at position"))
}
/// Called by the client when it is about to rename a file.
@ -91,59 +103,76 @@ pub(crate) fn will_rename_file(
Some(change)
}
fn find_definition(
fn find_definitions(
sema: &Semantics<RootDatabase>,
syntax: &SyntaxNode,
position: FilePosition,
) -> RenameResult<(ast::NameLike, Definition)> {
let name_like = sema
.find_node_at_offset_with_descend::<ast::NameLike>(syntax, position.offset)
.ok_or_else(|| format_err!("No references found at position"))?;
) -> RenameResult<impl Iterator<Item = (ast::NameLike, Definition)>> {
let symbols = sema
.find_nodes_at_offset_with_descend::<ast::NameLike>(syntax, position.offset)
.map(|name_like| {
let res = match &name_like {
// renaming aliases would rename the item being aliased as the HIR doesn't track aliases yet
ast::NameLike::Name(name)
if name
.syntax()
.parent()
.map_or(false, |it| ast::Rename::can_cast(it.kind())) =>
{
bail!("Renaming aliases is currently unsupported")
}
ast::NameLike::Name(name) => NameClass::classify(sema, name)
.map(|class| match class {
NameClass::Definition(it) | NameClass::ConstReference(it) => it,
NameClass::PatFieldShorthand { local_def, field_ref: _ } => {
Definition::Local(local_def)
}
})
.map(|def| (name_like.clone(), def))
.ok_or_else(|| format_err!("No references found at position")),
ast::NameLike::NameRef(name_ref) => NameRefClass::classify(sema, name_ref)
.map(|class| match class {
NameRefClass::Definition(def) => def,
NameRefClass::FieldShorthand { local_ref, field_ref: _ } => {
Definition::Local(local_ref)
}
})
.and_then(|def| {
// if the name differs from the definitions name it has to be an alias
if def.name(sema.db).map_or(false, |it| it.to_string() != name_ref.text()) {
None
} else {
Some((name_like.clone(), def))
}
})
.ok_or_else(|| format_err!("Renaming aliases is currently unsupported")),
ast::NameLike::Lifetime(lifetime) => {
NameRefClass::classify_lifetime(sema, lifetime)
.and_then(|class| match class {
NameRefClass::Definition(def) => Some(def),
_ => None,
})
.or_else(|| {
NameClass::classify_lifetime(sema, lifetime).and_then(|it| match it {
NameClass::Definition(it) => Some(it),
_ => None,
})
})
.map(|def| (name_like, def))
.ok_or_else(|| format_err!("No references found at position"))
}
};
res
});
let def = match &name_like {
// renaming aliases would rename the item being aliased as the HIR doesn't track aliases yet
ast::NameLike::Name(name)
if name.syntax().parent().map_or(false, |it| ast::Rename::can_cast(it.kind())) =>
{
bail!("Renaming aliases is currently unsupported")
}
ast::NameLike::Name(name) => NameClass::classify(sema, name).map(|class| match class {
NameClass::Definition(it) | NameClass::ConstReference(it) => it,
NameClass::PatFieldShorthand { local_def, field_ref: _ } => {
Definition::Local(local_def)
}
}),
ast::NameLike::NameRef(name_ref) => {
if let Some(def) = NameRefClass::classify(sema, name_ref).map(|class| match class {
NameRefClass::Definition(def) => def,
NameRefClass::FieldShorthand { local_ref, field_ref: _ } => {
Definition::Local(local_ref)
}
}) {
// if the name differs from the definitions name it has to be an alias
if def.name(sema.db).map_or(false, |it| it.to_string() != name_ref.text()) {
bail!("Renaming aliases is currently unsupported");
}
Some(def)
} else {
None
}
}
ast::NameLike::Lifetime(lifetime) => NameRefClass::classify_lifetime(sema, lifetime)
.and_then(|class| match class {
NameRefClass::Definition(def) => Some(def),
_ => None,
})
.or_else(|| {
NameClass::classify_lifetime(sema, lifetime).and_then(|it| match it {
NameClass::Definition(it) => Some(it),
_ => None,
})
}),
// TODO avoid collect() somehow?
let v: RenameResult<Vec<(ast::NameLike, Definition)>> = symbols.collect();
match v {
// remove duplicates
// TODO is "unique by `Definition`" correct?
Ok(v) => Ok(v.into_iter().unique_by(|t| t.1)),
Err(e) => Err(e),
}
.ok_or_else(|| format_err!("No references found at position"))?;
Ok((name_like, def))
}
fn rename_to_self(sema: &Semantics<RootDatabase>, local: hir::Local) -> RenameResult<SourceChange> {

View File

@ -54,6 +54,13 @@ impl SourceChange {
pub fn get_source_edit(&self, file_id: FileId) -> Option<&TextEdit> {
self.source_file_edits.get(&file_id)
}
pub fn merge(mut self, other: SourceChange) -> SourceChange {
self.extend(other.source_file_edits);
self.extend(other.file_system_edits);
self.is_snippet |= other.is_snippet; // TODO correct?
self
}
}
impl Extend<(FileId, TextEdit)> for SourceChange {
@ -62,6 +69,12 @@ impl Extend<(FileId, TextEdit)> for SourceChange {
}
}
impl Extend<FileSystemEdit> for SourceChange {
fn extend<T: IntoIterator<Item = FileSystemEdit>>(&mut self, iter: T) {
iter.into_iter().for_each(|edit| self.push_file_system_edit(edit));
}
}
impl From<FxHashMap<FileId, TextEdit>> for SourceChange {
fn from(source_file_edits: FxHashMap<FileId, TextEdit>) -> SourceChange {
SourceChange { source_file_edits, file_system_edits: Vec::new(), is_snippet: false }