Repository: rust-lang/rust-analyzer
Pull request #13248 (2022-09-17 08:31:15)

Feature: Add assist to unwrap tuple declarations
> Implement #12923 for only tuples.
> 
> Does not implement unwrapping for structs, as mentioned in the issue.

Add assist to unwrap tuples declarations to separate declarations.

```rust
fn main() {
	$0let (foo, bar, baz) = (1.0, "example", String::new())
}
```

becomes:

```rust
fn main() {
	let foo = 1.0;
	let bar = "example";
	let baz = String::new();
}
```

## Changelog

### Feature

- Added assist to unwrap tuple declarations.


Patch:
diff --git a/crates/ide-assists/src/handlers/unwrap_tuple.rs b/crates/ide-assists/src/handlers/unwrap_tuple.rs
new file mode 100644
index 0000000000..25c58d086e
--- /dev/null
+++ b/crates/ide-assists/src/handlers/unwrap_tuple.rs
@@ -0,0 +1,159 @@
+use syntax::{
+    ast::{self, edit::AstNodeEdit},
+    AstNode, T,
+};
+
+use crate::{AssistContext, AssistId, AssistKind, Assists};
+
+// Assist: unwrap_tuple
+//
+// Unwrap the tuple to different variables.
+//
+// ```
+// # //- minicore: result
+// fn main() {
+//     $0let (foo, bar) = ("Foo", "Bar");
+// }
+// ```
+// ->
+// ```
+// fn main() {
+//     let foo = "Foo";
+//     let bar = "Bar";
+// }
+// ```
+pub(crate) fn unwrap_tuple(acc: &mut Assists, ctx: &AssistContext<'_>) -> Option<()> {
+    let let_kw = ctx.find_token_syntax_at_offset(T![let])?;
+    let let_stmt = let_kw.parent().and_then(ast::LetStmt::cast)?;
+    let indent_level = let_stmt.indent_level().0 as usize;
+    let pat = let_stmt.pat()?;
+    let ty = let_stmt.ty();
+    let init = let_stmt.initializer()?;
+
+    // This only applies for tuple patterns, types, and initializers.
+    let tuple_pat = match pat {
+        ast::Pat::TuplePat(pat) => pat,
+        _ => return None,
+    };
+    let tuple_ty = ty.and_then(|it| match it {
+        ast::Type::TupleType(ty) => Some(ty),
+        _ => None,
+    });
+    let tuple_init = match init {
+        ast::Expr::TupleExpr(expr) => expr,
+        _ => return None,
+    };
+
+    if tuple_pat.fields().count() != tuple_init.fields().count() {
+        return None;
+    }
+    if let Some(tys) = &tuple_ty {
+        if tuple_pat.fields().count() != tys.fields().count() {
+            return None;
+        }
+    }
+
+    let parent = let_kw.parent()?;
+
+    acc.add(
+        AssistId("unwrap_tuple", AssistKind::RefactorRewrite),
+        "Unwrap tuple",
+        let_kw.text_range(),
+        |edit| {
+            let indents = "    ".repeat(indent_level);
+
+            // If there is an ascribed type, insert that type for each declaration,
+            // otherwise, omit that type.
+            if let Some(tys) = tuple_ty {
+                let mut zipped_decls = String::new();
+                for (pat, ty, expr) in
+                    itertools::izip!(tuple_pat.fields(), tys.fields(), tuple_init.fields())
+                {
+                    zipped_decls.push_str(&format!("{}let {pat}: {ty} = {expr};\n", indents))
+                }
+                edit.replace(parent.text_range(), zipped_decls.trim());
+            } else {
+                let mut zipped_decls = String::new();
+                for (pat, expr) in itertools::izip!(tuple_pat.fields(), tuple_init.fields()) {
+                    zipped_decls.push_str(&format!("{}let {pat} = {expr};\n", indents));
+                }
+                edit.replace(parent.text_range(), zipped_decls.trim());
+            }
+        },
+    )
+}
+
+#[cfg(test)]
+mod tests {
+    use crate::tests::check_assist;
+
+    use super::*;
+
+    #[test]
+    fn unwrap_tuples() {
+        check_assist(
+            unwrap_tuple,
+            r#"
+fn main() {
+    $0let (foo, bar) = ("Foo", "Bar");
+}
+"#,
+            r#"
+fn main() {
+    let foo = "Foo";
+    let bar = "Bar";
+}
+"#,
+        );
+
+        check_assist(
+            unwrap_tuple,
+            r#"
+fn main() {
+    $0let (foo, bar, baz) = ("Foo", "Bar", "Baz");
+}
+"#,
+            r#"
+fn main() {
+    let foo = "Foo";
+    let bar = "Bar";
+    let baz = "Baz";
+}
+"#,
+        );
+    }
+
+    #[test]
+    fn unwrap_tuple_with_types() {
+        check_assist(
+            unwrap_tuple,
+            r#"
+fn main() {
+    $0let (foo, bar): (u8, i32) = (5, 10);
+}
+"#,
+            r#"
+fn main() {
+    let foo: u8 = 5;
+    let bar: i32 = 10;
+}
+"#,
+        );
+
+        check_assist(
+            unwrap_tuple,
+            r#"
+fn main() {
+    $0let (foo, bar, baz): (u8, i32, f64) = (5, 10, 17.5);
+}
+"#,
+            r#"
+fn main() {
+    let foo: u8 = 5;
+    let bar: i32 = 10;
+    let baz: f64 = 17.5;
+}
+"#,
+        );
+    }
+}
diff --git a/crates/ide-assists/src/lib.rs b/crates/ide-assists/src/lib.rs
index 812d22efbd..82bcc3dfa5 100644
--- a/crates/ide-assists/src/lib.rs
+++ b/crates/ide-assists/src/lib.rs
@@ -189,6 +189,7 @@ mod handlers {
     mod replace_turbofish_with_explicit_type;
     mod split_import;
     mod unmerge_match_arm;
+    mod unwrap_tuple;
     mod sort_items;
     mod toggle_ignore;
     mod unmerge_use;
@@ -291,6 +292,7 @@ mod handlers {
             unnecessary_async::unnecessary_async,
             unwrap_block::unwrap_block,
             unwrap_result_return_type::unwrap_result_return_type,
+            unwrap_tuple::unwrap_tuple,
             wrap_return_type_in_result::wrap_return_type_in_result,
             // These are manually sorted for better priorities. By default,
             // priority is determined by the size of the target range (smaller


Test patch:
diff --git a/crates/ide-assists/src/tests/generated.rs b/crates/ide-assists/src/tests/generated.rs
index 3a696635af..d403f86c6d 100644
--- a/crates/ide-assists/src/tests/generated.rs
+++ b/crates/ide-assists/src/tests/generated.rs
@@ -2386,6 +2386,25 @@ fn foo() -> i32 { 42i32 }
     )
 }
 
+#[test]
+fn doctest_unwrap_tuple() {
+    check_doc_test(
+        "unwrap_tuple",
+        r#####"
+//- minicore: result
+fn main() {
+    $0let (foo, bar) = ("Foo", "Bar");
+}
+"#####,
+        r#####"
+fn main() {
+    let foo = "Foo";
+    let bar = "Bar";
+}
+"#####,
+    )
+}
+
 #[test]
 fn doctest_wrap_return_type_in_result() {
     check_doc_test(