Skip to content

Commit ea15e10

Browse files
committedJun 3, 2022
Auto merge of rust-lang#12452 - iDawer:assist.merge_selected_imports, r=Veykril
feature: `Merge imports` assist can merge multiple selected imports. The selected imports have to have a common prefix in paths. Select imports or use trees to merge: ```rust $0use std::fmt::Display; use std::fmt::Debug; use std::fmt::Write;$0 ``` Apply `Merge imports`: ```rust use std::fmt::{Display, Debug, Write}; ``` Closes rust-lang#12426
2 parents 29fae10 + ea8899a commit ea15e10

File tree

2 files changed

+150
-33
lines changed

2 files changed

+150
-33
lines changed
 

‎crates/ide-assists/src/handlers/merge_imports.rs

+146-33
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
1+
use either::Either;
12
use ide_db::imports::merge_imports::{try_merge_imports, try_merge_trees, MergeBehavior};
2-
use syntax::{algo::neighbor, ast, ted, AstNode};
3+
use syntax::{algo::neighbor, ast, match_ast, ted, AstNode, SyntaxElement, SyntaxNode};
34

45
use crate::{
56
assist_context::{AssistContext, Assists},
67
utils::next_prev,
78
AssistId, AssistKind,
89
};
910

11+
use Edit::*;
12+
1013
// Assist: merge_imports
1114
//
1215
// Merges two imports with a common prefix.
@@ -20,51 +23,115 @@ use crate::{
2023
// use std::{fmt::Formatter, io};
2124
// ```
2225
pub(crate) fn merge_imports(acc: &mut Assists, ctx: &AssistContext) -> Option<()> {
23-
let tree: ast::UseTree = ctx.find_node_at_offset()?;
24-
25-
let mut imports = None;
26-
let mut uses = None;
27-
if let Some(use_item) = tree.syntax().parent().and_then(ast::Use::cast) {
28-
let (merged, to_remove) =
29-
next_prev().filter_map(|dir| neighbor(&use_item, dir)).find_map(|use_item2| {
30-
try_merge_imports(&use_item, &use_item2, MergeBehavior::Crate).zip(Some(use_item2))
31-
})?;
32-
33-
imports = Some((use_item, merged, to_remove));
26+
let (target, edits) = if ctx.has_empty_selection() {
27+
// Merge a neighbor
28+
let tree: ast::UseTree = ctx.find_node_at_offset()?;
29+
let target = tree.syntax().text_range();
30+
31+
let edits = if let Some(use_item) = tree.syntax().parent().and_then(ast::Use::cast) {
32+
let mut neighbor = next_prev().find_map(|dir| neighbor(&use_item, dir)).into_iter();
33+
use_item.try_merge_from(&mut neighbor)
34+
} else {
35+
let mut neighbor = next_prev().find_map(|dir| neighbor(&tree, dir)).into_iter();
36+
tree.try_merge_from(&mut neighbor)
37+
};
38+
(target, edits?)
3439
} else {
35-
let (merged, to_remove) =
36-
next_prev().filter_map(|dir| neighbor(&tree, dir)).find_map(|use_tree| {
37-
try_merge_trees(&tree, &use_tree, MergeBehavior::Crate).zip(Some(use_tree))
38-
})?;
39-
40-
uses = Some((tree.clone(), merged, to_remove))
40+
// Merge selected
41+
let selection_range = ctx.selection_trimmed();
42+
let parent_node = match ctx.covering_element() {
43+
SyntaxElement::Node(n) => n,
44+
SyntaxElement::Token(t) => t.parent()?,
45+
};
46+
let mut selected_nodes =
47+
parent_node.children().filter(|it| selection_range.contains_range(it.text_range()));
48+
49+
let first_selected = selected_nodes.next()?;
50+
let edits = match_ast! {
51+
match first_selected {
52+
ast::Use(use_item) => {
53+
use_item.try_merge_from(&mut selected_nodes.filter_map(ast::Use::cast))
54+
},
55+
ast::UseTree(use_tree) => {
56+
use_tree.try_merge_from(&mut selected_nodes.filter_map(ast::UseTree::cast))
57+
},
58+
_ => return None,
59+
}
60+
};
61+
(selection_range, edits?)
4162
};
4263

43-
let target = tree.syntax().text_range();
4464
acc.add(
4565
AssistId("merge_imports", AssistKind::RefactorRewrite),
4666
"Merge imports",
4767
target,
4868
|builder| {
49-
if let Some((to_replace, replacement, to_remove)) = imports {
50-
let to_replace = builder.make_mut(to_replace);
51-
let to_remove = builder.make_mut(to_remove);
52-
53-
ted::replace(to_replace.syntax(), replacement.syntax());
54-
to_remove.remove();
55-
}
56-
57-
if let Some((to_replace, replacement, to_remove)) = uses {
58-
let to_replace = builder.make_mut(to_replace);
59-
let to_remove = builder.make_mut(to_remove);
60-
61-
ted::replace(to_replace.syntax(), replacement.syntax());
62-
to_remove.remove()
69+
let edits_mut: Vec<Edit> = edits
70+
.into_iter()
71+
.map(|it| match it {
72+
Remove(Either::Left(it)) => Remove(Either::Left(builder.make_mut(it))),
73+
Remove(Either::Right(it)) => Remove(Either::Right(builder.make_mut(it))),
74+
Replace(old, new) => Replace(builder.make_syntax_mut(old), new),
75+
})
76+
.collect();
77+
for edit in edits_mut {
78+
match edit {
79+
Remove(it) => it.as_ref().either(ast::Use::remove, ast::UseTree::remove),
80+
Replace(old, new) => ted::replace(old, new),
81+
}
6382
}
6483
},
6584
)
6685
}
6786

87+
trait Merge: AstNode + Clone {
88+
fn try_merge_from(self, items: &mut dyn Iterator<Item = Self>) -> Option<Vec<Edit>> {
89+
let mut edits = Vec::new();
90+
let mut merged = self.clone();
91+
while let Some(item) = items.next() {
92+
merged = merged.try_merge(&item)?;
93+
edits.push(Edit::Remove(item.into_either()));
94+
}
95+
if !edits.is_empty() {
96+
edits.push(Edit::replace(self, merged));
97+
Some(edits)
98+
} else {
99+
None
100+
}
101+
}
102+
fn try_merge(&self, other: &Self) -> Option<Self>;
103+
fn into_either(self) -> Either<ast::Use, ast::UseTree>;
104+
}
105+
106+
impl Merge for ast::Use {
107+
fn try_merge(&self, other: &Self) -> Option<Self> {
108+
try_merge_imports(self, other, MergeBehavior::Crate)
109+
}
110+
fn into_either(self) -> Either<ast::Use, ast::UseTree> {
111+
Either::Left(self)
112+
}
113+
}
114+
115+
impl Merge for ast::UseTree {
116+
fn try_merge(&self, other: &Self) -> Option<Self> {
117+
try_merge_trees(self, other, MergeBehavior::Crate)
118+
}
119+
fn into_either(self) -> Either<ast::Use, ast::UseTree> {
120+
Either::Right(self)
121+
}
122+
}
123+
124+
enum Edit {
125+
Remove(Either<ast::Use, ast::UseTree>),
126+
Replace(SyntaxNode, SyntaxNode),
127+
}
128+
129+
impl Edit {
130+
fn replace(old: impl AstNode, new: impl AstNode) -> Self {
131+
Edit::Replace(old.syntax().clone(), new.syntax().clone())
132+
}
133+
}
134+
68135
#[cfg(test)]
69136
mod tests {
70137
use crate::tests::{check_assist, check_assist_not_applicable};
@@ -454,4 +521,50 @@ use foo::{*, bar::Baz};
454521
",
455522
);
456523
}
524+
525+
#[test]
526+
fn merge_selection_uses() {
527+
check_assist(
528+
merge_imports,
529+
r"
530+
use std::fmt::Error;
531+
$0use std::fmt::Display;
532+
use std::fmt::Debug;
533+
use std::fmt::Write;
534+
$0use std::fmt::Result;
535+
",
536+
r"
537+
use std::fmt::Error;
538+
use std::fmt::{Display, Debug, Write};
539+
use std::fmt::Result;
540+
",
541+
);
542+
}
543+
544+
#[test]
545+
fn merge_selection_use_trees() {
546+
check_assist(
547+
merge_imports,
548+
r"
549+
use std::{
550+
fmt::Error,
551+
$0fmt::Display,
552+
fmt::Debug,
553+
fmt::Write,$0
554+
fmt::Result,
555+
};",
556+
r"
557+
use std::{
558+
fmt::Error,
559+
fmt::{Display, Debug, Write},
560+
fmt::Result,
561+
};",
562+
);
563+
// FIXME: Remove redundant braces. See also unnecessary-braces diagnostic.
564+
check_assist(
565+
merge_imports,
566+
r"use std::$0{fmt::Display, fmt::Debug}$0;",
567+
r"use std::{fmt::{Display, Debug}};",
568+
);
569+
}
457570
}

‎crates/ide-db/src/imports/merge_imports.rs

+4
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ impl MergeBehavior {
3030
}
3131
}
3232

33+
/// Merge `rhs` into `lhs` keeping both intact.
34+
/// Returned AST is mutable.
3335
pub fn try_merge_imports(
3436
lhs: &ast::Use,
3537
rhs: &ast::Use,
@@ -51,6 +53,8 @@ pub fn try_merge_imports(
5153
Some(lhs)
5254
}
5355

56+
/// Merge `rhs` into `lhs` keeping both intact.
57+
/// Returned AST is mutable.
5458
pub fn try_merge_trees(
5559
lhs: &ast::UseTree,
5660
rhs: &ast::UseTree,

0 commit comments

Comments
 (0)
Please sign in to comment.