diff --git a/crates/ra_assists/src/doc_tests/generated.rs b/crates/ra_assists/src/doc_tests/generated.rs index 543224232f5..0848ab6bc25 100644 --- a/crates/ra_assists/src/doc_tests/generated.rs +++ b/crates/ra_assists/src/doc_tests/generated.rs @@ -607,6 +607,32 @@ fn handle(action: Action) { ) } +#[test] +fn doctest_replace_let_with_if_let() { + check( + "replace_let_with_if_let", + r#####" +enum Option { Some(T), None } + +fn main(action: Action) { + <|>let x = compute(); +} + +fn compute() -> Option { None } +"#####, + r#####" +enum Option { Some(T), None } + +fn main(action: Action) { + if let Some(x) = compute() { + } +} + +fn compute() -> Option { None } +"#####, + ) +} + #[test] fn doctest_replace_qualified_name_with_use() { check( diff --git a/crates/ra_assists/src/handlers/replace_let_with_if_let.rs b/crates/ra_assists/src/handlers/replace_let_with_if_let.rs new file mode 100644 index 00000000000..10e41f97e37 --- /dev/null +++ b/crates/ra_assists/src/handlers/replace_let_with_if_let.rs @@ -0,0 +1,108 @@ +use hir::Adt; +use ra_syntax::{ + ast::{self, make}, + AstNode, T, +}; + +use crate::{ + assist_ctx::{Assist, AssistCtx}, + AssistId, +}; +use ast::edit::{AstNodeEdit, IndentLevel}; +use std::iter::once; + +// Assist: replace_let_with_if_let +// +// Replaces `if let` with an else branch with a `match` expression. +// +// ``` +// # enum Option { Some(T), None } +// +// fn main(action: Action) { +// <|>let x = compute(); +// } +// +// fn compute() -> Option { None } +// ``` +// -> +// ``` +// # enum Option { Some(T), None } +// +// fn main(action: Action) { +// if let Some(x) = compute() { +// } +// } +// +// fn compute() -> Option { None } +// ``` +pub(crate) fn replace_let_with_if_let(ctx: AssistCtx) -> Option { + let let_kw = ctx.find_token_at_offset(T![let])?; + let let_stmt = let_kw.ancestors().find_map(ast::LetStmt::cast)?; + let init = let_stmt.initializer()?; + let original_pat = let_stmt.pat()?; + let ty = ctx.sema.type_of_expr(&init)?; + let enum_ = match ty.as_adt() { + Some(Adt::Enum(it)) => it, + _ => return None, + }; + let happy_case = + [("Result", "Ok"), ("Option", "Some")].iter().find_map(|(known_type, happy_case)| { + if &enum_.name(ctx.db).to_string() == known_type { + return Some(happy_case); + } + None + }); + + ctx.add_assist(AssistId("replace_let_with_if_let"), "Replace with if-let", |edit| { + let with_placeholder: ast::Pat = match happy_case { + None => make::placeholder_pat().into(), + Some(var_name) => make::tuple_struct_pat( + make::path_unqualified(make::path_segment(make::name_ref(var_name))), + once(make::placeholder_pat().into()), + ) + .into(), + }; + let block = + IndentLevel::from_node(let_stmt.syntax()).increase_indent(make::block_expr(None, None)); + let if_ = make::expr_if(make::condition(init, Some(with_placeholder)), block); + let stmt = make::expr_stmt(if_); + + let placeholder = stmt.syntax().descendants().find_map(ast::PlaceholderPat::cast).unwrap(); + let target_offset = + let_stmt.syntax().text_range().start() + placeholder.syntax().text_range().start(); + let stmt = stmt.replace_descendant(placeholder.into(), original_pat); + + edit.replace_ast(ast::Stmt::from(let_stmt), ast::Stmt::from(stmt)); + edit.target(let_kw.text_range()); + edit.set_cursor(target_offset); + }) +} + +#[cfg(test)] +mod tests { + use crate::helpers::check_assist; + + use super::*; + + #[test] + fn replace_let_unknown_enum() { + check_assist( + replace_let_with_if_let, + r" +enum E { X(T), Y(T) } + +fn main() { + <|>let x = E::X(92); +} + ", + r" +enum E { X(T), Y(T) } + +fn main() { + if let <|>x = E::X(92) { + } +} + ", + ) + } +} diff --git a/crates/ra_assists/src/lib.rs b/crates/ra_assists/src/lib.rs index becd5e99da6..fa1f3dd26f4 100644 --- a/crates/ra_assists/src/lib.rs +++ b/crates/ra_assists/src/lib.rs @@ -118,6 +118,7 @@ mod handlers { mod remove_dbg; mod remove_mut; mod replace_if_let_with_match; + mod replace_let_with_if_let; mod replace_qualified_name_with_use; mod replace_unwrap_with_match; mod split_import; @@ -154,6 +155,7 @@ pub(crate) fn all() -> &'static [AssistHandler] { remove_dbg::remove_dbg, remove_mut::remove_mut, replace_if_let_with_match::replace_if_let_with_match, + replace_let_with_if_let::replace_let_with_if_let, replace_qualified_name_with_use::replace_qualified_name_with_use, replace_unwrap_with_match::replace_unwrap_with_match, split_import::split_import, diff --git a/crates/ra_syntax/src/ast/edit.rs b/crates/ra_syntax/src/ast/edit.rs index f74c9f9c62d..bdaecdc43d4 100644 --- a/crates/ra_syntax/src/ast/edit.rs +++ b/crates/ra_syntax/src/ast/edit.rs @@ -251,7 +251,7 @@ impl ast::UseItem { #[must_use] pub fn with_use_tree(&self, use_tree: ast::UseTree) -> ast::UseItem { if let Some(old) = self.use_tree() { - return self.replace_descendants(iter::once((old, use_tree))); + return self.replace_descendant(old, use_tree); } self.clone() } @@ -283,7 +283,7 @@ impl ast::UseTree { #[must_use] pub fn with_path(&self, path: ast::Path) -> ast::UseTree { if let Some(old) = self.path() { - return self.replace_descendants(iter::once((old, path))); + return self.replace_descendant(old, path); } self.clone() } @@ -291,7 +291,7 @@ pub fn with_path(&self, path: ast::Path) -> ast::UseTree { #[must_use] pub fn with_use_tree_list(&self, use_tree_list: ast::UseTreeList) -> ast::UseTree { if let Some(old) = self.use_tree_list() { - return self.replace_descendants(iter::once((old, use_tree_list))); + return self.replace_descendant(old, use_tree_list); } self.clone() } @@ -465,6 +465,11 @@ fn replace_children( Self::cast(new_syntax).unwrap() } + #[must_use] + fn replace_descendant(&self, old: D, new: D) -> Self { + self.replace_descendants(iter::once((old, new))) + } + #[must_use] fn replace_descendants( &self, diff --git a/crates/ra_syntax/src/ast/make.rs b/crates/ra_syntax/src/ast/make.rs index 69bacf224fc..4621c98880e 100644 --- a/crates/ra_syntax/src/ast/make.rs +++ b/crates/ra_syntax/src/ast/make.rs @@ -127,7 +127,7 @@ pub fn condition(expr: ast::Expr, pattern: Option) -> ast::Condition { match pattern { None => ast_from_text(&format!("const _: () = while {} {{}};", expr)), Some(pattern) => { - ast_from_text(&format!("const _: () = while {} = {} {{}};", pattern, expr)) + ast_from_text(&format!("const _: () = while let {} = {} {{}};", pattern, expr)) } } } @@ -245,7 +245,8 @@ pub fn let_stmt(pattern: ast::Pat, initializer: Option) -> ast::LetSt ast_from_text(&format!("fn f() {{ {} }}", text)) } pub fn expr_stmt(expr: ast::Expr) -> ast::ExprStmt { - ast_from_text(&format!("fn f() {{ {}; }}", expr)) + let semi = if expr.is_block_like() { "" } else { ";" }; + ast_from_text(&format!("fn f() {{ {}{} (); }}", expr, semi)) } pub fn token(kind: SyntaxKind) -> SyntaxToken { diff --git a/docs/user/assists.md b/docs/user/assists.md index b2568a954f3..2495af20ab8 100644 --- a/docs/user/assists.md +++ b/docs/user/assists.md @@ -583,6 +583,29 @@ fn handle(action: Action) { } ``` +## `replace_let_with_if_let` + +Replaces `if let` with an else branch with a `match` expression. + +```rust +// BEFORE + +fn main(action: Action) { + ┃let x = compute(); +} + +fn compute() -> Option { None } + +// AFTER + +fn main(action: Action) { + if let Some(x) = compute() { + } +} + +fn compute() -> Option { None } +``` + ## `replace_qualified_name_with_use` Adds a use statement for a given fully-qualified name.