diff --git a/crates/ide-assists/src/handlers/extract_function.rs b/crates/ide-assists/src/handlers/extract_function.rs index 6937d33ebc..2e363b0b62 100644 --- a/crates/ide-assists/src/handlers/extract_function.rs +++ b/crates/ide-assists/src/handlers/extract_function.rs @@ -7,6 +7,7 @@ use hir::{ TypeInfo, TypeParam, }; use ide_db::{ + assists::GroupLabel, defs::{Definition, NameRefClass}, famous_defs::FamousDefs, helpers::mod_path_to_ast, @@ -104,7 +105,8 @@ pub(crate) fn extract_function(acc: &mut Assists, ctx: &AssistContext<'_>) -> Op let scope = ImportScope::find_insert_use_container(&node, &ctx.sema)?; - acc.add( + acc.add_group( + &GroupLabel("Extract into...".to_owned()), AssistId("extract_function", crate::AssistKind::RefactorExtract), "Extract into function", target_range, diff --git a/crates/ide-assists/src/handlers/extract_variable.rs b/crates/ide-assists/src/handlers/extract_variable.rs index 61dc72e0b3..a8d71ed7f4 100644 --- a/crates/ide-assists/src/handlers/extract_variable.rs +++ b/crates/ide-assists/src/handlers/extract_variable.rs @@ -1,17 +1,15 @@ -use hir::TypeInfo; -use ide_db::syntax_helpers::suggest_name; +use hir::{HirDisplay, TypeInfo}; +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::{BLOCK_EXPR, BREAK_EXPR, COMMENT, LOOP_EXPR, MATCH_GUARD, PATH_EXPR, RETURN_EXPR}, - SyntaxNode, T, + NodeOrToken, SyntaxKind, SyntaxNode, T, }; -use crate::{AssistContext, AssistId, AssistKind, Assists}; +use crate::{utils::is_body_const, AssistContext, AssistId, AssistKind, Assists}; // Assist: extract_variable // @@ -29,6 +27,40 @@ use crate::{AssistContext, AssistId, AssistKind, Assists}; // var_name * 4; // } // ``` + +// Assist: extract_constant +// +// Extracts subexpression into a constant. +// +// ``` +// fn main() { +// $0(1 + 2)$0 * 4; +// } +// ``` +// -> +// ``` +// fn main() { +// const $0VAR_NAME: i32 = 1 + 2; +// 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![;]) { @@ -41,7 +73,7 @@ pub(crate) fn extract_variable(acc: &mut Assists, ctx: &AssistContext<'_>) -> Op } else { match ctx.covering_element() { NodeOrToken::Node(it) => it, - NodeOrToken::Token(it) if it.kind() == COMMENT => { + NodeOrToken::Token(it) if it.kind() == SyntaxKind::COMMENT => { cov_mark::hit!(extract_var_in_comment_is_not_applicable); return None; } @@ -87,116 +119,150 @@ pub(crate) fn extract_variable(acc: &mut Assists, ctx: &AssistContext<'_>) -> Op } _ => false, }; - - let anchor = Anchor::from(&to_extract)?; + let module = ctx.sema.scope(to_extract.syntax())?.module(); let target = to_extract.syntax().text_range(); - acc.add( - AssistId("extract_variable", AssistKind::RefactorExtract), - "Extract into variable", - target, - move |edit| { - let field_shorthand = to_extract - .syntax() - .parent() - .and_then(ast::RecordExprField::cast) - .filter(|field| field.name_ref().is_some()); + let needs_mut = match &parent { + Some(ast::Expr::RefExpr(expr)) => expr.mut_token().is_some(), + _ => needs_adjust && !needs_ref && ty.as_ref().is_some_and(|ty| ty.is_mutable_reference()), + }; + for kind in ExtractionKind::ALL { + let Some(anchor) = Anchor::from(&to_extract, kind) else { + continue; + }; - let (var_name, expr_replace) = match field_shorthand { - Some(field) => (field.to_string(), field.syntax().clone()), - None => ( - suggest_name::for_variable(&to_extract, &ctx.sema), - to_extract.syntax().clone(), - ), - }; + let ty_string = match kind { + ExtractionKind::Constant | ExtractionKind::Static => { + let Some(ty) = ty.clone() else { + continue; + }; - let make = SyntaxFactory::new(); - let mut editor = edit.make_editor(&expr_replace); - - let pat_name = make.name(&var_name); - let name_expr = make.expr_path(make::ext::ident_path(&var_name)); - - if let Some(cap) = ctx.config.snippet_cap { - let tabstop = edit.make_tabstop_before(cap); - editor.add_annotation(pat_name.syntax().clone(), tabstop); - } - - let ident_pat = match parent { - Some(ast::Expr::RefExpr(expr)) if expr.mut_token().is_some() => { - make.ident_pat(false, true, pat_name) - } - _ if needs_adjust - && !needs_ref - && ty.as_ref().is_some_and(|ty| ty.is_mutable_reference()) => + // 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() + || ty.is_mutable_reference() { - make.ident_pat(false, true, pat_name) + continue; } - _ => make.ident_pat(false, false, pat_name), - }; - let to_extract_no_ref = match ty.as_ref().filter(|_| needs_ref) { - Some(receiver_type) if receiver_type.is_mutable_reference() => { - make.expr_ref(to_extract_no_ref, true) - } - Some(receiver_type) if receiver_type.is_reference() => { - make.expr_ref(to_extract_no_ref, false) - } - _ => to_extract_no_ref, - }; + let Ok(type_string) = ty.display_source_code(ctx.db(), module.into(), false) else { + continue; + }; - let let_stmt = make.let_stmt(ident_pat.into(), None, Some(to_extract_no_ref)); - - 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![ - let_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); - - editor.replace(stmt.syntax(), let_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([let_stmt.into()], 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([let_stmt.into()], Some(to_wrap.clone())) - }; - - editor.replace(to_wrap.syntax(), block.syntax()); - - // fixup indentation of block - block.indent(indent_to); - } + type_string } + _ => "".to_owned(), + }; - editor.add_mappings(make.finish_with_mappings()); - edit.add_file_edits(ctx.file_id(), editor); - edit.rename(); - }, - ) + 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 make = SyntaxFactory::new(); + let mut editor = edit.make_editor(&expr_replace); + + let pat_name = make.name(&var_name); + let name_expr = make.expr_path(make::ext::ident_path(&var_name)); + + if let Some(cap) = ctx.config.snippet_cap { + let tabstop = edit.make_tabstop_before(cap); + editor.add_annotation(pat_name.syntax().clone(), tabstop); + } + + 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(expr_replace, name_expr.syntax()); + } + Anchor::Replace(stmt) => { + cov_mark::hit!(test_extract_var_expr_stmt); + + 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())) + }; + + 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(); + }, + ); + } + + Some(()) } fn peel_parens(mut expr: ast::Expr) -> ast::Expr { @@ -211,17 +277,71 @@ fn peel_parens(mut expr: ast::Expr) -> ast::Expr { /// In general that's true for any expression, but in some cases that would produce invalid code. fn valid_target_expr(node: SyntaxNode) -> Option { match node.kind() { - PATH_EXPR | LOOP_EXPR => None, - BREAK_EXPR => ast::BreakExpr::cast(node).and_then(|e| e.expr()), - RETURN_EXPR => ast::ReturnExpr::cast(node).and_then(|e| e.expr()), - BLOCK_EXPR => { + SyntaxKind::PATH_EXPR | SyntaxKind::LOOP_EXPR => None, + SyntaxKind::BREAK_EXPR => ast::BreakExpr::cast(node).and_then(|e| e.expr()), + SyntaxKind::RETURN_EXPR => ast::ReturnExpr::cast(node).and_then(|e| e.expr()), + SyntaxKind::BLOCK_EXPR => { ast::BlockExpr::cast(node).filter(|it| it.is_standalone()).map(ast::Expr::from) } _ => ast::Expr::cast(node), } } -#[derive(Debug)] +enum ExtractionKind { + Variable, + Constant, + Static, +} + +impl ExtractionKind { + 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) + } + + fn label(&self) -> &'static str { + match self { + ExtractionKind::Variable => "Extract into variable", + ExtractionKind::Constant => "Extract into constant", + ExtractionKind::Static => "Extract into static", + } + } + + fn get_name_and_expr( + &self, + ctx: &AssistContext<'_>, + to_extract: &ast::Expr, + ) -> (String, SyntaxNode) { + let field_shorthand = to_extract + .syntax() + .parent() + .and_then(ast::RecordExprField::cast) + .filter(|field| field.name_ref().is_some()); + let (var_name, expr_replace) = match field_shorthand { + Some(field) => (field.to_string(), field.syntax().clone()), + None => { + (suggest_name::for_variable(to_extract, &ctx.sema), to_extract.syntax().clone()) + } + }; + + let var_name = match self { + ExtractionKind::Variable => var_name, + ExtractionKind::Constant | ExtractionKind::Static => var_name.to_uppercase(), + }; + + (var_name, expr_replace) + } +} + +#[derive(Debug, Clone)] enum Anchor { Before(SyntaxNode), Replace(ast::ExprStmt), @@ -229,8 +349,8 @@ enum Anchor { } impl Anchor { - fn from(to_extract: &ast::Expr) -> Option { - to_extract + fn from(to_extract: &ast::Expr, kind: &ExtractionKind) -> Option { + let result = to_extract .syntax() .ancestors() .take_while(|it| !ast::Item::can_cast(it.kind()) || ast::MacroCall::can_cast(it.kind())) @@ -253,7 +373,7 @@ impl Anchor { return parent.body().map(Anchor::WrapInBlock); } if let Some(parent) = ast::MatchArm::cast(parent) { - if node.kind() == MATCH_GUARD { + if node.kind() == SyntaxKind::MATCH_GUARD { cov_mark::hit!(test_extract_var_in_match_guard); } else { cov_mark::hit!(test_extract_var_in_match_arm_no_block); @@ -271,19 +391,42 @@ impl Anchor { return Some(Anchor::Before(node)); } None - }) + }); + + match kind { + 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()?; + match parent.kind() { + SyntaxKind::ITEM_LIST + | SyntaxKind::SOURCE_FILE + | SyntaxKind::ASSOC_ITEM_LIST + | SyntaxKind::STMT_LIST => Some(Anchor::Before(node)), + _ => None, + } + }) + } + _ => result, + } } } #[cfg(test)] mod tests { - use crate::tests::{check_assist, check_assist_not_applicable, check_assist_target}; + // NOTE: We use check_assist_by_label, but not check_assist_not_applicable_by_label + // because all of our not-applicable tests should behave that way for both assists + // extract_variable offers, and check_assist_not_applicable ensures neither is offered + use crate::tests::{ + check_assist_by_label, check_assist_not_applicable, check_assist_not_applicable_by_label, + check_assist_target, + }; use super::*; #[test] - fn test_extract_var_simple_without_select() { - check_assist( + fn extract_var_simple_without_select() { + check_assist_by_label( extract_variable, r#" fn main() -> i32 { @@ -304,9 +447,10 @@ fn main() -> i32 { var_name } "#, + "Extract into variable", ); - check_assist( + check_assist_by_label( extract_variable, r#" fn foo() -> i32 { 1 } @@ -320,9 +464,10 @@ fn main() { let $0foo = foo(); } "#, + "Extract into variable", ); - check_assist( + check_assist_by_label( extract_variable, r#" fn main() { @@ -336,9 +481,10 @@ fn main() { let $0is_some = a.is_some(); } "#, + "Extract into variable", ); - check_assist( + check_assist_by_label( extract_variable, r#" fn main() { @@ -350,9 +496,10 @@ fn main() { let $0var_name = "hello"; } "#, + "Extract into variable", ); - check_assist( + check_assist_by_label( extract_variable, r#" fn main() { @@ -364,9 +511,10 @@ fn main() { let $0var_name = 1 + 2; } "#, + "Extract into variable", ); - check_assist( + check_assist_by_label( extract_variable, r#" fn main() { @@ -384,11 +532,202 @@ fn main() { }; } "#, + "Extract into variable", ); } #[test] - fn test_extract_var_unit_expr_without_select_not_applicable() { + fn extract_const_simple_without_select() { + check_assist_by_label( + extract_variable, + r#" +fn main() -> i32 { + if true { + 1 + } else { + 2 + }$0 +} +"#, + r#" +fn main() -> i32 { + const $0VAR_NAME: i32 = if true { + 1 + } else { + 2 + }; + VAR_NAME +} +"#, + "Extract into constant", + ); + + check_assist_by_label( + extract_variable, + r#" +const fn foo() -> i32 { 1 } +fn main() { + foo();$0 +} +"#, + r#" +const fn foo() -> i32 { 1 } +fn main() { + const $0FOO: i32 = foo(); +} +"#, + "Extract into constant", + ); + + check_assist_by_label( + extract_variable, + r#" +fn main() { + "hello"$0; +} +"#, + r#" +fn main() { + const $0VAR_NAME: &str = "hello"; +} +"#, + "Extract into constant", + ); + + check_assist_by_label( + extract_variable, + r#" +fn main() { + 1 + 2$0; +} +"#, + r#" +fn main() { + const $0VAR_NAME: i32 = 1 + 2; +} +"#, + "Extract into constant", + ); + + check_assist_by_label( + extract_variable, + r#" +fn main() { + match () { + () if true => 1, + _ => 2, + };$0 +} +"#, + r#" +fn main() { + const $0VAR_NAME: i32 = match () { + () if true => 1, + _ => 2, + }; +} +"#, + "Extract into constant", + ); + } + + #[test] + 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#" @@ -414,8 +753,8 @@ fn foo() { } #[test] - fn test_extract_var_simple() { - check_assist( + fn extract_var_simple() { + check_assist_by_label( extract_variable, r#" fn foo() { @@ -426,19 +765,54 @@ fn foo() { let $0var_name = 1 + 1; foo(var_name); }"#, + "Extract into variable", ); } #[test] - fn extract_var_in_comment_is_not_applicable() { - cov_mark::check!(extract_var_in_comment_is_not_applicable); - check_assist_not_applicable(extract_variable, "fn main() { 1 + /* $0comment$0 */ 1; }"); + fn extract_const_simple() { + check_assist_by_label( + extract_variable, + r#" +fn foo() { + foo($01 + 1$0); +}"#, + r#" +fn foo() { + const $0VAR_NAME: i32 = 1 + 1; + foo(VAR_NAME); +}"#, + "Extract into constant", + ); } #[test] - fn test_extract_var_expr_stmt() { + 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; }"#); + } + + #[test] + fn extract_var_expr_stmt() { cov_mark::check!(test_extract_var_expr_stmt); - check_assist( + check_assist_by_label( extract_variable, r#" fn foo() { @@ -448,42 +822,143 @@ fn foo() { fn foo() { let $0var_name = 1 + 1; }"#, + "Extract into variable", ); - check_assist( + check_assist_by_label( extract_variable, - r" + r#" fn foo() { $0{ let x = 0; x }$0; something_else(); -}", - r" +}"#, + r#" fn foo() { let $0var_name = { let x = 0; x }; something_else(); -}", +}"#, + "Extract into variable", ); } #[test] - fn test_extract_var_part_of_expr_stmt() { - check_assist( + fn extract_const_expr_stmt() { + cov_mark::check!(test_extract_var_expr_stmt); + check_assist_by_label( extract_variable, - r" + r#" +fn foo() { + $0 1 + 1$0; +}"#, + r#" +fn foo() { + const $0VAR_NAME: i32 = 1 + 1; +}"#, + "Extract into constant", + ); + // 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() { + const $0VAR_NAME: i32 = { let x = 0; x }; + something_else(); +}"#, + "Extract into constant", + ); + } + + #[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, + r#" fn foo() { $01$0 + 1; -}", - r" +}"#, + r#" fn foo() { let $0var_name = 1; var_name + 1; -}", +}"#, + "Extract into variable", ); } #[test] - fn test_extract_var_last_expr() { + fn extract_const_part_of_expr_stmt() { + check_assist_by_label( + extract_variable, + r#" +fn foo() { + $01$0 + 1; +}"#, + r#" +fn foo() { + const $0VAR_NAME: i32 = 1; + VAR_NAME + 1; +}"#, + "Extract into constant", + ); + } + + #[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( + check_assist_by_label( extract_variable, r#" fn foo() { @@ -496,8 +971,9 @@ fn foo() { bar(var_name) } "#, + "Extract into variable", ); - check_assist( + check_assist_by_label( extract_variable, r#" fn foo() -> i32 { @@ -518,13 +994,100 @@ fn bar(i: i32) -> i32 { i } "#, + "Extract into variable", ) } #[test] - fn test_extract_var_in_match_arm_no_block() { + fn extract_const_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() { + const $0VAR_NAME: i32 = 1 + 1; + bar(VAR_NAME) +} +"#, + "Extract into constant", + ); + 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 { + const $0BAR: i32 = bar(1 + 1); + BAR +} + +const fn bar(i: i32) -> i32 { + i +} +"#, + "Extract into constant", + ) + } + + #[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( + check_assist_by_label( extract_variable, r#" fn main() { @@ -547,12 +1110,13 @@ fn main() { }; } "#, + "Extract into variable", ); } #[test] - fn test_extract_var_in_match_arm_with_block() { - check_assist( + fn extract_var_in_match_arm_with_block() { + check_assist_by_label( extract_variable, r#" fn main() { @@ -579,13 +1143,14 @@ fn main() { }; } "#, + "Extract into variable", ); } #[test] - fn test_extract_var_in_match_guard() { + fn extract_var_in_match_guard() { cov_mark::check!(test_extract_var_in_match_guard); - check_assist( + check_assist_by_label( extract_variable, r#" fn main() { @@ -604,13 +1169,14 @@ fn main() { }; } "#, + "Extract into variable", ); } #[test] - fn test_extract_var_in_closure_no_block() { + fn extract_var_in_closure_no_block() { cov_mark::check!(test_extract_var_in_closure_no_block); - check_assist( + check_assist_by_label( extract_variable, r#" fn main() { @@ -625,12 +1191,13 @@ fn main() { }; } "#, + "Extract into variable", ); } #[test] - fn test_extract_var_in_closure_with_block() { - check_assist( + fn extract_var_in_closure_with_block() { + check_assist_by_label( extract_variable, r#" fn main() { @@ -642,104 +1209,110 @@ fn main() { let lambda = |x: u32| { let $0var_name = x * 2; var_name }; } "#, + "Extract into variable", ); } #[test] - fn test_extract_var_path_simple() { - check_assist( + fn extract_var_path_simple() { + check_assist_by_label( extract_variable, - " + r#" fn main() { let o = $0Some(true)$0; } -", - " +"#, + r#" fn main() { let $0var_name = Some(true); let o = var_name; } -", +"#, + "Extract into variable", ); } #[test] - fn test_extract_var_path_method() { - check_assist( + fn extract_var_path_method() { + check_assist_by_label( extract_variable, - " + r#" fn main() { let v = $0bar.foo()$0; } -", - " +"#, + r#" fn main() { let $0foo = bar.foo(); let v = foo; } -", +"#, + "Extract into variable", ); } #[test] - fn test_extract_var_return() { - check_assist( + fn extract_var_return() { + check_assist_by_label( extract_variable, - " + r#" fn foo() -> u32 { $0return 2 + 2$0; } -", - " +"#, + r#" fn foo() -> u32 { let $0var_name = 2 + 2; return var_name; } -", +"#, + "Extract into variable", ); } #[test] - fn test_extract_var_does_not_add_extra_whitespace() { - check_assist( + fn extract_var_does_not_add_extra_whitespace() { + check_assist_by_label( extract_variable, - " + r#" fn foo() -> u32 { $0return 2 + 2$0; } -", - " +"#, + r#" fn foo() -> u32 { let $0var_name = 2 + 2; return var_name; } -", +"#, + "Extract into variable", ); - check_assist( + check_assist_by_label( extract_variable, - " + r#" fn foo() -> u32 { $0return 2 + 2$0; } -", - " +"#, + r#" fn foo() -> u32 { let $0var_name = 2 + 2; return var_name; } -", +"#, + "Extract into variable", ); - check_assist( + check_assist_by_label( extract_variable, - " + r#" fn foo() -> u32 { let foo = 1; @@ -748,8 +1321,8 @@ fn foo() -> u32 { $0return 2 + 2$0; } -", - " +"#, + r#" fn foo() -> u32 { let foo = 1; @@ -759,53 +1332,56 @@ fn foo() -> u32 { let $0var_name = 2 + 2; return var_name; } -", +"#, + "Extract into variable", ); } #[test] - fn test_extract_var_break() { - check_assist( + fn extract_var_break() { + check_assist_by_label( extract_variable, - " + r#" fn main() { let result = loop { $0break 2 + 2$0; }; } -", - " +"#, + r#" fn main() { let result = loop { let $0var_name = 2 + 2; break var_name; }; } -", +"#, + "Extract into variable", ); } #[test] - fn test_extract_var_for_cast() { - check_assist( + fn extract_var_for_cast() { + check_assist_by_label( extract_variable, - " + r#" fn main() { let v = $00f32 as u32$0; } -", - " +"#, + r#" fn main() { let $0var_name = 0f32 as u32; let v = var_name; } -", +"#, + "Extract into variable", ); } #[test] fn extract_var_field_shorthand() { - check_assist( + check_assist_by_label( extract_variable, r#" struct S { @@ -826,12 +1402,13 @@ fn main() { S { foo } } "#, + "Extract into variable", ) } #[test] fn extract_var_name_from_type() { - check_assist( + check_assist_by_label( extract_variable, r#" struct Test(i32); @@ -848,12 +1425,13 @@ fn foo() -> Test { test } "#, + "Extract into variable", ) } #[test] fn extract_var_name_from_parameter() { - check_assist( + check_assist_by_label( extract_variable, r#" fn bar(test: u32, size: u32) @@ -870,12 +1448,13 @@ fn foo() { bar(1, size); } "#, + "Extract into variable", ) } #[test] fn extract_var_parameter_name_has_precedence_over_type() { - check_assist( + check_assist_by_label( extract_variable, r#" struct TextSize(u32); @@ -894,12 +1473,13 @@ fn foo() { bar(1, size); } "#, + "Extract into variable", ) } #[test] fn extract_var_name_from_function() { - check_assist( + check_assist_by_label( extract_variable, r#" fn is_required(test: u32, size: u32) -> bool @@ -916,12 +1496,13 @@ fn foo() -> bool { is_required } "#, + "Extract into variable", ) } #[test] fn extract_var_name_from_method() { - check_assist( + check_assist_by_label( extract_variable, r#" struct S; @@ -944,12 +1525,13 @@ fn foo() -> u32 { bar } "#, + "Extract into variable", ) } #[test] fn extract_var_name_from_method_param() { - check_assist( + check_assist_by_label( extract_variable, r#" struct S; @@ -972,12 +1554,13 @@ fn foo() { S.bar(n, 2) } "#, + "Extract into variable", ) } #[test] fn extract_var_name_from_ufcs_method_param() { - check_assist( + check_assist_by_label( extract_variable, r#" struct S; @@ -1000,12 +1583,13 @@ fn foo() { S::bar(&S, n, 2) } "#, + "Extract into variable", ) } #[test] fn extract_var_parameter_name_has_precedence_over_function() { - check_assist( + check_assist_by_label( extract_variable, r#" fn bar(test: u32, size: u32) @@ -1022,14 +1606,15 @@ fn foo() { bar(1, size); } "#, + "Extract into variable", ) } #[test] fn extract_macro_call() { - check_assist( + check_assist_by_label( extract_variable, - r" + r#" struct Vec; macro_rules! vec { () => {Vec} @@ -1037,8 +1622,8 @@ macro_rules! vec { fn main() { let _ = $0vec![]$0; } -", - r" +"#, + r#" struct Vec; macro_rules! vec { () => {Vec} @@ -1047,22 +1632,71 @@ fn main() { let $0vec = vec![]; let _ = vec; } -", +"#, + "Extract into variable", + ); + + 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() { + const $0VEC: Vec = vec![]; + let _ = VEC; +} +"#, + "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] - fn test_extract_var_for_return_not_applicable() { + fn extract_var_for_return_not_applicable() { check_assist_not_applicable(extract_variable, "fn foo() { $0return$0; } "); } #[test] - fn test_extract_var_for_break_not_applicable() { + fn extract_var_for_break_not_applicable() { check_assist_not_applicable(extract_variable, "fn main() { loop { $0break$0; }; }"); } #[test] - fn test_extract_var_unit_expr_not_applicable() { + fn extract_var_unit_expr_not_applicable() { check_assist_not_applicable( extract_variable, r#" @@ -1080,11 +1714,11 @@ fn foo() { // FIXME: This is not quite correct, but good enough(tm) for the sorting heuristic #[test] fn extract_var_target() { - check_assist_target(extract_variable, "fn foo() -> u32 { $0return 2 + 2$0; }", "2 + 2"); + check_assist_target(extract_variable, r#"fn foo() -> u32 { $0return 2 + 2$0; }"#, "2 + 2"); check_assist_target( extract_variable, - " + r#" fn main() { let x = true; let tuple = match x { @@ -1092,24 +1726,231 @@ fn main() { _ => (0, false) }; } -", +"#, "2 + 2", ); } #[test] fn extract_var_no_block_body() { - check_assist_not_applicable( + check_assist_not_applicable_by_label( extract_variable, - r" + r#" const X: usize = $0100$0; -", +"#, + "Extract into variable", ); } #[test] - fn test_extract_var_mutable_reference_parameter() { - check_assist( + fn extract_const_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 +} + +const $0X: i32 = 100; +const FOO: i32 = foo(X); +"#, + "Extract into constant", + ); + + check_assist_by_label( + extract_variable, + r#" +mod foo { + enum Foo { + Bar, + Baz = $042$0, + } +} +"#, + r#" +mod foo { + const $0VAR_NAME: isize = 42; + enum Foo { + Bar, + Baz = VAR_NAME, + } +} +"#, + "Extract into constant", + ); + + 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 { + const $0X: i32 = 42; + const World = foo(X); +} +"#, + "Extract into constant", + ); + + 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() { + const $0X: i32 = 42; + const BAZ: i32 = foo(X); +} +"#, + "Extract into constant", + ); + } + + #[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, r#" struct S { @@ -1138,12 +1979,55 @@ fn foo(s: &mut S) { let $0vec = &mut s.vec; vec.push(0); }"#, + "Extract into variable", ); } #[test] - fn test_extract_var_mutable_reference_parameter_deep_nesting() { - check_assist( + fn dont_extract_const_mutable_reference_parameter() { + check_assist_not_applicable_by_label( + extract_variable, + r#" +struct S { + vec: Vec +} + +struct Vec; +impl Vec { + fn push(&mut self, _:usize) {} +} + +fn foo(s: &mut S) { + $0s.vec$0.push(0); +}"#, + "Extract into constant", + ); + } + + #[test] + fn dont_extract_static_mutable_reference_parameter() { + check_assist_not_applicable_by_label( + extract_variable, + r#" +struct S { + vec: Vec +} + +struct Vec; +impl Vec { + fn push(&mut self, _:usize) {} +} + +fn foo(s: &mut S) { + $0s.vec$0.push(0); +}"#, + "Extract into static", + ); + } + + #[test] + fn extract_var_mutable_reference_parameter_deep_nesting() { + check_assist_by_label( extract_variable, r#" struct Y { @@ -1182,12 +2066,13 @@ fn foo(f: &mut Y) { let $0vec = &mut f.field.field.vec; vec.push(0); }"#, + "Extract into variable", ); } #[test] - fn test_extract_var_reference_parameter() { - check_assist( + fn extract_var_reference_parameter() { + check_assist_by_label( extract_variable, r#" struct X; @@ -1222,12 +2107,13 @@ fn foo(s: &S) { let $0x = &s.sub; x.do_thing(); }"#, + "Extract into variable", ); } #[test] - fn test_extract_var_index_deref() { - check_assist( + fn extract_var_index_deref() { + check_assist_by_label( extract_variable, r#" //- minicore: index @@ -1261,12 +2147,13 @@ fn foo(s: &S) { let $0sub = &s.sub; sub[0]; }"#, + "Extract into variable", ); } #[test] - fn test_extract_var_reference_parameter_deep_nesting() { - check_assist( + fn extract_var_reference_parameter_deep_nesting() { + check_assist_by_label( extract_variable, r#" struct Z; @@ -1315,12 +2202,13 @@ fn foo(s: &S) { let $0z = &s.sub.field.field; z.do_thing(); }"#, + "Extract into variable", ); } #[test] - fn test_extract_var_regular_parameter() { - check_assist( + fn extract_var_regular_parameter() { + check_assist_by_label( extract_variable, r#" struct X; @@ -1355,12 +2243,13 @@ fn foo(s: S) { let $0x = &s.sub; x.do_thing(); }"#, + "Extract into variable", ); } #[test] - fn test_extract_var_mutable_reference_local() { - check_assist( + fn extract_var_mutable_reference_local() { + check_assist_by_label( extract_variable, r#" struct X; @@ -1421,12 +2310,13 @@ fn foo() { let $0x = &local.sub; x.do_thing(); }"#, + "Extract into variable", ); } #[test] - fn test_extract_var_reference_local() { - check_assist( + fn extract_var_reference_local() { + check_assist_by_label( extract_variable, r#" struct X; @@ -1487,12 +2377,13 @@ fn foo() { let $0x = &local.sub; x.do_thing(); }"#, + "Extract into variable", ); } #[test] - fn test_extract_var_for_mutable_borrow() { - check_assist( + fn extract_var_for_mutable_borrow() { + check_assist_by_label( extract_variable, r#" fn foo() { @@ -1503,12 +2394,37 @@ fn foo() { let mut $0var_name = 0; let v = &mut var_name; }"#, + "Extract into variable", + ); + } + + #[test] + fn dont_extract_const_for_mutable_borrow() { + check_assist_not_applicable_by_label( + extract_variable, + r#" +fn foo() { + let v = &mut $00$0; +}"#, + "Extract into constant", + ); + } + + #[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( + check_assist_by_label( extract_variable, r#" struct S; @@ -1529,12 +2445,13 @@ fn foo() { let mut $0bar = bar(); bar.do_work(); }"#, + "Extract into variable", ); } #[test] fn generates_no_ref_for_deref() { - check_assist( + check_assist_by_label( extract_variable, r#" struct S; @@ -1559,6 +2476,7 @@ fn foo() { s.do_work(); } "#, + "Extract into variable", ); } } diff --git a/crates/ide-assists/src/handlers/promote_local_to_const.rs b/crates/ide-assists/src/handlers/promote_local_to_const.rs index 7c2dc0e0c1..0cc771ff39 100644 --- a/crates/ide-assists/src/handlers/promote_local_to_const.rs +++ b/crates/ide-assists/src/handlers/promote_local_to_const.rs @@ -1,19 +1,17 @@ -use hir::{HirDisplay, ModuleDef, PathResolution, Semantics}; +use hir::HirDisplay; use ide_db::{ assists::{AssistId, AssistKind}, defs::Definition, - syntax_helpers::node_ext::preorder_expr, - RootDatabase, }; use stdx::to_upper_snake_case; use syntax::{ ast::{self, make, HasName}, - ted, AstNode, WalkEvent, + ted, AstNode, }; use crate::{ assist_context::{AssistContext, Assists}, - utils, + utils::{self}, }; // Assist: promote_local_to_const @@ -63,7 +61,7 @@ pub(crate) fn promote_local_to_const(acc: &mut Assists, ctx: &AssistContext<'_>) }; let initializer = let_stmt.initializer()?; - if !is_body_const(&ctx.sema, &initializer) { + if !utils::is_body_const(&ctx.sema, &initializer) { cov_mark::hit!(promote_local_non_const); return None; } @@ -103,40 +101,6 @@ pub(crate) fn promote_local_to_const(acc: &mut Assists, ctx: &AssistContext<'_>) ) } -fn is_body_const(sema: &Semantics<'_, RootDatabase>, expr: &ast::Expr) -> bool { - let mut is_const = true; - preorder_expr(expr, &mut |ev| { - let expr = match ev { - WalkEvent::Enter(_) if !is_const => return true, - WalkEvent::Enter(expr) => expr, - WalkEvent::Leave(_) => return false, - }; - match expr { - ast::Expr::CallExpr(call) => { - if let Some(ast::Expr::PathExpr(path_expr)) = call.expr() { - if let Some(PathResolution::Def(ModuleDef::Function(func))) = - path_expr.path().and_then(|path| sema.resolve_path(&path)) - { - is_const &= func.is_const(sema.db); - } - } - } - ast::Expr::MethodCallExpr(call) => { - is_const &= - sema.resolve_method_call(&call).map(|it| it.is_const(sema.db)).unwrap_or(true) - } - ast::Expr::ForExpr(_) - | ast::Expr::ReturnExpr(_) - | ast::Expr::TryExpr(_) - | ast::Expr::YieldExpr(_) - | ast::Expr::AwaitExpr(_) => is_const = false, - _ => (), - } - !is_const - }); - is_const -} - #[cfg(test)] mod tests { use crate::tests::{check_assist, check_assist_not_applicable}; diff --git a/crates/ide-assists/src/tests.rs b/crates/ide-assists/src/tests.rs index 6469957fe1..0b1ff87c5c 100644 --- a/crates/ide-assists/src/tests.rs +++ b/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 function + Extract into... Replace if let with match "#]] .assert_eq(&expected); @@ -391,8 +390,7 @@ pub fn test_some_range(a: int) -> bool { expect![[r#" Convert integer base - Extract into variable - Extract into function + Extract into... Replace if let with match "#]] .assert_eq(&expected); @@ -405,8 +403,7 @@ pub fn test_some_range(a: int) -> bool { let expected = labels(&assists); expect![[r#" - Extract into variable - Extract into function + Extract into... "#]] .assert_eq(&expected); } @@ -440,7 +437,7 @@ pub fn test_some_range(a: int) -> bool { { let assists = assists(&db, &cfg, AssistResolveStrategy::None, frange.into()); - assert_eq!(2, assists.len()); + assert_eq!(4, assists.len()); let mut assists = assists.into_iter(); let extract_into_variable_assist = assists.next().unwrap(); @@ -451,7 +448,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, @@ -459,6 +460,46 @@ pub fn test_some_range(a: int) -> bool { "#]] .assert_debug_eq(&extract_into_variable_assist); + let extract_into_constant_assist = assists.next().unwrap(); + expect![[r#" + Assist { + id: AssistId( + "extract_constant", + RefactorExtract, + ), + label: "Extract into constant", + group: Some( + GroupLabel( + "Extract into...", + ), + ), + target: 59..60, + source_change: None, + command: None, + } + "#]] + .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 { @@ -467,7 +508,11 @@ pub fn test_some_range(a: int) -> bool { RefactorExtract, ), label: "Extract into function", - group: None, + group: Some( + GroupLabel( + "Extract into...", + ), + ), target: 59..60, source_change: None, command: None, @@ -486,7 +531,7 @@ pub fn test_some_range(a: int) -> bool { }), frange.into(), ); - assert_eq!(2, assists.len()); + assert_eq!(4, assists.len()); let mut assists = assists.into_iter(); let extract_into_variable_assist = assists.next().unwrap(); @@ -497,7 +542,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, @@ -505,6 +554,46 @@ pub fn test_some_range(a: int) -> bool { "#]] .assert_debug_eq(&extract_into_variable_assist); + let extract_into_constant_assist = assists.next().unwrap(); + expect![[r#" + Assist { + id: AssistId( + "extract_constant", + RefactorExtract, + ), + label: "Extract into constant", + group: Some( + GroupLabel( + "Extract into...", + ), + ), + target: 59..60, + source_change: None, + command: None, + } + "#]] + .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 { @@ -513,7 +602,11 @@ pub fn test_some_range(a: int) -> bool { RefactorExtract, ), label: "Extract into function", - group: None, + group: Some( + GroupLabel( + "Extract into...", + ), + ), target: 59..60, source_change: None, command: None, @@ -532,7 +625,7 @@ pub fn test_some_range(a: int) -> bool { }), frange.into(), ); - assert_eq!(2, assists.len()); + assert_eq!(4, assists.len()); let mut assists = assists.into_iter(); let extract_into_variable_assist = assists.next().unwrap(); @@ -543,7 +636,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 { @@ -594,6 +691,46 @@ pub fn test_some_range(a: int) -> bool { "#]] .assert_debug_eq(&extract_into_variable_assist); + let extract_into_constant_assist = assists.next().unwrap(); + expect![[r#" + Assist { + id: AssistId( + "extract_constant", + RefactorExtract, + ), + label: "Extract into constant", + group: Some( + GroupLabel( + "Extract into...", + ), + ), + target: 59..60, + source_change: None, + command: None, + } + "#]] + .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 { @@ -602,7 +739,11 @@ pub fn test_some_range(a: int) -> bool { RefactorExtract, ), label: "Extract into function", - group: None, + group: Some( + GroupLabel( + "Extract into...", + ), + ), target: 59..60, source_change: None, command: None, @@ -613,7 +754,7 @@ pub fn test_some_range(a: int) -> bool { { let assists = assists(&db, &cfg, AssistResolveStrategy::All, frange.into()); - assert_eq!(2, assists.len()); + assert_eq!(4, assists.len()); let mut assists = assists.into_iter(); let extract_into_variable_assist = assists.next().unwrap(); @@ -624,7 +765,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 { @@ -675,6 +820,140 @@ pub fn test_some_range(a: int) -> bool { "#]] .assert_debug_eq(&extract_into_variable_assist); + let extract_into_constant_assist = assists.next().unwrap(); + expect![[r#" + Assist { + id: AssistId( + "extract_constant", + RefactorExtract, + ), + label: "Extract into constant", + group: Some( + GroupLabel( + "Extract into...", + ), + ), + target: 59..60, + source_change: Some( + SourceChange { + source_file_edits: { + FileId( + 0, + ): ( + TextEdit { + indels: [ + Indel { + insert: "const", + 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, + 51..51, + ), + ], + ), + ), + ), + }, + file_system_edits: [], + is_snippet: true, + }, + ), + command: Some( + Rename, + ), + } + "#]] + .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 { @@ -683,7 +962,11 @@ pub fn test_some_range(a: int) -> bool { RefactorExtract, ), label: "Extract into function", - group: None, + group: Some( + GroupLabel( + "Extract into...", + ), + ), target: 59..60, source_change: Some( SourceChange { diff --git a/crates/ide-assists/src/tests/generated.rs b/crates/ide-assists/src/tests/generated.rs index 69ea200db1..87c3d166ee 100644 --- a/crates/ide-assists/src/tests/generated.rs +++ b/crates/ide-assists/src/tests/generated.rs @@ -932,6 +932,24 @@ enum TheEnum { ) } +#[test] +fn doctest_extract_constant() { + check_doc_test( + "extract_constant", + r#####" +fn main() { + $0(1 + 2)$0 * 4; +} +"#####, + r#####" +fn main() { + const $0VAR_NAME: i32 = 1 + 2; + VAR_NAME * 4; +} +"#####, + ) +} + #[test] fn doctest_extract_expressions_from_format_string() { check_doc_test( @@ -1006,6 +1024,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( diff --git a/crates/ide-assists/src/utils.rs b/crates/ide-assists/src/utils.rs index 0830017bd0..3c26b04359 100644 --- a/crates/ide-assists/src/utils.rs +++ b/crates/ide-assists/src/utils.rs @@ -3,11 +3,13 @@ pub(crate) use gen_trait_fn_body::gen_trait_fn_body; use hir::{ db::{ExpandDatabase, HirDatabase}, - HasAttrs as HirHasAttrs, HirDisplay, InFile, Semantics, + HasAttrs as HirHasAttrs, HirDisplay, InFile, ModuleDef, PathResolution, Semantics, }; use ide_db::{ - famous_defs::FamousDefs, path_transform::PathTransform, - syntax_helpers::prettify_macro_expansion, RootDatabase, + famous_defs::FamousDefs, + path_transform::PathTransform, + syntax_helpers::{node_ext::preorder_expr, prettify_macro_expansion}, + RootDatabase, }; use stdx::format_to; use syntax::{ @@ -19,7 +21,7 @@ use syntax::{ }, ted, AstNode, AstToken, Direction, Edition, NodeOrToken, SourceFile, SyntaxKind::*, - SyntaxNode, SyntaxToken, TextRange, TextSize, T, + SyntaxNode, SyntaxToken, TextRange, TextSize, WalkEvent, T, }; use crate::assist_context::{AssistContext, SourceChangeBuilder}; @@ -966,3 +968,37 @@ pub(crate) fn tt_from_syntax(node: SyntaxNode) -> Vec, expr: &ast::Expr) -> bool { + let mut is_const = true; + preorder_expr(expr, &mut |ev| { + let expr = match ev { + WalkEvent::Enter(_) if !is_const => return true, + WalkEvent::Enter(expr) => expr, + WalkEvent::Leave(_) => return false, + }; + match expr { + ast::Expr::CallExpr(call) => { + if let Some(ast::Expr::PathExpr(path_expr)) = call.expr() { + if let Some(PathResolution::Def(ModuleDef::Function(func))) = + path_expr.path().and_then(|path| sema.resolve_path(&path)) + { + is_const &= func.is_const(sema.db); + } + } + } + ast::Expr::MethodCallExpr(call) => { + is_const &= + sema.resolve_method_call(&call).map(|it| it.is_const(sema.db)).unwrap_or(true) + } + ast::Expr::ForExpr(_) + | ast::Expr::ReturnExpr(_) + | ast::Expr::TryExpr(_) + | ast::Expr::YieldExpr(_) + | ast::Expr::AwaitExpr(_) => is_const = false, + _ => (), + } + !is_const + }); + is_const +} diff --git a/crates/syntax/src/ast/make.rs b/crates/syntax/src/ast/make.rs index 05c2a8354d..eb96ab6ef5 100644 --- a/crates/syntax/src/ast/make.rs +++ b/crates/syntax/src/ast/make.rs @@ -895,7 +895,29 @@ pub fn item_const( None => String::new(), Some(it) => format!("{it} "), }; - ast_from_text(&format!("{visibility} const {name}: {ty} = {expr};")) + ast_from_text(&format!("{visibility}const {name}: {ty} = {expr};")) +} + +pub fn item_static( + visibility: Option, + is_unsafe: bool, + is_mut: bool, + name: ast::Name, + ty: ast::Type, + expr: Option, +) -> ast::Static { + let visibility = match visibility { + None => String::new(), + Some(it) => format!("{it} "), + }; + let is_unsafe = if is_unsafe { "unsafe " } else { "" }; + let is_mut = if is_mut { "mut " } else { "" }; + let expr = match expr { + Some(it) => &format!(" = {it}"), + None => "", + }; + + ast_from_text(&format!("{visibility}{is_unsafe}static {is_mut}{name}: {ty}{expr};")) } pub fn unnamed_param(ty: ast::Type) -> ast::Param { diff --git a/crates/syntax/src/ast/syntax_factory/constructors.rs b/crates/syntax/src/ast/syntax_factory/constructors.rs index e86c291f76..280c5c25cb 100644 --- a/crates/syntax/src/ast/syntax_factory/constructors.rs +++ b/crates/syntax/src/ast/syntax_factory/constructors.rs @@ -188,6 +188,73 @@ impl SyntaxFactory { ast } + pub fn item_const( + &self, + visibility: Option, + name: ast::Name, + ty: ast::Type, + expr: ast::Expr, + ) -> ast::Const { + let ast = make::item_const(visibility.clone(), name.clone(), ty.clone(), expr.clone()) + .clone_for_update(); + + if let Some(mut mapping) = self.mappings() { + let mut builder = SyntaxMappingBuilder::new(ast.syntax().clone()); + if let Some(visibility) = visibility { + builder.map_node( + visibility.syntax().clone(), + ast.visibility().unwrap().syntax().clone(), + ); + } + builder.map_node(name.syntax().clone(), ast.name().unwrap().syntax().clone()); + builder.map_node(ty.syntax().clone(), ast.ty().unwrap().syntax().clone()); + builder.map_node(expr.syntax().clone(), ast.body().unwrap().syntax().clone()); + builder.finish(&mut mapping); + } + + ast + } + + pub fn item_static( + &self, + visibility: Option, + is_unsafe: bool, + is_mut: bool, + name: ast::Name, + ty: ast::Type, + expr: Option, + ) -> ast::Static { + let ast = make::item_static( + visibility.clone(), + is_unsafe, + is_mut, + name.clone(), + ty.clone(), + expr.clone(), + ) + .clone_for_update(); + + if let Some(mut mapping) = self.mappings() { + let mut builder = SyntaxMappingBuilder::new(ast.syntax().clone()); + if let Some(visibility) = visibility { + builder.map_node( + visibility.syntax().clone(), + ast.visibility().unwrap().syntax().clone(), + ); + } + + builder.map_node(name.syntax().clone(), ast.name().unwrap().syntax().clone()); + builder.map_node(ty.syntax().clone(), ast.ty().unwrap().syntax().clone()); + + if let Some(expr) = expr { + builder.map_node(expr.syntax().clone(), ast.body().unwrap().syntax().clone()); + } + builder.finish(&mut mapping); + } + + ast + } + pub fn turbofish_generic_arg_list( &self, args: impl IntoIterator + Clone,