Skip to content

Commit 9528619

Browse files
authored
Merge pull request #19225 from Giga-Bowser/remove-assists
internal: Migrate some low-hanging `remove_*` assists to `SyntaxEditor`
2 parents 2f5e8d7 + f155aef commit 9528619

File tree

8 files changed

+253
-43
lines changed

8 files changed

+253
-43
lines changed

crates/ide-assists/src/handlers/remove_mut.rs

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use syntax::{SyntaxKind, TextRange, T};
1+
use syntax::{SyntaxKind, T};
22

33
use crate::{AssistContext, AssistId, AssistKind, Assists};
44

@@ -19,19 +19,20 @@ use crate::{AssistContext, AssistId, AssistKind, Assists};
1919
// ```
2020
pub(crate) fn remove_mut(acc: &mut Assists, ctx: &AssistContext<'_>) -> Option<()> {
2121
let mut_token = ctx.find_token_syntax_at_offset(T![mut])?;
22-
let delete_from = mut_token.text_range().start();
23-
let delete_to = match mut_token.next_token() {
24-
Some(it) if it.kind() == SyntaxKind::WHITESPACE => it.text_range().end(),
25-
_ => mut_token.text_range().end(),
26-
};
2722

2823
let target = mut_token.text_range();
2924
acc.add(
3025
AssistId("remove_mut", AssistKind::Refactor),
3126
"Remove `mut` keyword",
3227
target,
3328
|builder| {
34-
builder.delete(TextRange::new(delete_from, delete_to));
29+
let mut editor = builder.make_editor(&mut_token.parent().unwrap());
30+
match mut_token.next_token() {
31+
Some(it) if it.kind() == SyntaxKind::WHITESPACE => editor.delete(it),
32+
_ => (),
33+
}
34+
editor.delete(mut_token);
35+
builder.add_file_edits(ctx.file_id(), editor);
3536
},
3637
)
3738
}

crates/ide-assists/src/handlers/remove_parentheses.rs

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
1-
use syntax::{ast, AstNode, SyntaxKind, T};
1+
use syntax::{
2+
ast::{self, syntax_factory::SyntaxFactory},
3+
syntax_editor::Position,
4+
AstNode, SyntaxKind, T,
5+
};
26

37
use crate::{AssistContext, AssistId, AssistKind, Assists};
48

@@ -40,6 +44,7 @@ pub(crate) fn remove_parentheses(acc: &mut Assists, ctx: &AssistContext<'_>) ->
4044
"Remove redundant parentheses",
4145
target,
4246
|builder| {
47+
let mut editor = builder.make_editor(parens.syntax());
4348
let prev_token = parens.syntax().first_token().and_then(|it| it.prev_token());
4449
let need_to_add_ws = match prev_token {
4550
Some(it) => {
@@ -48,9 +53,13 @@ pub(crate) fn remove_parentheses(acc: &mut Assists, ctx: &AssistContext<'_>) ->
4853
}
4954
None => false,
5055
};
51-
let expr = if need_to_add_ws { format!(" {expr}") } else { expr.to_string() };
52-
53-
builder.replace(parens.syntax().text_range(), expr)
56+
if need_to_add_ws {
57+
let make = SyntaxFactory::new();
58+
editor.insert(Position::before(parens.syntax()), make.whitespace(" "));
59+
editor.add_mappings(make.finish_with_mappings());
60+
}
61+
editor.replace(parens.syntax(), expr.syntax());
62+
builder.add_file_edits(ctx.file_id(), editor);
5463
},
5564
)
5665
}

crates/ide-assists/src/handlers/remove_unused_param.rs

Lines changed: 48 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
use ide_db::{defs::Definition, search::FileReference, EditionedFileId};
22
use syntax::{
3-
algo::find_node_at_range,
3+
algo::{find_node_at_range, least_common_ancestor_element},
44
ast::{self, HasArgList},
5-
AstNode, SourceFile, SyntaxKind, SyntaxNode, TextRange, T,
5+
syntax_editor::Element,
6+
AstNode, SourceFile, SyntaxElement, SyntaxKind, SyntaxNode, TextRange, T,
67
};
78

89
use SyntaxKind::WHITESPACE;
@@ -74,15 +75,21 @@ pub(crate) fn remove_unused_param(acc: &mut Assists, ctx: &AssistContext<'_>) ->
7475
cov_mark::hit!(keep_used);
7576
return None;
7677
}
78+
let parent = param.syntax().parent()?;
7779
acc.add(
7880
AssistId("remove_unused_param", AssistKind::Refactor),
7981
"Remove unused parameter",
8082
param.syntax().text_range(),
8183
|builder| {
82-
builder.delete(range_to_remove(param.syntax()));
84+
let mut editor = builder.make_editor(&parent);
85+
let elements = elements_to_remove(param.syntax());
86+
for element in elements {
87+
editor.delete(element);
88+
}
8389
for (file_id, references) in fn_def.usages(&ctx.sema).all() {
8490
process_usages(ctx, builder, file_id, references, param_position, is_self_present);
8591
}
92+
builder.add_file_edits(ctx.file_id(), editor);
8693
},
8794
)
8895
}
@@ -96,20 +103,24 @@ fn process_usages(
96103
is_self_present: bool,
97104
) {
98105
let source_file = ctx.sema.parse(file_id);
99-
builder.edit_file(file_id);
100106
let possible_ranges = references
101107
.into_iter()
102108
.filter_map(|usage| process_usage(&source_file, usage, arg_to_remove, is_self_present));
103109

104-
let mut ranges_to_delete: Vec<TextRange> = vec![];
105-
for range in possible_ranges {
106-
if !ranges_to_delete.iter().any(|it| it.contains_range(range)) {
107-
ranges_to_delete.push(range)
110+
for element_range in possible_ranges {
111+
let Some(SyntaxElement::Node(parent)) = element_range
112+
.iter()
113+
.cloned()
114+
.reduce(|a, b| least_common_ancestor_element(&a, &b).unwrap().syntax_element())
115+
else {
116+
continue;
117+
};
118+
let mut editor = builder.make_editor(&parent);
119+
for element in element_range {
120+
editor.delete(element);
108121
}
109-
}
110122

111-
for range in ranges_to_delete {
112-
builder.delete(range)
123+
builder.add_file_edits(file_id, editor);
113124
}
114125
}
115126

@@ -118,7 +129,7 @@ fn process_usage(
118129
FileReference { range, .. }: FileReference,
119130
mut arg_to_remove: usize,
120131
is_self_present: bool,
121-
) -> Option<TextRange> {
132+
) -> Option<Vec<SyntaxElement>> {
122133
let call_expr_opt: Option<ast::CallExpr> = find_node_at_range(source_file.syntax(), range);
123134
if let Some(call_expr) = call_expr_opt {
124135
let call_expr_range = call_expr.expr()?.syntax().text_range();
@@ -127,7 +138,7 @@ fn process_usage(
127138
}
128139

129140
let arg = call_expr.arg_list()?.args().nth(arg_to_remove)?;
130-
return Some(range_to_remove(arg.syntax()));
141+
return Some(elements_to_remove(arg.syntax()));
131142
}
132143

133144
let method_call_expr_opt: Option<ast::MethodCallExpr> =
@@ -143,7 +154,7 @@ fn process_usage(
143154
}
144155

145156
let arg = method_call_expr.arg_list()?.args().nth(arg_to_remove)?;
146-
return Some(range_to_remove(arg.syntax()));
157+
return Some(elements_to_remove(arg.syntax()));
147158
}
148159

149160
None
@@ -174,6 +185,29 @@ pub(crate) fn range_to_remove(node: &SyntaxNode) -> TextRange {
174185
}
175186
}
176187

188+
pub(crate) fn elements_to_remove(node: &SyntaxNode) -> Vec<SyntaxElement> {
189+
let up_to_comma = next_prev().find_map(|dir| {
190+
node.siblings_with_tokens(dir)
191+
.filter_map(|it| it.into_token())
192+
.find(|it| it.kind() == T![,])
193+
.map(|it| (dir, it))
194+
});
195+
if let Some((dir, token)) = up_to_comma {
196+
let after = token.siblings_with_tokens(dir).nth(1).unwrap();
197+
let mut result: Vec<_> =
198+
node.siblings_with_tokens(dir).take_while(|it| it != &after).collect();
199+
if node.next_sibling().is_some() {
200+
result.extend(
201+
token.siblings_with_tokens(dir).skip(1).take_while(|it| it.kind() == WHITESPACE),
202+
);
203+
}
204+
205+
result
206+
} else {
207+
vec![node.syntax_element()]
208+
}
209+
}
210+
177211
#[cfg(test)]
178212
mod tests {
179213
use crate::tests::{check_assist, check_assist_not_applicable};

crates/syntax/src/algo.rs

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@
33
use itertools::Itertools;
44

55
use crate::{
6-
AstNode, Direction, NodeOrToken, SyntaxElement, SyntaxKind, SyntaxNode, SyntaxToken, TextRange,
7-
TextSize,
6+
syntax_editor::Element, AstNode, Direction, NodeOrToken, SyntaxElement, SyntaxKind, SyntaxNode,
7+
SyntaxToken, TextRange, TextSize,
88
};
99

1010
/// Returns ancestors of the node at the offset, sorted by length. This should
@@ -89,6 +89,26 @@ pub fn least_common_ancestor(u: &SyntaxNode, v: &SyntaxNode) -> Option<SyntaxNod
8989
Some(res)
9090
}
9191

92+
pub fn least_common_ancestor_element(u: impl Element, v: impl Element) -> Option<SyntaxNode> {
93+
let u = u.syntax_element();
94+
let v = v.syntax_element();
95+
if u == v {
96+
return match u {
97+
NodeOrToken::Node(node) => Some(node),
98+
NodeOrToken::Token(token) => token.parent(),
99+
};
100+
}
101+
102+
let u_depth = u.ancestors().count();
103+
let v_depth = v.ancestors().count();
104+
let keep = u_depth.min(v_depth);
105+
106+
let u_candidates = u.ancestors().skip(u_depth - keep);
107+
let v_candidates = v.ancestors().skip(v_depth - keep);
108+
let (res, _) = u_candidates.zip(v_candidates).find(|(x, y)| x == y)?;
109+
Some(res)
110+
}
111+
92112
pub fn neighbor<T: AstNode>(me: &T, direction: Direction) -> Option<T> {
93113
me.syntax().siblings(direction).skip(1).find_map(T::cast)
94114
}

crates/syntax/src/syntax_editor.rs

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
//! [`SyntaxEditor`]: https://github.com/dotnet/roslyn/blob/43b0b05cc4f492fd5de00f6f6717409091df8daa/src/Workspaces/Core/Portable/Editing/SyntaxEditor.cs
66
77
use std::{
8+
fmt,
89
num::NonZeroU32,
910
ops::RangeInclusive,
1011
sync::atomic::{AtomicU32, Ordering},
@@ -282,6 +283,64 @@ enum ChangeKind {
282283
Replace,
283284
}
284285

286+
impl fmt::Display for Change {
287+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
288+
match self {
289+
Change::Insert(position, node_or_token) => {
290+
let parent = position.parent();
291+
let mut parent_str = parent.to_string();
292+
let target_range = self.target_range().start() - parent.text_range().start();
293+
294+
parent_str.insert_str(
295+
target_range.into(),
296+
&format!("\x1b[42m{node_or_token}\x1b[0m\x1b[K"),
297+
);
298+
f.write_str(&parent_str)
299+
}
300+
Change::InsertAll(position, vec) => {
301+
let parent = position.parent();
302+
let mut parent_str = parent.to_string();
303+
let target_range = self.target_range().start() - parent.text_range().start();
304+
let insertion: String = vec.iter().map(|it| it.to_string()).collect();
305+
306+
parent_str
307+
.insert_str(target_range.into(), &format!("\x1b[42m{insertion}\x1b[0m\x1b[K"));
308+
f.write_str(&parent_str)
309+
}
310+
Change::Replace(old, new) => {
311+
if let Some(new) = new {
312+
write!(f, "\x1b[41m{old}\x1b[42m{new}\x1b[0m\x1b[K")
313+
} else {
314+
write!(f, "\x1b[41m{old}\x1b[0m\x1b[K")
315+
}
316+
}
317+
Change::ReplaceWithMany(old, vec) => {
318+
let new: String = vec.iter().map(|it| it.to_string()).collect();
319+
write!(f, "\x1b[41m{old}\x1b[42m{new}\x1b[0m\x1b[K")
320+
}
321+
Change::ReplaceAll(range, vec) => {
322+
let parent = range.start().parent().unwrap();
323+
let parent_str = parent.to_string();
324+
let pre_range =
325+
TextRange::new(parent.text_range().start(), range.start().text_range().start());
326+
let old_range = TextRange::new(
327+
range.start().text_range().start(),
328+
range.end().text_range().end(),
329+
);
330+
let post_range =
331+
TextRange::new(range.end().text_range().end(), parent.text_range().end());
332+
333+
let pre_str = &parent_str[pre_range - parent.text_range().start()];
334+
let old_str = &parent_str[old_range - parent.text_range().start()];
335+
let post_str = &parent_str[post_range - parent.text_range().start()];
336+
let new: String = vec.iter().map(|it| it.to_string()).collect();
337+
338+
write!(f, "{pre_str}\x1b[41m{old_str}\x1b[42m{new}\x1b[0m\x1b[K{post_str}")
339+
}
340+
}
341+
}
342+
}
343+
285344
/// Utility trait to allow calling syntax editor functions with references or owned
286345
/// nodes. Do not use outside of this module.
287346
pub trait Element {

0 commit comments

Comments
 (0)