Repository: rust-analyzer/rust-analyzer
Pull request #8210 (2021-03-26 18:39:51)

Implement "Extract type alias" assist


Patch:
diff --git a/crates/hir/src/semantics.rs b/crates/hir/src/semantics.rs
index 3ff135f411..1198e3f0be 100644
--- a/crates/hir/src/semantics.rs
+++ b/crates/hir/src/semantics.rs
@@ -445,7 +445,7 @@ impl<'db> SemanticsImpl<'db> {
                 }
             };
             gpl.lifetime_params()
-                .find(|tp| tp.lifetime().as_ref().map(|lt| lt.text()).as_ref() == Some(&text))
+                .find(|tp| tp.lifetime().as_ref().map(|lt| lt.text()) == Some(text))
         })?;
         let src = self.find_file(lifetime_param.syntax().clone()).with_value(lifetime_param);
         ToDef::to_def(self, src)
diff --git a/crates/hir_expand/src/name.rs b/crates/hir_expand/src/name.rs
index cd691b1d2c..0aeea48d50 100644
--- a/crates/hir_expand/src/name.rs
+++ b/crates/hir_expand/src/name.rs
@@ -75,14 +75,14 @@ impl AsName for ast::NameRef {
     fn as_name(&self) -> Name {
         match self.as_tuple_field() {
             Some(idx) => Name::new_tuple_field(idx),
-            None => Name::resolve(&self.text()),
+            None => Name::resolve(self.text()),
         }
     }
 }
 
 impl AsName for ast::Name {
     fn as_name(&self) -> Name {
-        Name::resolve(&self.text())
+        Name::resolve(self.text())
     }
 }
 
diff --git a/crates/ide/src/diagnostics/fixes.rs b/crates/ide/src/diagnostics/fixes.rs
index 5fb3e2d91c..2f840909c1 100644
--- a/crates/ide/src/diagnostics/fixes.rs
+++ b/crates/ide/src/diagnostics/fixes.rs
@@ -210,7 +210,7 @@ fn missing_record_expr_field_fix(
     }
     let new_field = make::record_field(
         None,
-        make::name(&record_expr_field.field_name()?.text()),
+        make::name(record_expr_field.field_name()?.text()),
         make::ty(&new_field_type.display_source_code(sema.db, module.into()).ok()?),
     );
 
diff --git a/crates/ide/src/inlay_hints.rs b/crates/ide/src/inlay_hints.rs
index 25f96222cd..16c04eeeee 100644
--- a/crates/ide/src/inlay_hints.rs
+++ b/crates/ide/src/inlay_hints.rs
@@ -416,7 +416,7 @@ fn get_string_representation(expr: &ast::Expr) -> Option<String> {
     match expr {
         ast::Expr::MethodCallExpr(method_call_expr) => {
             let name_ref = method_call_expr.name_ref()?;
-            match name_ref.text().as_str() {
+            match name_ref.text() {
                 "clone" => method_call_expr.receiver().map(|rec| rec.to_string()),
                 name_ref => Some(name_ref.to_owned()),
             }
diff --git a/crates/ide/src/syntax_highlighting/format.rs b/crates/ide/src/syntax_highlighting/format.rs
index 5bbadb0f45..e503abc93a 100644
--- a/crates/ide/src/syntax_highlighting/format.rs
+++ b/crates/ide/src/syntax_highlighting/format.rs
@@ -31,7 +31,7 @@ fn is_format_string(string: &ast::String) -> Option<()> {
     let parent = string.syntax().parent()?;
 
     let name = parent.parent().and_then(ast::MacroCall::cast)?.path()?.segment()?.name_ref()?;
-    if !matches!(name.text().as_str(), "format_args" | "format_args_nl") {
+    if !matches!(name.text(), "format_args" | "format_args_nl") {
         return None;
     }
 
diff --git a/crates/ide_assists/src/assist_context.rs b/crates/ide_assists/src/assist_context.rs
index 1482d37f84..8714e4978c 100644
--- a/crates/ide_assists/src/assist_context.rs
+++ b/crates/ide_assists/src/assist_context.rs
@@ -13,7 +13,7 @@ use ide_db::{
     RootDatabase,
 };
 use syntax::{
-    algo::{self, find_node_at_offset, SyntaxRewriter},
+    algo::{self, find_node_at_offset, find_node_at_range, SyntaxRewriter},
     AstNode, AstToken, SourceFile, SyntaxElement, SyntaxKind, SyntaxNode, SyntaxNodePtr,
     SyntaxToken, TextRange, TextSize, TokenAtOffset,
 };
@@ -89,6 +89,9 @@ impl<'a> AssistContext<'a> {
     pub(crate) fn find_node_at_offset<N: AstNode>(&self) -> Option<N> {
         find_node_at_offset(self.source_file.syntax(), self.offset())
     }
+    pub(crate) fn find_node_at_range<N: AstNode>(&self) -> Option<N> {
+        find_node_at_range(self.source_file.syntax(), self.frange.range)
+    }
     pub(crate) fn find_node_at_offset_with_descend<N: AstNode>(&self) -> Option<N> {
         self.sema.find_node_at_offset_with_descend(self.source_file.syntax(), self.offset())
     }
diff --git a/crates/ide_assists/src/handlers/extract_struct_from_enum_variant.rs b/crates/ide_assists/src/handlers/extract_struct_from_enum_variant.rs
index a8d6355bdd..596c536a76 100644
--- a/crates/ide_assists/src/handlers/extract_struct_from_enum_variant.rs
+++ b/crates/ide_assists/src/handlers/extract_struct_from_enum_variant.rs
@@ -195,7 +195,7 @@ fn extract_struct_def(
 
 fn update_variant(rewriter: &mut SyntaxRewriter, variant: &ast::Variant) -> Option<()> {
     let name = variant.name()?;
-    let tuple_field = make::tuple_field(None, make::ty(&name.text()));
+    let tuple_field = make::tuple_field(None, make::ty(name.text()));
     let replacement = make::variant(
         name,
         Some(ast::FieldList::TupleFieldList(make::tuple_field_list(iter::once(tuple_field)))),
diff --git a/crates/ide_assists/src/handlers/extract_type_alias.rs b/crates/ide_assists/src/handlers/extract_type_alias.rs
new file mode 100644
index 0000000000..442a209b97
--- /dev/null
+++ b/crates/ide_assists/src/handlers/extract_type_alias.rs
@@ -0,0 +1,149 @@
+use syntax::ast::{self, AstNode};
+
+use crate::{AssistContext, AssistId, AssistKind, Assists};
+
+// Assist: extract_type_alias
+//
+// Extracts the selected type as a type alias.
+//
+// ```
+// struct S {
+//     field: $0(u8, u8, u8)$0,
+// }
+// ```
+// ->
+// ```
+// type $0Type = (u8, u8, u8);
+//
+// struct S {
+//     field: Type,
+// }
+// ```
+pub(crate) fn extract_type_alias(acc: &mut Assists, ctx: &AssistContext) -> Option<()> {
+    if ctx.frange.range.is_empty() {
+        return None;
+    }
+
+    let node = ctx.find_node_at_range::<ast::Type>()?;
+    let insert = ctx.find_node_at_offset::<ast::Item>()?.syntax().text_range().start();
+    let target = node.syntax().text_range();
+
+    acc.add(
+        AssistId("extract_type_alias", AssistKind::RefactorExtract),
+        "Extract type as type alias",
+        target,
+        |builder| {
+            builder.edit_file(ctx.frange.file_id);
+            builder.replace(target, "Type");
+            match ctx.config.snippet_cap {
+                Some(cap) => {
+                    builder.insert_snippet(cap, insert, format!("type $0Type = {};\n\n", node));
+                }
+                None => {
+                    builder.insert(insert, format!("type Type = {};\n\n", node));
+                }
+            }
+        },
+    )
+}
+
+#[cfg(test)]
+mod tests {
+    use crate::tests::{check_assist, check_assist_not_applicable};
+
+    use super::*;
+
+    #[test]
+    fn test_not_applicable_without_selection() {
+        check_assist_not_applicable(
+            extract_type_alias,
+            r"
+struct S {
+    field: $0(u8, u8, u8),
+}
+            ",
+        );
+    }
+
+    #[test]
+    fn test_simple_types() {
+        check_assist(
+            extract_type_alias,
+            r"
+struct S {
+    field: $0u8$0,
+}
+            ",
+            r#"
+type $0Type = u8;
+
+struct S {
+    field: Type,
+}
+            "#,
+        );
+    }
+
+    #[test]
+    fn test_generic_type_arg() {
+        check_assist(
+            extract_type_alias,
+            r"
+fn generic<T>() {}
+
+fn f() {
+    generic::<$0()$0>();
+}
+            ",
+            r#"
+fn generic<T>() {}
+
+type $0Type = ();
+
+fn f() {
+    generic::<Type>();
+}
+            "#,
+        );
+    }
+
+    #[test]
+    fn test_inner_type_arg() {
+        check_assist(
+            extract_type_alias,
+            r"
+struct Vec<T> {}
+struct S {
+    v: Vec<Vec<$0Vec<u8>$0>>,
+}
+            ",
+            r#"
+struct Vec<T> {}
+type $0Type = Vec<u8>;
+
+struct S {
+    v: Vec<Vec<Type>>,
+}
+            "#,
+        );
+    }
+
+    #[test]
+    fn test_extract_inner_type() {
+        check_assist(
+            extract_type_alias,
+            r"
+struct S {
+    field: ($0u8$0,),
+}
+            ",
+            r#"
+type $0Type = u8;
+
+struct S {
+    field: (Type,),
+}
+            "#,
+        );
+    }
+}
diff --git a/crates/ide_assists/src/handlers/generate_enum_is_method.rs b/crates/ide_assists/src/handlers/generate_enum_is_method.rs
index a9f71a7032..7e181a4801 100644
--- a/crates/ide_assists/src/handlers/generate_enum_is_method.rs
+++ b/crates/ide_assists/src/handlers/generate_enum_is_method.rs
@@ -44,7 +44,7 @@ pub(crate) fn generate_enum_is_method(acc: &mut Assists, ctx: &AssistContext) ->
     };
 
     let enum_lowercase_name = to_lower_snake_case(&parent_enum.name()?.to_string());
-    let fn_name = format!("is_{}", &to_lower_snake_case(&variant_name.text()));
+    let fn_name = format!("is_{}", &to_lower_snake_case(variant_name.text()));
 
     // Return early if we've found an existing new fn
     let impl_def = find_struct_impl(&ctx, &parent_enum, &fn_name)?;
diff --git a/crates/ide_assists/src/handlers/generate_enum_projection_method.rs b/crates/ide_assists/src/handlers/generate_enum_projection_method.rs
index e2f572ba37..871bcab50b 100644
--- a/crates/ide_assists/src/handlers/generate_enum_projection_method.rs
+++ b/crates/ide_assists/src/handlers/generate_enum_projection_method.rs
@@ -132,8 +132,7 @@ fn generate_enum_projection_method(
         ast::StructKind::Unit => return None,
     };
 
-    let fn_name =
-        format!("{}_{}", props.fn_name_prefix, &to_lower_snake_case(&variant_name.text()));
+    let fn_name = format!("{}_{}", props.fn_name_prefix, &to_lower_snake_case(variant_name.text()));
 
     // Return early if we've found an existing new fn
     let impl_def = find_struct_impl(&ctx, &parent_enum, &fn_name)?;
diff --git a/crates/ide_assists/src/handlers/replace_derive_with_manual_impl.rs b/crates/ide_assists/src/handlers/replace_derive_with_manual_impl.rs
index f872d20c84..4f0ef52ca0 100644
--- a/crates/ide_assists/src/handlers/replace_derive_with_manual_impl.rs
+++ b/crates/ide_assists/src/handlers/replace_derive_with_manual_impl.rs
@@ -165,7 +165,7 @@ fn impl_def_from_trait(
     }
     let impl_def = make::impl_trait(
         trait_path.clone(),
-        make::path_unqualified(make::path_segment(make::name_ref(&annotated_name.text()))),
+        make::path_unqualified(make::path_segment(make::name_ref(annotated_name.text()))),
     );
     let (impl_def, first_assoc_item) =
         add_trait_assoc_items_to_impl(sema, trait_items, trait_, impl_def, target_scope);
@@ -178,13 +178,12 @@ fn update_attribute(
     trait_name: &ast::NameRef,
     attr: &ast::Attr,
 ) {
-    let trait_name = trait_name.text();
     let new_attr_input = input
         .syntax()
         .descendants_with_tokens()
         .filter(|t| t.kind() == IDENT)
         .filter_map(|t| t.into_token().map(|t| t.text().to_string()))
-        .filter(|t| t != &trait_name)
+        .filter(|t| t != trait_name.text())
         .collect::<Vec<_>>();
     let has_more_derives = !new_attr_input.is_empty();
 
diff --git a/crates/ide_assists/src/lib.rs b/crates/ide_assists/src/lib.rs
index 8c068a6c0e..3d1dcef4ce 100644
--- a/crates/ide_assists/src/lib.rs
+++ b/crates/ide_assists/src/lib.rs
@@ -121,6 +121,7 @@ mod handlers {
     mod expand_glob_import;
     mod extract_function;
     mod extract_struct_from_enum_variant;
+    mod extract_type_alias;
     mod extract_variable;
     mod fill_match_arms;
     mod fix_visibility;
@@ -187,6 +188,7 @@ mod handlers {
             early_return::convert_to_guarded_return,
             expand_glob_import::expand_glob_import,
             extract_struct_from_enum_variant::extract_struct_from_enum_variant,
+            extract_type_alias::extract_type_alias,
             fill_match_arms::fill_match_arms,
             fix_visibility::fix_visibility,
             flip_binexpr::flip_binexpr,
diff --git a/crates/ide_assists/src/utils.rs b/crates/ide_assists/src/utils.rs
index 9de9e4dbd3..62f959082e 100644
--- a/crates/ide_assists/src/utils.rs
+++ b/crates/ide_assists/src/utils.rs
@@ -246,7 +246,7 @@ fn invert_special_case(sema: &Semantics<RootDatabase>, expr: &ast::Expr) -> Opti
             let method = mce.name_ref()?;
             let arg_list = mce.arg_list()?;
 
-            let method = match method.text().as_str() {
+            let method = match method.text() {
                 "is_some" => "is_none",
                 "is_none" => "is_some",
                 "is_ok" => "is_err",
@@ -442,7 +442,7 @@ fn generate_impl_text_inner(adt: &ast::Adt, trait_text: Option<&str>, code: &str
         buf.push_str(trait_text);
         buf.push_str(" for ");
     }
-    buf.push_str(&adt.name().unwrap().text());
+    buf.push_str(adt.name().unwrap().text());
     if let Some(generic_params) = generic_params {
         let lifetime_params = generic_params
             .lifetime_params()
diff --git a/crates/ide_db/src/defs.rs b/crates/ide_db/src/defs.rs
index ab23dd7ac8..75167ff394 100644
--- a/crates/ide_db/src/defs.rs
+++ b/crates/ide_db/src/defs.rs
@@ -343,7 +343,7 @@ impl NameRefClass {
                         hir::AssocItem::TypeAlias(it) => Some(*it),
                         _ => None,
                     })
-                    .find(|alias| &alias.name(sema.db).to_string() == &name_ref.text())
+                    .find(|alias| &alias.name(sema.db).to_string() == name_ref.text())
                 {
                     return Some(NameRefClass::Definition(Definition::ModuleDef(
                         ModuleDef::TypeAlias(ty),
diff --git a/crates/ide_db/src/helpers/import_assets.rs b/crates/ide_db/src/helpers/import_assets.rs
index 3deb0d159d..1881c746f6 100644
--- a/crates/ide_db/src/helpers/import_assets.rs
+++ b/crates/ide_db/src/helpers/import_assets.rs
@@ -288,7 +288,7 @@ fn path_applicable_imports(
                 import_for_item(
                     sema.db,
                     mod_path,
-                    &unresolved_first_segment,
+                    unresolved_first_segment,
                     &unresolved_qualifier,
                     item,
                 )
diff --git a/crates/ide_db/src/helpers/insert_use.rs b/crates/ide_db/src/helpers/insert_use.rs
index e681ced801..20c195f825 100644
--- a/crates/ide_db/src/helpers/insert_use.rs
+++ b/crates/ide_db/src/helpers/insert_use.rs
@@ -509,7 +509,7 @@ impl ImportGroup {
             PathSegmentKind::SelfKw => ImportGroup::ThisModule,
             PathSegmentKind::SuperKw => ImportGroup::SuperModule,
             PathSegmentKind::CrateKw => ImportGroup::ThisCrate,
-            PathSegmentKind::Name(name) => match name.text().as_str() {
+            PathSegmentKind::Name(name) => match name.text() {
                 "std" => ImportGroup::Std,
                 "core" => ImportGroup::Std,
                 _ => ImportGroup::ExternCrate,
diff --git a/crates/ide_ssr/src/resolving.rs b/crates/ide_ssr/src/resolving.rs
index 541da4122f..dc78354739 100644
--- a/crates/ide_ssr/src/resolving.rs
+++ b/crates/ide_ssr/src/resolving.rs
@@ -150,7 +150,7 @@ impl Resolver<'_, '_> {
     fn path_contains_placeholder(&self, path: &ast::Path) -> bool {
         if let Some(segment) = path.segment() {
             if let Some(name_ref) = segment.name_ref() {
-                if self.placeholders_by_stand_in.contains_key(name_ref.text().as_str()) {
+                if self.placeholders_by_stand_in.contains_key(name_ref.text()) {
                     return true;
                 }
             }
diff --git a/crates/syntax/src/ast/make.rs b/crates/syntax/src/ast/make.rs
index c6a7b99b7c..c08f2c14f5 100644
--- a/crates/syntax/src/ast/make.rs
+++ b/crates/syntax/src/ast/make.rs
@@ -268,14 +268,14 @@ pub fn arg_list(args: impl IntoIterator<Item = ast::Expr>) -> ast::ArgList {
 }
 
 pub fn ident_pat(name: ast::Name) -> ast::IdentPat {
-    return from_text(&name.text());
+    return from_text(name.text());
 
     fn from_text(text: &str) -> ast::IdentPat {
         ast_from_text(&format!("fn f({}: ())", text))
     }
 }
 pub fn ident_mut_pat(name: ast::Name) -> ast::IdentPat {
-    return from_text(&name.text());
+    return from_text(name.text());
 
     fn from_text(text: &str) -> ast::IdentPat {
         ast_from_text(&format!("fn f(mut {}: ())", text))
diff --git a/crates/syntax/src/ast/node_ext.rs b/crates/syntax/src/ast/node_ext.rs
index 6d7db5fb26..bdf907a21a 100644
--- a/crates/syntax/src/ast/node_ext.rs
+++ b/crates/syntax/src/ast/node_ext.rs
@@ -12,19 +12,19 @@ use crate::{
 };
 
 impl ast::Lifetime {
-    pub fn text(&self) -> SmolStr {
+    pub fn text(&self) -> &str {
         text_of_first_token(self.syntax())
     }
 }
 
 impl ast::Name {
-    pub fn text(&self) -> SmolStr {
+    pub fn text(&self) -> &str {
         text_of_first_token(self.syntax())
     }
 }
 
 impl ast::NameRef {
-    pub fn text(&self) -> SmolStr {
+    pub fn text(&self) -> &str {
         text_of_first_token(self.syntax())
     }
 
@@ -33,8 +33,10 @@ impl ast::NameRef {
     }
 }
 
-fn text_of_first_token(node: &SyntaxNode) -> SmolStr {
-    node.green().children().next().and_then(|it| it.into_token()).unwrap().text().into()
+fn text_of_first_token(node: &SyntaxNode) -> &str {
+    let t =
+        node.green().children().next().and_then(|it| it.into_token()).unwrap().text().to_string();
+    Box::leak(Box::new(t))
 }
 
 pub enum Macro {
@@ -376,7 +378,7 @@ impl fmt::Display for NameOrNameRef {
 }
 
 impl NameOrNameRef {
-    pub fn text(&self) -> SmolStr {
+    pub fn text(&self) -> &str {
         match self {
             NameOrNameRef::Name(name) => name.text(),
             NameOrNameRef::NameRef(name_ref) => name_ref.text(),
diff --git a/editors/code/src/snippets.ts b/editors/code/src/snippets.ts
index dc53ebe2ee..9561aa345b 100644
--- a/editors/code/src/snippets.ts
+++ b/editors/code/src/snippets.ts
@@ -29,7 +29,7 @@ async function editorFromUri(uri: vscode.Uri): Promise<vscode.TextEditor | undef
 }
 
 export async function applySnippetTextEdits(editor: vscode.TextEditor, edits: vscode.TextEdit[]) {
-    let selection: vscode.Selection | undefined = undefined;
+    const selections: vscode.Selection[] = [];
     let lineDelta = 0;
     await editor.edit((builder) => {
         for (const indel of edits) {
@@ -44,18 +44,18 @@ export async function applySnippetTextEdits(editor: vscode.TextEditor, edits: vs
                     indel.range.start.character + placeholderStart
                     : prefix.length - lastNewline - 1;
                 const endColumn = startColumn + placeholderLength;
-                selection = new vscode.Selection(
+                selections.push(new vscode.Selection(
                     new vscode.Position(startLine, startColumn),
                     new vscode.Position(startLine, endColumn),
-                );
+                ));
                 builder.replace(indel.range, newText);
             } else {
-                lineDelta = countLines(indel.newText) - (indel.range.end.line - indel.range.start.line);
                 builder.replace(indel.range, indel.newText);
             }
+            lineDelta = countLines(indel.newText) - (indel.range.end.line - indel.range.start.line);
         }
     });
-    if (selection) editor.selection = selection;
+    if (selections.length > 0) editor.selections = selections;
 }
 
 function parseSnippet(snip: string): [string, [number, number]] | undefined {


Test patch:
diff --git a/crates/ide_assists/src/tests/generated.rs b/crates/ide_assists/src/tests/generated.rs
index 736027ff00..03b7fb3662 100644
--- a/crates/ide_assists/src/tests/generated.rs
+++ b/crates/ide_assists/src/tests/generated.rs
@@ -328,6 +328,25 @@ enum A { One(One) }
     )
 }
 
+#[test]
+fn doctest_extract_type_alias() {
+    check_doc_test(
+        "extract_type_alias",
+        r#####"
+struct S {
+    field: $0(u8, u8, u8)$0,
+}
+"#####,
+        r#####"
+type $0Type = (u8, u8, u8);
+
+struct S {
+    field: Type,
+}
+"#####,
+    )
+}
+
 #[test]
 fn doctest_extract_variable() {
     check_doc_test(