]> git.lizzy.rs Git - rust.git/blob - crates/ide_assists/src/handlers/replace_qualified_name_with_use.rs
Merge #9557
[rust.git] / crates / ide_assists / src / handlers / replace_qualified_name_with_use.rs
1 use hir::AsAssocItem;
2 use ide_db::helpers::{
3     insert_use::{insert_use, ImportScope},
4     mod_path_to_ast,
5 };
6 use syntax::{
7     ast::{self, make},
8     match_ast, ted, AstNode, SyntaxNode,
9 };
10
11 use crate::{AssistContext, AssistId, AssistKind, Assists};
12
13 // Assist: replace_qualified_name_with_use
14 //
15 // Adds a use statement for a given fully-qualified name.
16 //
17 // ```
18 // # mod std { pub mod collections { pub struct HashMap<T, U>(T, U); } }
19 // fn process(map: std::collections::$0HashMap<String, String>) {}
20 // ```
21 // ->
22 // ```
23 // use std::collections::HashMap;
24 //
25 // # mod std { pub mod collections { pub struct HashMap<T, U>(T, U); } }
26 // fn process(map: HashMap<String, String>) {}
27 // ```
28 pub(crate) fn replace_qualified_name_with_use(
29     acc: &mut Assists,
30     ctx: &AssistContext,
31 ) -> Option<()> {
32     let path: ast::Path = ctx.find_node_at_offset()?;
33     // We don't want to mess with use statements
34     if path.syntax().ancestors().find_map(ast::UseTree::cast).is_some() {
35         cov_mark::hit!(not_applicable_in_use);
36         return None;
37     }
38
39     if path.qualifier().is_none() {
40         cov_mark::hit!(dont_import_trivial_paths);
41         return None;
42     }
43
44     // only offer replacement for non assoc items
45     match ctx.sema.resolve_path(&path)? {
46         hir::PathResolution::Def(def) if def.as_assoc_item(ctx.sema.db).is_none() => (),
47         hir::PathResolution::Macro(_) => (),
48         _ => return None,
49     }
50     // then search for an import for the first path segment of what we want to replace
51     // that way it is less likely that we import the item from a different location due re-exports
52     let module = match ctx.sema.resolve_path(&path.first_qualifier_or_self())? {
53         hir::PathResolution::Def(module @ hir::ModuleDef::Module(_)) => module,
54         _ => return None,
55     };
56
57     let scope = ImportScope::find_insert_use_container_with_macros(path.syntax(), &ctx.sema)?;
58     let path_to_qualifier = ctx.sema.scope(path.syntax()).module()?.find_use_path_prefixed(
59         ctx.sema.db,
60         module,
61         ctx.config.insert_use.prefix_kind,
62     )?;
63     let target = path.syntax().text_range();
64     acc.add(
65         AssistId("replace_qualified_name_with_use", AssistKind::RefactorRewrite),
66         "Replace qualified path with use",
67         target,
68         |builder| {
69             // Now that we've brought the name into scope, re-qualify all paths that could be
70             // affected (that is, all paths inside the node we added the `use` to).
71             let scope = match scope {
72                 ImportScope::File(it) => ImportScope::File(builder.make_mut(it)),
73                 ImportScope::Module(it) => ImportScope::Module(builder.make_mut(it)),
74                 ImportScope::Block(it) => ImportScope::Block(builder.make_mut(it)),
75             };
76             // stick the found import in front of the to be replaced path
77             let path = match mod_path_to_ast(&path_to_qualifier).qualifier() {
78                 Some(qualifier) => make::path_concat(qualifier, path),
79                 None => path,
80             };
81             shorten_paths(scope.as_syntax_node(), &path.clone_for_update());
82             insert_use(&scope, path, &ctx.config.insert_use);
83         },
84     )
85 }
86
87 /// Adds replacements to `re` that shorten `path` in all descendants of `node`.
88 fn shorten_paths(node: &SyntaxNode, path: &ast::Path) {
89     for child in node.children() {
90         match_ast! {
91             match child {
92                 // Don't modify `use` items, as this can break the `use` item when injecting a new
93                 // import into the use tree.
94                 ast::Use(_it) => continue,
95                 // Don't descend into submodules, they don't have the same `use` items in scope.
96                 // FIXME: This isn't true due to `super::*` imports?
97                 ast::Module(_it) => continue,
98                 ast::Path(p) => if maybe_replace_path(p.clone(), path.clone()).is_none() {
99                     shorten_paths(p.syntax(), path);
100                 },
101                 _ => shorten_paths(&child, path),
102             }
103         }
104     }
105 }
106
107 fn maybe_replace_path(path: ast::Path, target: ast::Path) -> Option<()> {
108     if !path_eq_no_generics(path.clone(), target) {
109         return None;
110     }
111
112     // Shorten `path`, leaving only its last segment.
113     if let Some(parent) = path.qualifier() {
114         ted::remove(parent.syntax());
115     }
116     if let Some(double_colon) = path.coloncolon_token() {
117         ted::remove(&double_colon);
118     }
119
120     Some(())
121 }
122
123 fn path_eq_no_generics(lhs: ast::Path, rhs: ast::Path) -> bool {
124     let mut lhs_curr = lhs;
125     let mut rhs_curr = rhs;
126     loop {
127         match lhs_curr.segment().zip(rhs_curr.segment()) {
128             Some((lhs, rhs))
129                 if lhs.coloncolon_token().is_some() == rhs.coloncolon_token().is_some()
130                     && lhs
131                         .name_ref()
132                         .zip(rhs.name_ref())
133                         .map_or(false, |(lhs, rhs)| lhs.text() == rhs.text()) =>
134             {
135                 ()
136             }
137             _ => return false,
138         }
139
140         match (lhs_curr.qualifier(), rhs_curr.qualifier()) {
141             (Some(lhs), Some(rhs)) => {
142                 lhs_curr = lhs;
143                 rhs_curr = rhs;
144             }
145             (None, None) => return true,
146             _ => return false,
147         }
148     }
149 }
150
151 #[cfg(test)]
152 mod tests {
153     use crate::tests::{check_assist, check_assist_not_applicable};
154
155     use super::*;
156
157     #[test]
158     fn test_replace_already_imported() {
159         check_assist(
160             replace_qualified_name_with_use,
161             r"
162 mod std { pub mod fs { pub struct Path; } }
163 use std::fs;
164
165 fn main() {
166     std::f$0s::Path
167 }",
168             r"
169 mod std { pub mod fs { pub struct Path; } }
170 use std::fs;
171
172 fn main() {
173     fs::Path
174 }",
175         )
176     }
177
178     #[test]
179     fn test_replace_add_use_no_anchor() {
180         check_assist(
181             replace_qualified_name_with_use,
182             r"
183 mod std { pub mod fs { pub struct Path; } }
184 std::fs::Path$0
185     ",
186             r"
187 use std::fs::Path;
188
189 mod std { pub mod fs { pub struct Path; } }
190 Path
191     ",
192         );
193     }
194
195     #[test]
196     fn test_replace_add_use_no_anchor_middle_segment() {
197         check_assist(
198             replace_qualified_name_with_use,
199             r"
200 mod std { pub mod fs { pub struct Path; } }
201 std::fs$0::Path
202     ",
203             r"
204 use std::fs;
205
206 mod std { pub mod fs { pub struct Path; } }
207 fs::Path
208     ",
209         );
210     }
211     #[test]
212     #[test]
213     fn dont_import_trivial_paths() {
214         cov_mark::check!(dont_import_trivial_paths);
215         check_assist_not_applicable(replace_qualified_name_with_use, r"impl foo$0 for () {}");
216     }
217
218     #[test]
219     fn test_replace_not_applicable_in_use() {
220         cov_mark::check!(not_applicable_in_use);
221         check_assist_not_applicable(replace_qualified_name_with_use, r"use std::fmt$0;");
222     }
223
224     #[test]
225     fn replaces_all_affected_paths() {
226         check_assist(
227             replace_qualified_name_with_use,
228             r"
229 mod std { pub mod fmt { pub trait Debug {} } }
230 fn main() {
231     std::fmt::Debug$0;
232     let x: std::fmt::Debug = std::fmt::Debug;
233 }
234     ",
235             r"
236 use std::fmt::Debug;
237
238 mod std { pub mod fmt { pub trait Debug {} } }
239 fn main() {
240     Debug;
241     let x: Debug = Debug;
242 }
243     ",
244         );
245     }
246
247     #[test]
248     fn does_not_replace_in_submodules() {
249         check_assist(
250             replace_qualified_name_with_use,
251             r"
252 mod std { pub mod fmt { pub trait Debug {} } }
253 fn main() {
254     std::fmt::Debug$0;
255 }
256
257 mod sub {
258     fn f() {
259         std::fmt::Debug;
260     }
261 }
262     ",
263             r"
264 use std::fmt::Debug;
265
266 mod std { pub mod fmt { pub trait Debug {} } }
267 fn main() {
268     Debug;
269 }
270
271 mod sub {
272     fn f() {
273         std::fmt::Debug;
274     }
275 }
276     ",
277         );
278     }
279
280     #[test]
281     fn does_not_replace_in_use() {
282         check_assist(
283             replace_qualified_name_with_use,
284             r"
285 mod std { pub mod fmt { pub trait Display {} } }
286 use std::fmt::Display;
287
288 fn main() {
289     std::fmt$0;
290 }
291     ",
292             r"
293 mod std { pub mod fmt { pub trait Display {} } }
294 use std::fmt::{self, Display};
295
296 fn main() {
297     fmt;
298 }
299     ",
300         );
301     }
302
303     #[test]
304     fn does_not_replace_assoc_item_path() {
305         check_assist_not_applicable(
306             replace_qualified_name_with_use,
307             r"
308 pub struct Foo;
309 impl Foo {
310     pub fn foo() {}
311 }
312
313 fn main() {
314     Foo::foo$0();
315 }
316 ",
317         );
318     }
319
320     #[test]
321     fn replace_reuses_path_qualifier() {
322         check_assist(
323             replace_qualified_name_with_use,
324             r"
325 pub mod foo {
326     struct Foo;
327 }
328
329 mod bar {
330     pub use super::foo::Foo as Bar;
331 }
332
333 fn main() {
334     foo::Foo$0;
335 }
336 ",
337             r"
338 use foo::Foo;
339
340 pub mod foo {
341     struct Foo;
342 }
343
344 mod bar {
345     pub use super::foo::Foo as Bar;
346 }
347
348 fn main() {
349     Foo;
350 }
351 ",
352         );
353     }
354 }