Unnamed repository; edit this file 'description' to name the repository.
Diffstat (limited to 'crates/ide-assists/src/handlers/replace_qualified_name_with_use.rs')
| -rw-r--r-- | crates/ide-assists/src/handlers/replace_qualified_name_with_use.rs | 174 |
1 files changed, 147 insertions, 27 deletions
diff --git a/crates/ide-assists/src/handlers/replace_qualified_name_with_use.rs b/crates/ide-assists/src/handlers/replace_qualified_name_with_use.rs index f6526cdba4..e9f0e190d4 100644 --- a/crates/ide-assists/src/handlers/replace_qualified_name_with_use.rs +++ b/crates/ide-assists/src/handlers/replace_qualified_name_with_use.rs @@ -1,4 +1,4 @@ -use hir::AsAssocItem; +use hir::{AsAssocItem, ModuleDef, PathResolution}; use ide_db::{ helpers::mod_path_to_ast, imports::insert_use::{ImportScope, insert_use}, @@ -6,7 +6,8 @@ use ide_db::{ use syntax::{ AstNode, Edition, SyntaxNode, ast::{self, HasGenericArgs, make}, - match_ast, ted, + match_ast, + syntax_editor::SyntaxEditor, }; use crate::{AssistContext, AssistId, Assists}; @@ -30,26 +31,19 @@ pub(crate) fn replace_qualified_name_with_use( acc: &mut Assists, ctx: &AssistContext<'_>, ) -> Option<()> { - let mut original_path: ast::Path = ctx.find_node_at_offset()?; + let original_path: ast::Path = ctx.find_node_at_offset()?; // We don't want to mess with use statements if original_path.syntax().ancestors().find_map(ast::UseTree::cast).is_some() { cov_mark::hit!(not_applicable_in_use); return None; } - if original_path.qualifier().is_none() { - original_path = original_path.parent_path()?; - } + let original_path = target_path(ctx, original_path)?; - // only offer replacement for non assoc items - match ctx.sema.resolve_path(&original_path)? { - hir::PathResolution::Def(def) if def.as_assoc_item(ctx.sema.db).is_none() => (), - _ => return None, - } // then search for an import for the first path segment of what we want to replace // that way it is less likely that we import the item from a different location due re-exports let module = match ctx.sema.resolve_path(&original_path.first_qualifier_or_self())? { - hir::PathResolution::Def(module @ hir::ModuleDef::Module(_)) => module, + PathResolution::Def(module @ ModuleDef::Module(_)) => module, _ => return None, }; @@ -78,8 +72,10 @@ pub(crate) fn replace_qualified_name_with_use( |builder| { // Now that we've brought the name into scope, re-qualify all paths that could be // affected (that is, all paths inside the node we added the `use` to). - let scope = builder.make_import_scope_mut(scope); - shorten_paths(scope.as_syntax_node(), &original_path); + let scope_node = scope.as_syntax_node(); + let mut editor = builder.make_editor(scope_node); + shorten_paths(&mut editor, scope_node, &original_path); + builder.add_file_edits(ctx.vfs_file_id(), editor); let path = drop_generic_args(&original_path); let edition = ctx .sema @@ -92,23 +88,42 @@ pub(crate) fn replace_qualified_name_with_use( Some(qualifier) => make::path_concat(qualifier, path), None => path, }; + let scope = builder.make_import_scope_mut(scope); insert_use(&scope, path, &ctx.config.insert_use); }, ) } +fn target_path(ctx: &AssistContext<'_>, mut original_path: ast::Path) -> Option<ast::Path> { + let on_first = original_path.qualifier().is_none(); + + if on_first { + original_path = original_path.top_path(); + } + + match ctx.sema.resolve_path(&original_path)? { + PathResolution::Def(ModuleDef::Variant(_)) if on_first => original_path.qualifier(), + PathResolution::Def(def) if def.as_assoc_item(ctx.db()).is_some() => { + on_first.then_some(original_path.qualifier()?) + } + _ => Some(original_path), + } +} + fn drop_generic_args(path: &ast::Path) -> ast::Path { - let path = path.clone_for_update(); + let path = path.clone_subtree(); + let mut editor = SyntaxEditor::new(path.syntax().clone()); if let Some(segment) = path.segment() && let Some(generic_args) = segment.generic_arg_list() { - ted::remove(generic_args.syntax()); + editor.delete(generic_args.syntax()); } - path + + ast::Path::cast(editor.finish().new_root().clone()).unwrap() } /// Mutates `node` to shorten `path` in all descendants of `node`. -fn shorten_paths(node: &SyntaxNode, path: &ast::Path) { +fn shorten_paths(editor: &mut SyntaxEditor, node: &SyntaxNode, path: &ast::Path) { for child in node.children() { match_ast! { match child { @@ -118,26 +133,26 @@ fn shorten_paths(node: &SyntaxNode, path: &ast::Path) { // Don't descend into submodules, they don't have the same `use` items in scope. // FIXME: This isn't true due to `super::*` imports? ast::Module(_) => continue, - ast::Path(p) => if maybe_replace_path(p.clone(), path.clone()).is_none() { - shorten_paths(p.syntax(), path); + ast::Path(p) => if maybe_replace_path(editor, p.clone(), path.clone()).is_none() { + shorten_paths(editor, p.syntax(), path); }, - _ => shorten_paths(&child, path), + _ => shorten_paths(editor, &child, path), } } } } -fn maybe_replace_path(path: ast::Path, target: ast::Path) -> Option<()> { +fn maybe_replace_path(editor: &mut SyntaxEditor, path: ast::Path, target: ast::Path) -> Option<()> { if !path_eq_no_generics(path.clone(), target) { return None; } // Shorten `path`, leaving only its last segment. if let Some(parent) = path.qualifier() { - ted::remove(parent.syntax()); + editor.delete(parent.syntax()); } if let Some(double_colon) = path.coloncolon_token() { - ted::remove(&double_colon); + editor.delete(double_colon); } Some(()) @@ -270,12 +285,117 @@ fn main() { } ", r" -use std::fmt; +use std::fmt::Debug; mod std { pub mod fmt { pub trait Debug {} } } fn main() { - fmt::Debug; - let x: fmt::Debug = fmt::Debug; + Debug; + let x: Debug = Debug; +} + ", + ); + } + + #[test] + fn assist_runs_on_first_segment_for_enum() { + check_assist( + replace_qualified_name_with_use, + r" +mod std { pub mod option { pub enum Option<T> { Some(T), None } } } +fn main() { + $0std::option::Option; + let x: std::option::Option<()> = std::option::Option::Some(()); +} + ", + r" +use std::option::Option; + +mod std { pub mod option { pub enum Option<T> { Some(T), None } } } +fn main() { + Option; + let x: Option<()> = Option::Some(()); +} + ", + ); + + check_assist( + replace_qualified_name_with_use, + r" +mod std { pub mod option { pub enum Option<T> { Some(T), None } } } +fn main() { + std::option::Option; + let x: std::option::Option<()> = $0std::option::Option::Some(()); +} + ", + r" +use std::option::Option; + +mod std { pub mod option { pub enum Option<T> { Some(T), None } } } +fn main() { + Option; + let x: Option<()> = Option::Some(()); +} + ", + ); + } + + #[test] + fn assist_runs_on_first_segment_for_assoc_type() { + check_assist( + replace_qualified_name_with_use, + r" +mod foo { pub struct Foo; impl Foo { pub fn foo() {} } } +fn main() { + $0foo::Foo::foo(); +} + ", + r" +use foo::Foo; + +mod foo { pub struct Foo; impl Foo { pub fn foo() {} } } +fn main() { + Foo::foo(); +} + ", + ); + } + + #[test] + fn assist_runs_on_enum_variant() { + check_assist( + replace_qualified_name_with_use, + r" +mod std { pub mod option { pub enum Option<T> { Some(T), None } } } +fn main() { + let x = std::option::Option::Some$0(()); +} + ", + r" +use std::option::Option::Some; + +mod std { pub mod option { pub enum Option<T> { Some(T), None } } } +fn main() { + let x = Some(()); +} + ", + ); + + check_assist( + replace_qualified_name_with_use, + r" +mod std { pub mod option { pub enum Option<T> { Some(T), None } } } +fn main() { + std::option::Option; + let x: std::option::Option<()> = $0std::option::Option::Some(()); +} + ", + r" +use std::option::Option; + +mod std { pub mod option { pub enum Option<T> { Some(T), None } } } +fn main() { + Option; + let x: Option<()> = Option::Some(()); } ", ); |