use hir::{HirDisplay, TypeInfo}; use ide_db::{ assists::GroupLabel, syntax_helpers::{LexedStr, suggest_name}, }; use syntax::{ NodeOrToken, SyntaxKind, SyntaxNode, T, algo::ancestors_at_offset, ast::{ self, AstNode, edit::{AstNodeEdit, IndentLevel}, make, syntax_factory::SyntaxFactory, }, syntax_editor::Position, }; use crate::{AssistContext, AssistId, Assists, utils::is_body_const}; // Assist: extract_variable // // Extracts subexpression into a variable. // // ``` // fn main() { // $0(1 + 2)$0 * 4; // } // ``` // -> // ``` // fn main() { // let $0var_name = 1 + 2; // 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![;]) { t.parent().and_then(ast::ExprStmt::cast)?.syntax().clone() } else if let Some(expr) = ancestors_at_offset(ctx.source_file().syntax(), ctx.offset()) .next() .and_then(ast::Expr::cast) { expr.syntax().ancestors().find_map(valid_target_expr)?.syntax().clone() } else { return None; } } else { match ctx.covering_element() { NodeOrToken::Node(it) => it, NodeOrToken::Token(it) if it.kind() == SyntaxKind::COMMENT => { cov_mark::hit!(extract_var_in_comment_is_not_applicable); return None; } NodeOrToken::Token(it) => it.parent()?, } }; let node = node.ancestors().take_while(|anc| anc.text_range() == node.text_range()).last()?; let range = node.text_range(); let to_extract = node .descendants() .take_while(|it| range.contains_range(it.text_range())) .find_map(valid_target_expr)?; let ty = ctx.sema.type_of_expr(&to_extract).map(TypeInfo::adjusted); if matches!(&ty, Some(ty_info) if ty_info.is_unit()) { return None; } let parent = to_extract.syntax().parent().and_then(ast::Expr::cast); // Any expression that autoderefs may need adjustment. let mut needs_adjust = parent.as_ref().is_some_and(|it| match it { ast::Expr::FieldExpr(_) | ast::Expr::MethodCallExpr(_) | ast::Expr::CallExpr(_) | ast::Expr::AwaitExpr(_) => true, ast::Expr::IndexExpr(index) if index.base().as_ref() == Some(&to_extract) => true, _ => false, }); let mut to_extract_no_ref = peel_parens(to_extract.clone()); let needs_ref = needs_adjust && match &to_extract_no_ref { ast::Expr::FieldExpr(_) | ast::Expr::IndexExpr(_) | ast::Expr::MacroExpr(_) | ast::Expr::ParenExpr(_) | ast::Expr::PathExpr(_) => true, ast::Expr::PrefixExpr(prefix) if prefix.op_kind() == Some(ast::UnaryOp::Deref) => { to_extract_no_ref = prefix.expr()?; needs_adjust = false; false } _ => false, }; let module = ctx.sema.scope(to_extract.syntax())?.module(); let target = to_extract.syntax().text_range(); 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 ty_string = match kind { 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() || ty.is_mutable_reference() { continue; } let Ok(type_string) = ty.display_source_code(ctx.db(), module.into(), false) else { continue; }; type_string } _ => "".to_owned(), }; 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::with_mappings(); 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, 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())) } // fixup indentation of block .indent_with_mapping(indent_to, &make); editor.replace(to_wrap.syntax(), block.syntax()); } } editor.add_mappings(make.finish_with_mappings()); edit.add_file_edits(ctx.vfs_file_id(), editor); edit.rename(); }, ); } Some(()) } fn peel_parens(mut expr: ast::Expr) -> ast::Expr { while let ast::Expr::ParenExpr(parens) = &expr { let Some(expr_inside) = parens.expr() else { break }; expr = expr_inside; } expr } /// Check whether the node is a valid expression which can be extracted to a variable. /// 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() { SyntaxKind::PATH_EXPR | SyntaxKind::LOOP_EXPR | SyntaxKind::LET_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), } } 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::refactor_extract(s) } 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) { // We only do this sort of extraction for fields because they should have lowercase names if let ExtractionKind::Variable = self { let field_shorthand = to_extract .syntax() .parent() .and_then(ast::RecordExprField::cast) .filter(|field| field.name_ref().is_some()); if let Some(field) = field_shorthand { return (field.to_string(), field.syntax().clone()); } } let mut name_generator = suggest_name::NameGenerator::new_from_scope_locals(ctx.sema.scope(to_extract.syntax())); let var_name = if let Some(literal_name) = get_literal_name(ctx, to_extract) { name_generator.suggest_name(&literal_name) } else { name_generator.for_variable(to_extract, &ctx.sema) }; let var_name = match self { ExtractionKind::Variable => var_name.to_lowercase(), ExtractionKind::Constant | ExtractionKind::Static => var_name.to_uppercase(), }; (var_name, to_extract.syntax().clone()) } } fn get_literal_name(ctx: &AssistContext<'_>, expr: &ast::Expr) -> Option { let ast::Expr::Literal(literal) = expr else { return None; }; let inner = match literal.kind() { ast::LiteralKind::String(string) => string.value().ok()?.into_owned(), ast::LiteralKind::ByteString(byte_string) => { String::from_utf8(byte_string.value().ok()?.into_owned()).ok()? } ast::LiteralKind::CString(cstring) => { String::from_utf8(cstring.value().ok()?.into_owned()).ok()? } _ => return None, }; // Entirely arbitrary if inner.len() > 32 { return None; } match LexedStr::single_token(ctx.edition(), &inner) { Some((SyntaxKind::IDENT, None)) => Some(inner), _ => None, } } #[derive(Debug, Clone)] enum Anchor { Before(SyntaxNode), Replace(ast::ExprStmt), WrapInBlock(ast::Expr), } impl Anchor { 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())) .find_map(|node| { if ast::MacroCall::can_cast(node.kind()) { return None; } if let Some(expr) = node.parent().and_then(ast::StmtList::cast).and_then(|it| it.tail_expr()) && expr.syntax() == &node { cov_mark::hit!(test_extract_var_last_expr); return Some(Anchor::Before(node)); } if let Some(parent) = node.parent() { if let Some(parent) = ast::ClosureExpr::cast(parent.clone()) { cov_mark::hit!(test_extract_var_in_closure_no_block); return parent.body().map(Anchor::WrapInBlock); } if let Some(parent) = ast::MatchArm::cast(parent) { 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); return parent.expr().map(Anchor::WrapInBlock); } } } if let Some(stmt) = ast::Stmt::cast(node.clone()) { if let ast::Stmt::ExprStmt(stmt) = stmt && stmt.expr().as_ref() == Some(to_extract) { return Some(Anchor::Replace(stmt)); } 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 { // 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 extract_var_simple_without_select() { check_assist_by_label( extract_variable, r#" fn main() -> i32 { if$0 true { 1 } else { 2 } } "#, r#" fn main() -> i32 { let $0var_name = if true { 1 } else { 2 }; var_name } "#, "Extract into variable", ); check_assist_by_label( extract_variable, r#" fn foo() -> i32 { 1 } fn main() { foo();$0 } "#, r#" fn foo() -> i32 { 1 } fn main() { let $0foo = foo(); } "#, "Extract into variable", ); check_assist_by_label( extract_variable, r#" fn main() { let a = Some(2); a.is_some();$0 } "#, r#" fn main() { let a = Some(2); let $0is_some = a.is_some(); } "#, "Extract into variable", ); check_assist_by_label( extract_variable, r#" fn main() { "hello"$0; } "#, r#" fn main() { let $0hello = "hello"; } "#, "Extract into variable", ); check_assist_by_label( extract_variable, r#" fn main() { 1 + 2$0; } "#, r#" fn main() { let $0var_name = 1 + 2; } "#, "Extract into variable", ); check_assist_by_label( extract_variable, r#" fn main() { match () { () if true => 1, _ => 2, };$0 } "#, r#" fn main() { let $0var_name = match () { () if true => 1, _ => 2, }; } "#, "Extract into variable", ); } #[test] fn extract_const_simple_without_select() { check_assist_by_label( extract_variable, r#" fn main() -> i32 { if$0 true { 1 } else { 2 } } "#, 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 $0HELLO: &'static 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$0 true { 1 } else { 2 } } "#, 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 $0HELLO: &'static 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#" fn foo() {} fn main() { foo()$0; } "#, ); check_assist_not_applicable( extract_variable, r#" fn foo() { let mut i = 3; if i >= 0 { i += 1; } else { i -= 1; }$0 }"#, ); } #[test] fn extract_var_simple() { check_assist_by_label( extract_variable, r#" fn foo() { foo($01 + 1$0); }"#, r#" fn foo() { let $0var_name = 1 + 1; foo(var_name); }"#, "Extract into variable", ); } #[test] 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 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_by_label( extract_variable, r#" fn foo() { $0 1 + 1$0; }"#, r#" fn foo() { let $0var_name = 1 + 1; }"#, "Extract into variable", ); check_assist_by_label( extract_variable, r#" fn foo() { $0{ let x = 0; x }$0; something_else(); }"#, r#" fn foo() { let $0var_name = { let x = 0; x }; something_else(); }"#, "Extract into variable", ); } #[test] fn extract_const_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() { 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#" fn foo() { let $0var_name = 1; var_name + 1; }"#, "Extract into variable", ); } #[test] 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_by_label( extract_variable, r#" fn foo() { bar($01 + 1$0) } "#, r#" fn foo() { let $0var_name = 1 + 1; bar(var_name) } "#, "Extract into variable", ); check_assist_by_label( extract_variable, r#" fn foo() -> i32 { $0bar(1 + 1)$0 } fn bar(i: i32) -> i32 { i } "#, r#" fn foo() -> i32 { let $0bar = bar(1 + 1); bar } fn bar(i: i32) -> i32 { i } "#, "Extract into variable", ) } #[test] 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_by_label( extract_variable, r#" fn main() { let x = true; let tuple = match x { true => ($02 + 2$0, true) _ => (0, false) }; } "#, r#" fn main() { let x = true; let tuple = match x { true => { let $0var_name = 2 + 2; (var_name, true) } _ => (0, false) }; } "#, "Extract into variable", ); } #[test] fn extract_var_in_match_arm_with_block() { check_assist_by_label( extract_variable, r#" fn main() { let x = true; let tuple = match x { true => { let y = 1; ($02 + y$0, true) } _ => (0, false) }; } "#, r#" fn main() { let x = true; let tuple = match x { true => { let y = 1; let $0var_name = 2 + y; (var_name, true) } _ => (0, false) }; } "#, "Extract into variable", ); } #[test] fn extract_var_in_match_guard() { cov_mark::check!(test_extract_var_in_match_guard); check_assist_by_label( extract_variable, r#" fn main() { match () { () if $010 > 0$0 => 1 _ => 2 }; } "#, r#" fn main() { let $0var_name = 10 > 0; match () { () if var_name => 1 _ => 2 }; } "#, "Extract into variable", ); } #[test] fn extract_var_in_closure_no_block() { cov_mark::check!(test_extract_var_in_closure_no_block); check_assist_by_label( extract_variable, r#" fn main() { let lambda = |x: u32| $0x * 2$0; } "#, r#" fn main() { let lambda = |x: u32| { let $0var_name = x * 2; var_name }; } "#, "Extract into variable", ); } #[test] fn extract_var_in_closure_with_block() { check_assist_by_label( extract_variable, r#" fn main() { let lambda = |x: u32| { $0x * 2$0 }; } "#, r#" fn main() { let lambda = |x: u32| { let $0var_name = x * 2; var_name }; } "#, "Extract into variable", ); } #[test] 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 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 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 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_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_by_label( extract_variable, r#" fn foo() -> u32 { let foo = 1; // bar $0return 2 + 2$0; } "#, r#" fn foo() -> u32 { let foo = 1; // bar let $0var_name = 2 + 2; return var_name; } "#, "Extract into variable", ); } #[test] 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 extract_var_let_expr() { check_assist_by_label( extract_variable, r#" fn main() { if $0let$0 Some(x) = Some(2+2) {} } "#, r#" fn main() { let $0var_name = Some(2+2); if let Some(x) = var_name {} } "#, "Extract into variable", ); } #[test] 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_by_label( extract_variable, r#" struct S { foo: i32 } fn main() { S { foo: $01 + 1$0 } } "#, r#" struct S { foo: i32 } fn main() { let $0foo = 1 + 1; S { foo } } "#, "Extract into variable", ) } #[test] fn extract_var_name_from_type() { check_assist_by_label( extract_variable, r#" struct Test(i32); fn foo() -> Test { $0{ Test(10) }$0 } "#, r#" struct Test(i32); fn foo() -> Test { let $0test = { Test(10) }; test } "#, "Extract into variable", ) } #[test] fn extract_var_name_from_parameter() { check_assist_by_label( extract_variable, r#" fn bar(test: u32, size: u32) fn foo() { bar(1, $01+1$0); } "#, r#" fn bar(test: u32, size: u32) fn foo() { let $0size = 1+1; bar(1, size); } "#, "Extract into variable", ) } #[test] fn extract_var_parameter_name_has_precedence_over_type() { check_assist_by_label( extract_variable, r#" struct TextSize(u32); fn bar(test: u32, size: TextSize) fn foo() { bar(1, $0{ TextSize(1+1) }$0); } "#, r#" struct TextSize(u32); fn bar(test: u32, size: TextSize) fn foo() { let $0size = { TextSize(1+1) }; bar(1, size); } "#, "Extract into variable", ) } #[test] fn extract_var_name_from_function() { check_assist_by_label( extract_variable, r#" fn is_required(test: u32, size: u32) -> bool fn foo() -> bool { $0is_required(1, 2)$0 } "#, r#" fn is_required(test: u32, size: u32) -> bool fn foo() -> bool { let $0is_required = is_required(1, 2); is_required } "#, "Extract into variable", ) } #[test] fn extract_var_name_from_method() { check_assist_by_label( extract_variable, r#" struct S; impl S { fn bar(&self, n: u32) -> u32 { n } } fn foo() -> u32 { $0S.bar(1)$0 } "#, r#" struct S; impl S { fn bar(&self, n: u32) -> u32 { n } } fn foo() -> u32 { let $0bar = S.bar(1); bar } "#, "Extract into variable", ) } #[test] fn extract_var_name_from_method_param() { check_assist_by_label( extract_variable, r#" struct S; impl S { fn bar(&self, n: u32, size: u32) { n } } fn foo() { S.bar($01 + 1$0, 2) } "#, r#" struct S; impl S { fn bar(&self, n: u32, size: u32) { n } } fn foo() { let $0n = 1 + 1; S.bar(n, 2) } "#, "Extract into variable", ) } #[test] fn extract_var_name_from_ufcs_method_param() { check_assist_by_label( extract_variable, r#" struct S; impl S { fn bar(&self, n: u32, size: u32) { n } } fn foo() { S::bar(&S, $01 + 1$0, 2) } "#, r#" struct S; impl S { fn bar(&self, n: u32, size: u32) { n } } fn foo() { let $0n = 1 + 1; S::bar(&S, n, 2) } "#, "Extract into variable", ) } #[test] fn extract_var_parameter_name_has_precedence_over_function() { check_assist_by_label( extract_variable, r#" fn bar(test: u32, size: u32) fn foo() { bar(1, $0symbol_size(1, 2)$0); } "#, r#" fn bar(test: u32, size: u32) fn foo() { let $0size = symbol_size(1, 2); bar(1, size); } "#, "Extract into variable", ) } #[test] fn extract_macro_call() { 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() { let $0items = vec![]; let _ = items; } "#, "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 $0ITEMS: Vec = vec![]; let _ = ITEMS; } "#, "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 $0ITEMS: Vec = vec![]; let _ = ITEMS; } "#, "Extract into static", ); } #[test] fn extract_var_for_return_not_applicable() { check_assist_not_applicable(extract_variable, "fn foo() { $0return$0; } "); } #[test] fn extract_var_for_break_not_applicable() { check_assist_not_applicable(extract_variable, "fn main() { loop { $0break$0; }; }"); } #[test] fn extract_var_for_let_expr_not_applicable() { check_assist_not_applicable( extract_variable, "fn main() { if $0let Some(x) = Some(2+2) {} }", ); } #[test] fn extract_var_unit_expr_not_applicable() { check_assist_not_applicable( extract_variable, r#" fn foo() { let mut i = 3; $0if i >= 0 { i += 1; } else { i -= 1; }$0 }"#, ); } // FIXME: This is not quite correct, but good enough(tm) for the sorting heuristic #[test] fn extract_var_target() { 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 { true => ($02 + 2$0, true) _ => (0, false) }; } "#, "2 + 2", ); } #[test] fn extract_var_no_block_body() { check_assist_not_applicable_by_label( extract_variable, r#" const X: usize = $0100$0; "#, "Extract into variable", ); } #[test] 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 { vec: Vec } struct Vec; impl Vec { fn push(&mut self, _:usize) {} } fn foo(s: &mut S) { $0s.vec$0.push(0); }"#, r#" struct S { vec: Vec } struct Vec; impl Vec { fn push(&mut self, _:usize) {} } fn foo(s: &mut S) { let $0items = &mut s.vec; items.push(0); }"#, "Extract into variable", ); } #[test] 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 { field: X } struct X { field: S } struct S { vec: Vec } struct Vec; impl Vec { fn push(&mut self, _:usize) {} } fn foo(f: &mut Y) { $0f.field.field.vec$0.push(0); }"#, r#" struct Y { field: X } struct X { field: S } struct S { vec: Vec } struct Vec; impl Vec { fn push(&mut self, _:usize) {} } fn foo(f: &mut Y) { let $0items = &mut f.field.field.vec; items.push(0); }"#, "Extract into variable", ); } #[test] fn extract_var_reference_parameter() { check_assist_by_label( extract_variable, r#" struct X; impl X { fn do_thing(&self) { } } struct S { sub: X } fn foo(s: &S) { $0s.sub$0.do_thing(); }"#, r#" struct X; impl X { fn do_thing(&self) { } } struct S { sub: X } fn foo(s: &S) { let $0x = &s.sub; x.do_thing(); }"#, "Extract into variable", ); } #[test] fn extract_var_index_deref() { check_assist_by_label( extract_variable, r#" //- minicore: index struct X; impl core::ops::Index for X { type Output = i32; fn index(&self) -> &Self::Output { 0 } } struct S { sub: X } fn foo(s: &S) { $0s.sub$0[0]; }"#, r#" struct X; impl core::ops::Index for X { type Output = i32; fn index(&self) -> &Self::Output { 0 } } struct S { sub: X } fn foo(s: &S) { let $0x = &s.sub; x[0]; }"#, "Extract into variable", ); } #[test] fn extract_var_reference_parameter_deep_nesting() { check_assist_by_label( extract_variable, r#" struct Z; impl Z { fn do_thing(&self) { } } struct Y { field: Z } struct X { field: Y } struct S { sub: X } fn foo(s: &S) { $0s.sub.field.field$0.do_thing(); }"#, r#" struct Z; impl Z { fn do_thing(&self) { } } struct Y { field: Z } struct X { field: Y } struct S { sub: X } fn foo(s: &S) { let $0z = &s.sub.field.field; z.do_thing(); }"#, "Extract into variable", ); } #[test] fn extract_var_regular_parameter() { check_assist_by_label( extract_variable, r#" struct X; impl X { fn do_thing(&self) { } } struct S { sub: X } fn foo(s: S) { $0s.sub$0.do_thing(); }"#, r#" struct X; impl X { fn do_thing(&self) { } } struct S { sub: X } fn foo(s: S) { let $0x = &s.sub; x.do_thing(); }"#, "Extract into variable", ); } #[test] fn extract_var_mutable_reference_local() { check_assist_by_label( extract_variable, r#" struct X; struct S { sub: X } impl S { fn new() -> S { S { sub: X::new() } } } impl X { fn new() -> X { X { } } fn do_thing(&self) { } } fn foo() { let local = &mut S::new(); $0local.sub$0.do_thing(); }"#, r#" struct X; struct S { sub: X } impl S { fn new() -> S { S { sub: X::new() } } } impl X { fn new() -> X { X { } } fn do_thing(&self) { } } fn foo() { let local = &mut S::new(); let $0x = &local.sub; x.do_thing(); }"#, "Extract into variable", ); } #[test] fn extract_var_reference_local() { check_assist_by_label( extract_variable, r#" struct X; struct S { sub: X } impl S { fn new() -> S { S { sub: X::new() } } } impl X { fn new() -> X { X { } } fn do_thing(&self) { } } fn foo() { let local = &S::new(); $0local.sub$0.do_thing(); }"#, r#" struct X; struct S { sub: X } impl S { fn new() -> S { S { sub: X::new() } } } impl X { fn new() -> X { X { } } fn do_thing(&self) { } } fn foo() { let local = &S::new(); let $0x = &local.sub; x.do_thing(); }"#, "Extract into variable", ); } #[test] fn extract_var_for_mutable_borrow() { check_assist_by_label( extract_variable, r#" fn foo() { let v = &mut $00$0; }"#, r#" 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_by_label( extract_variable, r#" struct S; impl S { fn do_work(&mut self) {} } fn bar() -> S { S } fn foo() { $0bar()$0.do_work(); }"#, r#" struct S; impl S { fn do_work(&mut self) {} } fn bar() -> S { S } fn foo() { let mut $0bar = bar(); bar.do_work(); }"#, "Extract into variable", ); } #[test] fn generates_no_ref_for_deref() { check_assist_by_label( extract_variable, r#" struct S; impl S { fn do_work(&mut self) {} } fn bar() -> S { S } fn foo() { let v = &mut &mut bar(); $0(**v)$0.do_work(); } "#, r#" struct S; impl S { fn do_work(&mut self) {} } fn bar() -> S { S } fn foo() { let v = &mut &mut bar(); let $0s = *v; s.do_work(); } "#, "Extract into variable", ); } #[test] fn extract_string_literal() { check_assist_by_label( extract_variable, r#" struct Entry<'a>(&'a str); fn foo() { let entry = Entry($0"Hello"$0); } "#, r#" struct Entry<'a>(&'a str); fn foo() { let $0hello = "Hello"; let entry = Entry(hello); } "#, "Extract into variable", ); check_assist_by_label( extract_variable, r#" struct Entry<'a>(&'a str); fn foo() { let entry = Entry($0"Hello"$0); } "#, r#" struct Entry<'a>(&'a str); fn foo() { const $0HELLO: &str = "Hello"; let entry = Entry(HELLO); } "#, "Extract into constant", ); check_assist_by_label( extract_variable, r#" struct Entry<'a>(&'a str); fn foo() { let entry = Entry($0"Hello"$0); } "#, r#" struct Entry<'a>(&'a str); fn foo() { static $0HELLO: &str = "Hello"; let entry = Entry(HELLO); } "#, "Extract into static", ); } #[test] fn extract_variable_string_literal_use_field_shorthand() { // When field shorthand is available, it should // only be used when extracting into a variable check_assist_by_label( extract_variable, r#" struct Entry<'a> { message: &'a str } fn foo() { let entry = Entry { message: $0"Hello"$0 }; } "#, r#" struct Entry<'a> { message: &'a str } fn foo() { let $0message = "Hello"; let entry = Entry { message }; } "#, "Extract into variable", ); check_assist_by_label( extract_variable, r#" struct Entry<'a> { message: &'a str } fn foo() { let entry = Entry { message: $0"Hello"$0 }; } "#, r#" struct Entry<'a> { message: &'a str } fn foo() { const $0HELLO: &str = "Hello"; let entry = Entry { message: HELLO }; } "#, "Extract into constant", ); check_assist_by_label( extract_variable, r#" struct Entry<'a> { message: &'a str } fn foo() { let entry = Entry { message: $0"Hello"$0 }; } "#, r#" struct Entry<'a> { message: &'a str } fn foo() { static $0HELLO: &str = "Hello"; let entry = Entry { message: HELLO }; } "#, "Extract into static", ); } #[test] fn extract_variable_name_conflicts() { check_assist_by_label( extract_variable, r#" struct S { x: i32 }; fn main() { let s = 2; let t = $0S { x: 1 }$0; let t2 = t; let x = s; } "#, r#" struct S { x: i32 }; fn main() { let s = 2; let $0s1 = S { x: 1 }; let t = s1; let t2 = t; let x = s; } "#, "Extract into variable", ); } }