use hir::{AsAssocItem, AssocItemContainer, HasCrate, HasSource}; use ide_db::{assists::AssistId, base_db::FileRange, defs::Definition, search::SearchScope}; use syntax::{ ast::{self, edit::IndentLevel, edit_in_place::Indent, AstNode}, SyntaxKind, }; use crate::{ assist_context::{AssistContext, Assists}, utils, }; // NOTE: Code may break if the self type implements a trait that has associated const with the same // name, but it's pretty expensive to check that (`hir::Impl::all_for_type()`) and we assume that's // pretty rare case. // Assist: move_const_to_impl // // Move a local constant item in a method to impl's associated constant. All the references will be // qualified with `Self::`. // // ``` // struct S; // impl S { // fn foo() -> usize { // /// The answer. // const C$0: usize = 42; // // C * C // } // } // ``` // -> // ``` // struct S; // impl S { // /// The answer. // const C: usize = 42; // // fn foo() -> usize { // Self::C * Self::C // } // } // ``` pub(crate) fn move_const_to_impl(acc: &mut Assists, ctx: &AssistContext<'_>) -> Option<()> { let db = ctx.db(); let const_: ast::Const = ctx.find_node_at_offset()?; // Don't show the assist when the cursor is at the const's body. if let Some(body) = const_.body() { if body.syntax().text_range().contains(ctx.offset()) { return None; } } let parent_fn = const_.syntax().ancestors().find_map(ast::Fn::cast)?; // NOTE: We can technically provide this assist for default methods in trait definitions, but // it's somewhat complex to handle it correctly when the const's name conflicts with // supertrait's item. We may want to consider implementing it in the future. let AssocItemContainer::Impl(impl_) = ctx.sema.to_def(&parent_fn)?.as_assoc_item(db)?.container(db) else { return None; }; if impl_.trait_(db).is_some() { return None; } let def = ctx.sema.to_def(&const_)?; let name = def.name(db)?; let items = impl_.source(db)?.value.assoc_item_list()?; let ty = impl_.self_ty(db); // If there exists another associated item with the same name, skip the assist. if ty .iterate_assoc_items(db, ty.krate(db), |assoc| { // Type aliases wouldn't conflict due to different namespaces, but we're only checking // the items in inherent impls, so we assume `assoc` is never type alias for the sake // of brevity (inherent associated types exist in nightly Rust, but it's *very* // unstable and we don't support them either). assoc.name(db).filter(|it| it == &name) }) .is_some() { return None; } let usages = Definition::Const(def).usages(&ctx.sema).in_scope(SearchScope::file_range(FileRange { file_id: ctx.file_id(), range: parent_fn.syntax().text_range(), })); acc.add( AssistId("move_const_to_impl", crate::AssistKind::RefactorRewrite), "Move const to impl block", const_.syntax().text_range(), |builder| { let range_to_delete = match const_.syntax().next_sibling_or_token() { Some(s) if matches!(s.kind(), SyntaxKind::WHITESPACE) => { // Remove following whitespaces too. const_.syntax().text_range().cover(s.text_range()) } _ => const_.syntax().text_range(), }; builder.delete(range_to_delete); let const_ref = format!("Self::{name}"); for range in usages.all().file_ranges().map(|it| it.range) { builder.replace(range, const_ref.clone()); } // Heuristically inserting the extracted const after the consecutive existing consts // from the beginning of assoc items. We assume there are no inherent assoc type as // above. let last_const = items.assoc_items().take_while(|it| matches!(it, ast::AssocItem::Const(_))).last(); let insert_offset = match &last_const { Some(it) => it.syntax().text_range().end(), None => match items.l_curly_token() { Some(l_curly) => l_curly.text_range().end(), // Not sure if this branch is ever reachable, but it wouldn't hurt to have a // fallback. None => items.syntax().text_range().start(), }, }; // If the moved const will be the first item of the impl, add a new line after that. // // We're assuming the code is formatted according to Rust's standard style guidelines // (i.e. no empty lines between impl's `{` token and its first assoc item). let fixup = if last_const.is_none() { "\n" } else { "" }; let indent = IndentLevel::from_node(parent_fn.syntax()); let const_ = const_.clone_for_update(); const_.reindent_to(indent); let mut const_text = format!("\n{indent}{const_}{fixup}"); utils::escape_non_snippet(&mut const_text); builder.insert(insert_offset, const_text); }, ) } #[cfg(test)] mod tests { use crate::tests::{check_assist, check_assist_not_applicable}; use super::*; #[test] fn not_applicable_to_top_level_const() { check_assist_not_applicable( move_const_to_impl, r#" const C$0: () = (); "#, ); } #[test] fn not_applicable_to_free_fn() { check_assist_not_applicable( move_const_to_impl, r#" fn f() { const C$0: () = (); } "#, ); } #[test] fn not_applicable_when_at_const_body() { check_assist_not_applicable( move_const_to_impl, r#" struct S; impl S { fn f() { const C: () = ($0); } } "#, ); } #[test] fn not_applicable_when_inside_const_body_block() { check_assist_not_applicable( move_const_to_impl, r#" struct S; impl S { fn f() { const C: () = { ($0) }; } } "#, ); } #[test] fn not_applicable_to_trait_impl_fn() { check_assist_not_applicable( move_const_to_impl, r#" trait Trait { fn f(); } impl Trait for () { fn f() { const C$0: () = (); } } "#, ); } #[test] fn not_applicable_to_non_assoc_fn_inside_impl() { check_assist_not_applicable( move_const_to_impl, r#" struct S; impl S { fn f() { fn g() { const C$0: () = (); } } } "#, ); } #[test] fn not_applicable_when_const_with_same_name_exists() { check_assist_not_applicable( move_const_to_impl, r#" struct S; impl S { const C: usize = 42; fn f() { const C$0: () = (); } "#, ); check_assist_not_applicable( move_const_to_impl, r#" struct S; impl S { const C: usize = 42; } impl S { fn f() { const C$0: () = (); } "#, ); } #[test] fn move_const_simple_body() { check_assist( move_const_to_impl, r#" struct S; impl S { fn f() -> usize { /// doc comment const C$0: usize = 42; C * C } } "#, r#" struct S; impl S { /// doc comment const C: usize = 42; fn f() -> usize { Self::C * Self::C } } "#, ); } #[test] fn move_const_simple_body_existing_const() { check_assist( move_const_to_impl, r#" struct S; impl S { const X: () = (); const Y: () = (); fn f() -> usize { /// doc comment const C$0: usize = 42; C * C } } "#, r#" struct S; impl S { const X: () = (); const Y: () = (); /// doc comment const C: usize = 42; fn f() -> usize { Self::C * Self::C } } "#, ); } #[test] fn move_const_block_body() { check_assist( move_const_to_impl, r#" struct S; impl S { fn f() -> usize { /// doc comment const C$0: usize = { let a = 3; let b = 4; a * b }; C * C } } "#, r#" struct S; impl S { /// doc comment const C: usize = { let a = 3; let b = 4; a * b }; fn f() -> usize { Self::C * Self::C } } "#, ); } #[test] fn correct_indent_when_nested() { check_assist( move_const_to_impl, r#" fn main() { struct S; impl S { fn f() -> usize { /// doc comment const C$0: usize = 42; C * C } } } "#, r#" fn main() { struct S; impl S { /// doc comment const C: usize = 42; fn f() -> usize { Self::C * Self::C } } } "#, ) } #[test] fn move_const_in_nested_scope_with_same_name_in_other_scope() { check_assist( move_const_to_impl, r#" struct S; impl S { fn f() -> usize { const C: &str = "outer"; let n = { /// doc comment const C$0: usize = 42; let m = { const C: &str = "inner"; C.len() }; C * m }; n + C.len() } } "#, r#" struct S; impl S { /// doc comment const C: usize = 42; fn f() -> usize { const C: &str = "outer"; let n = { let m = { const C: &str = "inner"; C.len() }; Self::C * m }; n + C.len() } } "#, ); } #[test] fn moved_const_body_is_escaped() { // Note that the last argument is what *lsp clients would see* rather than // what users would see. Unescaping happens thereafter. check_assist( move_const_to_impl, r#" struct S; impl S { fn f() -> usize { /// doc comment /// \\ /// ${snippet} const C$0: &str = "\ and $1"; C.len() } } "#, r#" struct S; impl S { /// doc comment /// \\\\ /// \${snippet} const C: &str = "\\ and \$1"; fn f() -> usize { Self::C.len() } } "#, ) } }