Skip to content

Commit 962be38

Browse files
bors[bot]Anatol UlrichVeykrilspookyvision
authored
Merge #10645
10645: fix: make `rename` multi-token mapping aware r=Veykril a=spookyvision Co-authored-by: Anatol Ulrich <[email protected]> Co-authored-by: Lukas Wirth <[email protected]> Co-authored-by: Anatol Ulrich <[email protected]>
2 parents c96481e + e8416bb commit 962be38

File tree

3 files changed

+179
-75
lines changed

3 files changed

+179
-75
lines changed

crates/ide/src/rename.rs

Lines changed: 148 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,15 @@
33
//! This is mostly front-end for [`ide_db::rename`], but it also includes the
44
//! tests. This module also implements a couple of magic tricks, like renaming
55
//! `self` and to `self` (to switch between associated function and method).
6+
67
use hir::{AsAssocItem, InFile, Semantics};
78
use ide_db::{
89
base_db::FileId,
910
defs::{Definition, NameClass, NameRefClass},
1011
rename::{bail, format_err, source_edit_from_references, IdentifierKind},
1112
RootDatabase,
1213
};
14+
use itertools::Itertools;
1315
use stdx::{always, never};
1416
use syntax::{ast, AstNode, SyntaxNode};
1517

@@ -31,14 +33,33 @@ pub(crate) fn prepare_rename(
3133
let source_file = sema.parse(position.file_id);
3234
let syntax = source_file.syntax();
3335

34-
let (name_like, def) = find_definition(&sema, syntax, position)?;
35-
if def.range_for_rename(&sema).is_none() {
36-
bail!("No references found at position")
37-
}
36+
let res = find_definitions(&sema, syntax, position)?
37+
.map(|(name_like, def)| {
38+
// ensure all ranges are valid
3839

39-
let frange = sema.original_range(name_like.syntax());
40-
always!(frange.range.contains_inclusive(position.offset) && frange.file_id == position.file_id);
41-
Ok(RangeInfo::new(frange.range, ()))
40+
if def.range_for_rename(&sema).is_none() {
41+
bail!("No references found at position")
42+
}
43+
let frange = sema.original_range(name_like.syntax());
44+
45+
always!(
46+
frange.range.contains_inclusive(position.offset)
47+
&& frange.file_id == position.file_id
48+
);
49+
Ok(frange.range)
50+
})
51+
.reduce(|acc, cur| match (acc, cur) {
52+
// ensure all ranges are the same
53+
(Ok(acc_inner), Ok(cur_inner)) if acc_inner == cur_inner => Ok(acc_inner),
54+
(Err(e), _) => Err(e),
55+
_ => bail!("inconsistent text range"),
56+
});
57+
58+
match res {
59+
// ensure at least one definition was found
60+
Some(res) => res.map(|range| RangeInfo::new(range, ())),
61+
None => bail!("No references found at position"),
62+
}
4263
}
4364

4465
// Feature: Rename
@@ -61,20 +82,27 @@ pub(crate) fn rename(
6182
let source_file = sema.parse(position.file_id);
6283
let syntax = source_file.syntax();
6384

64-
let (_name_like, def) = find_definition(&sema, syntax, position)?;
85+
let defs = find_definitions(&sema, syntax, position)?;
6586

66-
if let Definition::Local(local) = def {
67-
if let Some(self_param) = local.as_self_param(sema.db) {
68-
cov_mark::hit!(rename_self_to_param);
69-
return rename_self_to_param(&sema, local, self_param, new_name);
70-
}
71-
if new_name == "self" {
72-
cov_mark::hit!(rename_to_self);
73-
return rename_to_self(&sema, local);
74-
}
75-
}
87+
let ops: RenameResult<Vec<SourceChange>> = defs
88+
.map(|(_namelike, def)| {
89+
if let Definition::Local(local) = def {
90+
if let Some(self_param) = local.as_self_param(sema.db) {
91+
cov_mark::hit!(rename_self_to_param);
92+
return rename_self_to_param(&sema, local, self_param, new_name);
93+
}
94+
if new_name == "self" {
95+
cov_mark::hit!(rename_to_self);
96+
return rename_to_self(&sema, local);
97+
}
98+
}
99+
def.rename(&sema, new_name)
100+
})
101+
.collect();
76102

77-
def.rename(&sema, new_name)
103+
ops?.into_iter()
104+
.reduce(|acc, elem| acc.merge(elem))
105+
.ok_or_else(|| format_err!("No references found at position"))
78106
}
79107

80108
/// Called by the client when it is about to rename a file.
@@ -91,59 +119,86 @@ pub(crate) fn will_rename_file(
91119
Some(change)
92120
}
93121

94-
fn find_definition(
122+
fn find_definitions(
95123
sema: &Semantics<RootDatabase>,
96124
syntax: &SyntaxNode,
97125
position: FilePosition,
98-
) -> RenameResult<(ast::NameLike, Definition)> {
99-
let name_like = sema
100-
.find_node_at_offset_with_descend::<ast::NameLike>(syntax, position.offset)
101-
.ok_or_else(|| format_err!("No references found at position"))?;
102-
103-
let def = match &name_like {
104-
// renaming aliases would rename the item being aliased as the HIR doesn't track aliases yet
105-
ast::NameLike::Name(name)
106-
if name.syntax().parent().map_or(false, |it| ast::Rename::can_cast(it.kind())) =>
107-
{
108-
bail!("Renaming aliases is currently unsupported")
109-
}
110-
ast::NameLike::Name(name) => NameClass::classify(sema, name).map(|class| match class {
111-
NameClass::Definition(it) | NameClass::ConstReference(it) => it,
112-
NameClass::PatFieldShorthand { local_def, field_ref: _ } => {
113-
Definition::Local(local_def)
114-
}
115-
}),
116-
ast::NameLike::NameRef(name_ref) => {
117-
if let Some(def) = NameRefClass::classify(sema, name_ref).map(|class| match class {
118-
NameRefClass::Definition(def) => def,
119-
NameRefClass::FieldShorthand { local_ref, field_ref: _ } => {
120-
Definition::Local(local_ref)
126+
) -> RenameResult<impl Iterator<Item = (ast::NameLike, Definition)>> {
127+
let symbols = sema
128+
.find_nodes_at_offset_with_descend::<ast::NameLike>(syntax, position.offset)
129+
.map(|name_like| {
130+
let res = match &name_like {
131+
// renaming aliases would rename the item being aliased as the HIR doesn't track aliases yet
132+
ast::NameLike::Name(name)
133+
if name
134+
.syntax()
135+
.parent()
136+
.map_or(false, |it| ast::Rename::can_cast(it.kind())) =>
137+
{
138+
bail!("Renaming aliases is currently unsupported")
139+
}
140+
ast::NameLike::Name(name) => NameClass::classify(sema, name)
141+
.map(|class| match class {
142+
NameClass::Definition(it) | NameClass::ConstReference(it) => it,
143+
NameClass::PatFieldShorthand { local_def, field_ref: _ } => {
144+
Definition::Local(local_def)
145+
}
146+
})
147+
.map(|def| (name_like.clone(), def))
148+
.ok_or_else(|| format_err!("No references found at position")),
149+
ast::NameLike::NameRef(name_ref) => {
150+
NameRefClass::classify(sema, name_ref)
151+
.map(|class| match class {
152+
NameRefClass::Definition(def) => def,
153+
NameRefClass::FieldShorthand { local_ref, field_ref: _ } => {
154+
Definition::Local(local_ref)
155+
}
156+
})
157+
.ok_or_else(|| format_err!("No references found at position"))
158+
.and_then(|def| {
159+
// if the name differs from the definitions name it has to be an alias
160+
if def
161+
.name(sema.db)
162+
.map_or(false, |it| it.to_string() != name_ref.text())
163+
{
164+
Err(format_err!("Renaming aliases is currently unsupported"))
165+
} else {
166+
Ok((name_like.clone(), def))
167+
}
168+
})
121169
}
122-
}) {
123-
// if the name differs from the definitions name it has to be an alias
124-
if def.name(sema.db).map_or(false, |it| it.to_string() != name_ref.text()) {
125-
bail!("Renaming aliases is currently unsupported");
170+
ast::NameLike::Lifetime(lifetime) => {
171+
NameRefClass::classify_lifetime(sema, lifetime)
172+
.and_then(|class| match class {
173+
NameRefClass::Definition(def) => Some(def),
174+
_ => None,
175+
})
176+
.or_else(|| {
177+
NameClass::classify_lifetime(sema, lifetime).and_then(|it| match it {
178+
NameClass::Definition(it) => Some(it),
179+
_ => None,
180+
})
181+
})
182+
.map(|def| (name_like, def))
183+
.ok_or_else(|| format_err!("No references found at position"))
126184
}
127-
Some(def)
185+
};
186+
res
187+
});
188+
189+
let res: RenameResult<Vec<_>> = symbols.collect();
190+
match res {
191+
Ok(v) => {
192+
if v.is_empty() {
193+
// FIXME: some semantic duplication between "empty vec" and "Err()"
194+
Err(format_err!("No references found at position"))
128195
} else {
129-
None
196+
// remove duplicates, comparing `Definition`s
197+
Ok(v.into_iter().unique_by(|t| t.1))
130198
}
131199
}
132-
ast::NameLike::Lifetime(lifetime) => NameRefClass::classify_lifetime(sema, lifetime)
133-
.and_then(|class| match class {
134-
NameRefClass::Definition(def) => Some(def),
135-
_ => None,
136-
})
137-
.or_else(|| {
138-
NameClass::classify_lifetime(sema, lifetime).and_then(|it| match it {
139-
NameClass::Definition(it) => Some(it),
140-
_ => None,
141-
})
142-
}),
200+
Err(e) => Err(e),
143201
}
144-
.ok_or_else(|| format_err!("No references found at position"))?;
145-
146-
Ok((name_like, def))
147202
}
148203

149204
fn rename_to_self(sema: &Semantics<RootDatabase>, local: hir::Local) -> RenameResult<SourceChange> {
@@ -515,6 +570,36 @@ fn main() {
515570
);
516571
}
517572

573+
#[test]
574+
fn test_rename_macro_multiple_occurrences() {
575+
check(
576+
"Baaah",
577+
r#"macro_rules! foo {
578+
($ident:ident) => {
579+
const $ident: () = ();
580+
struct $ident {}
581+
};
582+
}
583+
584+
foo!($0Foo);
585+
const _: () = Foo;
586+
const _: Foo = Foo {};
587+
"#,
588+
r#"
589+
macro_rules! foo {
590+
($ident:ident) => {
591+
const $ident: () = ();
592+
struct $ident {}
593+
};
594+
}
595+
596+
foo!(Baaah);
597+
const _: () = Baaah;
598+
const _: Baaah = Baaah {};
599+
"#,
600+
)
601+
}
602+
518603
#[test]
519604
fn test_rename_for_macro_args() {
520605
check(

crates/ide_db/src/source_change.rs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,13 @@ impl SourceChange {
5454
pub fn get_source_edit(&self, file_id: FileId) -> Option<&TextEdit> {
5555
self.source_file_edits.get(&file_id)
5656
}
57+
58+
pub fn merge(mut self, other: SourceChange) -> SourceChange {
59+
self.extend(other.source_file_edits);
60+
self.extend(other.file_system_edits);
61+
self.is_snippet |= other.is_snippet;
62+
self
63+
}
5764
}
5865

5966
impl Extend<(FileId, TextEdit)> for SourceChange {
@@ -62,6 +69,12 @@ impl Extend<(FileId, TextEdit)> for SourceChange {
6269
}
6370
}
6471

72+
impl Extend<FileSystemEdit> for SourceChange {
73+
fn extend<T: IntoIterator<Item = FileSystemEdit>>(&mut self, iter: T) {
74+
iter.into_iter().for_each(|edit| self.push_file_system_edit(edit));
75+
}
76+
}
77+
6578
impl From<FxHashMap<FileId, TextEdit>> for SourceChange {
6679
fn from(source_file_edits: FxHashMap<FileId, TextEdit>) -> SourceChange {
6780
SourceChange { source_file_edits, file_system_edits: Vec::new(), is_snippet: false }

crates/text_edit/src/lib.rs

Lines changed: 18 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,13 @@
33
//! `rust-analyzer` never mutates text itself and only sends diffs to clients,
44
//! so `TextEdit` is the ultimate representation of the work done by
55
//! rust-analyzer.
6+
67
pub use text_size::{TextRange, TextSize};
78

89
/// `InsertDelete` -- a single "atomic" change to text
910
///
1011
/// Must not overlap with other `InDel`s
11-
#[derive(Debug, Clone)]
12+
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
1213
pub struct Indel {
1314
pub insert: String,
1415
/// Refers to offsets in the original text
@@ -116,11 +117,14 @@ impl TextEdit {
116117
pub fn union(&mut self, other: TextEdit) -> Result<(), TextEdit> {
117118
// FIXME: can be done without allocating intermediate vector
118119
let mut all = self.iter().chain(other.iter()).collect::<Vec<_>>();
119-
if !check_disjoint(&mut all) {
120+
if !check_disjoint_and_sort(&mut all) {
120121
return Err(other);
121122
}
123+
122124
self.indels.extend(other.indels);
123-
assert_disjoint(&mut self.indels);
125+
check_disjoint_and_sort(&mut self.indels);
126+
// Only dedup deletions and replacements, keep all insertions
127+
self.indels.dedup_by(|a, b| a == b && !a.delete.is_empty());
124128
Ok(())
125129
}
126130

@@ -173,7 +177,7 @@ impl TextEditBuilder {
173177
}
174178
pub fn finish(self) -> TextEdit {
175179
let mut indels = self.indels;
176-
assert_disjoint(&mut indels);
180+
assert_disjoint_or_equal(&mut indels);
177181
TextEdit { indels }
178182
}
179183
pub fn invalidates_offset(&self, offset: TextSize) -> bool {
@@ -182,18 +186,20 @@ impl TextEditBuilder {
182186
fn indel(&mut self, indel: Indel) {
183187
self.indels.push(indel);
184188
if self.indels.len() <= 16 {
185-
assert_disjoint(&mut self.indels);
189+
assert_disjoint_or_equal(&mut self.indels);
186190
}
187191
}
188192
}
189193

190-
fn assert_disjoint(indels: &mut [impl std::borrow::Borrow<Indel>]) {
191-
assert!(check_disjoint(indels));
194+
fn assert_disjoint_or_equal(indels: &mut [Indel]) {
195+
assert!(check_disjoint_and_sort(indels));
192196
}
193-
fn check_disjoint(indels: &mut [impl std::borrow::Borrow<Indel>]) -> bool {
197+
// FIXME: Remove the impl Bound here, it shouldn't be needed
198+
fn check_disjoint_and_sort(indels: &mut [impl std::borrow::Borrow<Indel>]) -> bool {
194199
indels.sort_by_key(|indel| (indel.borrow().delete.start(), indel.borrow().delete.end()));
195-
indels
196-
.iter()
197-
.zip(indels.iter().skip(1))
198-
.all(|(l, r)| l.borrow().delete.end() <= r.borrow().delete.start())
200+
indels.iter().zip(indels.iter().skip(1)).all(|(l, r)| {
201+
let l = l.borrow();
202+
let r = r.borrow();
203+
l.delete.end() <= r.delete.start() || l == r
204+
})
199205
}

0 commit comments

Comments
 (0)