aboutsummaryrefslogtreecommitdiff
path: root/crates/assists/src/handlers/merge_imports.rs
diff options
context:
space:
mode:
Diffstat (limited to 'crates/assists/src/handlers/merge_imports.rs')
-rw-r--r--crates/assists/src/handlers/merge_imports.rs321
1 files changed, 321 insertions, 0 deletions
diff --git a/crates/assists/src/handlers/merge_imports.rs b/crates/assists/src/handlers/merge_imports.rs
new file mode 100644
index 000000000..47d465404
--- /dev/null
+++ b/crates/assists/src/handlers/merge_imports.rs
@@ -0,0 +1,321 @@
1use std::iter::successors;
2
3use syntax::{
4 algo::{neighbor, skip_trivia_token, SyntaxRewriter},
5 ast::{self, edit::AstNodeEdit, make},
6 AstNode, Direction, InsertPosition, SyntaxElement, T,
7};
8
9use crate::{
10 assist_context::{AssistContext, Assists},
11 AssistId, AssistKind,
12};
13
14// Assist: merge_imports
15//
16// Merges two imports with a common prefix.
17//
18// ```
19// use std::<|>fmt::Formatter;
20// use std::io;
21// ```
22// ->
23// ```
24// use std::{fmt::Formatter, io};
25// ```
26pub(crate) fn merge_imports(acc: &mut Assists, ctx: &AssistContext) -> Option<()> {
27 let tree: ast::UseTree = ctx.find_node_at_offset()?;
28 let mut rewriter = SyntaxRewriter::default();
29 let mut offset = ctx.offset();
30
31 if let Some(use_item) = tree.syntax().parent().and_then(ast::Use::cast) {
32 let (merged, to_delete) = next_prev()
33 .filter_map(|dir| neighbor(&use_item, dir))
34 .filter_map(|it| Some((it.clone(), it.use_tree()?)))
35 .find_map(|(use_item, use_tree)| {
36 Some((try_merge_trees(&tree, &use_tree)?, use_item))
37 })?;
38
39 rewriter.replace_ast(&tree, &merged);
40 rewriter += to_delete.remove();
41
42 if to_delete.syntax().text_range().end() < offset {
43 offset -= to_delete.syntax().text_range().len();
44 }
45 } else {
46 let (merged, to_delete) = next_prev()
47 .filter_map(|dir| neighbor(&tree, dir))
48 .find_map(|use_tree| Some((try_merge_trees(&tree, &use_tree)?, use_tree.clone())))?;
49
50 rewriter.replace_ast(&tree, &merged);
51 rewriter += to_delete.remove();
52
53 if to_delete.syntax().text_range().end() < offset {
54 offset -= to_delete.syntax().text_range().len();
55 }
56 };
57
58 let target = tree.syntax().text_range();
59 acc.add(
60 AssistId("merge_imports", AssistKind::RefactorRewrite),
61 "Merge imports",
62 target,
63 |builder| {
64 builder.rewrite(rewriter);
65 },
66 )
67}
68
69fn next_prev() -> impl Iterator<Item = Direction> {
70 [Direction::Next, Direction::Prev].iter().copied()
71}
72
73fn try_merge_trees(old: &ast::UseTree, new: &ast::UseTree) -> Option<ast::UseTree> {
74 let lhs_path = old.path()?;
75 let rhs_path = new.path()?;
76
77 let (lhs_prefix, rhs_prefix) = common_prefix(&lhs_path, &rhs_path)?;
78
79 let lhs = old.split_prefix(&lhs_prefix);
80 let rhs = new.split_prefix(&rhs_prefix);
81
82 let should_insert_comma = lhs
83 .use_tree_list()?
84 .r_curly_token()
85 .and_then(|it| skip_trivia_token(it.prev_token()?, Direction::Prev))
86 .map(|it| it.kind() != T![,])
87 .unwrap_or(true);
88
89 let mut to_insert: Vec<SyntaxElement> = Vec::new();
90 if should_insert_comma {
91 to_insert.push(make::token(T![,]).into());
92 to_insert.push(make::tokens::single_space().into());
93 }
94 to_insert.extend(
95 rhs.use_tree_list()?
96 .syntax()
97 .children_with_tokens()
98 .filter(|it| it.kind() != T!['{'] && it.kind() != T!['}']),
99 );
100 let use_tree_list = lhs.use_tree_list()?;
101 let pos = InsertPosition::Before(use_tree_list.r_curly_token()?.into());
102 let use_tree_list = use_tree_list.insert_children(pos, to_insert);
103 Some(lhs.with_use_tree_list(use_tree_list))
104}
105
106fn common_prefix(lhs: &ast::Path, rhs: &ast::Path) -> Option<(ast::Path, ast::Path)> {
107 let mut res = None;
108 let mut lhs_curr = first_path(&lhs);
109 let mut rhs_curr = first_path(&rhs);
110 loop {
111 match (lhs_curr.segment(), rhs_curr.segment()) {
112 (Some(lhs), Some(rhs)) if lhs.syntax().text() == rhs.syntax().text() => (),
113 _ => break,
114 }
115 res = Some((lhs_curr.clone(), rhs_curr.clone()));
116
117 match (lhs_curr.parent_path(), rhs_curr.parent_path()) {
118 (Some(lhs), Some(rhs)) => {
119 lhs_curr = lhs;
120 rhs_curr = rhs;
121 }
122 _ => break,
123 }
124 }
125
126 res
127}
128
129fn first_path(path: &ast::Path) -> ast::Path {
130 successors(Some(path.clone()), |it| it.qualifier()).last().unwrap()
131}
132
133#[cfg(test)]
134mod tests {
135 use crate::tests::{check_assist, check_assist_not_applicable};
136
137 use super::*;
138
139 #[test]
140 fn test_merge_first() {
141 check_assist(
142 merge_imports,
143 r"
144use std::fmt<|>::Debug;
145use std::fmt::Display;
146",
147 r"
148use std::fmt::{Debug, Display};
149",
150 )
151 }
152
153 #[test]
154 fn test_merge_second() {
155 check_assist(
156 merge_imports,
157 r"
158use std::fmt::Debug;
159use std::fmt<|>::Display;
160",
161 r"
162use std::fmt::{Display, Debug};
163",
164 );
165 }
166
167 #[test]
168 fn merge_self1() {
169 check_assist(
170 merge_imports,
171 r"
172use std::fmt<|>;
173use std::fmt::Display;
174",
175 r"
176use std::fmt::{self, Display};
177",
178 );
179 }
180
181 #[test]
182 fn merge_self2() {
183 check_assist(
184 merge_imports,
185 r"
186use std::{fmt, <|>fmt::Display};
187",
188 r"
189use std::{fmt::{Display, self}};
190",
191 );
192 }
193
194 #[test]
195 fn test_merge_nested() {
196 check_assist(
197 merge_imports,
198 r"
199use std::{fmt<|>::Debug, fmt::Display};
200",
201 r"
202use std::{fmt::{Debug, Display}};
203",
204 );
205 check_assist(
206 merge_imports,
207 r"
208use std::{fmt::Debug, fmt<|>::Display};
209",
210 r"
211use std::{fmt::{Display, Debug}};
212",
213 );
214 }
215
216 #[test]
217 fn test_merge_single_wildcard_diff_prefixes() {
218 check_assist(
219 merge_imports,
220 r"
221use std<|>::cell::*;
222use std::str;
223",
224 r"
225use std::{cell::*, str};
226",
227 )
228 }
229
230 #[test]
231 fn test_merge_both_wildcard_diff_prefixes() {
232 check_assist(
233 merge_imports,
234 r"
235use std<|>::cell::*;
236use std::str::*;
237",
238 r"
239use std::{cell::*, str::*};
240",
241 )
242 }
243
244 #[test]
245 fn removes_just_enough_whitespace() {
246 check_assist(
247 merge_imports,
248 r"
249use foo<|>::bar;
250use foo::baz;
251
252/// Doc comment
253",
254 r"
255use foo::{bar, baz};
256
257/// Doc comment
258",
259 );
260 }
261
262 #[test]
263 fn works_with_trailing_comma() {
264 check_assist(
265 merge_imports,
266 r"
267use {
268 foo<|>::bar,
269 foo::baz,
270};
271",
272 r"
273use {
274 foo::{bar, baz},
275};
276",
277 );
278 check_assist(
279 merge_imports,
280 r"
281use {
282 foo::baz,
283 foo<|>::bar,
284};
285",
286 r"
287use {
288 foo::{bar, baz},
289};
290",
291 );
292 }
293
294 #[test]
295 fn test_double_comma() {
296 check_assist(
297 merge_imports,
298 r"
299use foo::bar::baz;
300use foo::<|>{
301 FooBar,
302};
303",
304 r"
305use foo::{
306 FooBar,
307bar::baz};
308",
309 )
310 }
311
312 #[test]
313 fn test_empty_use() {
314 check_assist_not_applicable(
315 merge_imports,
316 r"
317use std::<|>
318fn main() {}",
319 );
320 }
321}