about summary refs log tree commit diff
path: root/src
diff options
context:
space:
mode:
authorGiga Bowser <45986823+Giga-Bowser@users.noreply.github.com>2024-12-11 14:04:54 -0500
committerGiga Bowser <45986823+Giga-Bowser@users.noreply.github.com>2024-12-11 14:04:54 -0500
commita63defa6ed1bc6baef3a9b03f4d0fc601bf2eba6 (patch)
treec8e2188a08ec466e6d5be02dd7814056d57266b5 /src
parent2d54e06b36509e1c7d06d77ec3975fdcc8453034 (diff)
downloadrust-a63defa6ed1bc6baef3a9b03f4d0fc601bf2eba6.tar.gz
rust-a63defa6ed1bc6baef3a9b03f4d0fc601bf2eba6.zip
feat: Add an assist to extract an expression into a static
Diffstat (limited to 'src')
-rw-r--r--src/tools/rust-analyzer/crates/ide-assists/src/handlers/extract_variable.rs596
-rw-r--r--src/tools/rust-analyzer/crates/ide-assists/src/tests.rs192
-rw-r--r--src/tools/rust-analyzer/crates/ide-assists/src/tests/generated.rs18
3 files changed, 686 insertions, 120 deletions
diff --git a/src/tools/rust-analyzer/crates/ide-assists/src/handlers/extract_variable.rs b/src/tools/rust-analyzer/crates/ide-assists/src/handlers/extract_variable.rs
index 6670eac6a7a..a8d71ed7f4d 100644
--- a/src/tools/rust-analyzer/crates/ide-assists/src/handlers/extract_variable.rs
+++ b/src/tools/rust-analyzer/crates/ide-assists/src/handlers/extract_variable.rs
@@ -1,14 +1,12 @@
 use hir::{HirDisplay, TypeInfo};
-use ide_db::syntax_helpers::suggest_name;
+use ide_db::{assists::GroupLabel, syntax_helpers::suggest_name};
 use syntax::{
     ast::{
         self, edit::IndentLevel, edit_in_place::Indent, make, syntax_factory::SyntaxFactory,
         AstNode,
     },
     syntax_editor::Position,
-    NodeOrToken,
-    SyntaxKind::{self},
-    SyntaxNode, T,
+    NodeOrToken, SyntaxKind, SyntaxNode, T,
 };
 
 use crate::{utils::is_body_const, AssistContext, AssistId, AssistKind, Assists};
@@ -46,6 +44,23 @@ use crate::{utils::is_body_const, AssistContext, AssistId, AssistKind, Assists};
 //     VAR_NAME * 4;
 // }
 // ```
+
+// Assist: extract_static
+//
+// Extracts subexpression into a static.
+//
+// ```
+// fn main() {
+//     $0(1 + 2)$0 * 4;
+// }
+// ```
+// ->
+// ```
+// fn main() {
+//     static $0VAR_NAME: i32 = 1 + 2;
+//     VAR_NAME * 4;
+// }
+// ```
 pub(crate) fn extract_variable(acc: &mut Assists, ctx: &AssistContext<'_>) -> Option<()> {
     let node = if ctx.has_empty_selection() {
         if let Some(t) = ctx.token_at_offset().find(|it| it.kind() == T![;]) {
@@ -114,15 +129,20 @@ pub(crate) fn extract_variable(acc: &mut Assists, ctx: &AssistContext<'_>) -> Op
         let Some(anchor) = Anchor::from(&to_extract, kind) else {
             continue;
         };
+
         let ty_string = match kind {
-            ExtractionKind::Constant => {
+            ExtractionKind::Constant | ExtractionKind::Static => {
                 let Some(ty) = ty.clone() else {
                     continue;
                 };
 
                 // We can't mutably reference a const, nor can we define
                 // one using a non-const expression or one of unknown type
-                if needs_mut || !is_body_const(&ctx.sema, &to_extract_no_ref) || ty.is_unknown() {
+                if needs_mut
+                    || !is_body_const(&ctx.sema, &to_extract_no_ref)
+                    || ty.is_unknown()
+                    || ty.is_mutable_reference()
+                {
                     continue;
                 }
 
@@ -135,92 +155,111 @@ pub(crate) fn extract_variable(acc: &mut Assists, ctx: &AssistContext<'_>) -> Op
             _ => "".to_owned(),
         };
 
-        acc.add(kind.assist_id(), kind.label(), target, |edit| {
-            let (var_name, expr_replace) = kind.get_name_and_expr(ctx, &to_extract);
-
-            let make = SyntaxFactory::new();
-            let mut editor = edit.make_editor(&expr_replace);
+        acc.add_group(
+            &GroupLabel("Extract into...".to_owned()),
+            kind.assist_id(),
+            kind.label(),
+            target,
+            |edit| {
+                let (var_name, expr_replace) = kind.get_name_and_expr(ctx, &to_extract);
 
-            let pat_name = make.name(&var_name);
-            let name_expr = make.expr_path(make::ext::ident_path(&var_name));
+                let make = SyntaxFactory::new();
+                let mut editor = edit.make_editor(&expr_replace);
 
-            if let Some(cap) = ctx.config.snippet_cap {
-                let tabstop = edit.make_tabstop_before(cap);
-                editor.add_annotation(pat_name.syntax().clone(), tabstop);
-            }
+                let pat_name = make.name(&var_name);
+                let name_expr = make.expr_path(make::ext::ident_path(&var_name));
 
-            let initializer = match ty.as_ref().filter(|_| needs_ref) {
-                Some(receiver_type) if receiver_type.is_mutable_reference() => {
-                    make.expr_ref(to_extract_no_ref.clone(), true)
+                if let Some(cap) = ctx.config.snippet_cap {
+                    let tabstop = edit.make_tabstop_before(cap);
+                    editor.add_annotation(pat_name.syntax().clone(), tabstop);
                 }
-                Some(receiver_type) if receiver_type.is_reference() => {
-                    make.expr_ref(to_extract_no_ref.clone(), false)
-                }
-                _ => to_extract_no_ref.clone(),
-            };
 
-            let new_stmt: ast::Stmt = match kind {
-                ExtractionKind::Variable => {
-                    let ident_pat = make.ident_pat(false, needs_mut, pat_name);
-                    make.let_stmt(ident_pat.into(), None, Some(initializer)).into()
-                }
-                ExtractionKind::Constant => {
-                    let ast_ty = make.ty(&ty_string);
-                    ast::Item::Const(make.item_const(None, pat_name, ast_ty, initializer)).into()
-                }
-            };
-
-            match &anchor {
-                Anchor::Before(place) => {
-                    let prev_ws = place.prev_sibling_or_token().and_then(|it| it.into_token());
-                    let indent_to = IndentLevel::from_node(place);
-
-                    // Adjust ws to insert depending on if this is all inline or on separate lines
-                    let trailing_ws = if prev_ws.is_some_and(|it| it.text().starts_with('\n')) {
-                        format!("\n{indent_to}")
-                    } else {
-                        " ".to_owned()
-                    };
-
-                    editor.insert_all(
-                        Position::before(place),
-                        vec![
-                            new_stmt.syntax().clone().into(),
-                            make::tokens::whitespace(&trailing_ws).into(),
-                        ],
-                    );
-
-                    editor.replace(expr_replace, name_expr.syntax());
-                }
-                Anchor::Replace(stmt) => {
-                    cov_mark::hit!(test_extract_var_expr_stmt);
+                let initializer = match ty.as_ref().filter(|_| needs_ref) {
+                    Some(receiver_type) if receiver_type.is_mutable_reference() => {
+                        make.expr_ref(to_extract_no_ref.clone(), true)
+                    }
+                    Some(receiver_type) if receiver_type.is_reference() => {
+                        make.expr_ref(to_extract_no_ref.clone(), false)
+                    }
+                    _ => to_extract_no_ref.clone(),
+                };
+
+                let new_stmt: ast::Stmt = match kind {
+                    ExtractionKind::Variable => {
+                        let ident_pat = make.ident_pat(false, needs_mut, pat_name);
+                        make.let_stmt(ident_pat.into(), None, Some(initializer)).into()
+                    }
+                    ExtractionKind::Constant => {
+                        let ast_ty = make.ty(&ty_string);
+                        ast::Item::Const(make.item_const(None, pat_name, ast_ty, initializer))
+                            .into()
+                    }
+                    ExtractionKind::Static => {
+                        let ast_ty = make.ty(&ty_string);
+                        ast::Item::Static(make.item_static(
+                            None,
+                            false,
+                            false,
+                            pat_name,
+                            ast_ty,
+                            Some(initializer),
+                        ))
+                        .into()
+                    }
+                };
+
+                match &anchor {
+                    Anchor::Before(place) => {
+                        let prev_ws = place.prev_sibling_or_token().and_then(|it| it.into_token());
+                        let indent_to = IndentLevel::from_node(place);
+
+                        // Adjust ws to insert depending on if this is all inline or on separate lines
+                        let trailing_ws = if prev_ws.is_some_and(|it| it.text().starts_with('\n')) {
+                            format!("\n{indent_to}")
+                        } else {
+                            " ".to_owned()
+                        };
+
+                        editor.insert_all(
+                            Position::before(place),
+                            vec![
+                                new_stmt.syntax().clone().into(),
+                                make::tokens::whitespace(&trailing_ws).into(),
+                            ],
+                        );
 
-                    editor.replace(stmt.syntax(), new_stmt.syntax());
-                }
-                Anchor::WrapInBlock(to_wrap) => {
-                    let indent_to = to_wrap.indent_level();
-
-                    let block = if to_wrap.syntax() == &expr_replace {
-                        // Since `expr_replace` is the same that needs to be wrapped in a block,
-                        // we can just directly replace it with a block
-                        make.block_expr([new_stmt], Some(name_expr))
-                    } else {
-                        // `expr_replace` is a descendant of `to_wrap`, so we just replace it with `name_expr`.
                         editor.replace(expr_replace, name_expr.syntax());
-                        make.block_expr([new_stmt], Some(to_wrap.clone()))
-                    };
+                    }
+                    Anchor::Replace(stmt) => {
+                        cov_mark::hit!(test_extract_var_expr_stmt);
 
-                    editor.replace(to_wrap.syntax(), block.syntax());
+                        editor.replace(stmt.syntax(), new_stmt.syntax());
+                    }
+                    Anchor::WrapInBlock(to_wrap) => {
+                        let indent_to = to_wrap.indent_level();
+
+                        let block = if to_wrap.syntax() == &expr_replace {
+                            // Since `expr_replace` is the same that needs to be wrapped in a block,
+                            // we can just directly replace it with a block
+                            make.block_expr([new_stmt], Some(name_expr))
+                        } else {
+                            // `expr_replace` is a descendant of `to_wrap`, so we just replace it with `name_expr`.
+                            editor.replace(expr_replace, name_expr.syntax());
+                            make.block_expr([new_stmt], Some(to_wrap.clone()))
+                        };
 
-                    // fixup indentation of block
-                    block.indent(indent_to);
+                        editor.replace(to_wrap.syntax(), block.syntax());
+
+                        // fixup indentation of block
+                        block.indent(indent_to);
+                    }
                 }
-            }
 
-            editor.add_mappings(make.finish_with_mappings());
-            edit.add_file_edits(ctx.file_id(), editor);
-            edit.rename();
-        });
+                editor.add_mappings(make.finish_with_mappings());
+                edit.add_file_edits(ctx.file_id(), editor);
+                edit.rename();
+            },
+        );
     }
 
     Some(())
@@ -251,15 +290,18 @@ fn valid_target_expr(node: SyntaxNode) -> Option<ast::Expr> {
 enum ExtractionKind {
     Variable,
     Constant,
+    Static,
 }
 
 impl ExtractionKind {
-    const ALL: &'static [ExtractionKind] = &[ExtractionKind::Variable, ExtractionKind::Constant];
+    const ALL: &'static [ExtractionKind] =
+        &[ExtractionKind::Variable, ExtractionKind::Constant, ExtractionKind::Static];
 
     fn assist_id(&self) -> AssistId {
         let s = match self {
             ExtractionKind::Variable => "extract_variable",
             ExtractionKind::Constant => "extract_constant",
+            ExtractionKind::Static => "extract_static",
         };
 
         AssistId(s, AssistKind::RefactorExtract)
@@ -269,6 +311,7 @@ impl ExtractionKind {
         match self {
             ExtractionKind::Variable => "Extract into variable",
             ExtractionKind::Constant => "Extract into constant",
+            ExtractionKind::Static => "Extract into static",
         }
     }
 
@@ -291,7 +334,7 @@ impl ExtractionKind {
 
         let var_name = match self {
             ExtractionKind::Variable => var_name,
-            ExtractionKind::Constant => var_name.to_uppercase(),
+            ExtractionKind::Constant | ExtractionKind::Static => var_name.to_uppercase(),
         };
 
         (var_name, expr_replace)
@@ -351,7 +394,7 @@ impl Anchor {
             });
 
         match kind {
-            ExtractionKind::Constant if result.is_none() => {
+            ExtractionKind::Constant | ExtractionKind::Static if result.is_none() => {
                 to_extract.syntax().ancestors().find_map(|node| {
                     let item = ast::Item::cast(node.clone())?;
                     let parent = item.syntax().parent()?;
@@ -382,21 +425,6 @@ mod tests {
     use super::*;
 
     #[test]
-    fn now_bad() {
-        // unknown type
-        check_assist_not_applicable_by_label(
-            extract_variable,
-            r#"
-fn main() {
-    let a = Some(2);
-    a.is_some();$0
-}
-"#,
-            "Extract into constant",
-        );
-    }
-
-    #[test]
     fn extract_var_simple_without_select() {
         check_assist_by_label(
             extract_variable,
@@ -604,7 +632,102 @@ fn main() {
     }
 
     #[test]
-    fn extract_var_unit_expr_without_select_not_applicable() {
+    fn extract_static_simple_without_select() {
+        check_assist_by_label(
+            extract_variable,
+            r#"
+fn main() -> i32 {
+    if true {
+        1
+    } else {
+        2
+    }$0
+}
+"#,
+            r#"
+fn main() -> i32 {
+    static $0VAR_NAME: i32 = if true {
+        1
+    } else {
+        2
+    };
+    VAR_NAME
+}
+"#,
+            "Extract into static",
+        );
+
+        check_assist_by_label(
+            extract_variable,
+            r#"
+const fn foo() -> i32 { 1 }
+fn main() {
+    foo();$0
+}
+"#,
+            r#"
+const fn foo() -> i32 { 1 }
+fn main() {
+    static $0FOO: i32 = foo();
+}
+"#,
+            "Extract into static",
+        );
+
+        check_assist_by_label(
+            extract_variable,
+            r#"
+fn main() {
+    "hello"$0;
+}
+"#,
+            r#"
+fn main() {
+    static $0VAR_NAME: &str = "hello";
+}
+"#,
+            "Extract into static",
+        );
+
+        check_assist_by_label(
+            extract_variable,
+            r#"
+fn main() {
+    1  + 2$0;
+}
+"#,
+            r#"
+fn main() {
+    static $0VAR_NAME: i32 = 1  + 2;
+}
+"#,
+            "Extract into static",
+        );
+
+        check_assist_by_label(
+            extract_variable,
+            r#"
+fn main() {
+    match () {
+        () if true => 1,
+        _ => 2,
+    };$0
+}
+"#,
+            r#"
+fn main() {
+    static $0VAR_NAME: i32 = match () {
+        () if true => 1,
+        _ => 2,
+    };
+}
+"#,
+            "Extract into static",
+        );
+    }
+
+    #[test]
+    fn dont_extract_unit_expr_without_select() {
         check_assist_not_applicable(
             extract_variable,
             r#"
@@ -664,7 +787,24 @@ fn foo() {
     }
 
     #[test]
-    fn extract_var_in_comment_is_not_applicable() {
+    fn extract_static_simple() {
+        check_assist_by_label(
+            extract_variable,
+            r#"
+fn foo() {
+    foo($01 + 1$0);
+}"#,
+            r#"
+fn foo() {
+    static $0VAR_NAME: i32 = 1 + 1;
+    foo(VAR_NAME);
+}"#,
+            "Extract into static",
+        );
+    }
+
+    #[test]
+    fn dont_extract_in_comment() {
         cov_mark::check!(extract_var_in_comment_is_not_applicable);
         check_assist_not_applicable(extract_variable, r#"fn main() { 1 + /* $0comment$0 */ 1; }"#);
     }
@@ -733,6 +873,38 @@ fn foo() {
     }
 
     #[test]
+    fn extract_static_expr_stmt() {
+        cov_mark::check!(test_extract_var_expr_stmt);
+        check_assist_by_label(
+            extract_variable,
+            r#"
+fn foo() {
+  $0  1 + 1$0;
+}"#,
+            r#"
+fn foo() {
+    static $0VAR_NAME: i32 = 1 + 1;
+}"#,
+            "Extract into static",
+        );
+        // This is hilarious but as far as I know, it's valid
+        check_assist_by_label(
+            extract_variable,
+            r#"
+fn foo() {
+    $0{ let x = 0; x }$0;
+    something_else();
+}"#,
+            r#"
+fn foo() {
+    static $0VAR_NAME: i32 = { let x = 0; x };
+    something_else();
+}"#,
+            "Extract into static",
+        );
+    }
+
+    #[test]
     fn extract_var_part_of_expr_stmt() {
         check_assist_by_label(
             extract_variable,
@@ -767,6 +939,23 @@ fn foo() {
     }
 
     #[test]
+    fn extract_static_part_of_expr_stmt() {
+        check_assist_by_label(
+            extract_variable,
+            r#"
+fn foo() {
+    $01$0 + 1;
+}"#,
+            r#"
+fn foo() {
+    static $0VAR_NAME: i32 = 1;
+    VAR_NAME + 1;
+}"#,
+            "Extract into static",
+        );
+    }
+
+    #[test]
     fn extract_var_last_expr() {
         cov_mark::check!(test_extract_var_last_expr);
         check_assist_by_label(
@@ -853,6 +1042,49 @@ const fn bar(i: i32) -> i32 {
     }
 
     #[test]
+    fn extract_static_last_expr() {
+        cov_mark::check!(test_extract_var_last_expr);
+        check_assist_by_label(
+            extract_variable,
+            r#"
+fn foo() {
+    bar($01 + 1$0)
+}
+"#,
+            r#"
+fn foo() {
+    static $0VAR_NAME: i32 = 1 + 1;
+    bar(VAR_NAME)
+}
+"#,
+            "Extract into static",
+        );
+        check_assist_by_label(
+            extract_variable,
+            r#"
+fn foo() -> i32 {
+    $0bar(1 + 1)$0
+}
+
+const fn bar(i: i32) -> i32 {
+    i
+}
+"#,
+            r#"
+fn foo() -> i32 {
+    static $0BAR: i32 = bar(1 + 1);
+    BAR
+}
+
+const fn bar(i: i32) -> i32 {
+    i
+}
+"#,
+            "Extract into static",
+        )
+    }
+
+    #[test]
     fn extract_var_in_match_arm_no_block() {
         cov_mark::check!(test_extract_var_in_match_arm_no_block);
         check_assist_by_label(
@@ -1427,6 +1659,30 @@ fn main() {
 "#,
             "Extract into constant",
         );
+
+        check_assist_by_label(
+            extract_variable,
+            r#"
+struct Vec;
+macro_rules! vec {
+    () => {Vec}
+}
+fn main() {
+    let _ = $0vec![]$0;
+}
+"#,
+            r#"
+struct Vec;
+macro_rules! vec {
+    () => {Vec}
+}
+fn main() {
+    static $0VEC: Vec = vec![];
+    let _ = VEC;
+}
+"#,
+            "Extract into static",
+        );
     }
 
     #[test]
@@ -1590,6 +1846,109 @@ fn bar() {
     }
 
     #[test]
+    fn extract_static_no_block_body() {
+        check_assist_by_label(
+            extract_variable,
+            r#"
+const fn foo(x: i32) -> i32 {
+    x
+}
+
+const FOO: i32 = foo($0100$0);
+"#,
+            r#"
+const fn foo(x: i32) -> i32 {
+    x
+}
+
+static $0X: i32 = 100;
+const FOO: i32 = foo(X);
+"#,
+            "Extract into static",
+        );
+
+        check_assist_by_label(
+            extract_variable,
+            r#"
+mod foo {
+    enum Foo {
+        Bar,
+        Baz = $042$0,
+    }
+}
+"#,
+            r#"
+mod foo {
+    static $0VAR_NAME: isize = 42;
+    enum Foo {
+        Bar,
+        Baz = VAR_NAME,
+    }
+}
+"#,
+            "Extract into static",
+        );
+
+        check_assist_by_label(
+            extract_variable,
+            r#"
+const fn foo(x: i32) -> i32 {
+    x
+}
+
+trait Hello {
+    const World: i32;
+}
+
+struct Bar;
+impl Hello for Bar {
+    const World = foo($042$0);
+}
+"#,
+            r#"
+const fn foo(x: i32) -> i32 {
+    x
+}
+
+trait Hello {
+    const World: i32;
+}
+
+struct Bar;
+impl Hello for Bar {
+    static $0X: i32 = 42;
+    const World = foo(X);
+}
+"#,
+            "Extract into static",
+        );
+
+        check_assist_by_label(
+            extract_variable,
+            r#"
+const fn foo(x: i32) -> i32 {
+    x
+}
+
+fn bar() {
+    const BAZ: i32 = foo($042$0);
+}
+"#,
+            r#"
+const fn foo(x: i32) -> i32 {
+    x
+}
+
+fn bar() {
+    static $0X: i32 = 42;
+    const BAZ: i32 = foo(X);
+}
+"#,
+            "Extract into static",
+        );
+    }
+
+    #[test]
     fn extract_var_mutable_reference_parameter() {
         check_assist_by_label(
             extract_variable,
@@ -1641,7 +2000,28 @@ impl<T> Vec<T> {
 fn foo(s: &mut S) {
     $0s.vec$0.push(0);
 }"#,
-            "Extract into const",
+            "Extract into constant",
+        );
+    }
+
+    #[test]
+    fn dont_extract_static_mutable_reference_parameter() {
+        check_assist_not_applicable_by_label(
+            extract_variable,
+            r#"
+struct S {
+    vec: Vec<u8>
+}
+
+struct Vec<T>;
+impl<T> Vec<T> {
+    fn push(&mut self, _:usize) {}
+}
+
+fn foo(s: &mut S) {
+    $0s.vec$0.push(0);
+}"#,
+            "Extract into static",
         );
     }
 
@@ -2031,6 +2411,18 @@ fn foo() {
     }
 
     #[test]
+    fn dont_extract_static_for_mutable_borrow() {
+        check_assist_not_applicable_by_label(
+            extract_variable,
+            r#"
+fn foo() {
+    let v = &mut $00$0;
+}"#,
+            "Extract into static",
+        );
+    }
+
+    #[test]
     fn generates_no_ref_on_calls() {
         check_assist_by_label(
             extract_variable,
diff --git a/src/tools/rust-analyzer/crates/ide-assists/src/tests.rs b/src/tools/rust-analyzer/crates/ide-assists/src/tests.rs
index 4f7f03764f5..e517dd46824 100644
--- a/src/tools/rust-analyzer/crates/ide-assists/src/tests.rs
+++ b/src/tools/rust-analyzer/crates/ide-assists/src/tests.rs
@@ -362,8 +362,7 @@ pub fn test_some_range(a: int) -> bool {
 
     expect![[r#"
         Convert integer base
-        Extract into variable
-        Extract into constant
+        Extract into...
         Extract into function
         Replace if let with match
     "#]]
@@ -392,8 +391,7 @@ pub fn test_some_range(a: int) -> bool {
 
         expect![[r#"
             Convert integer base
-            Extract into variable
-            Extract into constant
+            Extract into...
             Extract into function
             Replace if let with match
         "#]]
@@ -407,8 +405,7 @@ pub fn test_some_range(a: int) -> bool {
         let expected = labels(&assists);
 
         expect![[r#"
-            Extract into variable
-            Extract into constant
+            Extract into...
             Extract into function
         "#]]
         .assert_eq(&expected);
@@ -443,7 +440,7 @@ pub fn test_some_range(a: int) -> bool {
 
     {
         let assists = assists(&db, &cfg, AssistResolveStrategy::None, frange.into());
-        assert_eq!(3, assists.len());
+        assert_eq!(4, assists.len());
         let mut assists = assists.into_iter();
 
         let extract_into_variable_assist = assists.next().unwrap();
@@ -454,7 +451,11 @@ pub fn test_some_range(a: int) -> bool {
                     RefactorExtract,
                 ),
                 label: "Extract into variable",
-                group: None,
+                group: Some(
+                    GroupLabel(
+                        "Extract into...",
+                    ),
+                ),
                 target: 59..60,
                 source_change: None,
                 command: None,
@@ -470,7 +471,11 @@ pub fn test_some_range(a: int) -> bool {
                     RefactorExtract,
                 ),
                 label: "Extract into constant",
-                group: None,
+                group: Some(
+                    GroupLabel(
+                        "Extract into...",
+                    ),
+                ),
                 target: 59..60,
                 source_change: None,
                 command: None,
@@ -478,6 +483,26 @@ pub fn test_some_range(a: int) -> bool {
         "#]]
         .assert_debug_eq(&extract_into_constant_assist);
 
+        let extract_into_static_assist = assists.next().unwrap();
+        expect![[r#"
+            Assist {
+                id: AssistId(
+                    "extract_static",
+                    RefactorExtract,
+                ),
+                label: "Extract into static",
+                group: Some(
+                    GroupLabel(
+                        "Extract into...",
+                    ),
+                ),
+                target: 59..60,
+                source_change: None,
+                command: None,
+            }
+        "#]]
+        .assert_debug_eq(&extract_into_static_assist);
+
         let extract_into_function_assist = assists.next().unwrap();
         expect![[r#"
             Assist {
@@ -505,7 +530,7 @@ pub fn test_some_range(a: int) -> bool {
             }),
             frange.into(),
         );
-        assert_eq!(3, assists.len());
+        assert_eq!(4, assists.len());
         let mut assists = assists.into_iter();
 
         let extract_into_variable_assist = assists.next().unwrap();
@@ -516,7 +541,11 @@ pub fn test_some_range(a: int) -> bool {
                     RefactorExtract,
                 ),
                 label: "Extract into variable",
-                group: None,
+                group: Some(
+                    GroupLabel(
+                        "Extract into...",
+                    ),
+                ),
                 target: 59..60,
                 source_change: None,
                 command: None,
@@ -532,7 +561,11 @@ pub fn test_some_range(a: int) -> bool {
                     RefactorExtract,
                 ),
                 label: "Extract into constant",
-                group: None,
+                group: Some(
+                    GroupLabel(
+                        "Extract into...",
+                    ),
+                ),
                 target: 59..60,
                 source_change: None,
                 command: None,
@@ -540,6 +573,26 @@ pub fn test_some_range(a: int) -> bool {
         "#]]
         .assert_debug_eq(&extract_into_constant_assist);
 
+        let extract_into_static_assist = assists.next().unwrap();
+        expect![[r#"
+            Assist {
+                id: AssistId(
+                    "extract_static",
+                    RefactorExtract,
+                ),
+                label: "Extract into static",
+                group: Some(
+                    GroupLabel(
+                        "Extract into...",
+                    ),
+                ),
+                target: 59..60,
+                source_change: None,
+                command: None,
+            }
+        "#]]
+        .assert_debug_eq(&extract_into_static_assist);
+
         let extract_into_function_assist = assists.next().unwrap();
         expect![[r#"
             Assist {
@@ -567,7 +620,7 @@ pub fn test_some_range(a: int) -> bool {
             }),
             frange.into(),
         );
-        assert_eq!(3, assists.len());
+        assert_eq!(4, assists.len());
         let mut assists = assists.into_iter();
 
         let extract_into_variable_assist = assists.next().unwrap();
@@ -578,7 +631,11 @@ pub fn test_some_range(a: int) -> bool {
                     RefactorExtract,
                 ),
                 label: "Extract into variable",
-                group: None,
+                group: Some(
+                    GroupLabel(
+                        "Extract into...",
+                    ),
+                ),
                 target: 59..60,
                 source_change: Some(
                     SourceChange {
@@ -637,7 +694,11 @@ pub fn test_some_range(a: int) -> bool {
                     RefactorExtract,
                 ),
                 label: "Extract into constant",
-                group: None,
+                group: Some(
+                    GroupLabel(
+                        "Extract into...",
+                    ),
+                ),
                 target: 59..60,
                 source_change: None,
                 command: None,
@@ -645,6 +706,26 @@ pub fn test_some_range(a: int) -> bool {
         "#]]
         .assert_debug_eq(&extract_into_constant_assist);
 
+        let extract_into_static_assist = assists.next().unwrap();
+        expect![[r#"
+            Assist {
+                id: AssistId(
+                    "extract_static",
+                    RefactorExtract,
+                ),
+                label: "Extract into static",
+                group: Some(
+                    GroupLabel(
+                        "Extract into...",
+                    ),
+                ),
+                target: 59..60,
+                source_change: None,
+                command: None,
+            }
+        "#]]
+        .assert_debug_eq(&extract_into_static_assist);
+
         let extract_into_function_assist = assists.next().unwrap();
         expect![[r#"
             Assist {
@@ -664,7 +745,7 @@ pub fn test_some_range(a: int) -> bool {
 
     {
         let assists = assists(&db, &cfg, AssistResolveStrategy::All, frange.into());
-        assert_eq!(3, assists.len());
+        assert_eq!(4, assists.len());
         let mut assists = assists.into_iter();
 
         let extract_into_variable_assist = assists.next().unwrap();
@@ -675,7 +756,11 @@ pub fn test_some_range(a: int) -> bool {
                     RefactorExtract,
                 ),
                 label: "Extract into variable",
-                group: None,
+                group: Some(
+                    GroupLabel(
+                        "Extract into...",
+                    ),
+                ),
                 target: 59..60,
                 source_change: Some(
                     SourceChange {
@@ -734,7 +819,11 @@ pub fn test_some_range(a: int) -> bool {
                     RefactorExtract,
                 ),
                 label: "Extract into constant",
-                group: None,
+                group: Some(
+                    GroupLabel(
+                        "Extract into...",
+                    ),
+                ),
                 target: 59..60,
                 source_change: Some(
                     SourceChange {
@@ -789,6 +878,73 @@ pub fn test_some_range(a: int) -> bool {
         "#]]
         .assert_debug_eq(&extract_into_constant_assist);
 
+        let extract_into_static_assist = assists.next().unwrap();
+        expect![[r#"
+            Assist {
+                id: AssistId(
+                    "extract_static",
+                    RefactorExtract,
+                ),
+                label: "Extract into static",
+                group: Some(
+                    GroupLabel(
+                        "Extract into...",
+                    ),
+                ),
+                target: 59..60,
+                source_change: Some(
+                    SourceChange {
+                        source_file_edits: {
+                            FileId(
+                                0,
+                            ): (
+                                TextEdit {
+                                    indels: [
+                                        Indel {
+                                            insert: "static",
+                                            delete: 45..47,
+                                        },
+                                        Indel {
+                                            insert: "VAR_NAME:",
+                                            delete: 48..60,
+                                        },
+                                        Indel {
+                                            insert: "i32",
+                                            delete: 61..81,
+                                        },
+                                        Indel {
+                                            insert: "=",
+                                            delete: 82..86,
+                                        },
+                                        Indel {
+                                            insert: "5;\n    if let 2..6 = VAR_NAME {\n        true\n    } else {\n        false\n    }",
+                                            delete: 87..108,
+                                        },
+                                    ],
+                                },
+                                Some(
+                                    SnippetEdit(
+                                        [
+                                            (
+                                                0,
+                                                52..52,
+                                            ),
+                                        ],
+                                    ),
+                                ),
+                            ),
+                        },
+                        file_system_edits: [],
+                        is_snippet: true,
+                    },
+                ),
+                command: Some(
+                    Rename,
+                ),
+            }
+        "#]]
+        .assert_debug_eq(&extract_into_static_assist);
+
         let extract_into_function_assist = assists.next().unwrap();
         expect![[r#"
             Assist {
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 c1799b48ed4..87c3d166ee6 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
@@ -1025,6 +1025,24 @@ fn bar(name: i32) -> i32 {
 }
 
 #[test]
+fn doctest_extract_static() {
+    check_doc_test(
+        "extract_static",
+        r#####"
+fn main() {
+    $0(1 + 2)$0 * 4;
+}
+"#####,
+        r#####"
+fn main() {
+    static $0VAR_NAME: i32 = 1 + 2;
+    VAR_NAME * 4;
+}
+"#####,
+    )
+}
+
+#[test]
 fn doctest_extract_struct_from_enum_variant() {
     check_doc_test(
         "extract_struct_from_enum_variant",