Skip to content

Commit 4a41883

Browse files
authored
Merge pull request #18575 from Giga-Bowser/flip-assists
minor: Migrate `flip_*` assists to `SyntaxEditor`
2 parents 02aca11 + d329329 commit 4a41883

File tree

6 files changed

+217
-87
lines changed

6 files changed

+217
-87
lines changed

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

Lines changed: 29 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
1-
use syntax::ast::{self, AstNode, BinExpr};
1+
use syntax::{
2+
ast::{self, syntax_factory::SyntaxFactory, AstNode, BinExpr},
3+
SyntaxKind, T,
4+
};
25

36
use crate::{AssistContext, AssistId, AssistKind, Assists};
47

@@ -19,22 +22,17 @@ use crate::{AssistContext, AssistId, AssistKind, Assists};
1922
// ```
2023
pub(crate) fn flip_binexpr(acc: &mut Assists, ctx: &AssistContext<'_>) -> Option<()> {
2124
let expr = ctx.find_node_at_offset::<BinExpr>()?;
22-
let rhs = expr.rhs()?.syntax().clone();
23-
let lhs = expr.lhs()?.syntax().clone();
24-
25-
let lhs = if let Some(bin_expr) = BinExpr::cast(lhs.clone()) {
26-
if bin_expr.op_kind() == expr.op_kind() {
27-
bin_expr.rhs()?.syntax().clone()
28-
} else {
29-
lhs
30-
}
31-
} else {
32-
lhs
25+
let lhs = expr.lhs()?;
26+
let rhs = expr.rhs()?;
27+
28+
let lhs = match &lhs {
29+
ast::Expr::BinExpr(bin_expr) if bin_expr.op_kind() == expr.op_kind() => bin_expr.rhs()?,
30+
_ => lhs,
3331
};
3432

35-
let op_range = expr.op_token()?.text_range();
33+
let op_token = expr.op_token()?;
3634
// The assist should be applied only if the cursor is on the operator
37-
let cursor_in_range = op_range.contains_range(ctx.selection_trimmed());
35+
let cursor_in_range = op_token.text_range().contains_range(ctx.selection_trimmed());
3836
if !cursor_in_range {
3937
return None;
4038
}
@@ -47,13 +45,18 @@ pub(crate) fn flip_binexpr(acc: &mut Assists, ctx: &AssistContext<'_>) -> Option
4745
acc.add(
4846
AssistId("flip_binexpr", AssistKind::RefactorRewrite),
4947
"Flip binary expression",
50-
op_range,
51-
|edit| {
52-
if let FlipAction::FlipAndReplaceOp(new_op) = action {
53-
edit.replace(op_range, new_op);
54-
}
55-
edit.replace(lhs.text_range(), rhs.text());
56-
edit.replace(rhs.text_range(), lhs.text());
48+
op_token.text_range(),
49+
|builder| {
50+
let mut editor = builder.make_editor(&expr.syntax().parent().unwrap());
51+
let make = SyntaxFactory::new();
52+
if let FlipAction::FlipAndReplaceOp(binary_op) = action {
53+
editor.replace(op_token, make.token(binary_op))
54+
};
55+
// FIXME: remove `clone_for_update` when `SyntaxEditor` handles it for us
56+
editor.replace(lhs.syntax(), rhs.syntax().clone_for_update());
57+
editor.replace(rhs.syntax(), lhs.syntax().clone_for_update());
58+
editor.add_mappings(make.finish_with_mappings());
59+
builder.add_file_edits(ctx.file_id(), editor);
5760
},
5861
)
5962
}
@@ -62,7 +65,7 @@ enum FlipAction {
6265
// Flip the expression
6366
Flip,
6467
// Flip the expression and replace the operator with this string
65-
FlipAndReplaceOp(&'static str),
68+
FlipAndReplaceOp(SyntaxKind),
6669
// Do not flip the expression
6770
DontFlip,
6871
}
@@ -73,10 +76,10 @@ impl From<ast::BinaryOp> for FlipAction {
7376
ast::BinaryOp::Assignment { .. } => FlipAction::DontFlip,
7477
ast::BinaryOp::CmpOp(ast::CmpOp::Ord { ordering, strict }) => {
7578
let rev_op = match (ordering, strict) {
76-
(ast::Ordering::Less, true) => ">",
77-
(ast::Ordering::Less, false) => ">=",
78-
(ast::Ordering::Greater, true) => "<",
79-
(ast::Ordering::Greater, false) => "<=",
79+
(ast::Ordering::Less, true) => T![>],
80+
(ast::Ordering::Less, false) => T![>=],
81+
(ast::Ordering::Greater, true) => T![<],
82+
(ast::Ordering::Greater, false) => T![<=],
8083
};
8184
FlipAction::FlipAndReplaceOp(rev_op)
8285
}

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

Lines changed: 79 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1-
use ide_db::base_db::SourceDatabase;
2-
use syntax::TextSize;
31
use syntax::{
4-
algo::non_trivia_sibling, ast, AstNode, Direction, SyntaxKind, SyntaxToken, TextRange, T,
2+
algo::non_trivia_sibling,
3+
ast::{self, syntax_factory::SyntaxFactory},
4+
syntax_editor::{Element, SyntaxMapping},
5+
AstNode, Direction, NodeOrToken, SyntaxElement, SyntaxKind, SyntaxToken, T,
56
};
67

78
use crate::{AssistContext, AssistId, AssistKind, Assists};
@@ -25,8 +26,6 @@ pub(crate) fn flip_comma(acc: &mut Assists, ctx: &AssistContext<'_>) -> Option<(
2526
let comma = ctx.find_token_syntax_at_offset(T![,])?;
2627
let prev = non_trivia_sibling(comma.clone().into(), Direction::Prev)?;
2728
let next = non_trivia_sibling(comma.clone().into(), Direction::Next)?;
28-
let (mut prev_text, mut next_text) = (prev.to_string(), next.to_string());
29-
let (mut prev_range, mut next_range) = (prev.text_range(), next.text_range());
3029

3130
// Don't apply a "flip" in case of a last comma
3231
// that typically comes before punctuation
@@ -40,53 +39,85 @@ pub(crate) fn flip_comma(acc: &mut Assists, ctx: &AssistContext<'_>) -> Option<(
4039
return None;
4140
}
4241

43-
if let Some(parent) = comma.parent().and_then(ast::TokenTree::cast) {
44-
// An attribute. It often contains a path followed by a token tree (e.g. `align(2)`), so we have
45-
// to be smarter.
46-
let prev_start =
47-
match comma.siblings_with_tokens(Direction::Prev).skip(1).find(|it| it.kind() == T![,])
48-
{
49-
Some(it) => position_after_token(it.as_token().unwrap()),
50-
None => position_after_token(&parent.left_delimiter_token()?),
51-
};
52-
let prev_end = prev.text_range().end();
53-
let next_start = next.text_range().start();
54-
let next_end =
55-
match comma.siblings_with_tokens(Direction::Next).skip(1).find(|it| it.kind() == T![,])
56-
{
57-
Some(it) => position_before_token(it.as_token().unwrap()),
58-
None => position_before_token(&parent.right_delimiter_token()?),
59-
};
60-
prev_range = TextRange::new(prev_start, prev_end);
61-
next_range = TextRange::new(next_start, next_end);
62-
let file_text = ctx.db().file_text(ctx.file_id().file_id());
63-
prev_text = file_text[prev_range].to_owned();
64-
next_text = file_text[next_range].to_owned();
65-
}
42+
// FIXME: remove `clone_for_update` when `SyntaxEditor` handles it for us
43+
let prev = match prev {
44+
SyntaxElement::Node(node) => node.clone_for_update().syntax_element(),
45+
_ => prev,
46+
};
47+
let next = match next {
48+
SyntaxElement::Node(node) => node.clone_for_update().syntax_element(),
49+
_ => next,
50+
};
6651

6752
acc.add(
6853
AssistId("flip_comma", AssistKind::RefactorRewrite),
6954
"Flip comma",
7055
comma.text_range(),
71-
|edit| {
72-
edit.replace(prev_range, next_text);
73-
edit.replace(next_range, prev_text);
56+
|builder| {
57+
let parent = comma.parent().unwrap();
58+
let mut editor = builder.make_editor(&parent);
59+
60+
if let Some(parent) = ast::TokenTree::cast(parent) {
61+
// An attribute. It often contains a path followed by a
62+
// token tree (e.g. `align(2)`), so we have to be smarter.
63+
let (new_tree, mapping) = flip_tree(parent.clone(), comma);
64+
editor.replace(parent.syntax(), new_tree.syntax());
65+
editor.add_mappings(mapping);
66+
} else {
67+
editor.replace(prev.clone(), next.clone());
68+
editor.replace(next.clone(), prev.clone());
69+
}
70+
71+
builder.add_file_edits(ctx.file_id(), editor);
7472
},
7573
)
7674
}
7775

78-
fn position_before_token(token: &SyntaxToken) -> TextSize {
79-
match non_trivia_sibling(token.clone().into(), Direction::Prev) {
80-
Some(prev_token) => prev_token.text_range().end(),
81-
None => token.text_range().start(),
82-
}
83-
}
84-
85-
fn position_after_token(token: &SyntaxToken) -> TextSize {
86-
match non_trivia_sibling(token.clone().into(), Direction::Next) {
87-
Some(prev_token) => prev_token.text_range().start(),
88-
None => token.text_range().end(),
89-
}
76+
fn flip_tree(tree: ast::TokenTree, comma: SyntaxToken) -> (ast::TokenTree, SyntaxMapping) {
77+
let mut tree_iter = tree.token_trees_and_tokens();
78+
let before: Vec<_> =
79+
tree_iter.by_ref().take_while(|it| it.as_token() != Some(&comma)).collect();
80+
let after: Vec<_> = tree_iter.collect();
81+
82+
let not_ws = |element: &NodeOrToken<_, SyntaxToken>| match element {
83+
NodeOrToken::Token(token) => token.kind() != SyntaxKind::WHITESPACE,
84+
NodeOrToken::Node(_) => true,
85+
};
86+
87+
let is_comma = |element: &NodeOrToken<_, SyntaxToken>| match element {
88+
NodeOrToken::Token(token) => token.kind() == T![,],
89+
NodeOrToken::Node(_) => false,
90+
};
91+
92+
let prev_start_untrimmed = match before.iter().rposition(is_comma) {
93+
Some(pos) => pos + 1,
94+
None => 1,
95+
};
96+
let prev_end = 1 + before.iter().rposition(not_ws).unwrap();
97+
let prev_start = prev_start_untrimmed
98+
+ before[prev_start_untrimmed..prev_end].iter().position(not_ws).unwrap();
99+
100+
let next_start = after.iter().position(not_ws).unwrap();
101+
let next_end_untrimmed = match after.iter().position(is_comma) {
102+
Some(pos) => pos,
103+
None => after.len() - 1,
104+
};
105+
let next_end = 1 + after[..next_end_untrimmed].iter().rposition(not_ws).unwrap();
106+
107+
let result = [
108+
&before[1..prev_start],
109+
&after[next_start..next_end],
110+
&before[prev_end..],
111+
&[NodeOrToken::Token(comma)],
112+
&after[..next_start],
113+
&before[prev_start..prev_end],
114+
&after[next_end..after.len() - 1],
115+
]
116+
.concat();
117+
118+
let make = SyntaxFactory::new();
119+
let new_token_tree = make.token_tree(tree.left_delimiter_token().unwrap().kind(), result);
120+
(new_token_tree, make.finish_with_mappings())
90121
}
91122

92123
#[cfg(test)]
@@ -147,4 +178,9 @@ mod tests {
147178
r#"#[foo(bar, qux, baz(1 + 1), other)] struct Foo;"#,
148179
);
149180
}
181+
182+
#[test]
183+
fn flip_comma_attribute_incomplete() {
184+
check_assist_not_applicable(flip_comma, r#"#[repr(align(2),$0)] struct Foo;"#);
185+
}
150186
}

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

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -23,21 +23,23 @@ pub(crate) fn flip_trait_bound(acc: &mut Assists, ctx: &AssistContext<'_>) -> Op
2323
let plus = ctx.find_token_syntax_at_offset(T![+])?;
2424

2525
// Make sure we're in a `TypeBoundList`
26-
ast::TypeBoundList::cast(plus.parent()?)?;
26+
let parent = ast::TypeBoundList::cast(plus.parent()?)?;
2727

2828
let (before, after) = (
29-
non_trivia_sibling(plus.clone().into(), Direction::Prev)?,
30-
non_trivia_sibling(plus.clone().into(), Direction::Next)?,
29+
non_trivia_sibling(plus.clone().into(), Direction::Prev)?.into_node()?,
30+
non_trivia_sibling(plus.clone().into(), Direction::Next)?.into_node()?,
3131
);
3232

3333
let target = plus.text_range();
3434
acc.add(
3535
AssistId("flip_trait_bound", AssistKind::RefactorRewrite),
3636
"Flip trait bounds",
3737
target,
38-
|edit| {
39-
edit.replace(before.text_range(), after.to_string());
40-
edit.replace(after.text_range(), before.to_string());
38+
|builder| {
39+
let mut editor = builder.make_editor(parent.syntax());
40+
editor.replace(before.clone(), after.clone_for_update());
41+
editor.replace(after.clone(), before.clone_for_update());
42+
builder.add_file_edits(ctx.file_id(), editor);
4143
},
4244
)
4345
}

crates/syntax/src/ast/syntax_factory/constructors.rs

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ use itertools::Itertools;
44
use crate::{
55
ast::{self, make, HasName, HasTypeBounds},
66
syntax_editor::SyntaxMappingBuilder,
7-
AstNode,
7+
AstNode, NodeOrToken, SyntaxKind, SyntaxNode, SyntaxToken,
88
};
99

1010
use super::SyntaxFactory;
@@ -80,6 +80,23 @@ impl SyntaxFactory {
8080
ast
8181
}
8282

83+
pub fn expr_bin(&self, lhs: ast::Expr, op: ast::BinaryOp, rhs: ast::Expr) -> ast::BinExpr {
84+
let ast::Expr::BinExpr(ast) =
85+
make::expr_bin_op(lhs.clone(), op, rhs.clone()).clone_for_update()
86+
else {
87+
unreachable!()
88+
};
89+
90+
if let Some(mut mapping) = self.mappings() {
91+
let mut builder = SyntaxMappingBuilder::new(ast.syntax().clone());
92+
builder.map_node(lhs.syntax().clone(), ast.lhs().unwrap().syntax().clone());
93+
builder.map_node(rhs.syntax().clone(), ast.rhs().unwrap().syntax().clone());
94+
builder.finish(&mut mapping);
95+
}
96+
97+
ast
98+
}
99+
83100
pub fn expr_path(&self, path: ast::Path) -> ast::Expr {
84101
let ast::Expr::PathExpr(ast) = make::expr_path(path.clone()).clone_for_update() else {
85102
unreachable!()
@@ -151,4 +168,34 @@ impl SyntaxFactory {
151168

152169
ast
153170
}
171+
172+
pub fn token_tree(
173+
&self,
174+
delimiter: SyntaxKind,
175+
tt: Vec<NodeOrToken<ast::TokenTree, SyntaxToken>>,
176+
) -> ast::TokenTree {
177+
let tt: Vec<_> = tt.into_iter().collect();
178+
let input: Vec<_> = tt.iter().cloned().filter_map(only_nodes).collect();
179+
180+
let ast = make::token_tree(delimiter, tt).clone_for_update();
181+
182+
if let Some(mut mapping) = self.mappings() {
183+
let mut builder = SyntaxMappingBuilder::new(ast.syntax().clone());
184+
builder.map_children(
185+
input.into_iter(),
186+
ast.token_trees_and_tokens().filter_map(only_nodes),
187+
);
188+
builder.finish(&mut mapping);
189+
}
190+
191+
return ast;
192+
193+
fn only_nodes(element: NodeOrToken<ast::TokenTree, SyntaxToken>) -> Option<SyntaxNode> {
194+
element.as_node().map(|it| it.syntax().clone())
195+
}
196+
}
197+
198+
pub fn token(&self, kind: SyntaxKind) -> SyntaxToken {
199+
make::token(kind)
200+
}
154201
}

0 commit comments

Comments
 (0)