aboutsummaryrefslogtreecommitdiff
path: root/crates/ide_db
diff options
context:
space:
mode:
Diffstat (limited to 'crates/ide_db')
-rw-r--r--crates/ide_db/src/helpers/insert_use.rs105
-rw-r--r--crates/ide_db/src/helpers/insert_use/tests.rs141
-rw-r--r--crates/ide_db/src/helpers/merge_imports.rs6
3 files changed, 237 insertions, 15 deletions
diff --git a/crates/ide_db/src/helpers/insert_use.rs b/crates/ide_db/src/helpers/insert_use.rs
index 55cdc4da3..aa61c5bcb 100644
--- a/crates/ide_db/src/helpers/insert_use.rs
+++ b/crates/ide_db/src/helpers/insert_use.rs
@@ -4,20 +4,36 @@ use std::cmp::Ordering;
4use hir::Semantics; 4use hir::Semantics;
5use syntax::{ 5use syntax::{
6 algo, 6 algo,
7 ast::{self, make, AstNode, PathSegmentKind}, 7 ast::{self, make, AstNode, AttrsOwner, ModuleItemOwner, PathSegmentKind, VisibilityOwner},
8 ted, AstToken, Direction, NodeOrToken, SyntaxNode, SyntaxToken, 8 ted, AstToken, Direction, NodeOrToken, SyntaxNode, SyntaxToken,
9}; 9};
10 10
11use crate::{ 11use crate::{
12 helpers::merge_imports::{try_merge_imports, use_tree_path_cmp, MergeBehavior}, 12 helpers::merge_imports::{
13 common_prefix, eq_attrs, eq_visibility, try_merge_imports, use_tree_path_cmp, MergeBehavior,
14 },
13 RootDatabase, 15 RootDatabase,
14}; 16};
15 17
16pub use hir::PrefixKind; 18pub use hir::PrefixKind;
17 19
20/// How imports should be grouped into use statements.
21#[derive(Copy, Clone, Debug, PartialEq, Eq)]
22pub enum ImportGranularity {
23 /// Do not change the granularity of any imports and preserve the original structure written by the developer.
24 Preserve,
25 /// Merge imports from the same crate into a single use statement.
26 Crate,
27 /// Merge imports from the same module into a single use statement.
28 Module,
29 /// Flatten imports so that each has its own use statement.
30 Item,
31}
32
18#[derive(Clone, Copy, Debug, PartialEq, Eq)] 33#[derive(Clone, Copy, Debug, PartialEq, Eq)]
19pub struct InsertUseConfig { 34pub struct InsertUseConfig {
20 pub merge: Option<MergeBehavior>, 35 pub granularity: ImportGranularity,
36 pub enforce_granularity: bool,
21 pub prefix_kind: PrefixKind, 37 pub prefix_kind: PrefixKind,
22 pub group: bool, 38 pub group: bool,
23} 39}
@@ -65,15 +81,96 @@ impl ImportScope {
65 ImportScope::Module(item_list) => ImportScope::Module(item_list.clone_for_update()), 81 ImportScope::Module(item_list) => ImportScope::Module(item_list.clone_for_update()),
66 } 82 }
67 } 83 }
84
85 fn guess_granularity_from_scope(&self) -> ImportGranularityGuess {
86 // The idea is simple, just check each import as well as the import and its precedent together for
87 // whether they fulfill a granularity criteria.
88 let use_stmt = |item| match item {
89 ast::Item::Use(use_) => {
90 let use_tree = use_.use_tree()?;
91 Some((use_tree, use_.visibility(), use_.attrs()))
92 }
93 _ => None,
94 };
95 let mut use_stmts = match self {
96 ImportScope::File(f) => f.items(),
97 ImportScope::Module(m) => m.items(),
98 }
99 .filter_map(use_stmt);
100 let mut res = ImportGranularityGuess::Unknown;
101 let (mut prev, mut prev_vis, mut prev_attrs) = match use_stmts.next() {
102 Some(it) => it,
103 None => return res,
104 };
105 loop {
106 if let Some(use_tree_list) = prev.use_tree_list() {
107 if use_tree_list.use_trees().any(|tree| tree.use_tree_list().is_some()) {
108 // Nested tree lists can only occur in crate style, or with no proper style being enforced in the file.
109 break ImportGranularityGuess::Crate;
110 } else {
111 // Could still be crate-style so continue looking.
112 res = ImportGranularityGuess::CrateOrModule;
113 }
114 }
115
116 let (curr, curr_vis, curr_attrs) = match use_stmts.next() {
117 Some(it) => it,
118 None => break res,
119 };
120 if eq_visibility(prev_vis, curr_vis.clone()) && eq_attrs(prev_attrs, curr_attrs.clone())
121 {
122 if let Some((prev_path, curr_path)) = prev.path().zip(curr.path()) {
123 if let Some(_) = common_prefix(&prev_path, &curr_path) {
124 if prev.use_tree_list().is_none() && curr.use_tree_list().is_none() {
125 // Same prefix but no use tree lists so this has to be of item style.
126 break ImportGranularityGuess::Item; // this overwrites CrateOrModule, technically the file doesn't adhere to anything here.
127 } else {
128 // Same prefix with item tree lists, has to be module style as it
129 // can't be crate style since the trees wouldn't share a prefix then.
130 break ImportGranularityGuess::Module;
131 }
132 }
133 }
134 }
135 prev = curr;
136 prev_vis = curr_vis;
137 prev_attrs = curr_attrs;
138 }
139 }
140}
141
142#[derive(PartialEq, PartialOrd, Debug, Clone, Copy)]
143enum ImportGranularityGuess {
144 Unknown,
145 Item,
146 Module,
147 Crate,
148 CrateOrModule,
68} 149}
69 150
70/// Insert an import path into the given file/node. A `merge` value of none indicates that no import merging is allowed to occur. 151/// Insert an import path into the given file/node. A `merge` value of none indicates that no import merging is allowed to occur.
71pub fn insert_use<'a>(scope: &ImportScope, path: ast::Path, cfg: InsertUseConfig) { 152pub fn insert_use<'a>(scope: &ImportScope, path: ast::Path, cfg: InsertUseConfig) {
72 let _p = profile::span("insert_use"); 153 let _p = profile::span("insert_use");
154 let mut mb = match cfg.granularity {
155 ImportGranularity::Crate => Some(MergeBehavior::Crate),
156 ImportGranularity::Module => Some(MergeBehavior::Module),
157 ImportGranularity::Item | ImportGranularity::Preserve => None,
158 };
159 if !cfg.enforce_granularity {
160 let file_granularity = scope.guess_granularity_from_scope();
161 mb = match file_granularity {
162 ImportGranularityGuess::Unknown => mb,
163 ImportGranularityGuess::Item => None,
164 ImportGranularityGuess::Module => Some(MergeBehavior::Module),
165 ImportGranularityGuess::Crate => Some(MergeBehavior::Crate),
166 ImportGranularityGuess::CrateOrModule => mb.or(Some(MergeBehavior::Crate)),
167 };
168 }
169
73 let use_item = 170 let use_item =
74 make::use_(None, make::use_tree(path.clone(), None, None, false)).clone_for_update(); 171 make::use_(None, make::use_tree(path.clone(), None, None, false)).clone_for_update();
75 // merge into existing imports if possible 172 // merge into existing imports if possible
76 if let Some(mb) = cfg.merge { 173 if let Some(mb) = mb {
77 for existing_use in scope.as_syntax_node().children().filter_map(ast::Use::cast) { 174 for existing_use in scope.as_syntax_node().children().filter_map(ast::Use::cast) {
78 if let Some(merged) = try_merge_imports(&existing_use, &use_item, mb) { 175 if let Some(merged) = try_merge_imports(&existing_use, &use_item, mb) {
79 ted::replace(existing_use.syntax(), merged.syntax()); 176 ted::replace(existing_use.syntax(), merged.syntax());
diff --git a/crates/ide_db/src/helpers/insert_use/tests.rs b/crates/ide_db/src/helpers/insert_use/tests.rs
index 248227d29..78a2a87b3 100644
--- a/crates/ide_db/src/helpers/insert_use/tests.rs
+++ b/crates/ide_db/src/helpers/insert_use/tests.rs
@@ -21,7 +21,7 @@ use crate::bar::A;
21use self::bar::A; 21use self::bar::A;
22use super::bar::A; 22use super::bar::A;
23use external_crate2::bar::A;", 23use external_crate2::bar::A;",
24 None, 24 ImportGranularity::Item,
25 false, 25 false,
26 false, 26 false,
27 ); 27 );
@@ -36,7 +36,7 @@ fn insert_not_group_empty() {
36 r"use external_crate2::bar::A; 36 r"use external_crate2::bar::A;
37 37
38", 38",
39 None, 39 ImportGranularity::Item,
40 false, 40 false,
41 false, 41 false,
42 ); 42 );
@@ -281,7 +281,7 @@ fn insert_empty_module() {
281 r"{ 281 r"{
282 use foo::bar; 282 use foo::bar;
283}", 283}",
284 None, 284 ImportGranularity::Item,
285 true, 285 true,
286 true, 286 true,
287 ) 287 )
@@ -631,11 +631,121 @@ fn merge_last_fail3() {
631 ); 631 );
632} 632}
633 633
634#[test]
635fn guess_empty() {
636 check_guess("", ImportGranularityGuess::Unknown);
637}
638
639#[test]
640fn guess_single() {
641 check_guess(r"use foo::{baz::{qux, quux}, bar};", ImportGranularityGuess::Crate);
642 check_guess(r"use foo::bar;", ImportGranularityGuess::Unknown);
643 check_guess(r"use foo::bar::{baz, qux};", ImportGranularityGuess::CrateOrModule);
644}
645
646#[test]
647fn guess_unknown() {
648 check_guess(
649 r"
650use foo::bar::baz;
651use oof::rab::xuq;
652",
653 ImportGranularityGuess::Unknown,
654 );
655}
656
657#[test]
658fn guess_item() {
659 check_guess(
660 r"
661use foo::bar::baz;
662use foo::bar::qux;
663",
664 ImportGranularityGuess::Item,
665 );
666}
667
668#[test]
669fn guess_module() {
670 check_guess(
671 r"
672use foo::bar::baz;
673use foo::bar::{qux, quux};
674",
675 ImportGranularityGuess::Module,
676 );
677 // this is a rather odd case, technically this file isn't following any style properly.
678 check_guess(
679 r"
680use foo::bar::baz;
681use foo::{baz::{qux, quux}, bar};
682",
683 ImportGranularityGuess::Module,
684 );
685}
686
687#[test]
688fn guess_crate_or_module() {
689 check_guess(
690 r"
691use foo::bar::baz;
692use oof::bar::{qux, quux};
693",
694 ImportGranularityGuess::CrateOrModule,
695 );
696}
697
698#[test]
699fn guess_crate() {
700 check_guess(
701 r"
702use frob::bar::baz;
703use foo::{baz::{qux, quux}, bar};
704",
705 ImportGranularityGuess::Crate,
706 );
707}
708
709#[test]
710fn guess_skips_differing_vis() {
711 check_guess(
712 r"
713use foo::bar::baz;
714pub use foo::bar::qux;
715",
716 ImportGranularityGuess::Unknown,
717 );
718}
719
720#[test]
721fn guess_skips_differing_attrs() {
722 check_guess(
723 r"
724pub use foo::bar::baz;
725#[doc(hidden)]
726pub use foo::bar::qux;
727",
728 ImportGranularityGuess::Unknown,
729 );
730}
731
732#[test]
733fn guess_grouping_matters() {
734 check_guess(
735 r"
736use foo::bar::baz;
737use oof::bar::baz;
738use foo::bar::qux;
739",
740 ImportGranularityGuess::Unknown,
741 );
742}
743
634fn check( 744fn check(
635 path: &str, 745 path: &str,
636 ra_fixture_before: &str, 746 ra_fixture_before: &str,
637 ra_fixture_after: &str, 747 ra_fixture_after: &str,
638 mb: Option<MergeBehavior>, 748 granularity: ImportGranularity,
639 module: bool, 749 module: bool,
640 group: bool, 750 group: bool,
641) { 751) {
@@ -651,21 +761,30 @@ fn check(
651 .find_map(ast::Path::cast) 761 .find_map(ast::Path::cast)
652 .unwrap(); 762 .unwrap();
653 763
654 insert_use(&file, path, InsertUseConfig { merge: mb, prefix_kind: PrefixKind::Plain, group }); 764 insert_use(
765 &file,
766 path,
767 InsertUseConfig {
768 granularity,
769 enforce_granularity: true,
770 prefix_kind: PrefixKind::Plain,
771 group,
772 },
773 );
655 let result = file.as_syntax_node().to_string(); 774 let result = file.as_syntax_node().to_string();
656 assert_eq_text!(ra_fixture_after, &result); 775 assert_eq_text!(ra_fixture_after, &result);
657} 776}
658 777
659fn check_crate(path: &str, ra_fixture_before: &str, ra_fixture_after: &str) { 778fn check_crate(path: &str, ra_fixture_before: &str, ra_fixture_after: &str) {
660 check(path, ra_fixture_before, ra_fixture_after, Some(MergeBehavior::Crate), false, true) 779 check(path, ra_fixture_before, ra_fixture_after, ImportGranularity::Crate, false, true)
661} 780}
662 781
663fn check_module(path: &str, ra_fixture_before: &str, ra_fixture_after: &str) { 782fn check_module(path: &str, ra_fixture_before: &str, ra_fixture_after: &str) {
664 check(path, ra_fixture_before, ra_fixture_after, Some(MergeBehavior::Module), false, true) 783 check(path, ra_fixture_before, ra_fixture_after, ImportGranularity::Module, false, true)
665} 784}
666 785
667fn check_none(path: &str, ra_fixture_before: &str, ra_fixture_after: &str) { 786fn check_none(path: &str, ra_fixture_before: &str, ra_fixture_after: &str) {
668 check(path, ra_fixture_before, ra_fixture_after, None, false, true) 787 check(path, ra_fixture_before, ra_fixture_after, ImportGranularity::Item, false, true)
669} 788}
670 789
671fn check_merge_only_fail(ra_fixture0: &str, ra_fixture1: &str, mb: MergeBehavior) { 790fn check_merge_only_fail(ra_fixture0: &str, ra_fixture1: &str, mb: MergeBehavior) {
@@ -686,3 +805,9 @@ fn check_merge_only_fail(ra_fixture0: &str, ra_fixture1: &str, mb: MergeBehavior
686 let result = try_merge_imports(&use0, &use1, mb); 805 let result = try_merge_imports(&use0, &use1, mb);
687 assert_eq!(result.map(|u| u.to_string()), None); 806 assert_eq!(result.map(|u| u.to_string()), None);
688} 807}
808
809fn check_guess(ra_fixture: &str, expected: ImportGranularityGuess) {
810 let syntax = ast::SourceFile::parse(ra_fixture).tree().syntax().clone();
811 let file = super::ImportScope::from(syntax).unwrap();
812 assert_eq!(file.guess_granularity_from_scope(), expected);
813}
diff --git a/crates/ide_db/src/helpers/merge_imports.rs b/crates/ide_db/src/helpers/merge_imports.rs
index 8fb40e837..697e8bcff 100644
--- a/crates/ide_db/src/helpers/merge_imports.rs
+++ b/crates/ide_db/src/helpers/merge_imports.rs
@@ -181,7 +181,7 @@ fn recursive_merge(
181} 181}
182 182
183/// Traverses both paths until they differ, returning the common prefix of both. 183/// Traverses both paths until they differ, returning the common prefix of both.
184fn common_prefix(lhs: &ast::Path, rhs: &ast::Path) -> Option<(ast::Path, ast::Path)> { 184pub fn common_prefix(lhs: &ast::Path, rhs: &ast::Path) -> Option<(ast::Path, ast::Path)> {
185 let mut res = None; 185 let mut res = None;
186 let mut lhs_curr = lhs.first_qualifier_or_self(); 186 let mut lhs_curr = lhs.first_qualifier_or_self();
187 let mut rhs_curr = rhs.first_qualifier_or_self(); 187 let mut rhs_curr = rhs.first_qualifier_or_self();
@@ -289,7 +289,7 @@ fn path_segment_cmp(a: &ast::PathSegment, b: &ast::PathSegment) -> Ordering {
289 a.as_ref().map(ast::NameRef::text).cmp(&b.as_ref().map(ast::NameRef::text)) 289 a.as_ref().map(ast::NameRef::text).cmp(&b.as_ref().map(ast::NameRef::text))
290} 290}
291 291
292fn eq_visibility(vis0: Option<ast::Visibility>, vis1: Option<ast::Visibility>) -> bool { 292pub fn eq_visibility(vis0: Option<ast::Visibility>, vis1: Option<ast::Visibility>) -> bool {
293 match (vis0, vis1) { 293 match (vis0, vis1) {
294 (None, None) => true, 294 (None, None) => true,
295 // FIXME: Don't use the string representation to check for equality 295 // FIXME: Don't use the string representation to check for equality
@@ -299,7 +299,7 @@ fn eq_visibility(vis0: Option<ast::Visibility>, vis1: Option<ast::Visibility>) -
299 } 299 }
300} 300}
301 301
302fn eq_attrs( 302pub fn eq_attrs(
303 attrs0: impl Iterator<Item = ast::Attr>, 303 attrs0: impl Iterator<Item = ast::Attr>,
304 attrs1: impl Iterator<Item = ast::Attr>, 304 attrs1: impl Iterator<Item = ast::Attr>,
305) -> bool { 305) -> bool {