Skip to main content

ide_assists/handlers/
merge_imports.rs

1use either::Either;
2use ide_db::imports::{
3    insert_use::{ImportGranularity, InsertUseConfig},
4    merge_imports::{MergeBehavior, try_merge_imports, try_merge_trees},
5};
6use syntax::{
7    AstNode, SyntaxElement, SyntaxNode, algo::neighbor, ast, match_ast, syntax_editor::Removable,
8};
9
10use crate::{
11    AssistId,
12    assist_context::{AssistContext, Assists},
13    utils::next_prev,
14};
15
16use Edit::*;
17
18// Assist: merge_imports
19//
20// Merges neighbor imports with a common prefix.
21//
22// ```
23// use std::$0fmt::Formatter;
24// use std::io;
25// ```
26// ->
27// ```
28// use std::{fmt::Formatter, io};
29// ```
30pub(crate) fn merge_imports(acc: &mut Assists, ctx: &AssistContext<'_, '_>) -> Option<()> {
31    let (target, edits) = if ctx.has_empty_selection() {
32        // Merge a neighbor
33        cov_mark::hit!(merge_with_use_item_neighbors);
34        let tree = ctx.find_node_at_offset::<ast::UseTree>()?.top_use_tree();
35        let target = tree.syntax().text_range();
36
37        let use_item = tree.syntax().parent().and_then(ast::Use::cast)?;
38        let mut neighbor = next_prev().find_map(|dir| neighbor(&use_item, dir)).into_iter();
39        let edits = use_item.try_merge_from(&mut neighbor, &ctx.config.insert_use);
40        (target, edits?)
41    } else {
42        // Merge selected
43        let selection_range = ctx.selection_trimmed();
44        let parent_node = match ctx.covering_element() {
45            SyntaxElement::Node(n) => n,
46            SyntaxElement::Token(t) => t.parent()?,
47        };
48        let mut selected_nodes = parent_node.children().filter(|it| {
49            selection_range.intersect(it.text_range()).is_some_and(|it| !it.is_empty())
50        });
51
52        let first_selected = selected_nodes.next()?;
53        let edits = match_ast! {
54            match first_selected {
55                ast::Use(use_item) => {
56                    cov_mark::hit!(merge_with_selected_use_item_neighbors);
57                    use_item.try_merge_from(&mut selected_nodes.filter_map(ast::Use::cast), &ctx.config.insert_use)
58                },
59                ast::UseTree(use_tree) => {
60                    cov_mark::hit!(merge_with_selected_use_tree_neighbors);
61                    use_tree.try_merge_from(&mut selected_nodes.filter_map(ast::UseTree::cast), &ctx.config.insert_use)
62                },
63                _ => return None,
64            }
65        };
66        (selection_range, edits?)
67    };
68
69    let parent_node = match ctx.covering_element() {
70        SyntaxElement::Node(n) => n,
71        SyntaxElement::Token(t) => t.parent()?,
72    };
73
74    acc.add(AssistId::refactor_rewrite("merge_imports"), "Merge imports", target, |builder| {
75        let editor = builder.make_editor(&parent_node);
76
77        for edit in edits {
78            match edit {
79                Remove(it) => {
80                    let node = it.as_ref();
81                    if let Some(left) = node.left() {
82                        left.remove(&editor);
83                    } else if let Some(right) = node.right() {
84                        right.remove(&editor);
85                    }
86                }
87                Replace(old, new) => {
88                    editor.replace(old, &new);
89                }
90            }
91        }
92        builder.add_file_edits(ctx.vfs_file_id(), editor);
93    })
94}
95
96trait Merge: AstNode + Clone {
97    fn try_merge_from(
98        self,
99        items: &mut dyn Iterator<Item = Self>,
100        cfg: &InsertUseConfig,
101    ) -> Option<Vec<Edit>> {
102        let mut edits = Vec::new();
103        let mut merged = self.clone();
104        for item in items {
105            merged = merged.try_merge(&item, cfg)?;
106            edits.push(Edit::Remove(item.into_either()));
107        }
108        if !edits.is_empty() {
109            edits.push(Edit::replace(self, merged));
110            Some(edits)
111        } else {
112            None
113        }
114    }
115    fn try_merge(&self, other: &Self, cfg: &InsertUseConfig) -> Option<Self>;
116    fn into_either(self) -> Either<ast::Use, ast::UseTree>;
117}
118
119impl Merge for ast::Use {
120    fn try_merge(&self, other: &Self, cfg: &InsertUseConfig) -> Option<Self> {
121        let mb = match cfg.granularity {
122            ImportGranularity::One => MergeBehavior::One,
123            _ => MergeBehavior::Crate,
124        };
125        try_merge_imports(self, other, mb)
126    }
127    fn into_either(self) -> Either<ast::Use, ast::UseTree> {
128        Either::Left(self)
129    }
130}
131
132impl Merge for ast::UseTree {
133    fn try_merge(&self, other: &Self, _: &InsertUseConfig) -> Option<Self> {
134        try_merge_trees(self, other, MergeBehavior::Crate)
135    }
136    fn into_either(self) -> Either<ast::Use, ast::UseTree> {
137        Either::Right(self)
138    }
139}
140
141#[derive(Debug)]
142enum Edit {
143    Remove(Either<ast::Use, ast::UseTree>),
144    Replace(SyntaxNode, SyntaxNode),
145}
146
147impl Edit {
148    fn replace(old: impl AstNode, new: impl AstNode) -> Self {
149        Edit::Replace(old.syntax().clone(), new.syntax().clone())
150    }
151}
152
153#[cfg(test)]
154mod tests {
155    use crate::tests::{
156        check_assist, check_assist_import_one, check_assist_not_applicable,
157        check_assist_not_applicable_for_import_one,
158    };
159
160    use super::*;
161
162    macro_rules! check_assist_import_one_variations {
163        ($first: literal, $second: literal, $expected: literal) => {
164            check_assist_import_one(
165                merge_imports,
166                concat!(concat!("use ", $first, ";"), concat!("use ", $second, ";")),
167                $expected,
168            );
169            check_assist_import_one(
170                merge_imports,
171                concat!(concat!("use {", $first, "};"), concat!("use ", $second, ";")),
172                $expected,
173            );
174            check_assist_import_one(
175                merge_imports,
176                concat!(concat!("use ", $first, ";"), concat!("use {", $second, "};")),
177                $expected,
178            );
179            check_assist_import_one(
180                merge_imports,
181                concat!(concat!("use {", $first, "};"), concat!("use {", $second, "};")),
182                $expected,
183            );
184        };
185    }
186
187    #[test]
188    fn test_merge_equal() {
189        cov_mark::check!(merge_with_use_item_neighbors);
190        check_assist(
191            merge_imports,
192            r"
193use std::fmt$0::{Display, Debug};
194use std::fmt::{Display, Debug};
195",
196            r"
197use std::fmt::{Debug, Display};
198",
199        );
200
201        // The assist macro below calls `check_assist_import_one` 4 times with different input
202        // use item variations based on the first 2 input parameters.
203        cov_mark::check_count!(merge_with_use_item_neighbors, 4);
204        check_assist_import_one_variations!(
205            "std::fmt$0::{Display, Debug}",
206            "std::fmt::{Display, Debug}",
207            "use {std::fmt::{Debug, Display}};"
208        );
209    }
210
211    #[test]
212    fn test_merge_first() {
213        check_assist(
214            merge_imports,
215            r"
216use std::fmt$0::Debug;
217use std::fmt::Display;
218",
219            r"
220use std::fmt::{Debug, Display};
221",
222        );
223        check_assist_import_one_variations!(
224            "std::fmt$0::Debug",
225            "std::fmt::Display",
226            "use {std::fmt::{Debug, Display}};"
227        );
228    }
229
230    #[test]
231    fn test_merge_second() {
232        check_assist(
233            merge_imports,
234            r"
235use std::fmt::Debug;
236use std::fmt$0::Display;
237",
238            r"
239use std::fmt::{Debug, Display};
240",
241        );
242        check_assist_import_one_variations!(
243            "std::fmt::Debug",
244            "std::fmt$0::Display",
245            "use {std::fmt::{Debug, Display}};"
246        );
247    }
248
249    #[test]
250    fn merge_self() {
251        check_assist(
252            merge_imports,
253            r"
254use std::fmt$0;
255use std::fmt::Display;
256",
257            r"
258use std::fmt::{self, Display};
259",
260        );
261        check_assist_import_one_variations!(
262            "std::fmt$0",
263            "std::fmt::Display",
264            "use {std::fmt::{self, Display}};"
265        );
266    }
267
268    #[test]
269    fn not_applicable_to_single_import() {
270        check_assist_not_applicable(merge_imports, "use std::{fmt, $0fmt::Display};");
271        check_assist_not_applicable_for_import_one(
272            merge_imports,
273            "use {std::{fmt, $0fmt::Display}};",
274        );
275    }
276
277    #[test]
278    fn skip_pub1() {
279        check_assist_not_applicable(
280            merge_imports,
281            r"
282pub use std::fmt$0::Debug;
283use std::fmt::Display;
284",
285        );
286    }
287
288    #[test]
289    fn skip_pub_last() {
290        check_assist_not_applicable(
291            merge_imports,
292            r"
293use std::fmt$0::Debug;
294pub use std::fmt::Display;
295",
296        );
297    }
298
299    #[test]
300    fn skip_pub_crate_pub() {
301        check_assist_not_applicable(
302            merge_imports,
303            r"
304pub(crate) use std::fmt$0::Debug;
305pub use std::fmt::Display;
306",
307        );
308    }
309
310    #[test]
311    fn skip_pub_pub_crate() {
312        check_assist_not_applicable(
313            merge_imports,
314            r"
315pub use std::fmt$0::Debug;
316pub(crate) use std::fmt::Display;
317",
318        );
319    }
320
321    #[test]
322    fn merge_pub() {
323        check_assist(
324            merge_imports,
325            r"
326pub use std::fmt$0::Debug;
327pub use std::fmt::Display;
328",
329            r"
330pub use std::fmt::{Debug, Display};
331",
332        )
333    }
334
335    #[test]
336    fn merge_pub_crate() {
337        check_assist(
338            merge_imports,
339            r"
340pub(crate) use std::fmt$0::Debug;
341pub(crate) use std::fmt::Display;
342",
343            r"
344pub(crate) use std::fmt::{Debug, Display};
345",
346        )
347    }
348
349    #[test]
350    fn merge_pub_in_path_crate() {
351        check_assist(
352            merge_imports,
353            r"
354pub(in this::path) use std::fmt$0::Debug;
355pub(in this::path) use std::fmt::Display;
356",
357            r"
358pub(in this::path) use std::fmt::{Debug, Display};
359",
360        )
361    }
362
363    #[test]
364    fn test_merge_nested() {
365        check_assist(
366            merge_imports,
367            r"
368use std::{fmt$0::Debug, fmt::Error};
369use std::{fmt::Write, fmt::Display};
370",
371            r"
372use std::fmt::{Debug, Display, Error, Write};
373",
374        );
375    }
376
377    #[test]
378    fn test_merge_nested2() {
379        check_assist(
380            merge_imports,
381            r"
382use std::{fmt::Debug, fmt$0::Error};
383use std::{fmt::Write, fmt::Display};
384",
385            r"
386use std::fmt::{Debug, Display, Error, Write};
387",
388        );
389    }
390
391    #[test]
392    fn test_merge_with_nested_self_item() {
393        check_assist(
394            merge_imports,
395            r"
396use std$0::{fmt::{Write, Display}};
397use std::{fmt::{self, Debug}};
398",
399            r"
400use std::fmt::{self, Debug, Display, Write};
401",
402        );
403        check_assist_import_one_variations!(
404            "std$0::{fmt::{Write, Display}}",
405            "std::{fmt::{self, Debug}}",
406            "use {std::fmt::{self, Debug, Display, Write}};"
407        );
408    }
409
410    #[test]
411    fn test_merge_with_nested_self_item2() {
412        check_assist(
413            merge_imports,
414            r"
415use std$0::{fmt::{self, Debug}};
416use std::{fmt::{Write, Display}};
417",
418            r"
419use std::fmt::{self, Debug, Display, Write};
420",
421        );
422        check_assist_import_one_variations!(
423            "std$0::{fmt::{self, Debug}}",
424            "std::{fmt::{Write, Display}}",
425            "use {std::fmt::{self, Debug, Display, Write}};"
426        );
427    }
428
429    #[test]
430    fn test_merge_nested_self_and_empty() {
431        check_assist(
432            merge_imports,
433            r"
434use foo::$0{bar::{self}};
435use foo::{bar};
436",
437            r"
438use foo::bar;
439",
440        );
441        check_assist_import_one_variations!(
442            "foo::$0{bar::{self}}",
443            "foo::{bar}",
444            "use {foo::bar};"
445        );
446    }
447
448    #[test]
449    fn test_merge_nested_empty_and_self() {
450        check_assist(
451            merge_imports,
452            r"
453use foo::$0{bar};
454use foo::{bar::{self}};
455",
456            r"
457use foo::bar;
458",
459        );
460        check_assist_import_one_variations!(
461            "foo::$0{bar}",
462            "foo::{bar::{self}}",
463            "use {foo::bar};"
464        );
465    }
466
467    #[test]
468    fn test_merge_nested_empty_and_self_with_other() {
469        check_assist(
470            merge_imports,
471            r"
472use foo::$0{bar};
473use foo::{bar::{self, other}};
474",
475            r"
476use foo::bar::{self, other};
477",
478        );
479        check_assist_import_one_variations!(
480            "foo::$0{bar}",
481            "foo::{bar::{self, other}}",
482            "use {foo::bar::{self, other}};"
483        );
484    }
485
486    #[test]
487    fn test_merge_nested_list_self_and_glob() {
488        check_assist(
489            merge_imports,
490            r"
491use std$0::{fmt::*};
492use std::{fmt::{self, Display}};
493",
494            r"
495use std::fmt::{self, Display, *};
496",
497        );
498        check_assist_import_one_variations!(
499            "std$0::{fmt::*}",
500            "std::{fmt::{self, Display}}",
501            "use {std::fmt::{self, Display, *}};"
502        );
503    }
504
505    #[test]
506    fn test_merge_single_wildcard_diff_prefixes() {
507        check_assist(
508            merge_imports,
509            r"
510use std$0::cell::*;
511use std::str;
512",
513            r"
514use std::{cell::*, str};
515",
516        );
517        check_assist_import_one_variations!(
518            "std$0::cell::*",
519            "std::str",
520            "use {std::{cell::*, str}};"
521        );
522    }
523
524    #[test]
525    fn test_merge_both_wildcard_diff_prefixes() {
526        check_assist(
527            merge_imports,
528            r"
529use std$0::cell::*;
530use std::str::*;
531",
532            r"
533use std::{cell::*, str::*};
534",
535        );
536        check_assist_import_one_variations!(
537            "std$0::cell::*",
538            "std::str::*",
539            "use {std::{cell::*, str::*}};"
540        );
541    }
542
543    #[test]
544    fn removes_just_enough_whitespace() {
545        check_assist(
546            merge_imports,
547            r"
548use foo$0::bar;
549use foo::baz;
550
551/// Doc comment
552",
553            r"
554use foo::{bar, baz};
555
556/// Doc comment
557",
558        );
559    }
560
561    #[test]
562    fn works_with_trailing_comma() {
563        check_assist(
564            merge_imports,
565            r"
566use foo$0::{
567    bar, baz,
568};
569use foo::qux;
570",
571            r"
572use foo::{
573    bar, baz, qux,
574};
575",
576        );
577        check_assist(
578            merge_imports,
579            r"
580use foo::{
581    baz, bar,
582};
583use foo$0::qux;
584",
585            r"
586use foo::{bar, baz, qux};
587",
588        );
589    }
590
591    #[test]
592    fn test_double_comma() {
593        check_assist(
594            merge_imports,
595            r"
596use foo::bar::baz;
597use foo::$0{
598    FooBar,
599};
600",
601            r"
602use foo::{
603    FooBar, bar::baz,
604};
605",
606        )
607    }
608
609    #[test]
610    fn test_empty_use() {
611        check_assist_not_applicable(
612            merge_imports,
613            r"
614use std::$0
615fn main() {}",
616        );
617    }
618
619    #[test]
620    fn split_glob() {
621        check_assist(
622            merge_imports,
623            r"
624use foo::$0*;
625use foo::bar::Baz;
626",
627            r"
628use foo::{bar::Baz, *};
629",
630        );
631        check_assist_import_one_variations!(
632            "foo::$0*",
633            "foo::bar::Baz",
634            "use {foo::{bar::Baz, *}};"
635        );
636    }
637
638    #[test]
639    fn merge_selection_uses() {
640        cov_mark::check!(merge_with_selected_use_item_neighbors);
641        check_assist(
642            merge_imports,
643            r"
644use std::fmt::Error;
645$0use std::fmt::Display;
646use std::fmt::Debug;
647use std::fmt::Write;
648$0use std::fmt::Result;
649",
650            r"
651use std::fmt::Error;
652use std::fmt::{Debug, Display, Write};
653use std::fmt::Result;
654",
655        );
656
657        cov_mark::check!(merge_with_selected_use_item_neighbors);
658        check_assist_import_one(
659            merge_imports,
660            r"
661use std::fmt::Error;
662$0use std::fmt::Display;
663use std::fmt::Debug;
664use std::fmt::Write;
665$0use std::fmt::Result;
666",
667            r"
668use std::fmt::Error;
669use {std::fmt::{Debug, Display, Write}};
670use std::fmt::Result;
671",
672        );
673    }
674
675    #[test]
676    fn merge_partial_selection_uses() {
677        cov_mark::check!(merge_with_selected_use_item_neighbors);
678        check_assist(
679            merge_imports,
680            r"
681use std::fmt::Error;
682$0use std::fmt::Display;
683use std::fmt::Debug;
684use std::fmt::Write;
685use$0 std::fmt::Result;
686",
687            r"
688use std::fmt::Error;
689use std::fmt::{Debug, Display, Result, Write};
690",
691        );
692    }
693
694    #[test]
695    fn merge_selection_use_trees() {
696        cov_mark::check!(merge_with_selected_use_tree_neighbors);
697        check_assist(
698            merge_imports,
699            r"
700use std::{
701    fmt::Error,
702    $0fmt::Display,
703    fmt::Debug,
704    fmt::Write,$0
705    fmt::Result,
706};",
707            r"
708use std::{
709    fmt::Error,
710    fmt::{Debug, Display, Write},
711    fmt::Result,
712};",
713        );
714
715        cov_mark::check!(merge_with_selected_use_tree_neighbors);
716        check_assist(
717            merge_imports,
718            r"use std::{fmt::Result, $0fmt::Display, fmt::Debug$0};",
719            r"use std::{fmt::Result, fmt::{Debug, Display}};",
720        );
721
722        cov_mark::check!(merge_with_selected_use_tree_neighbors);
723        check_assist(
724            merge_imports,
725            r"use std::$0{fmt::Display, fmt::Debug}$0;",
726            r"use std::{fmt::{Debug, Display}};",
727        );
728    }
729
730    #[test]
731    fn test_merge_with_synonymous_imports_1() {
732        check_assist(
733            merge_imports,
734            r"
735mod top {
736    pub(crate) mod a {
737        pub(crate) struct A;
738    }
739    pub(crate) mod b {
740        pub(crate) struct B;
741        pub(crate) struct D;
742    }
743}
744
745use top::a::A;
746use $0top::b::{B, B as C};
747",
748            r"
749mod top {
750    pub(crate) mod a {
751        pub(crate) struct A;
752    }
753    pub(crate) mod b {
754        pub(crate) struct B;
755        pub(crate) struct D;
756    }
757}
758
759use top::{a::A, b::{B, B as C}};
760",
761        );
762    }
763
764    #[test]
765    fn test_merge_with_synonymous_imports_2() {
766        check_assist(
767            merge_imports,
768            r"
769mod top {
770    pub(crate) mod a {
771        pub(crate) struct A;
772    }
773    pub(crate) mod b {
774        pub(crate) struct B;
775        pub(crate) struct D;
776    }
777}
778
779use top::a::A;
780use $0top::b::{B as D, B as C};
781",
782            r"
783mod top {
784    pub(crate) mod a {
785        pub(crate) struct A;
786    }
787    pub(crate) mod b {
788        pub(crate) struct B;
789        pub(crate) struct D;
790    }
791}
792
793use top::{a::A, b::{B as D, B as C}};
794",
795        );
796    }
797}