| use std::iter; |
| |
| use either::Either; |
| use hir::{HasSource, HirFileIdExt, ModuleSource}; |
| use ide_db::{ |
| assists::{AssistId, AssistKind}, |
| base_db::FileId, |
| defs::{Definition, NameClass, NameRefClass}, |
| search::{FileReference, SearchScope}, |
| FxHashMap, FxHashSet, |
| }; |
| use itertools::Itertools; |
| use smallvec::SmallVec; |
| use syntax::{ |
| algo::find_node_at_range, |
| ast::{ |
| self, |
| edit::{AstNodeEdit, IndentLevel}, |
| make, HasVisibility, |
| }, |
| match_ast, ted, AstNode, |
| SyntaxKind::{self, WHITESPACE}, |
| SyntaxNode, TextRange, TextSize, |
| }; |
| |
| use crate::{AssistContext, Assists}; |
| |
| use super::remove_unused_param::range_to_remove; |
| |
| // Assist: extract_module |
| // |
| // Extracts a selected region as separate module. All the references, visibility and imports are |
| // resolved. |
| // |
| // ``` |
| // $0fn foo(name: i32) -> i32 { |
| // name + 1 |
| // }$0 |
| // |
| // fn bar(name: i32) -> i32 { |
| // name + 2 |
| // } |
| // ``` |
| // -> |
| // ``` |
| // mod modname { |
| // pub(crate) fn foo(name: i32) -> i32 { |
| // name + 1 |
| // } |
| // } |
| // |
| // fn bar(name: i32) -> i32 { |
| // name + 2 |
| // } |
| // ``` |
| pub(crate) fn extract_module(acc: &mut Assists, ctx: &AssistContext<'_>) -> Option<()> { |
| if ctx.has_empty_selection() { |
| return None; |
| } |
| |
| let node = ctx.covering_element(); |
| let node = match node { |
| syntax::NodeOrToken::Node(n) => n, |
| syntax::NodeOrToken::Token(t) => t.parent()?, |
| }; |
| |
| //If the selection is inside impl block, we need to place new module outside impl block, |
| //as impl blocks cannot contain modules |
| |
| let mut impl_parent: Option<ast::Impl> = None; |
| let mut impl_child_count: usize = 0; |
| if let Some(parent_assoc_list) = node.parent() { |
| if let Some(parent_impl) = parent_assoc_list.parent() { |
| if let Some(impl_) = ast::Impl::cast(parent_impl) { |
| impl_child_count = parent_assoc_list.children().count(); |
| impl_parent = Some(impl_); |
| } |
| } |
| } |
| |
| let mut curr_parent_module: Option<ast::Module> = None; |
| if let Some(mod_syn_opt) = node.ancestors().find(|it| ast::Module::can_cast(it.kind())) { |
| curr_parent_module = ast::Module::cast(mod_syn_opt); |
| } |
| |
| let mut module = extract_target(&node, ctx.selection_trimmed())?; |
| if module.body_items.is_empty() { |
| return None; |
| } |
| |
| let old_item_indent = module.body_items[0].indent_level(); |
| |
| acc.add( |
| AssistId("extract_module", AssistKind::RefactorExtract), |
| "Extract Module", |
| module.text_range, |
| |builder| { |
| //This takes place in three steps: |
| // |
| //- Firstly, we will update the references(usages) e.g. converting a |
| // function call bar() to modname::bar(), and similarly for other items |
| // |
| //- Secondly, changing the visibility of each item inside the newly selected module |
| // i.e. making a fn a() {} to pub(crate) fn a() {} |
| // |
| //- Thirdly, resolving all the imports this includes removing paths from imports |
| // outside the module, shifting/cloning them inside new module, or shifting the imports, or making |
| // new import statements |
| |
| //We are getting item usages and record_fields together, record_fields |
| //for change_visibility and usages for first point mentioned above in the process |
| |
| let (usages_to_be_processed, record_fields, use_stmts_to_be_inserted) = |
| module.get_usages_and_record_fields(ctx); |
| |
| builder.edit_file(ctx.file_id()); |
| use_stmts_to_be_inserted.into_iter().for_each(|(_, use_stmt)| { |
| builder.insert(ctx.selection_trimmed().end(), format!("\n{use_stmt}")); |
| }); |
| |
| let import_paths_to_be_removed = module.resolve_imports(curr_parent_module, ctx); |
| module.change_visibility(record_fields); |
| |
| let module_def = generate_module_def(&impl_parent, &mut module, old_item_indent); |
| |
| let mut usages_to_be_processed_for_cur_file = vec![]; |
| for (file_id, usages) in usages_to_be_processed { |
| if file_id == ctx.file_id() { |
| usages_to_be_processed_for_cur_file = usages; |
| continue; |
| } |
| builder.edit_file(file_id); |
| for (text_range, usage) in usages { |
| builder.replace(text_range, usage) |
| } |
| } |
| |
| builder.edit_file(ctx.file_id()); |
| for (text_range, usage) in usages_to_be_processed_for_cur_file { |
| builder.replace(text_range, usage); |
| } |
| |
| if let Some(impl_) = impl_parent { |
| // Remove complete impl block if it has only one child (as such it will be empty |
| // after deleting that child) |
| let node_to_be_removed = if impl_child_count == 1 { |
| impl_.syntax() |
| } else { |
| //Remove selected node |
| &node |
| }; |
| |
| builder.delete(node_to_be_removed.text_range()); |
| // Remove preceding indentation from node |
| if let Some(range) = indent_range_before_given_node(node_to_be_removed) { |
| builder.delete(range); |
| } |
| |
| builder.insert(impl_.syntax().text_range().end(), format!("\n\n{module_def}")); |
| } else { |
| for import_path_text_range in import_paths_to_be_removed { |
| if module.text_range.intersect(import_path_text_range).is_some() { |
| module.text_range = module.text_range.cover(import_path_text_range); |
| } else { |
| builder.delete(import_path_text_range); |
| } |
| } |
| |
| builder.replace(module.text_range, module_def) |
| } |
| }, |
| ) |
| } |
| |
| fn generate_module_def( |
| parent_impl: &Option<ast::Impl>, |
| module: &mut Module, |
| old_indent: IndentLevel, |
| ) -> String { |
| let (items_to_be_processed, new_item_indent) = if parent_impl.is_some() { |
| (Either::Left(module.body_items.iter()), old_indent + 2) |
| } else { |
| (Either::Right(module.use_items.iter().chain(module.body_items.iter())), old_indent + 1) |
| }; |
| |
| let mut body = items_to_be_processed |
| .map(|item| item.indent(IndentLevel(1))) |
| .map(|item| format!("{new_item_indent}{item}")) |
| .join("\n\n"); |
| |
| if let Some(self_ty) = parent_impl.as_ref().and_then(|imp| imp.self_ty()) { |
| let impl_indent = old_indent + 1; |
| body = format!("{impl_indent}impl {self_ty} {{\n{body}\n{impl_indent}}}"); |
| |
| // Add the import for enum/struct corresponding to given impl block |
| module.make_use_stmt_of_node_with_super(self_ty.syntax()); |
| for item in module.use_items.iter() { |
| body = format!("{impl_indent}{item}\n\n{body}"); |
| } |
| } |
| |
| let module_name = module.name; |
| format!("mod {module_name} {{\n{body}\n{old_indent}}}") |
| } |
| |
| #[derive(Debug)] |
| struct Module { |
| text_range: TextRange, |
| name: &'static str, |
| /// All items except use items. |
| body_items: Vec<ast::Item>, |
| /// Use items are kept separately as they help when the selection is inside an impl block, |
| /// we can directly take these items and keep them outside generated impl block inside |
| /// generated module. |
| use_items: Vec<ast::Item>, |
| } |
| |
| fn extract_target(node: &SyntaxNode, selection_range: TextRange) -> Option<Module> { |
| let selected_nodes = node |
| .children() |
| .filter(|node| selection_range.contains_range(node.text_range())) |
| .chain(iter::once(node.clone())); |
| let (use_items, body_items) = selected_nodes |
| .filter_map(ast::Item::cast) |
| .partition(|item| matches!(item, ast::Item::Use(..))); |
| |
| Some(Module { text_range: selection_range, name: "modname", body_items, use_items }) |
| } |
| |
| impl Module { |
| fn get_usages_and_record_fields( |
| &self, |
| ctx: &AssistContext<'_>, |
| ) -> (FxHashMap<FileId, Vec<(TextRange, String)>>, Vec<SyntaxNode>, FxHashMap<TextSize, ast::Use>) |
| { |
| let mut adt_fields = Vec::new(); |
| let mut refs: FxHashMap<FileId, Vec<(TextRange, String)>> = FxHashMap::default(); |
| // use `TextSize` as key to avoid repeated use stmts |
| let mut use_stmts_to_be_inserted = FxHashMap::default(); |
| |
| //Here impl is not included as each item inside impl will be tied to the parent of |
| //implementing block(a struct, enum, etc), if the parent is in selected module, it will |
| //get updated by ADT section given below or if it is not, then we dont need to do any operation |
| |
| for item in &self.body_items { |
| match_ast! { |
| match (item.syntax()) { |
| ast::Adt(it) => { |
| if let Some( nod ) = ctx.sema.to_def(&it) { |
| let node_def = Definition::Adt(nod); |
| self.expand_and_group_usages_file_wise(ctx, node_def, &mut refs, &mut use_stmts_to_be_inserted); |
| |
| //Enum Fields are not allowed to explicitly specify pub, it is implied |
| match it { |
| ast::Adt::Struct(x) => { |
| if let Some(field_list) = x.field_list() { |
| match field_list { |
| ast::FieldList::RecordFieldList(record_field_list) => { |
| record_field_list.fields().for_each(|record_field| { |
| adt_fields.push(record_field.syntax().clone()); |
| }); |
| }, |
| ast::FieldList::TupleFieldList(tuple_field_list) => { |
| tuple_field_list.fields().for_each(|tuple_field| { |
| adt_fields.push(tuple_field.syntax().clone()); |
| }); |
| }, |
| } |
| } |
| }, |
| ast::Adt::Union(x) => { |
| if let Some(record_field_list) = x.record_field_list() { |
| record_field_list.fields().for_each(|record_field| { |
| adt_fields.push(record_field.syntax().clone()); |
| }); |
| } |
| }, |
| ast::Adt::Enum(_) => {}, |
| } |
| } |
| }, |
| ast::TypeAlias(it) => { |
| if let Some( nod ) = ctx.sema.to_def(&it) { |
| let node_def = Definition::TypeAlias(nod); |
| self.expand_and_group_usages_file_wise(ctx, node_def, &mut refs, &mut use_stmts_to_be_inserted); |
| } |
| }, |
| ast::Const(it) => { |
| if let Some( nod ) = ctx.sema.to_def(&it) { |
| let node_def = Definition::Const(nod); |
| self.expand_and_group_usages_file_wise(ctx, node_def, &mut refs, &mut use_stmts_to_be_inserted); |
| } |
| }, |
| ast::Static(it) => { |
| if let Some( nod ) = ctx.sema.to_def(&it) { |
| let node_def = Definition::Static(nod); |
| self.expand_and_group_usages_file_wise(ctx, node_def, &mut refs, &mut use_stmts_to_be_inserted); |
| } |
| }, |
| ast::Fn(it) => { |
| if let Some( nod ) = ctx.sema.to_def(&it) { |
| let node_def = Definition::Function(nod); |
| self.expand_and_group_usages_file_wise(ctx, node_def, &mut refs, &mut use_stmts_to_be_inserted); |
| } |
| }, |
| ast::Macro(it) => { |
| if let Some(nod) = ctx.sema.to_def(&it) { |
| self.expand_and_group_usages_file_wise(ctx, Definition::Macro(nod), &mut refs, &mut use_stmts_to_be_inserted); |
| } |
| }, |
| _ => (), |
| } |
| } |
| } |
| |
| (refs, adt_fields, use_stmts_to_be_inserted) |
| } |
| |
| fn expand_and_group_usages_file_wise( |
| &self, |
| ctx: &AssistContext<'_>, |
| node_def: Definition, |
| refs_in_files: &mut FxHashMap<FileId, Vec<(TextRange, String)>>, |
| use_stmts_to_be_inserted: &mut FxHashMap<TextSize, ast::Use>, |
| ) { |
| let mod_name = self.name; |
| let covering_node = match ctx.covering_element() { |
| syntax::NodeOrToken::Node(node) => node, |
| syntax::NodeOrToken::Token(tok) => tok.parent().unwrap(), // won't panic |
| }; |
| let out_of_sel = |node: &SyntaxNode| !self.text_range.contains_range(node.text_range()); |
| let mut use_stmts_set = FxHashSet::default(); |
| |
| for (file_id, refs) in node_def.usages(&ctx.sema).all() { |
| let source_file = ctx.sema.parse(file_id); |
| let usages = refs.into_iter().filter_map(|FileReference { range, .. }| { |
| // handle normal usages |
| let name_ref = find_node_at_range::<ast::NameRef>(source_file.syntax(), range)?; |
| |
| if out_of_sel(name_ref.syntax()) { |
| let new_ref = format!("{mod_name}::{name_ref}"); |
| return Some((range, new_ref)); |
| } else if let Some(use_) = name_ref.syntax().ancestors().find_map(ast::Use::cast) { |
| // handle usages in use_stmts which is in_sel |
| // check if `use` is top stmt in selection |
| if use_.syntax().parent().is_some_and(|parent| parent == covering_node) |
| && use_stmts_set.insert(use_.syntax().text_range().start()) |
| { |
| let use_ = use_stmts_to_be_inserted |
| .entry(use_.syntax().text_range().start()) |
| .or_insert_with(|| use_.clone_subtree().clone_for_update()); |
| for seg in use_ |
| .syntax() |
| .descendants() |
| .filter_map(ast::NameRef::cast) |
| .filter(|seg| seg.syntax().to_string() == name_ref.to_string()) |
| { |
| let new_ref = make::path_from_text(&format!("{mod_name}::{seg}")) |
| .clone_for_update(); |
| ted::replace(seg.syntax().parent()?, new_ref.syntax()); |
| } |
| } |
| } |
| |
| None |
| }); |
| refs_in_files.entry(file_id).or_default().extend(usages); |
| } |
| } |
| |
| fn change_visibility(&mut self, record_fields: Vec<SyntaxNode>) { |
| let (mut replacements, record_field_parents, impls) = |
| get_replacements_for_visibility_change(&mut self.body_items, false); |
| |
| let mut impl_items = impls |
| .into_iter() |
| .flat_map(|impl_| impl_.syntax().descendants()) |
| .filter_map(ast::Item::cast) |
| .collect_vec(); |
| |
| let (mut impl_item_replacements, _, _) = |
| get_replacements_for_visibility_change(&mut impl_items, true); |
| |
| replacements.append(&mut impl_item_replacements); |
| |
| for (_, field_owner) in record_field_parents { |
| for desc in field_owner.descendants().filter_map(ast::RecordField::cast) { |
| let is_record_field_present = |
| record_fields.clone().into_iter().any(|x| x.to_string() == desc.to_string()); |
| if is_record_field_present { |
| replacements.push((desc.visibility(), desc.syntax().clone())); |
| } |
| } |
| } |
| |
| for (vis, syntax) in replacements { |
| let item = syntax.children_with_tokens().find(|node_or_token| { |
| match node_or_token.kind() { |
| // We're skipping comments, doc comments, and attribute macros that may precede the keyword |
| // that the visibility should be placed before. |
| SyntaxKind::COMMENT | SyntaxKind::ATTR | SyntaxKind::WHITESPACE => false, |
| _ => true, |
| } |
| }); |
| |
| add_change_vis(vis, item); |
| } |
| } |
| |
| fn resolve_imports( |
| &mut self, |
| module: Option<ast::Module>, |
| ctx: &AssistContext<'_>, |
| ) -> Vec<TextRange> { |
| let mut imports_to_remove = vec![]; |
| let mut node_set = FxHashSet::default(); |
| |
| for item in self.body_items.clone() { |
| item.syntax() |
| .descendants() |
| .filter_map(|x| { |
| if let Some(name) = ast::Name::cast(x.clone()) { |
| NameClass::classify(&ctx.sema, &name).and_then(|nc| match nc { |
| NameClass::Definition(def) => Some((name.syntax().clone(), def)), |
| _ => None, |
| }) |
| } else if let Some(name_ref) = ast::NameRef::cast(x) { |
| NameRefClass::classify(&ctx.sema, &name_ref).and_then(|nc| match nc { |
| NameRefClass::Definition(def) => Some((name_ref.syntax().clone(), def)), |
| _ => None, |
| }) |
| } else { |
| None |
| } |
| }) |
| .for_each(|(node, def)| { |
| if node_set.insert(node.to_string()) { |
| if let Some(import) = self.process_def_in_sel(def, &node, &module, ctx) { |
| check_intersection_and_push(&mut imports_to_remove, import); |
| } |
| } |
| }) |
| } |
| |
| imports_to_remove |
| } |
| |
| fn process_def_in_sel( |
| &mut self, |
| def: Definition, |
| use_node: &SyntaxNode, |
| curr_parent_module: &Option<ast::Module>, |
| ctx: &AssistContext<'_>, |
| ) -> Option<TextRange> { |
| //We only need to find in the current file |
| let selection_range = ctx.selection_trimmed(); |
| let file_id = ctx.file_id(); |
| let usage_res = def.usages(&ctx.sema).in_scope(&SearchScope::single_file(file_id)).all(); |
| let file = ctx.sema.parse(file_id); |
| |
| // track uses which does not exists in `Use` |
| let mut uses_exist_in_sel = false; |
| let mut uses_exist_out_sel = false; |
| 'outside: for (_, refs) in usage_res.iter() { |
| for x in refs |
| .iter() |
| .filter(|x| find_node_at_range::<ast::Use>(file.syntax(), x.range).is_none()) |
| .filter_map(|x| find_node_at_range::<ast::Path>(file.syntax(), x.range)) |
| { |
| let in_selection = selection_range.contains_range(x.syntax().text_range()); |
| uses_exist_in_sel |= in_selection; |
| uses_exist_out_sel |= !in_selection; |
| |
| if uses_exist_in_sel && uses_exist_out_sel { |
| break 'outside; |
| } |
| } |
| } |
| |
| let (def_in_mod, def_out_sel) = |
| check_def_in_mod_and_out_sel(def, ctx, curr_parent_module, selection_range, file_id); |
| |
| // Find use stmt that use def in current file |
| let use_stmt: Option<ast::Use> = usage_res |
| .into_iter() |
| .filter(|(use_file_id, _)| *use_file_id == file_id) |
| .flat_map(|(_, refs)| refs.into_iter().rev()) |
| .find_map(|fref| find_node_at_range(file.syntax(), fref.range)); |
| let use_stmt_not_in_sel = use_stmt.as_ref().is_some_and(|use_stmt| { |
| !selection_range.contains_range(use_stmt.syntax().text_range()) |
| }); |
| |
| let mut use_tree_paths: Option<Vec<ast::Path>> = None; |
| //Exists inside and outside selection |
| // - Use stmt for item is present -> get the use_tree_str and reconstruct the path in new |
| // module |
| // - Use stmt for item is not present -> |
| //If it is not found, the definition is either ported inside new module or it stays |
| //outside: |
| //- Def is inside: Nothing to import |
| //- Def is outside: Import it inside with super |
| |
| //Exists inside selection but not outside -> Check for the import of it in original module, |
| //get the use_tree_str, reconstruct the use stmt in new module |
| |
| let mut import_path_to_be_removed: Option<TextRange> = None; |
| if uses_exist_in_sel && uses_exist_out_sel { |
| //Changes to be made only inside new module |
| |
| //If use_stmt exists, find the use_tree_str, reconstruct it inside new module |
| //If not, insert a use stmt with super and the given nameref |
| match self.process_use_stmt_for_import_resolve(use_stmt, use_node) { |
| Some((use_tree_str, _)) => use_tree_paths = Some(use_tree_str), |
| None if def_in_mod && def_out_sel => { |
| //Considered only after use_stmt is not present |
| //def_in_mod && def_out_sel | exists_outside_sel(exists_inside_sel = |
| //true for all cases) |
| // false | false -> Do nothing |
| // false | true -> If source is in selection -> nothing to do, If source is outside |
| // mod -> ust_stmt transversal |
| // true | false -> super import insertion |
| // true | true -> super import insertion |
| self.make_use_stmt_of_node_with_super(use_node); |
| } |
| None => {} |
| } |
| } else if uses_exist_in_sel && !uses_exist_out_sel { |
| //Changes to be made inside new module, and remove import from outside |
| |
| if let Some((mut use_tree_str, text_range_opt)) = |
| self.process_use_stmt_for_import_resolve(use_stmt, use_node) |
| { |
| if let Some(text_range) = text_range_opt { |
| import_path_to_be_removed = Some(text_range); |
| } |
| |
| if def_in_mod && def_out_sel { |
| if let Some(first_path_in_use_tree) = use_tree_str.last() { |
| let first_path_in_use_tree_str = first_path_in_use_tree.to_string(); |
| if !first_path_in_use_tree_str.contains("super") |
| && !first_path_in_use_tree_str.contains("crate") |
| { |
| let super_path = make::ext::ident_path("super"); |
| use_tree_str.push(super_path); |
| } |
| } |
| } |
| |
| use_tree_paths = Some(use_tree_str); |
| } else if def_in_mod && def_out_sel { |
| self.make_use_stmt_of_node_with_super(use_node); |
| } |
| } |
| |
| if let Some(mut use_tree_paths) = use_tree_paths { |
| use_tree_paths.reverse(); |
| |
| if uses_exist_out_sel || !uses_exist_in_sel || !def_in_mod || !def_out_sel { |
| if let Some(first_path_in_use_tree) = use_tree_paths.first() { |
| if first_path_in_use_tree.to_string().contains("super") { |
| use_tree_paths.insert(0, make::ext::ident_path("super")); |
| } |
| } |
| } |
| |
| let is_item = matches!( |
| def, |
| Definition::Macro(_) |
| | Definition::Module(_) |
| | Definition::Function(_) |
| | Definition::Adt(_) |
| | Definition::Const(_) |
| | Definition::Static(_) |
| | Definition::Trait(_) |
| | Definition::TraitAlias(_) |
| | Definition::TypeAlias(_) |
| ); |
| |
| if (def_out_sel || !is_item) && use_stmt_not_in_sel { |
| let use_ = make::use_( |
| None, |
| make::use_tree(make::join_paths(use_tree_paths), None, None, false), |
| ); |
| self.use_items.insert(0, ast::Item::from(use_)); |
| } |
| } |
| |
| import_path_to_be_removed |
| } |
| |
| fn make_use_stmt_of_node_with_super(&mut self, node_syntax: &SyntaxNode) -> ast::Item { |
| let super_path = make::ext::ident_path("super"); |
| let node_path = make::ext::ident_path(&node_syntax.to_string()); |
| let use_ = make::use_( |
| None, |
| make::use_tree(make::join_paths(vec![super_path, node_path]), None, None, false), |
| ); |
| |
| let item = ast::Item::from(use_); |
| self.use_items.insert(0, item.clone()); |
| item |
| } |
| |
| fn process_use_stmt_for_import_resolve( |
| &self, |
| use_stmt: Option<ast::Use>, |
| node_syntax: &SyntaxNode, |
| ) -> Option<(Vec<ast::Path>, Option<TextRange>)> { |
| let use_stmt = use_stmt?; |
| for path_seg in use_stmt.syntax().descendants().filter_map(ast::PathSegment::cast) { |
| if path_seg.syntax().to_string() == node_syntax.to_string() { |
| let mut use_tree_str = vec![path_seg.parent_path()]; |
| get_use_tree_paths_from_path(path_seg.parent_path(), &mut use_tree_str); |
| |
| //Here we are looking for use_tree with same string value as node |
| //passed above as the range_to_remove function looks for a comma and |
| //then includes it in the text range to remove it. But the comma only |
| //appears at the use_tree level |
| for use_tree in path_seg.syntax().ancestors().filter_map(ast::UseTree::cast) { |
| if use_tree.syntax().to_string() == node_syntax.to_string() { |
| return Some((use_tree_str, Some(range_to_remove(use_tree.syntax())))); |
| } |
| } |
| |
| return Some((use_tree_str, None)); |
| } |
| } |
| |
| None |
| } |
| } |
| |
| fn check_intersection_and_push( |
| import_paths_to_be_removed: &mut Vec<TextRange>, |
| mut import_path: TextRange, |
| ) { |
| // Text ranges received here for imports are extended to the |
| // next/previous comma which can cause intersections among them |
| // and later deletion of these can cause panics similar |
| // to reported in #11766. So to mitigate it, we |
| // check for intersection between all current members |
| // and combine all such ranges into one. |
| let s: SmallVec<[_; 2]> = import_paths_to_be_removed |
| .iter_mut() |
| .positions(|it| it.intersect(import_path).is_some()) |
| .collect(); |
| for pos in s.into_iter().rev() { |
| let intersecting_path = import_paths_to_be_removed.swap_remove(pos); |
| import_path = import_path.cover(intersecting_path); |
| } |
| import_paths_to_be_removed.push(import_path); |
| } |
| |
| fn check_def_in_mod_and_out_sel( |
| def: Definition, |
| ctx: &AssistContext<'_>, |
| curr_parent_module: &Option<ast::Module>, |
| selection_range: TextRange, |
| curr_file_id: FileId, |
| ) -> (bool, bool) { |
| macro_rules! check_item { |
| ($x:ident) => { |
| if let Some(source) = $x.source(ctx.db()) { |
| let have_same_parent = if let Some(ast_module) = &curr_parent_module { |
| ctx.sema.to_module_def(ast_module).is_some_and(|it| it == $x.module(ctx.db())) |
| } else { |
| source.file_id.original_file(ctx.db()) == curr_file_id |
| }; |
| |
| let in_sel = !selection_range.contains_range(source.value.syntax().text_range()); |
| return (have_same_parent, in_sel); |
| } |
| }; |
| } |
| |
| match def { |
| Definition::Module(x) => { |
| let source = x.definition_source(ctx.db()); |
| let have_same_parent = match (&curr_parent_module, x.parent(ctx.db())) { |
| (Some(ast_module), Some(hir_module)) => { |
| ctx.sema.to_module_def(ast_module).is_some_and(|it| it == hir_module) |
| } |
| _ => source.file_id.original_file(ctx.db()) == curr_file_id, |
| }; |
| |
| if have_same_parent { |
| if let ModuleSource::Module(module_) = source.value { |
| let in_sel = !selection_range.contains_range(module_.syntax().text_range()); |
| return (have_same_parent, in_sel); |
| } |
| } |
| |
| return (have_same_parent, false); |
| } |
| Definition::Function(x) => check_item!(x), |
| Definition::Adt(x) => check_item!(x), |
| Definition::Variant(x) => check_item!(x), |
| Definition::Const(x) => check_item!(x), |
| Definition::Static(x) => check_item!(x), |
| Definition::Trait(x) => check_item!(x), |
| Definition::TypeAlias(x) => check_item!(x), |
| _ => {} |
| } |
| |
| (false, false) |
| } |
| |
| fn get_replacements_for_visibility_change( |
| items: &mut [ast::Item], |
| is_clone_for_updated: bool, |
| ) -> ( |
| Vec<(Option<ast::Visibility>, SyntaxNode)>, |
| Vec<(Option<ast::Visibility>, SyntaxNode)>, |
| Vec<ast::Impl>, |
| ) { |
| let mut replacements = Vec::new(); |
| let mut record_field_parents = Vec::new(); |
| let mut impls = Vec::new(); |
| |
| for item in items { |
| if !is_clone_for_updated { |
| *item = item.clone_for_update(); |
| } |
| //Use stmts are ignored |
| macro_rules! push_to_replacement { |
| ($it:ident) => { |
| replacements.push(($it.visibility(), $it.syntax().clone())) |
| }; |
| } |
| |
| match item { |
| ast::Item::Const(it) => push_to_replacement!(it), |
| ast::Item::Enum(it) => push_to_replacement!(it), |
| ast::Item::ExternCrate(it) => push_to_replacement!(it), |
| ast::Item::Fn(it) => push_to_replacement!(it), |
| //Associated item's visibility should not be changed |
| ast::Item::Impl(it) if it.for_token().is_none() => impls.push(it.clone()), |
| ast::Item::MacroDef(it) => push_to_replacement!(it), |
| ast::Item::Module(it) => push_to_replacement!(it), |
| ast::Item::Static(it) => push_to_replacement!(it), |
| ast::Item::Struct(it) => { |
| push_to_replacement!(it); |
| record_field_parents.push((it.visibility(), it.syntax().clone())); |
| } |
| ast::Item::Trait(it) => push_to_replacement!(it), |
| ast::Item::TypeAlias(it) => push_to_replacement!(it), |
| ast::Item::Union(it) => { |
| push_to_replacement!(it); |
| record_field_parents.push((it.visibility(), it.syntax().clone())); |
| } |
| _ => (), |
| } |
| } |
| |
| (replacements, record_field_parents, impls) |
| } |
| |
| fn get_use_tree_paths_from_path( |
| path: ast::Path, |
| use_tree_str: &mut Vec<ast::Path>, |
| ) -> Option<&mut Vec<ast::Path>> { |
| path.syntax() |
| .ancestors() |
| .filter(|x| x.to_string() != path.to_string()) |
| .filter_map(ast::UseTree::cast) |
| .find_map(|use_tree| { |
| if let Some(upper_tree_path) = use_tree.path() { |
| if upper_tree_path.to_string() != path.to_string() { |
| use_tree_str.push(upper_tree_path.clone()); |
| get_use_tree_paths_from_path(upper_tree_path, use_tree_str); |
| return Some(use_tree); |
| } |
| } |
| None |
| })?; |
| |
| Some(use_tree_str) |
| } |
| |
| fn add_change_vis(vis: Option<ast::Visibility>, node_or_token_opt: Option<syntax::SyntaxElement>) { |
| if vis.is_none() { |
| if let Some(node_or_token) = node_or_token_opt { |
| let pub_crate_vis = make::visibility_pub_crate().clone_for_update(); |
| ted::insert(ted::Position::before(node_or_token), pub_crate_vis.syntax()); |
| } |
| } |
| } |
| |
| fn indent_range_before_given_node(node: &SyntaxNode) -> Option<TextRange> { |
| node.siblings_with_tokens(syntax::Direction::Prev) |
| .find(|x| x.kind() == WHITESPACE) |
| .map(|x| x.text_range()) |
| } |
| |
| #[cfg(test)] |
| mod tests { |
| use crate::tests::{check_assist, check_assist_not_applicable}; |
| |
| use super::*; |
| |
| #[test] |
| fn test_not_applicable_without_selection() { |
| check_assist_not_applicable( |
| extract_module, |
| r" |
| $0pub struct PublicStruct { |
| field: i32, |
| } |
| ", |
| ) |
| } |
| |
| #[test] |
| fn test_extract_module() { |
| check_assist( |
| extract_module, |
| r" |
| mod thirdpartycrate { |
| pub mod nest { |
| pub struct SomeType; |
| pub struct SomeType2; |
| } |
| pub struct SomeType1; |
| } |
| |
| mod bar { |
| use crate::thirdpartycrate::{nest::{SomeType, SomeType2}, SomeType1}; |
| |
| pub struct PublicStruct { |
| field: PrivateStruct, |
| field1: SomeType1, |
| } |
| |
| impl PublicStruct { |
| pub fn new() -> Self { |
| Self { field: PrivateStruct::new(), field1: SomeType1 } |
| } |
| } |
| |
| fn foo() { |
| let _s = PrivateStruct::new(); |
| let _a = bar(); |
| } |
| |
| $0struct PrivateStruct { |
| inner: SomeType, |
| } |
| |
| pub struct PrivateStruct1 { |
| pub inner: i32, |
| } |
| |
| impl PrivateStruct { |
| fn new() -> Self { |
| PrivateStruct { inner: SomeType } |
| } |
| } |
| |
| fn bar() -> i32 { |
| 2 |
| }$0 |
| } |
| ", |
| r" |
| mod thirdpartycrate { |
| pub mod nest { |
| pub struct SomeType; |
| pub struct SomeType2; |
| } |
| pub struct SomeType1; |
| } |
| |
| mod bar { |
| use crate::thirdpartycrate::{nest::{SomeType2}, SomeType1}; |
| |
| pub struct PublicStruct { |
| field: modname::PrivateStruct, |
| field1: SomeType1, |
| } |
| |
| impl PublicStruct { |
| pub fn new() -> Self { |
| Self { field: modname::PrivateStruct::new(), field1: SomeType1 } |
| } |
| } |
| |
| fn foo() { |
| let _s = modname::PrivateStruct::new(); |
| let _a = modname::bar(); |
| } |
| |
| mod modname { |
| use crate::thirdpartycrate::nest::SomeType; |
| |
| pub(crate) struct PrivateStruct { |
| pub(crate) inner: SomeType, |
| } |
| |
| pub struct PrivateStruct1 { |
| pub inner: i32, |
| } |
| |
| impl PrivateStruct { |
| pub(crate) fn new() -> Self { |
| PrivateStruct { inner: SomeType } |
| } |
| } |
| |
| pub(crate) fn bar() -> i32 { |
| 2 |
| } |
| } |
| } |
| ", |
| ); |
| } |
| |
| #[test] |
| fn test_extract_module_for_function_only() { |
| check_assist( |
| extract_module, |
| r" |
| $0fn foo(name: i32) -> i32 { |
| name + 1 |
| }$0 |
| |
| fn bar(name: i32) -> i32 { |
| name + 2 |
| } |
| ", |
| r" |
| mod modname { |
| pub(crate) fn foo(name: i32) -> i32 { |
| name + 1 |
| } |
| } |
| |
| fn bar(name: i32) -> i32 { |
| name + 2 |
| } |
| ", |
| ) |
| } |
| |
| #[test] |
| fn test_extract_module_for_impl_having_corresponding_adt_in_selection() { |
| check_assist( |
| extract_module, |
| r" |
| mod impl_play { |
| $0struct A {} |
| |
| impl A { |
| pub fn new_a() -> i32 { |
| 2 |
| } |
| }$0 |
| |
| fn a() { |
| let _a = A::new_a(); |
| } |
| } |
| ", |
| r" |
| mod impl_play { |
| mod modname { |
| pub(crate) struct A {} |
| |
| impl A { |
| pub fn new_a() -> i32 { |
| 2 |
| } |
| } |
| } |
| |
| fn a() { |
| let _a = modname::A::new_a(); |
| } |
| } |
| ", |
| ) |
| } |
| |
| #[test] |
| fn test_import_resolve_when_its_only_inside_selection() { |
| check_assist( |
| extract_module, |
| r" |
| mod foo { |
| pub struct PrivateStruct; |
| pub struct PrivateStruct1; |
| } |
| |
| mod bar { |
| use super::foo::{PrivateStruct, PrivateStruct1}; |
| |
| $0struct Strukt { |
| field: PrivateStruct, |
| }$0 |
| |
| struct Strukt1 { |
| field: PrivateStruct1, |
| } |
| } |
| ", |
| r" |
| mod foo { |
| pub struct PrivateStruct; |
| pub struct PrivateStruct1; |
| } |
| |
| mod bar { |
| use super::foo::{PrivateStruct1}; |
| |
| mod modname { |
| use super::super::foo::PrivateStruct; |
| |
| pub(crate) struct Strukt { |
| pub(crate) field: PrivateStruct, |
| } |
| } |
| |
| struct Strukt1 { |
| field: PrivateStruct1, |
| } |
| } |
| ", |
| ) |
| } |
| |
| #[test] |
| fn test_import_resolve_when_its_inside_and_outside_selection_and_source_not_in_same_mod() { |
| check_assist( |
| extract_module, |
| r" |
| mod foo { |
| pub struct PrivateStruct; |
| } |
| |
| mod bar { |
| use super::foo::PrivateStruct; |
| |
| $0struct Strukt { |
| field: PrivateStruct, |
| }$0 |
| |
| struct Strukt1 { |
| field: PrivateStruct, |
| } |
| } |
| ", |
| r" |
| mod foo { |
| pub struct PrivateStruct; |
| } |
| |
| mod bar { |
| use super::foo::PrivateStruct; |
| |
| mod modname { |
| use super::super::foo::PrivateStruct; |
| |
| pub(crate) struct Strukt { |
| pub(crate) field: PrivateStruct, |
| } |
| } |
| |
| struct Strukt1 { |
| field: PrivateStruct, |
| } |
| } |
| ", |
| ) |
| } |
| |
| #[test] |
| fn test_import_resolve_when_its_inside_and_outside_selection_and_source_is_in_same_mod() { |
| check_assist( |
| extract_module, |
| r" |
| mod bar { |
| pub struct PrivateStruct; |
| |
| $0struct Strukt { |
| field: PrivateStruct, |
| }$0 |
| |
| struct Strukt1 { |
| field: PrivateStruct, |
| } |
| } |
| ", |
| r" |
| mod bar { |
| pub struct PrivateStruct; |
| |
| mod modname { |
| use super::PrivateStruct; |
| |
| pub(crate) struct Strukt { |
| pub(crate) field: PrivateStruct, |
| } |
| } |
| |
| struct Strukt1 { |
| field: PrivateStruct, |
| } |
| } |
| ", |
| ) |
| } |
| |
| #[test] |
| fn test_extract_module_for_corresponding_adt_of_impl_present_in_same_mod_but_not_in_selection() |
| { |
| check_assist( |
| extract_module, |
| r" |
| mod impl_play { |
| struct A {} |
| |
| $0impl A { |
| pub fn new_a() -> i32 { |
| 2 |
| } |
| }$0 |
| |
| fn a() { |
| let _a = A::new_a(); |
| } |
| } |
| ", |
| r" |
| mod impl_play { |
| struct A {} |
| |
| mod modname { |
| use super::A; |
| |
| impl A { |
| pub fn new_a() -> i32 { |
| 2 |
| } |
| } |
| } |
| |
| fn a() { |
| let _a = A::new_a(); |
| } |
| } |
| ", |
| ) |
| } |
| |
| #[test] |
| fn test_extract_module_for_impl_not_having_corresponding_adt_in_selection_and_not_in_same_mod_but_with_super( |
| ) { |
| check_assist( |
| extract_module, |
| r" |
| mod foo { |
| pub struct A {} |
| } |
| mod impl_play { |
| use super::foo::A; |
| |
| $0impl A { |
| pub fn new_a() -> i32 { |
| 2 |
| } |
| }$0 |
| |
| fn a() { |
| let _a = A::new_a(); |
| } |
| } |
| ", |
| r" |
| mod foo { |
| pub struct A {} |
| } |
| mod impl_play { |
| use super::foo::A; |
| |
| mod modname { |
| use super::super::foo::A; |
| |
| impl A { |
| pub fn new_a() -> i32 { |
| 2 |
| } |
| } |
| } |
| |
| fn a() { |
| let _a = A::new_a(); |
| } |
| } |
| ", |
| ) |
| } |
| |
| #[test] |
| fn test_import_resolve_for_trait_bounds_on_function() { |
| check_assist( |
| extract_module, |
| r" |
| mod impl_play2 { |
| trait JustATrait {} |
| |
| $0struct A {} |
| |
| fn foo<T: JustATrait>(arg: T) -> T { |
| arg |
| } |
| |
| impl JustATrait for A {} |
| |
| fn bar() { |
| let a = A {}; |
| foo(a); |
| }$0 |
| } |
| ", |
| r" |
| mod impl_play2 { |
| trait JustATrait {} |
| |
| mod modname { |
| use super::JustATrait; |
| |
| pub(crate) struct A {} |
| |
| pub(crate) fn foo<T: JustATrait>(arg: T) -> T { |
| arg |
| } |
| |
| impl JustATrait for A {} |
| |
| pub(crate) fn bar() { |
| let a = A {}; |
| foo(a); |
| } |
| } |
| } |
| ", |
| ) |
| } |
| |
| #[test] |
| fn test_extract_module_for_module() { |
| check_assist( |
| extract_module, |
| r" |
| mod impl_play2 { |
| $0mod impl_play { |
| pub struct A {} |
| }$0 |
| } |
| ", |
| r" |
| mod impl_play2 { |
| mod modname { |
| pub(crate) mod impl_play { |
| pub struct A {} |
| } |
| } |
| } |
| ", |
| ) |
| } |
| |
| #[test] |
| fn test_extract_module_with_multiple_files() { |
| check_assist( |
| extract_module, |
| r" |
| //- /main.rs |
| mod foo; |
| |
| use foo::PrivateStruct; |
| |
| pub struct Strukt { |
| field: PrivateStruct, |
| } |
| |
| fn main() { |
| $0struct Strukt1 { |
| field: Strukt, |
| }$0 |
| } |
| //- /foo.rs |
| pub struct PrivateStruct; |
| ", |
| r" |
| mod foo; |
| |
| use foo::PrivateStruct; |
| |
| pub struct Strukt { |
| field: PrivateStruct, |
| } |
| |
| fn main() { |
| mod modname { |
| use super::Strukt; |
| |
| pub(crate) struct Strukt1 { |
| pub(crate) field: Strukt, |
| } |
| } |
| } |
| ", |
| ) |
| } |
| |
| #[test] |
| fn test_extract_module_macro_rules() { |
| check_assist( |
| extract_module, |
| r" |
| $0macro_rules! m { |
| () => {}; |
| }$0 |
| m! {} |
| ", |
| r" |
| mod modname { |
| macro_rules! m { |
| () => {}; |
| } |
| } |
| modname::m! {} |
| ", |
| ); |
| } |
| |
| #[test] |
| fn test_do_not_apply_visibility_modifier_to_trait_impl_items() { |
| check_assist( |
| extract_module, |
| r" |
| trait ATrait { |
| fn function(); |
| } |
| |
| struct A {} |
| |
| $0impl ATrait for A { |
| fn function() {} |
| }$0 |
| ", |
| r" |
| trait ATrait { |
| fn function(); |
| } |
| |
| struct A {} |
| |
| mod modname { |
| use super::A; |
| |
| use super::ATrait; |
| |
| impl ATrait for A { |
| fn function() {} |
| } |
| } |
| ", |
| ) |
| } |
| |
| #[test] |
| fn test_if_inside_impl_block_generate_module_outside() { |
| check_assist( |
| extract_module, |
| r" |
| struct A {} |
| |
| impl A { |
| $0fn foo() {}$0 |
| fn bar() {} |
| } |
| ", |
| r" |
| struct A {} |
| |
| impl A { |
| fn bar() {} |
| } |
| |
| mod modname { |
| use super::A; |
| |
| impl A { |
| pub(crate) fn foo() {} |
| } |
| } |
| ", |
| ) |
| } |
| |
| #[test] |
| fn test_if_inside_impl_block_generate_module_outside_but_impl_block_having_one_child() { |
| check_assist( |
| extract_module, |
| r" |
| struct A {} |
| struct B {} |
| |
| impl A { |
| $0fn foo(x: B) {}$0 |
| } |
| ", |
| r" |
| struct A {} |
| struct B {} |
| |
| mod modname { |
| use super::B; |
| |
| use super::A; |
| |
| impl A { |
| pub(crate) fn foo(x: B) {} |
| } |
| } |
| ", |
| ) |
| } |
| |
| #[test] |
| fn test_issue_11766() { |
| //https://github.com/rust-lang/rust-analyzer/issues/11766 |
| check_assist( |
| extract_module, |
| r" |
| mod x { |
| pub struct Foo; |
| pub struct Bar; |
| } |
| |
| use x::{Bar, Foo}; |
| |
| $0type A = (Foo, Bar);$0 |
| ", |
| r" |
| mod x { |
| pub struct Foo; |
| pub struct Bar; |
| } |
| |
| use x::{}; |
| |
| mod modname { |
| use super::x::Bar; |
| |
| use super::x::Foo; |
| |
| pub(crate) type A = (Foo, Bar); |
| } |
| ", |
| ) |
| } |
| |
| #[test] |
| fn test_issue_12790() { |
| check_assist( |
| extract_module, |
| r" |
| $0/// A documented function |
| fn documented_fn() {} |
| |
| // A commented function with a #[] attribute macro |
| #[cfg(test)] |
| fn attribute_fn() {} |
| |
| // A normally commented function |
| fn normal_fn() {} |
| |
| /// A documented Struct |
| struct DocumentedStruct { |
| // Normal field |
| x: i32, |
| |
| /// Documented field |
| y: i32, |
| |
| // Macroed field |
| #[cfg(test)] |
| z: i32, |
| } |
| |
| // A macroed Struct |
| #[cfg(test)] |
| struct MacroedStruct { |
| // Normal field |
| x: i32, |
| |
| /// Documented field |
| y: i32, |
| |
| // Macroed field |
| #[cfg(test)] |
| z: i32, |
| } |
| |
| // A normal Struct |
| struct NormalStruct { |
| // Normal field |
| x: i32, |
| |
| /// Documented field |
| y: i32, |
| |
| // Macroed field |
| #[cfg(test)] |
| z: i32, |
| } |
| |
| /// A documented type |
| type DocumentedType = i32; |
| |
| // A macroed type |
| #[cfg(test)] |
| type MacroedType = i32; |
| |
| /// A module to move |
| mod module {} |
| |
| /// An impl to move |
| impl NormalStruct { |
| /// A method |
| fn new() {} |
| } |
| |
| /// A documented trait |
| trait DocTrait { |
| /// Inner function |
| fn doc() {} |
| } |
| |
| /// An enum |
| enum DocumentedEnum { |
| /// A variant |
| A, |
| /// Another variant |
| B { x: i32, y: i32 } |
| } |
| |
| /// Documented const |
| const MY_CONST: i32 = 0;$0 |
| ", |
| r" |
| mod modname { |
| /// A documented function |
| pub(crate) fn documented_fn() {} |
| |
| // A commented function with a #[] attribute macro |
| #[cfg(test)] |
| pub(crate) fn attribute_fn() {} |
| |
| // A normally commented function |
| pub(crate) fn normal_fn() {} |
| |
| /// A documented Struct |
| pub(crate) struct DocumentedStruct { |
| // Normal field |
| pub(crate) x: i32, |
| |
| /// Documented field |
| pub(crate) y: i32, |
| |
| // Macroed field |
| #[cfg(test)] |
| pub(crate) z: i32, |
| } |
| |
| // A macroed Struct |
| #[cfg(test)] |
| pub(crate) struct MacroedStruct { |
| // Normal field |
| pub(crate) x: i32, |
| |
| /// Documented field |
| pub(crate) y: i32, |
| |
| // Macroed field |
| #[cfg(test)] |
| pub(crate) z: i32, |
| } |
| |
| // A normal Struct |
| pub(crate) struct NormalStruct { |
| // Normal field |
| pub(crate) x: i32, |
| |
| /// Documented field |
| pub(crate) y: i32, |
| |
| // Macroed field |
| #[cfg(test)] |
| pub(crate) z: i32, |
| } |
| |
| /// A documented type |
| pub(crate) type DocumentedType = i32; |
| |
| // A macroed type |
| #[cfg(test)] |
| pub(crate) type MacroedType = i32; |
| |
| /// A module to move |
| pub(crate) mod module {} |
| |
| /// An impl to move |
| impl NormalStruct { |
| /// A method |
| pub(crate) fn new() {} |
| } |
| |
| /// A documented trait |
| pub(crate) trait DocTrait { |
| /// Inner function |
| fn doc() {} |
| } |
| |
| /// An enum |
| pub(crate) enum DocumentedEnum { |
| /// A variant |
| A, |
| /// Another variant |
| B { x: i32, y: i32 } |
| } |
| |
| /// Documented const |
| pub(crate) const MY_CONST: i32 = 0; |
| } |
| ", |
| ) |
| } |
| |
| #[test] |
| fn test_merge_multiple_intersections() { |
| check_assist( |
| extract_module, |
| r#" |
| mod dep { |
| pub struct A; |
| pub struct B; |
| pub struct C; |
| } |
| |
| use dep::{A, B, C}; |
| |
| $0struct S { |
| inner: A, |
| state: C, |
| condvar: B, |
| }$0 |
| "#, |
| r#" |
| mod dep { |
| pub struct A; |
| pub struct B; |
| pub struct C; |
| } |
| |
| use dep::{}; |
| |
| mod modname { |
| use super::dep::B; |
| |
| use super::dep::C; |
| |
| use super::dep::A; |
| |
| pub(crate) struct S { |
| pub(crate) inner: A, |
| pub(crate) state: C, |
| pub(crate) condvar: B, |
| } |
| } |
| "#, |
| ); |
| } |
| |
| #[test] |
| fn test_remove_import_path_inside_selection() { |
| check_assist( |
| extract_module, |
| r#" |
| $0struct Point; |
| impl Point { |
| pub const fn direction(self, other: Self) -> Option<Direction> { |
| Some(Vertical) |
| } |
| } |
| |
| pub enum Direction { |
| Horizontal, |
| Vertical, |
| } |
| use Direction::{Horizontal, Vertical};$0 |
| |
| fn main() { |
| let x = Vertical; |
| } |
| "#, |
| r#" |
| mod modname { |
| use Direction::{Horizontal, Vertical}; |
| |
| pub(crate) struct Point; |
| |
| impl Point { |
| pub const fn direction(self, other: Self) -> Option<Direction> { |
| Some(Vertical) |
| } |
| } |
| |
| pub enum Direction { |
| Horizontal, |
| Vertical, |
| } |
| } |
| use modname::Direction::{Horizontal, Vertical}; |
| |
| fn main() { |
| let x = Vertical; |
| } |
| "#, |
| ); |
| } |
| } |