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