Skip to content

Commit 57d4b5b

Browse files
committed
add "one" import granularity
1 parent 9d9b343 commit 57d4b5b

File tree

4 files changed

+243
-60
lines changed

4 files changed

+243
-60
lines changed

crates/ide-db/src/imports/insert_use.rs

Lines changed: 68 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ use syntax::{
99
algo,
1010
ast::{
1111
self, edit_in_place::Removable, make, AstNode, HasAttrs, HasModuleItem, HasVisibility,
12-
PathSegmentKind, UseTree,
12+
PathSegmentKind,
1313
},
1414
ted, Direction, NodeOrToken, SyntaxKind, SyntaxNode,
1515
};
@@ -26,14 +26,18 @@ pub use hir::PrefixKind;
2626
/// How imports should be grouped into use statements.
2727
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
2828
pub enum ImportGranularity {
29-
/// Do not change the granularity of any imports and preserve the original structure written by the developer.
29+
/// Do not change the granularity of any imports and preserve the original structure written
30+
/// by the developer.
3031
Preserve,
3132
/// Merge imports from the same crate into a single use statement.
3233
Crate,
3334
/// Merge imports from the same module into a single use statement.
3435
Module,
3536
/// Flatten imports so that each has its own use statement.
3637
Item,
38+
/// Merge all imports into a single use statement as long as they have the same visibility
39+
/// and attributes.
40+
One,
3741
}
3842

3943
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
@@ -167,7 +171,7 @@ pub fn insert_use_as_alias(scope: &ImportScope, path: ast::Path, cfg: &InsertUse
167171
.tree()
168172
.syntax()
169173
.descendants()
170-
.find_map(UseTree::cast)
174+
.find_map(ast::UseTree::cast)
171175
.expect("Failed to make ast node `Rename`");
172176
let alias = node.rename();
173177

@@ -184,6 +188,7 @@ fn insert_use_with_alias_option(
184188
let mut mb = match cfg.granularity {
185189
ImportGranularity::Crate => Some(MergeBehavior::Crate),
186190
ImportGranularity::Module => Some(MergeBehavior::Module),
191+
ImportGranularity::One => Some(MergeBehavior::One),
187192
ImportGranularity::Item | ImportGranularity::Preserve => None,
188193
};
189194
if !cfg.enforce_granularity {
@@ -195,11 +200,16 @@ fn insert_use_with_alias_option(
195200
ImportGranularityGuess::ModuleOrItem => mb.and(Some(MergeBehavior::Module)),
196201
ImportGranularityGuess::Crate => Some(MergeBehavior::Crate),
197202
ImportGranularityGuess::CrateOrModule => mb.or(Some(MergeBehavior::Crate)),
203+
ImportGranularityGuess::One => Some(MergeBehavior::One),
198204
};
199205
}
200206

201-
let use_item =
202-
make::use_(None, make::use_tree(path.clone(), None, alias, false)).clone_for_update();
207+
let mut use_tree = make::use_tree(path.clone(), None, alias, false);
208+
if mb == Some(MergeBehavior::One) && use_tree.path().is_some() {
209+
use_tree = use_tree.clone_for_update();
210+
use_tree.wrap_in_tree_list();
211+
}
212+
let use_item = make::use_(None, use_tree).clone_for_update();
203213

204214
// merge into existing imports if possible
205215
if let Some(mb) = mb {
@@ -216,7 +226,7 @@ fn insert_use_with_alias_option(
216226

217227
// either we weren't allowed to merge or there is no import that fits the merge conditions
218228
// so look for the place we have to insert to
219-
insert_use_(scope, &path, cfg.group, use_item);
229+
insert_use_(scope, use_item, cfg.group);
220230
}
221231

222232
pub fn ast_to_remove_for_path_in_use_stmt(path: &ast::Path) -> Option<Box<dyn Removable>> {
@@ -248,15 +258,18 @@ enum ImportGroup {
248258
ThisCrate,
249259
ThisModule,
250260
SuperModule,
261+
One,
251262
}
252263

253264
impl ImportGroup {
254-
fn new(path: &ast::Path) -> ImportGroup {
255-
let default = ImportGroup::ExternCrate;
265+
fn new(use_tree: &ast::UseTree) -> ImportGroup {
266+
if use_tree.path().is_none() && use_tree.use_tree_list().is_some() {
267+
return ImportGroup::One;
268+
}
256269

257-
let first_segment = match path.first_segment() {
258-
Some(it) => it,
259-
None => return default,
270+
let Some(first_segment) = use_tree.path().as_ref().and_then(ast::Path::first_segment)
271+
else {
272+
return ImportGroup::ExternCrate;
260273
};
261274

262275
let kind = first_segment.kind().unwrap_or(PathSegmentKind::SelfKw);
@@ -284,6 +297,7 @@ enum ImportGranularityGuess {
284297
ModuleOrItem,
285298
Crate,
286299
CrateOrModule,
300+
One,
287301
}
288302

289303
fn guess_granularity_from_scope(scope: &ImportScope) -> ImportGranularityGuess {
@@ -303,12 +317,24 @@ fn guess_granularity_from_scope(scope: &ImportScope) -> ImportGranularityGuess {
303317
}
304318
.filter_map(use_stmt);
305319
let mut res = ImportGranularityGuess::Unknown;
306-
let (mut prev, mut prev_vis, mut prev_attrs) = match use_stmts.next() {
307-
Some(it) => it,
308-
None => return res,
309-
};
320+
let Some((mut prev, mut prev_vis, mut prev_attrs)) = use_stmts.next() else { return res };
321+
322+
let is_tree_one_style =
323+
|use_tree: &ast::UseTree| use_tree.path().is_none() && use_tree.use_tree_list().is_some();
324+
let mut seen_one_style_groups = Vec::new();
325+
310326
loop {
311-
if let Some(use_tree_list) = prev.use_tree_list() {
327+
if is_tree_one_style(&prev) {
328+
if res != ImportGranularityGuess::One {
329+
if res != ImportGranularityGuess::Unknown {
330+
// This scope has a mix of one-style and other style imports.
331+
break ImportGranularityGuess::Unknown;
332+
}
333+
334+
res = ImportGranularityGuess::One;
335+
seen_one_style_groups.push((prev_vis.clone(), prev_attrs.clone()));
336+
}
337+
} else if let Some(use_tree_list) = prev.use_tree_list() {
312338
if use_tree_list.use_trees().any(|tree| tree.use_tree_list().is_some()) {
313339
// Nested tree lists can only occur in crate style, or with no proper style being enforced in the file.
314340
break ImportGranularityGuess::Crate;
@@ -318,11 +344,22 @@ fn guess_granularity_from_scope(scope: &ImportScope) -> ImportGranularityGuess {
318344
}
319345
}
320346

321-
let (curr, curr_vis, curr_attrs) = match use_stmts.next() {
322-
Some(it) => it,
323-
None => break res,
324-
};
325-
if eq_visibility(prev_vis, curr_vis.clone()) && eq_attrs(prev_attrs, curr_attrs.clone()) {
347+
let Some((curr, curr_vis, curr_attrs)) = use_stmts.next() else { break res };
348+
if is_tree_one_style(&curr) {
349+
if res != ImportGranularityGuess::One
350+
|| seen_one_style_groups.iter().any(|(prev_vis, prev_attrs)| {
351+
eq_visibility(prev_vis.clone(), curr_vis.clone())
352+
&& eq_attrs(prev_attrs.clone(), curr_attrs.clone())
353+
})
354+
{
355+
// This scope has either a mix of one-style and other style imports or
356+
// multiple one-style imports with the same visibility and attributes.
357+
break ImportGranularityGuess::Unknown;
358+
}
359+
seen_one_style_groups.push((curr_vis.clone(), curr_attrs.clone()));
360+
} else if eq_visibility(prev_vis, curr_vis.clone())
361+
&& eq_attrs(prev_attrs, curr_attrs.clone())
362+
{
326363
if let Some((prev_path, curr_path)) = prev.path().zip(curr.path()) {
327364
if let Some((prev_prefix, _)) = common_prefix(&prev_path, &curr_path) {
328365
if prev.use_tree_list().is_none() && curr.use_tree_list().is_none() {
@@ -350,39 +387,33 @@ fn guess_granularity_from_scope(scope: &ImportScope) -> ImportGranularityGuess {
350387
}
351388
}
352389

353-
fn insert_use_(
354-
scope: &ImportScope,
355-
insert_path: &ast::Path,
356-
group_imports: bool,
357-
use_item: ast::Use,
358-
) {
390+
fn insert_use_(scope: &ImportScope, use_item: ast::Use, group_imports: bool) {
359391
let scope_syntax = scope.as_syntax_node();
360392
let insert_use_tree =
361393
use_item.use_tree().expect("`use_item` should have a use tree for `insert_path`");
362-
let group = ImportGroup::new(insert_path);
394+
let group = ImportGroup::new(&insert_use_tree);
363395
let path_node_iter = scope_syntax
364396
.children()
365397
.filter_map(|node| ast::Use::cast(node.clone()).zip(Some(node)))
366398
.flat_map(|(use_, node)| {
367399
let tree = use_.use_tree()?;
368-
let path = tree.path()?;
369-
Some((path, tree, node))
400+
Some((tree, node))
370401
});
371402

372403
if group_imports {
373-
// Iterator that discards anything thats not in the required grouping
404+
// Iterator that discards anything that's not in the required grouping
374405
// This implementation allows the user to rearrange their import groups as this only takes the first group that fits
375406
let group_iter = path_node_iter
376407
.clone()
377-
.skip_while(|(path, ..)| ImportGroup::new(path) != group)
378-
.take_while(|(path, ..)| ImportGroup::new(path) == group);
408+
.skip_while(|(use_tree, ..)| ImportGroup::new(use_tree) != group)
409+
.take_while(|(use_tree, ..)| ImportGroup::new(use_tree) == group);
379410

380411
// track the last element we iterated over, if this is still None after the iteration then that means we never iterated in the first place
381412
let mut last = None;
382413
// find the element that would come directly after our new import
383-
let post_insert: Option<(_, _, SyntaxNode)> = group_iter
414+
let post_insert: Option<(_, SyntaxNode)> = group_iter
384415
.inspect(|(.., node)| last = Some(node.clone()))
385-
.find(|(_, use_tree, _)| use_tree_cmp(&insert_use_tree, use_tree) != Ordering::Greater);
416+
.find(|(use_tree, _)| use_tree_cmp(&insert_use_tree, use_tree) != Ordering::Greater);
386417

387418
if let Some((.., node)) = post_insert {
388419
cov_mark::hit!(insert_group);
@@ -401,7 +432,7 @@ fn insert_use_(
401432
// find the group that comes after where we want to insert
402433
let post_group = path_node_iter
403434
.inspect(|(.., node)| last = Some(node.clone()))
404-
.find(|(p, ..)| ImportGroup::new(p) > group);
435+
.find(|(use_tree, ..)| ImportGroup::new(use_tree) > group);
405436
if let Some((.., node)) = post_group {
406437
cov_mark::hit!(insert_group_new_group);
407438
ted::insert(ted::Position::before(&node), use_item.syntax());
@@ -419,7 +450,7 @@ fn insert_use_(
419450
}
420451
} else {
421452
// There exists a group, so append to the end of it
422-
if let Some((_, _, node)) = path_node_iter.last() {
453+
if let Some((_, node)) = path_node_iter.last() {
423454
cov_mark::hit!(insert_no_grouping_last);
424455
ted::insert(ted::Position::after(node), use_item.syntax());
425456
return;

0 commit comments

Comments
 (0)