From 1b85befbd9ffd44b8effc9607baec40a483c7d28 Mon Sep 17 00:00:00 2001 From: Lukas Wirth Date: Sun, 2 Mar 2025 08:03:01 +0100 Subject: [PATCH] Add flip or-pattern assist --- .../crates/ide-assists/src/assist_context.rs | 20 ++++- .../ide-assists/src/handlers/flip_comma.rs | 49 +++++------- .../src/handlers/flip_or_pattern.rs | 80 +++++++++++++++++++ .../src/handlers/flip_trait_bound.rs | 9 +-- .../crates/ide-assists/src/lib.rs | 2 + .../crates/ide-assists/src/tests/generated.rs | 17 ++++ .../docs/book/src/assists_generated.md | 24 +++++- 7 files changed, 159 insertions(+), 42 deletions(-) create mode 100644 src/tools/rust-analyzer/crates/ide-assists/src/handlers/flip_or_pattern.rs diff --git a/src/tools/rust-analyzer/crates/ide-assists/src/assist_context.rs b/src/tools/rust-analyzer/crates/ide-assists/src/assist_context.rs index 64e77b2d6982..b1189f0d0b06 100644 --- a/src/tools/rust-analyzer/crates/ide-assists/src/assist_context.rs +++ b/src/tools/rust-analyzer/crates/ide-assists/src/assist_context.rs @@ -52,6 +52,10 @@ pub(crate) struct AssistContext<'a> { frange: FileRange, trimmed_range: TextRange, source_file: SourceFile, + // We cache this here to speed up things slightly + token_at_offset: TokenAtOffset, + // We cache this here to speed up things slightly + covering_element: SyntaxElement, } impl<'a> AssistContext<'a> { @@ -78,8 +82,18 @@ impl<'a> AssistContext<'a> { // Selection solely consists of whitespace so just fall back to the original _ => frange.range, }; + let token_at_offset = source_file.syntax().token_at_offset(frange.range.start()); + let covering_element = source_file.syntax().covering_element(trimmed_range); - AssistContext { config, sema, frange, source_file, trimmed_range } + AssistContext { + config, + sema, + frange, + source_file, + trimmed_range, + token_at_offset, + covering_element, + } } pub(crate) fn db(&self) -> &RootDatabase { @@ -114,7 +128,7 @@ impl<'a> AssistContext<'a> { } pub(crate) fn token_at_offset(&self) -> TokenAtOffset { - self.source_file.syntax().token_at_offset(self.offset()) + self.token_at_offset.clone() } pub(crate) fn find_token_syntax_at_offset(&self, kind: SyntaxKind) -> Option { self.token_at_offset().find(|it| it.kind() == kind) @@ -136,7 +150,7 @@ impl<'a> AssistContext<'a> { } /// Returns the element covered by the selection range, this excludes trailing whitespace in the selection. pub(crate) fn covering_element(&self) -> SyntaxElement { - self.source_file.syntax().covering_element(self.selection_trimmed()) + self.covering_element.clone() } } diff --git a/src/tools/rust-analyzer/crates/ide-assists/src/handlers/flip_comma.rs b/src/tools/rust-analyzer/crates/ide-assists/src/handlers/flip_comma.rs index 95e035c05379..dd27269b001c 100644 --- a/src/tools/rust-analyzer/crates/ide-assists/src/handlers/flip_comma.rs +++ b/src/tools/rust-analyzer/crates/ide-assists/src/handlers/flip_comma.rs @@ -1,8 +1,8 @@ use syntax::{ algo::non_trivia_sibling, ast::{self, syntax_factory::SyntaxFactory}, - syntax_editor::{Element, SyntaxMapping}, - AstNode, Direction, NodeOrToken, SyntaxElement, SyntaxKind, SyntaxToken, T, + syntax_editor::SyntaxMapping, + AstNode, Direction, NodeOrToken, SyntaxKind, SyntaxToken, T, }; use crate::{AssistContext, AssistId, AssistKind, Assists}; @@ -39,37 +39,24 @@ pub(crate) fn flip_comma(acc: &mut Assists, ctx: &AssistContext<'_>) -> Option<( return None; } - let prev = match prev { - SyntaxElement::Node(node) => node.syntax_element(), - _ => prev, - }; - let next = match next { - SyntaxElement::Node(node) => node.syntax_element(), - _ => next, - }; + let target = comma.text_range(); + acc.add(AssistId("flip_comma", AssistKind::RefactorRewrite), "Flip comma", target, |builder| { + let parent = comma.parent().unwrap(); + let mut editor = builder.make_editor(&parent); - acc.add( - AssistId("flip_comma", AssistKind::RefactorRewrite), - "Flip comma", - comma.text_range(), - |builder| { - let parent = comma.parent().unwrap(); - let mut editor = builder.make_editor(&parent); + if let Some(parent) = ast::TokenTree::cast(parent) { + // An attribute. It often contains a path followed by a + // token tree (e.g. `align(2)`), so we have to be smarter. + let (new_tree, mapping) = flip_tree(parent.clone(), comma); + editor.replace(parent.syntax(), new_tree.syntax()); + editor.add_mappings(mapping); + } else { + editor.replace(prev.clone(), next.clone()); + editor.replace(next.clone(), prev.clone()); + } - if let Some(parent) = ast::TokenTree::cast(parent) { - // An attribute. It often contains a path followed by a - // token tree (e.g. `align(2)`), so we have to be smarter. - let (new_tree, mapping) = flip_tree(parent.clone(), comma); - editor.replace(parent.syntax(), new_tree.syntax()); - editor.add_mappings(mapping); - } else { - editor.replace(prev.clone(), next.clone()); - editor.replace(next.clone(), prev.clone()); - } - - builder.add_file_edits(ctx.file_id(), editor); - }, - ) + builder.add_file_edits(ctx.file_id(), editor); + }) } fn flip_tree(tree: ast::TokenTree, comma: SyntaxToken) -> (ast::TokenTree, SyntaxMapping) { diff --git a/src/tools/rust-analyzer/crates/ide-assists/src/handlers/flip_or_pattern.rs b/src/tools/rust-analyzer/crates/ide-assists/src/handlers/flip_or_pattern.rs new file mode 100644 index 000000000000..d9fa03e7191b --- /dev/null +++ b/src/tools/rust-analyzer/crates/ide-assists/src/handlers/flip_or_pattern.rs @@ -0,0 +1,80 @@ +use syntax::{ + algo::non_trivia_sibling, + ast::{self, AstNode}, + Direction, T, +}; + +use crate::{AssistContext, AssistId, AssistKind, Assists}; + +// Assist: flip_or_pattern +// +// Flips two patterns in an or-pattern. +// +// ``` +// fn foo() { +// let (a |$0 b) = 1; +// } +// ``` +// -> +// ``` +// fn foo() { +// let (b | a) = 1; +// } +// ``` +pub(crate) fn flip_or_pattern(acc: &mut Assists, ctx: &AssistContext<'_>) -> Option<()> { + // Only flip on the `|` token + let pipe = ctx.find_token_syntax_at_offset(T![|])?; + + let parent = ast::OrPat::cast(pipe.parent()?)?; + + let before = non_trivia_sibling(pipe.clone().into(), Direction::Prev)?.into_node()?; + let after = non_trivia_sibling(pipe.clone().into(), Direction::Next)?.into_node()?; + + let target = pipe.text_range(); + acc.add( + AssistId("flip_or_pattern", AssistKind::RefactorRewrite), + "Flip patterns", + target, + |builder| { + let mut editor = builder.make_editor(parent.syntax()); + editor.replace(before.clone(), after.clone()); + editor.replace(after, before); + builder.add_file_edits(ctx.file_id(), editor); + }, + ) +} + +#[cfg(test)] +mod tests { + use super::*; + + use crate::tests::{check_assist, check_assist_not_applicable, check_assist_target}; + + #[test] + fn flip_or_pattern_assist_available() { + check_assist_target(flip_or_pattern, "fn main(a |$0 b: ()) {}", "|") + } + + #[test] + fn flip_or_pattern_not_applicable_for_leading_pipe() { + check_assist_not_applicable(flip_or_pattern, "fn main(|$0 b: ()) {}") + } + + #[test] + fn flip_or_pattern_works() { + check_assist( + flip_or_pattern, + "fn foo() { let (a | b |$0 c | d) = 1; }", + "fn foo() { let (a | c | b | d) = 1; }", + ) + } + + #[test] + fn flip_or_pattern_works_match_guard() { + check_assist( + flip_or_pattern, + "fn foo() { match() { a |$0 b if true => () }}", + "fn foo() { match() { b | a if true => () }}", + ) + } +} diff --git a/src/tools/rust-analyzer/crates/ide-assists/src/handlers/flip_trait_bound.rs b/src/tools/rust-analyzer/crates/ide-assists/src/handlers/flip_trait_bound.rs index 298e5bd82c98..3528f5e81324 100644 --- a/src/tools/rust-analyzer/crates/ide-assists/src/handlers/flip_trait_bound.rs +++ b/src/tools/rust-analyzer/crates/ide-assists/src/handlers/flip_trait_bound.rs @@ -18,17 +18,14 @@ use crate::{AssistContext, AssistId, AssistKind, Assists}; // fn foo() { } // ``` pub(crate) fn flip_trait_bound(acc: &mut Assists, ctx: &AssistContext<'_>) -> Option<()> { - // We want to replicate the behavior of `flip_binexpr` by only suggesting - // the assist when the cursor is on a `+` + // Only flip on the `+` token let plus = ctx.find_token_syntax_at_offset(T![+])?; // Make sure we're in a `TypeBoundList` let parent = ast::TypeBoundList::cast(plus.parent()?)?; - let (before, after) = ( - non_trivia_sibling(plus.clone().into(), Direction::Prev)?.into_node()?, - non_trivia_sibling(plus.clone().into(), Direction::Next)?.into_node()?, - ); + let before = non_trivia_sibling(plus.clone().into(), Direction::Prev)?.into_node()?; + let after = non_trivia_sibling(plus.clone().into(), Direction::Next)?.into_node()?; let target = plus.text_range(); acc.add( diff --git a/src/tools/rust-analyzer/crates/ide-assists/src/lib.rs b/src/tools/rust-analyzer/crates/ide-assists/src/lib.rs index 179742f91b4d..448bcadb8ef2 100644 --- a/src/tools/rust-analyzer/crates/ide-assists/src/lib.rs +++ b/src/tools/rust-analyzer/crates/ide-assists/src/lib.rs @@ -149,6 +149,7 @@ mod handlers { mod fix_visibility; mod flip_binexpr; mod flip_comma; + mod flip_or_pattern; mod flip_trait_bound; mod generate_constant; mod generate_default_from_enum_variant; @@ -279,6 +280,7 @@ mod handlers { fix_visibility::fix_visibility, flip_binexpr::flip_binexpr, flip_comma::flip_comma, + flip_or_pattern::flip_or_pattern, flip_trait_bound::flip_trait_bound, generate_constant::generate_constant, generate_default_from_enum_variant::generate_default_from_enum_variant, diff --git a/src/tools/rust-analyzer/crates/ide-assists/src/tests/generated.rs b/src/tools/rust-analyzer/crates/ide-assists/src/tests/generated.rs index 74ae126adae8..91c1a3e1bd79 100644 --- a/src/tools/rust-analyzer/crates/ide-assists/src/tests/generated.rs +++ b/src/tools/rust-analyzer/crates/ide-assists/src/tests/generated.rs @@ -1195,6 +1195,23 @@ fn main() { ) } +#[test] +fn doctest_flip_or_pattern() { + check_doc_test( + "flip_or_pattern", + r#####" +fn foo() { + let (a |$0 b) = 1; +} +"#####, + r#####" +fn foo() { + let (b | a) = 1; +} +"#####, + ) +} + #[test] fn doctest_flip_trait_bound() { check_doc_test( diff --git a/src/tools/rust-analyzer/docs/book/src/assists_generated.md b/src/tools/rust-analyzer/docs/book/src/assists_generated.md index 2d233ca62ad6..8efc7cbabe9c 100644 --- a/src/tools/rust-analyzer/docs/book/src/assists_generated.md +++ b/src/tools/rust-analyzer/docs/book/src/assists_generated.md @@ -257,7 +257,7 @@ fn main() { ### `apply_demorgan` -**Source:** [apply_demorgan.rs](https://github.com/rust-lang/rust-analyzer/blob/master/crates/ide-assists/src/handlers/apply_demorgan.rs#L16) +**Source:** [apply_demorgan.rs](https://github.com/rust-lang/rust-analyzer/blob/master/crates/ide-assists/src/handlers/apply_demorgan.rs#L23) Apply [De Morgan's law](https://en.wikipedia.org/wiki/De_Morgan%27s_laws). This transforms expressions of the form `!l || !r` into `!(l && r)`. @@ -280,7 +280,7 @@ fn main() { ### `apply_demorgan_iterator` -**Source:** [apply_demorgan.rs](https://github.com/rust-lang/rust-analyzer/blob/master/crates/ide-assists/src/handlers/apply_demorgan.rs#L147) +**Source:** [apply_demorgan.rs](https://github.com/rust-lang/rust-analyzer/blob/master/crates/ide-assists/src/handlers/apply_demorgan.rs#L154) Apply [De Morgan's law](https://en.wikipedia.org/wiki/De_Morgan%27s_laws) to `Iterator::all` and `Iterator::any`. @@ -1345,6 +1345,26 @@ fn main() { ``` +### `flip_or_pattern` +**Source:** [flip_or_pattern.rs](https://github.com/rust-lang/rust-analyzer/blob/master/crates/ide-assists/src/handlers/flip_or_pattern.rs#L9) + +Flips two trait bounds. + +#### Before +```rust +fn foo() { + let (a |┃ b) = 1; +} +``` + +#### After +```rust +fn foo() { + let (b | a) = 1; +} +``` + + ### `flip_trait_bound` **Source:** [flip_trait_bound.rs](https://github.com/rust-lang/rust-analyzer/blob/master/crates/ide-assists/src/handlers/flip_trait_bound.rs#L9)