ide_assists/handlers/
replace_qualified_name_with_use.rs

1use hir::{AsAssocItem, ModuleDef, PathResolution};
2use ide_db::{
3    helpers::mod_path_to_ast,
4    imports::insert_use::{ImportScope, insert_use},
5};
6use syntax::{
7    AstNode, Edition, SyntaxNode,
8    ast::{self, HasGenericArgs, make},
9    match_ast,
10    syntax_editor::SyntaxEditor,
11};
12
13use crate::{AssistContext, AssistId, Assists};
14
15// Assist: replace_qualified_name_with_use
16//
17// Adds a use statement for a given fully-qualified name.
18//
19// ```
20// # mod std { pub mod collections { pub struct HashMap<T, U>(T, U); } }
21// fn process(map: std::collections::$0HashMap<String, String>) {}
22// ```
23// ->
24// ```
25// use std::collections::HashMap;
26//
27// # mod std { pub mod collections { pub struct HashMap<T, U>(T, U); } }
28// fn process(map: HashMap<String, String>) {}
29// ```
30pub(crate) fn replace_qualified_name_with_use(
31    acc: &mut Assists,
32    ctx: &AssistContext<'_>,
33) -> Option<()> {
34    let original_path: ast::Path = ctx.find_node_at_offset()?;
35    // We don't want to mess with use statements
36    if original_path.syntax().ancestors().find_map(ast::UseTree::cast).is_some() {
37        cov_mark::hit!(not_applicable_in_use);
38        return None;
39    }
40
41    let original_path = target_path(ctx, original_path)?;
42
43    // then search for an import for the first path segment of what we want to replace
44    // that way it is less likely that we import the item from a different location due re-exports
45    let module = match ctx.sema.resolve_path(&original_path.first_qualifier_or_self())? {
46        PathResolution::Def(module @ ModuleDef::Module(_)) => module,
47        _ => return None,
48    };
49
50    let starts_with_name_ref = !matches!(
51        original_path.first_segment().and_then(|it| it.kind()),
52        Some(
53            ast::PathSegmentKind::CrateKw
54                | ast::PathSegmentKind::SuperKw
55                | ast::PathSegmentKind::SelfKw
56        )
57    );
58    let path_to_qualifier = starts_with_name_ref
59        .then(|| {
60            let mod_ = ctx.sema.scope(original_path.syntax())?.module();
61            let cfg = ctx.config.find_path_config(ctx.sema.is_nightly(mod_.krate(ctx.sema.db)));
62            mod_.find_use_path(ctx.sema.db, module, ctx.config.insert_use.prefix_kind, cfg)
63        })
64        .flatten();
65
66    let scope = ImportScope::find_insert_use_container(original_path.syntax(), &ctx.sema)?;
67    let target = original_path.syntax().text_range();
68    acc.add(
69        AssistId::refactor_rewrite("replace_qualified_name_with_use"),
70        "Replace qualified path with use",
71        target,
72        |builder| {
73            // Now that we've brought the name into scope, re-qualify all paths that could be
74            // affected (that is, all paths inside the node we added the `use` to).
75            let scope_node = scope.as_syntax_node();
76            let mut editor = builder.make_editor(scope_node);
77            shorten_paths(&mut editor, scope_node, &original_path);
78            builder.add_file_edits(ctx.vfs_file_id(), editor);
79            let path = drop_generic_args(&original_path);
80            let edition = ctx
81                .sema
82                .scope(original_path.syntax())
83                .map(|semantics_scope| semantics_scope.krate().edition(ctx.db()))
84                .unwrap_or(Edition::CURRENT);
85            // stick the found import in front of the to be replaced path
86            let path =
87                match path_to_qualifier.and_then(|it| mod_path_to_ast(&it, edition).qualifier()) {
88                    Some(qualifier) => make::path_concat(qualifier, path),
89                    None => path,
90                };
91            let scope = builder.make_import_scope_mut(scope);
92            insert_use(&scope, path, &ctx.config.insert_use);
93        },
94    )
95}
96
97fn target_path(ctx: &AssistContext<'_>, mut original_path: ast::Path) -> Option<ast::Path> {
98    let on_first = original_path.qualifier().is_none();
99
100    if on_first {
101        original_path = original_path.top_path();
102    }
103
104    match ctx.sema.resolve_path(&original_path)? {
105        PathResolution::Def(ModuleDef::Variant(_)) if on_first => original_path.qualifier(),
106        PathResolution::Def(def) if def.as_assoc_item(ctx.db()).is_some() => {
107            on_first.then_some(original_path.qualifier()?)
108        }
109        _ => Some(original_path),
110    }
111}
112
113fn drop_generic_args(path: &ast::Path) -> ast::Path {
114    let path = path.clone_subtree();
115    let mut editor = SyntaxEditor::new(path.syntax().clone());
116    if let Some(segment) = path.segment()
117        && let Some(generic_args) = segment.generic_arg_list()
118    {
119        editor.delete(generic_args.syntax());
120    }
121
122    ast::Path::cast(editor.finish().new_root().clone()).unwrap()
123}
124
125/// Mutates `node` to shorten `path` in all descendants of `node`.
126fn shorten_paths(editor: &mut SyntaxEditor, node: &SyntaxNode, path: &ast::Path) {
127    for child in node.children() {
128        match_ast! {
129            match child {
130                // Don't modify `use` items, as this can break the `use` item when injecting a new
131                // import into the use tree.
132                ast::Use(_) => continue,
133                // Don't descend into submodules, they don't have the same `use` items in scope.
134                // FIXME: This isn't true due to `super::*` imports?
135                ast::Module(_) => continue,
136                ast::Path(p) => if maybe_replace_path(editor, p.clone(), path.clone()).is_none() {
137                    shorten_paths(editor, p.syntax(), path);
138                },
139                _ => shorten_paths(editor, &child, path),
140            }
141        }
142    }
143}
144
145fn maybe_replace_path(editor: &mut SyntaxEditor, path: ast::Path, target: ast::Path) -> Option<()> {
146    if !path_eq_no_generics(path.clone(), target) {
147        return None;
148    }
149
150    // Shorten `path`, leaving only its last segment.
151    if let Some(parent) = path.qualifier() {
152        editor.delete(parent.syntax());
153    }
154    if let Some(double_colon) = path.coloncolon_token() {
155        editor.delete(double_colon);
156    }
157
158    Some(())
159}
160
161fn path_eq_no_generics(lhs: ast::Path, rhs: ast::Path) -> bool {
162    let mut lhs_curr = lhs;
163    let mut rhs_curr = rhs;
164    loop {
165        match lhs_curr.segment().zip(rhs_curr.segment()) {
166            Some((lhs, rhs))
167                if lhs.coloncolon_token().is_some() == rhs.coloncolon_token().is_some()
168                    && lhs
169                        .name_ref()
170                        .zip(rhs.name_ref())
171                        .is_some_and(|(lhs, rhs)| lhs.text() == rhs.text()) => {}
172            _ => return false,
173        }
174
175        match (lhs_curr.qualifier(), rhs_curr.qualifier()) {
176            (Some(lhs), Some(rhs)) => {
177                lhs_curr = lhs;
178                rhs_curr = rhs;
179            }
180            (None, None) => return true,
181            _ => return false,
182        }
183    }
184}
185
186#[cfg(test)]
187mod tests {
188    use crate::tests::{check_assist, check_assist_not_applicable};
189
190    use super::*;
191
192    #[test]
193    fn test_replace_already_imported() {
194        check_assist(
195            replace_qualified_name_with_use,
196            r"
197mod std { pub mod fs { pub struct Path; } }
198use std::fs;
199
200fn main() {
201    std::f$0s::Path
202}",
203            r"
204mod std { pub mod fs { pub struct Path; } }
205use std::fs;
206
207fn main() {
208    fs::Path
209}",
210        )
211    }
212
213    #[test]
214    fn test_replace_add_use_no_anchor() {
215        check_assist(
216            replace_qualified_name_with_use,
217            r"
218mod std { pub mod fs { pub struct Path; } }
219std::fs::Path$0
220    ",
221            r"
222use std::fs::Path;
223
224mod std { pub mod fs { pub struct Path; } }
225Path
226    ",
227        );
228    }
229
230    #[test]
231    fn test_replace_add_use_no_anchor_middle_segment() {
232        check_assist(
233            replace_qualified_name_with_use,
234            r"
235mod std { pub mod fs { pub struct Path; } }
236std::fs$0::Path
237    ",
238            r"
239use std::fs;
240
241mod std { pub mod fs { pub struct Path; } }
242fs::Path
243    ",
244        );
245    }
246
247    #[test]
248    fn test_replace_not_applicable_in_use() {
249        cov_mark::check!(not_applicable_in_use);
250        check_assist_not_applicable(replace_qualified_name_with_use, r"use std::fmt$0;");
251    }
252
253    #[test]
254    fn replaces_all_affected_paths() {
255        check_assist(
256            replace_qualified_name_with_use,
257            r"
258mod std { pub mod fmt { pub trait Debug {} } }
259fn main() {
260    std::fmt::Debug$0;
261    let x: std::fmt::Debug = std::fmt::Debug;
262}
263    ",
264            r"
265use std::fmt::Debug;
266
267mod std { pub mod fmt { pub trait Debug {} } }
268fn main() {
269    Debug;
270    let x: Debug = Debug;
271}
272    ",
273        );
274    }
275
276    #[test]
277    fn assist_runs_on_first_segment() {
278        check_assist(
279            replace_qualified_name_with_use,
280            r"
281mod std { pub mod fmt { pub trait Debug {} } }
282fn main() {
283    $0std::fmt::Debug;
284    let x: std::fmt::Debug = std::fmt::Debug;
285}
286    ",
287            r"
288use std::fmt::Debug;
289
290mod std { pub mod fmt { pub trait Debug {} } }
291fn main() {
292    Debug;
293    let x: Debug = Debug;
294}
295    ",
296        );
297    }
298
299    #[test]
300    fn assist_runs_on_first_segment_for_enum() {
301        check_assist(
302            replace_qualified_name_with_use,
303            r"
304mod std { pub mod option { pub enum Option<T> { Some(T), None } } }
305fn main() {
306    $0std::option::Option;
307    let x: std::option::Option<()> = std::option::Option::Some(());
308}
309    ",
310            r"
311use std::option::Option;
312
313mod std { pub mod option { pub enum Option<T> { Some(T), None } } }
314fn main() {
315    Option;
316    let x: Option<()> = Option::Some(());
317}
318    ",
319        );
320
321        check_assist(
322            replace_qualified_name_with_use,
323            r"
324mod std { pub mod option { pub enum Option<T> { Some(T), None } } }
325fn main() {
326    std::option::Option;
327    let x: std::option::Option<()> = $0std::option::Option::Some(());
328}
329    ",
330            r"
331use std::option::Option;
332
333mod std { pub mod option { pub enum Option<T> { Some(T), None } } }
334fn main() {
335    Option;
336    let x: Option<()> = Option::Some(());
337}
338    ",
339        );
340    }
341
342    #[test]
343    fn assist_runs_on_first_segment_for_assoc_type() {
344        check_assist(
345            replace_qualified_name_with_use,
346            r"
347mod foo { pub struct Foo; impl Foo { pub fn foo() {} } }
348fn main() {
349    $0foo::Foo::foo();
350}
351    ",
352            r"
353use foo::Foo;
354
355mod foo { pub struct Foo; impl Foo { pub fn foo() {} } }
356fn main() {
357    Foo::foo();
358}
359    ",
360        );
361    }
362
363    #[test]
364    fn assist_runs_on_enum_variant() {
365        check_assist(
366            replace_qualified_name_with_use,
367            r"
368mod std { pub mod option { pub enum Option<T> { Some(T), None } } }
369fn main() {
370    let x = std::option::Option::Some$0(());
371}
372    ",
373            r"
374use std::option::Option::Some;
375
376mod std { pub mod option { pub enum Option<T> { Some(T), None } } }
377fn main() {
378    let x = Some(());
379}
380    ",
381        );
382
383        check_assist(
384            replace_qualified_name_with_use,
385            r"
386mod std { pub mod option { pub enum Option<T> { Some(T), None } } }
387fn main() {
388    std::option::Option;
389    let x: std::option::Option<()> = $0std::option::Option::Some(());
390}
391    ",
392            r"
393use std::option::Option;
394
395mod std { pub mod option { pub enum Option<T> { Some(T), None } } }
396fn main() {
397    Option;
398    let x: Option<()> = Option::Some(());
399}
400    ",
401        );
402    }
403
404    #[test]
405    fn does_not_replace_in_submodules() {
406        check_assist(
407            replace_qualified_name_with_use,
408            r"
409mod std { pub mod fmt { pub trait Debug {} } }
410fn main() {
411    std::fmt::Debug$0;
412}
413
414mod sub {
415    fn f() {
416        std::fmt::Debug;
417    }
418}
419    ",
420            r"
421use std::fmt::Debug;
422
423mod std { pub mod fmt { pub trait Debug {} } }
424fn main() {
425    Debug;
426}
427
428mod sub {
429    fn f() {
430        std::fmt::Debug;
431    }
432}
433    ",
434        );
435    }
436
437    #[test]
438    fn does_not_replace_in_use() {
439        check_assist(
440            replace_qualified_name_with_use,
441            r"
442mod std { pub mod fmt { pub trait Display {} } }
443use std::fmt::Display;
444
445fn main() {
446    std::fmt$0;
447}
448    ",
449            r"
450mod std { pub mod fmt { pub trait Display {} } }
451use std::fmt::{self, Display};
452
453fn main() {
454    fmt;
455}
456    ",
457        );
458    }
459
460    #[test]
461    fn does_not_replace_assoc_item_path() {
462        check_assist_not_applicable(
463            replace_qualified_name_with_use,
464            r"
465pub struct Foo;
466impl Foo {
467    pub fn foo() {}
468}
469
470fn main() {
471    Foo::foo$0();
472}
473",
474        );
475    }
476
477    #[test]
478    fn replace_reuses_path_qualifier() {
479        check_assist(
480            replace_qualified_name_with_use,
481            r"
482pub mod foo {
483    pub struct Foo;
484}
485
486mod bar {
487    pub use super::foo::Foo as Bar;
488}
489
490fn main() {
491    foo::Foo$0;
492}
493",
494            r"
495use foo::Foo;
496
497pub mod foo {
498    pub struct Foo;
499}
500
501mod bar {
502    pub use super::foo::Foo as Bar;
503}
504
505fn main() {
506    Foo;
507}
508",
509        );
510    }
511
512    #[test]
513    fn replace_does_not_always_try_to_replace_by_full_item_path() {
514        check_assist(
515            replace_qualified_name_with_use,
516            r"
517use std::mem;
518
519mod std {
520    pub mod mem {
521        pub fn drop<T>(_: T) {}
522    }
523}
524
525fn main() {
526    mem::drop$0(0);
527}
528",
529            r"
530use std::mem::{self, drop};
531
532mod std {
533    pub mod mem {
534        pub fn drop<T>(_: T) {}
535    }
536}
537
538fn main() {
539    drop(0);
540}
541",
542        );
543    }
544
545    #[test]
546    fn replace_should_drop_generic_args_in_use() {
547        check_assist(
548            replace_qualified_name_with_use,
549            r"
550mod std {
551    pub mod mem {
552        pub fn drop<T>(_: T) {}
553    }
554}
555
556fn main() {
557    std::mem::drop::<usize>$0(0);
558}
559",
560            r"
561use std::mem::drop;
562
563mod std {
564    pub mod mem {
565        pub fn drop<T>(_: T) {}
566    }
567}
568
569fn main() {
570    drop::<usize>(0);
571}
572",
573        );
574    }
575}