Skip to main content

ide_assists/handlers/
unmerge_imports.rs

1use syntax::{
2    AstNode, SyntaxKind,
3    ast::{self, HasAttrs, HasVisibility, edit::IndentLevel, syntax_factory::SyntaxFactory},
4    syntax_editor::{Element, Position, Removable, SyntaxEditor},
5};
6
7use crate::{
8    AssistId,
9    assist_context::{AssistContext, Assists},
10};
11
12// Assist: unmerge_imports
13//
14// Extracts a use item from a use list into a standalone use list.
15//
16// ```
17// use std::fmt::{Debug, Display$0};
18// ```
19// ->
20// ```
21// use std::fmt::{Debug};
22// use std::fmt::Display;
23// ```
24pub(crate) fn unmerge_imports(acc: &mut Assists, ctx: &AssistContext<'_, '_>) -> Option<()> {
25    let (editor, _) = SyntaxEditor::new(ctx.source_file().syntax().clone());
26    let make = editor.make();
27    let tree = ctx.find_node_at_offset::<ast::UseTree>()?;
28
29    let tree_list = tree.syntax().parent().and_then(ast::UseTreeList::cast)?;
30    if tree_list.use_trees().count() < 2 {
31        cov_mark::hit!(skip_single_import);
32        return None;
33    }
34
35    let use_ = tree_list.syntax().ancestors().find_map(ast::Use::cast)?;
36    let path = resolve_full_path(&tree, make)?;
37
38    // If possible, explain what is going to be done.
39    let label = match tree.path().and_then(|path| path.first_segment()) {
40        Some(name) => format!("Unmerge use of `{name}`"),
41        None => "Unmerge use".into(),
42    };
43
44    let target = tree.syntax().text_range();
45    acc.add(AssistId::refactor_rewrite("unmerge_imports"), label, target, |builder| {
46        let make = editor.make();
47        let new_use = make.use_(
48            use_.attrs(),
49            use_.visibility(),
50            make.use_tree(path, tree.use_tree_list(), tree.rename(), tree.star_token().is_some()),
51        );
52
53        // Remove the use tree from the current use item
54        tree.remove(&editor);
55        // Insert a newline and indentation, followed by the new use item
56        editor.insert_all(
57            Position::after(use_.syntax()),
58            vec![
59                make.whitespace(&format!("\n{}", IndentLevel::from_node(use_.syntax())))
60                    .syntax_element(),
61                new_use.syntax().syntax_element(),
62            ],
63        );
64        builder.add_file_edits(ctx.vfs_file_id(), editor);
65    })
66}
67
68fn resolve_full_path(tree: &ast::UseTree, make: &SyntaxFactory) -> Option<ast::Path> {
69    let paths = tree
70        .syntax()
71        .ancestors()
72        .take_while(|n| n.kind() != SyntaxKind::USE)
73        .filter_map(ast::UseTree::cast)
74        .filter_map(|t| t.path());
75
76    let final_path = paths.reduce(|prev, next| make.path_concat(next, prev))?;
77    if final_path.segment().is_some_and(|it| it.self_token().is_some()) {
78        final_path.qualifier()
79    } else {
80        Some(final_path)
81    }
82}
83
84#[cfg(test)]
85mod tests {
86    use crate::tests::{check_assist, check_assist_not_applicable};
87
88    use super::*;
89
90    #[test]
91    fn skip_single_import() {
92        cov_mark::check!(skip_single_import);
93        check_assist_not_applicable(
94            unmerge_imports,
95            r"
96use std::fmt::Debug$0;
97",
98        );
99        check_assist_not_applicable(
100            unmerge_imports,
101            r"
102use std::fmt::{Debug$0};
103",
104        );
105        check_assist_not_applicable(
106            unmerge_imports,
107            r"
108use std::fmt::Debug as Dbg$0;
109",
110        );
111    }
112
113    #[test]
114    fn skip_single_glob_import() {
115        check_assist_not_applicable(
116            unmerge_imports,
117            r"
118use std::fmt::*$0;
119",
120        );
121    }
122
123    #[test]
124    fn unmerge_import() {
125        check_assist(
126            unmerge_imports,
127            r"
128use std::fmt::{Debug, Display$0};
129",
130            r"
131use std::fmt::{Debug};
132use std::fmt::Display;
133",
134        );
135
136        check_assist(
137            unmerge_imports,
138            r"
139use std::fmt::{Debug, format$0, Display};
140",
141            r"
142use std::fmt::{Debug, Display};
143use std::fmt::format;
144",
145        );
146    }
147
148    #[test]
149    fn unmerge_glob_import() {
150        check_assist(
151            unmerge_imports,
152            r"
153use std::fmt::{*$0, Display};
154",
155            r"
156use std::fmt::{Display};
157use std::fmt::*;
158",
159        );
160    }
161
162    #[test]
163    fn unmerge_renamed_import() {
164        check_assist(
165            unmerge_imports,
166            r"
167use std::fmt::{Debug, Display as Disp$0};
168",
169            r"
170use std::fmt::{Debug};
171use std::fmt::Display as Disp;
172",
173        );
174    }
175
176    #[test]
177    fn unmerge_indented_import() {
178        check_assist(
179            unmerge_imports,
180            r"
181mod format {
182    use std::fmt::{Debug, Display$0 as Disp, format};
183}
184",
185            r"
186mod format {
187    use std::fmt::{Debug, format};
188    use std::fmt::Display as Disp;
189}
190",
191        );
192    }
193
194    #[test]
195    fn unmerge_nested_import() {
196        check_assist(
197            unmerge_imports,
198            r"
199use foo::bar::{baz::{qux$0, foobar}, barbaz};
200",
201            r"
202use foo::bar::{baz::{foobar}, barbaz};
203use foo::bar::baz::qux;
204",
205        );
206        check_assist(
207            unmerge_imports,
208            r"
209use foo::bar::{baz$0::{qux, foobar}, barbaz};
210",
211            r"
212use foo::bar::{barbaz};
213use foo::bar::baz::{qux, foobar};
214",
215        );
216    }
217
218    #[test]
219    fn unmerge_import_with_visibility() {
220        check_assist(
221            unmerge_imports,
222            r"
223pub use std::fmt::{Debug, Display$0};
224",
225            r"
226pub use std::fmt::{Debug};
227pub use std::fmt::Display;
228",
229        );
230    }
231
232    #[test]
233    fn unmerge_import_on_self() {
234        check_assist(
235            unmerge_imports,
236            r"use std::process::{Command, self$0};",
237            r"use std::process::{Command};
238use std::process;",
239        );
240    }
241
242    #[test]
243    fn unmerge_import_with_attributes() {
244        check_assist(
245            unmerge_imports,
246            r"
247#[allow(deprecated)]
248use foo::{bar, baz$0};",
249            r"
250#[allow(deprecated)]
251use foo::{bar};
252#[allow(deprecated)]
253use foo::baz;",
254        );
255    }
256}