diff options
author | Lukas Wirth <[email protected]> | 2020-09-02 14:21:10 +0100 |
---|---|---|
committer | Lukas Wirth <[email protected]> | 2020-09-03 17:36:07 +0100 |
commit | ed37335c012a66f5d028a160221b1ca152a61325 (patch) | |
tree | cef02c8c4cc8ad519ecd8f8040a7100d30329da7 /crates/assists/src/utils | |
parent | 74e7422b69d35c55ff6fde77258047f0292d36e0 (diff) |
Begin refactor of import insertion
Diffstat (limited to 'crates/assists/src/utils')
-rw-r--r-- | crates/assists/src/utils/insert_use.rs | 908 |
1 files changed, 440 insertions, 468 deletions
diff --git a/crates/assists/src/utils/insert_use.rs b/crates/assists/src/utils/insert_use.rs index 49096a67c..8ee5e0c9c 100644 --- a/crates/assists/src/utils/insert_use.rs +++ b/crates/assists/src/utils/insert_use.rs | |||
@@ -1,17 +1,13 @@ | |||
1 | //! Handle syntactic aspects of inserting a new `use`. | 1 | use std::iter::{self, successors}; |
2 | // FIXME: rewrite according to the plan, outlined in | ||
3 | // https://github.com/rust-analyzer/rust-analyzer/issues/3301#issuecomment-592931553 | ||
4 | |||
5 | use std::iter::successors; | ||
6 | 2 | ||
3 | use algo::skip_trivia_token; | ||
4 | use ast::{edit::AstNodeEdit, PathSegmentKind, VisibilityOwner}; | ||
7 | use either::Either; | 5 | use either::Either; |
8 | use syntax::{ | 6 | use syntax::{ |
9 | ast::{self, NameOwner, VisibilityOwner}, | 7 | algo, |
10 | AstNode, AstToken, Direction, SmolStr, | 8 | ast::{self, make, AstNode}, |
11 | SyntaxKind::{PATH, PATH_SEGMENT}, | 9 | Direction, InsertPosition, SyntaxElement, SyntaxNode, T, |
12 | SyntaxNode, SyntaxToken, T, | ||
13 | }; | 10 | }; |
14 | use text_edit::TextEditBuilder; | ||
15 | 11 | ||
16 | use crate::assist_context::AssistContext; | 12 | use crate::assist_context::AssistContext; |
17 | 13 | ||
@@ -22,525 +18,501 @@ pub(crate) fn find_insert_use_container( | |||
22 | ) -> Option<Either<ast::ItemList, ast::SourceFile>> { | 18 | ) -> Option<Either<ast::ItemList, ast::SourceFile>> { |
23 | ctx.sema.ancestors_with_macros(position.clone()).find_map(|n| { | 19 | ctx.sema.ancestors_with_macros(position.clone()).find_map(|n| { |
24 | if let Some(module) = ast::Module::cast(n.clone()) { | 20 | if let Some(module) = ast::Module::cast(n.clone()) { |
25 | return module.item_list().map(|it| Either::Left(it)); | 21 | return module.item_list().map(Either::Left); |
26 | } | 22 | } |
27 | Some(Either::Right(ast::SourceFile::cast(n)?)) | 23 | Some(Either::Right(ast::SourceFile::cast(n)?)) |
28 | }) | 24 | }) |
29 | } | 25 | } |
30 | 26 | ||
31 | /// Creates and inserts a use statement for the given path to import. | ||
32 | /// The use statement is inserted in the scope most appropriate to the | ||
33 | /// the cursor position given, additionally merged with the existing use imports. | ||
34 | pub(crate) fn insert_use_statement( | 27 | pub(crate) fn insert_use_statement( |
35 | // Ideally the position of the cursor, used to | 28 | // Ideally the position of the cursor, used to |
36 | position: &SyntaxNode, | 29 | position: &SyntaxNode, |
37 | path_to_import: &str, | 30 | path_to_import: &str, |
38 | ctx: &AssistContext, | 31 | ctx: &crate::assist_context::AssistContext, |
39 | builder: &mut TextEditBuilder, | 32 | builder: &mut text_edit::TextEditBuilder, |
40 | ) { | 33 | ) { |
41 | let target = path_to_import.split("::").map(SmolStr::new).collect::<Vec<_>>(); | 34 | insert_use(position.clone(), make::path_from_text(path_to_import), Some(MergeBehaviour::Full)); |
42 | let container = find_insert_use_container(position, ctx); | 35 | } |
36 | |||
37 | pub fn insert_use( | ||
38 | where_: SyntaxNode, | ||
39 | path: ast::Path, | ||
40 | merge_behaviour: Option<MergeBehaviour>, | ||
41 | ) -> SyntaxNode { | ||
42 | let use_item = make::use_(make::use_tree(path.clone(), None, None, false)); | ||
43 | // merge into existing imports if possible | ||
44 | if let Some(mb) = merge_behaviour { | ||
45 | for existing_use in where_.children().filter_map(ast::Use::cast) { | ||
46 | if let Some(merged) = try_merge_imports(&existing_use, &use_item, mb) { | ||
47 | let to_delete: SyntaxElement = existing_use.syntax().clone().into(); | ||
48 | let to_delete = to_delete.clone()..=to_delete; | ||
49 | let to_insert = iter::once(merged.syntax().clone().into()); | ||
50 | return algo::replace_children(&where_, to_delete, to_insert); | ||
51 | } | ||
52 | } | ||
53 | } | ||
54 | |||
55 | // either we weren't allowed to merge or there is no import that fits the merge conditions | ||
56 | // so look for the place we have to insert to | ||
57 | let (insert_position, add_blank) = find_insert_position(&where_, path); | ||
58 | |||
59 | let to_insert: Vec<SyntaxElement> = { | ||
60 | let mut buf = Vec::new(); | ||
61 | |||
62 | if add_blank == AddBlankLine::Before { | ||
63 | buf.push(make::tokens::single_newline().into()); | ||
64 | } | ||
65 | |||
66 | buf.push(use_item.syntax().clone().into()); | ||
67 | |||
68 | if add_blank == AddBlankLine::After { | ||
69 | buf.push(make::tokens::single_newline().into()); | ||
70 | } else if add_blank == AddBlankLine::AfterTwice { | ||
71 | buf.push(make::tokens::single_newline().into()); | ||
72 | buf.push(make::tokens::single_newline().into()); | ||
73 | } | ||
43 | 74 | ||
44 | if let Some(container) = container { | 75 | buf |
45 | let syntax = container.either(|l| l.syntax().clone(), |r| r.syntax().clone()); | 76 | }; |
46 | let action = best_action_for_target(syntax, position.clone(), &target); | 77 | |
47 | make_assist(&action, &target, builder); | 78 | algo::insert_children(&where_, insert_position, to_insert) |
79 | } | ||
80 | |||
81 | fn try_merge_imports( | ||
82 | old: &ast::Use, | ||
83 | new: &ast::Use, | ||
84 | merge_behaviour: MergeBehaviour, | ||
85 | ) -> Option<ast::Use> { | ||
86 | // dont merge into re-exports | ||
87 | if old.visibility().map(|vis| vis.pub_token()).is_some() { | ||
88 | return None; | ||
89 | } | ||
90 | let old_tree = old.use_tree()?; | ||
91 | let new_tree = new.use_tree()?; | ||
92 | let merged = try_merge_trees(&old_tree, &new_tree, merge_behaviour)?; | ||
93 | Some(old.with_use_tree(merged)) | ||
94 | } | ||
95 | |||
96 | /// Simple function that checks if a UseTreeList is deeper than one level | ||
97 | fn use_tree_list_is_nested(tl: &ast::UseTreeList) -> bool { | ||
98 | tl.use_trees().any(|use_tree| { | ||
99 | use_tree.use_tree_list().is_some() || use_tree.path().and_then(|p| p.qualifier()).is_some() | ||
100 | }) | ||
101 | } | ||
102 | |||
103 | pub fn try_merge_trees( | ||
104 | old: &ast::UseTree, | ||
105 | new: &ast::UseTree, | ||
106 | merge_behaviour: MergeBehaviour, | ||
107 | ) -> Option<ast::UseTree> { | ||
108 | let lhs_path = old.path()?; | ||
109 | let rhs_path = new.path()?; | ||
110 | |||
111 | let (lhs_prefix, rhs_prefix) = common_prefix(&lhs_path, &rhs_path)?; | ||
112 | let lhs = old.split_prefix(&lhs_prefix); | ||
113 | let rhs = new.split_prefix(&rhs_prefix); | ||
114 | let lhs_tl = lhs.use_tree_list()?; | ||
115 | let rhs_tl = rhs.use_tree_list()?; | ||
116 | |||
117 | // if we are only allowed to merge the last level check if the paths are only one level deep | ||
118 | // FIXME: This shouldn't work yet i think | ||
119 | if merge_behaviour == MergeBehaviour::Last && use_tree_list_is_nested(&lhs_tl) | ||
120 | || use_tree_list_is_nested(&rhs_tl) | ||
121 | { | ||
122 | return None; | ||
48 | } | 123 | } |
124 | |||
125 | let should_insert_comma = lhs_tl | ||
126 | .r_curly_token() | ||
127 | .and_then(|it| skip_trivia_token(it.prev_token()?, Direction::Prev)) | ||
128 | .map(|it| it.kind() != T![,]) | ||
129 | .unwrap_or(true); | ||
130 | let mut to_insert: Vec<SyntaxElement> = Vec::new(); | ||
131 | if should_insert_comma { | ||
132 | to_insert.push(make::token(T![,]).into()); | ||
133 | to_insert.push(make::tokens::single_space().into()); | ||
134 | } | ||
135 | to_insert.extend( | ||
136 | rhs_tl | ||
137 | .syntax() | ||
138 | .children_with_tokens() | ||
139 | .filter(|it| it.kind() != T!['{'] && it.kind() != T!['}']), | ||
140 | ); | ||
141 | let pos = InsertPosition::Before(lhs_tl.r_curly_token()?.into()); | ||
142 | let use_tree_list = lhs_tl.insert_children(pos, to_insert); | ||
143 | Some(lhs.with_use_tree_list(use_tree_list)) | ||
49 | } | 144 | } |
50 | 145 | ||
51 | fn collect_path_segments_raw( | 146 | /// Traverses both paths until they differ, returning the common prefix of both. |
52 | segments: &mut Vec<ast::PathSegment>, | 147 | fn common_prefix(lhs: &ast::Path, rhs: &ast::Path) -> Option<(ast::Path, ast::Path)> { |
53 | mut path: ast::Path, | 148 | let mut res = None; |
54 | ) -> Option<usize> { | 149 | let mut lhs_curr = first_path(&lhs); |
55 | let oldlen = segments.len(); | 150 | let mut rhs_curr = first_path(&rhs); |
56 | loop { | 151 | loop { |
57 | let mut children = path.syntax().children_with_tokens(); | 152 | match (lhs_curr.segment(), rhs_curr.segment()) { |
58 | let (first, second, third) = ( | 153 | (Some(lhs), Some(rhs)) if lhs.syntax().text() == rhs.syntax().text() => (), |
59 | children.next().map(|n| (n.clone(), n.kind())), | 154 | _ => break, |
60 | children.next().map(|n| (n.clone(), n.kind())), | 155 | } |
61 | children.next().map(|n| (n.clone(), n.kind())), | 156 | res = Some((lhs_curr.clone(), rhs_curr.clone())); |
62 | ); | 157 | |
63 | match (first, second, third) { | 158 | match lhs_curr.parent_path().zip(rhs_curr.parent_path()) { |
64 | (Some((subpath, PATH)), Some((_, T![::])), Some((segment, PATH_SEGMENT))) => { | 159 | Some((lhs, rhs)) => { |
65 | path = ast::Path::cast(subpath.as_node()?.clone())?; | 160 | lhs_curr = lhs; |
66 | segments.push(ast::PathSegment::cast(segment.as_node()?.clone())?); | 161 | rhs_curr = rhs; |
67 | } | ||
68 | (Some((segment, PATH_SEGMENT)), _, _) => { | ||
69 | segments.push(ast::PathSegment::cast(segment.as_node()?.clone())?); | ||
70 | break; | ||
71 | } | 162 | } |
72 | (_, _, _) => return None, | 163 | _ => break, |
73 | } | 164 | } |
74 | } | 165 | } |
75 | // We need to reverse only the new added segments | 166 | |
76 | let only_new_segments = segments.split_at_mut(oldlen).1; | 167 | res |
77 | only_new_segments.reverse(); | ||
78 | Some(segments.len() - oldlen) | ||
79 | } | 168 | } |
80 | 169 | ||
81 | fn fmt_segments_raw(segments: &[SmolStr], buf: &mut String) { | 170 | /// What type of merges are allowed. |
82 | let mut iter = segments.iter(); | 171 | #[derive(Copy, Clone, PartialEq, Eq)] |
83 | if let Some(s) = iter.next() { | 172 | pub enum MergeBehaviour { |
84 | buf.push_str(s); | 173 | /// Merge everything together creating deeply nested imports. |
85 | } | 174 | Full, |
86 | for s in iter { | 175 | /// Only merge the last import level, doesn't allow import nesting. |
87 | buf.push_str("::"); | 176 | Last, |
88 | buf.push_str(s); | ||
89 | } | ||
90 | } | 177 | } |
91 | 178 | ||
92 | /// Returns the number of common segments. | 179 | #[derive(Eq, PartialEq, PartialOrd, Ord)] |
93 | fn compare_path_segments(left: &[SmolStr], right: &[ast::PathSegment]) -> usize { | 180 | enum ImportGroup { |
94 | left.iter().zip(right).take_while(|(l, r)| compare_path_segment(l, r)).count() | 181 | // the order here defines the order of new group inserts |
182 | Std, | ||
183 | ExternCrate, | ||
184 | ThisCrate, | ||
185 | ThisModule, | ||
186 | SuperModule, | ||
95 | } | 187 | } |
96 | 188 | ||
97 | fn compare_path_segment(a: &SmolStr, b: &ast::PathSegment) -> bool { | 189 | impl ImportGroup { |
98 | if let Some(kb) = b.kind() { | 190 | fn new(path: &ast::Path) -> ImportGroup { |
99 | match kb { | 191 | let default = ImportGroup::ExternCrate; |
100 | ast::PathSegmentKind::Name(nameref_b) => a == nameref_b.text(), | 192 | |
101 | ast::PathSegmentKind::SelfKw => a == "self", | 193 | let first_segment = match first_segment(path) { |
102 | ast::PathSegmentKind::SuperKw => a == "super", | 194 | Some(it) => it, |
103 | ast::PathSegmentKind::CrateKw => a == "crate", | 195 | None => return default, |
104 | ast::PathSegmentKind::Type { .. } => false, // not allowed in imports | 196 | }; |
197 | |||
198 | let kind = first_segment.kind().unwrap_or(PathSegmentKind::SelfKw); | ||
199 | match kind { | ||
200 | PathSegmentKind::SelfKw => ImportGroup::ThisModule, | ||
201 | PathSegmentKind::SuperKw => ImportGroup::SuperModule, | ||
202 | PathSegmentKind::CrateKw => ImportGroup::ThisCrate, | ||
203 | PathSegmentKind::Name(name) => match name.text().as_str() { | ||
204 | "std" => ImportGroup::Std, | ||
205 | "core" => ImportGroup::Std, | ||
206 | // FIXME: can be ThisModule as well | ||
207 | _ => ImportGroup::ExternCrate, | ||
208 | }, | ||
209 | PathSegmentKind::Type { .. } => unreachable!(), | ||
105 | } | 210 | } |
106 | } else { | ||
107 | false | ||
108 | } | 211 | } |
109 | } | 212 | } |
110 | 213 | ||
111 | fn compare_path_segment_with_name(a: &SmolStr, b: &ast::Name) -> bool { | 214 | fn first_segment(path: &ast::Path) -> Option<ast::PathSegment> { |
112 | a == b.text() | 215 | first_path(path).segment() |
113 | } | 216 | } |
114 | 217 | ||
115 | #[derive(Clone, Debug)] | 218 | fn first_path(path: &ast::Path) -> ast::Path { |
116 | enum ImportAction { | 219 | successors(Some(path.clone()), ast::Path::qualifier).last().unwrap() |
117 | Nothing, | ||
118 | // Add a brand new use statement. | ||
119 | AddNewUse { | ||
120 | anchor: Option<SyntaxNode>, // anchor node | ||
121 | add_after_anchor: bool, | ||
122 | }, | ||
123 | |||
124 | // To split an existing use statement creating a nested import. | ||
125 | AddNestedImport { | ||
126 | // how may segments matched with the target path | ||
127 | common_segments: usize, | ||
128 | path_to_split: ast::Path, | ||
129 | // the first segment of path_to_split we want to add into the new nested list | ||
130 | first_segment_to_split: Option<ast::PathSegment>, | ||
131 | // Wether to add 'self' in addition to the target path | ||
132 | add_self: bool, | ||
133 | }, | ||
134 | // To add the target path to an existing nested import tree list. | ||
135 | AddInTreeList { | ||
136 | common_segments: usize, | ||
137 | // The UseTreeList where to add the target path | ||
138 | tree_list: ast::UseTreeList, | ||
139 | add_self: bool, | ||
140 | }, | ||
141 | } | 220 | } |
142 | 221 | ||
143 | impl ImportAction { | 222 | fn segment_iter(path: &ast::Path) -> impl Iterator<Item = ast::PathSegment> + Clone { |
144 | fn add_new_use(anchor: Option<SyntaxNode>, add_after_anchor: bool) -> Self { | 223 | path.syntax().children().flat_map(ast::PathSegment::cast) |
145 | ImportAction::AddNewUse { anchor, add_after_anchor } | 224 | } |
225 | |||
226 | #[derive(PartialEq, Eq)] | ||
227 | enum AddBlankLine { | ||
228 | Before, | ||
229 | After, | ||
230 | AfterTwice, | ||
231 | } | ||
232 | |||
233 | fn find_insert_position( | ||
234 | scope: &SyntaxNode, | ||
235 | insert_path: ast::Path, | ||
236 | ) -> (InsertPosition<SyntaxElement>, AddBlankLine) { | ||
237 | let group = ImportGroup::new(&insert_path); | ||
238 | let path_node_iter = scope | ||
239 | .children() | ||
240 | .filter_map(|node| ast::Use::cast(node.clone()).zip(Some(node))) | ||
241 | .flat_map(|(use_, node)| use_.use_tree().and_then(|tree| tree.path()).zip(Some(node))); | ||
242 | // Iterator that discards anything thats not in the required grouping | ||
243 | // This implementation allows the user to rearrange their import groups as this only takes the first group that fits | ||
244 | let group_iter = path_node_iter | ||
245 | .clone() | ||
246 | .skip_while(|(path, _)| ImportGroup::new(path) != group) | ||
247 | .take_while(|(path, _)| ImportGroup::new(path) == group); | ||
248 | |||
249 | let segments = segment_iter(&insert_path); | ||
250 | // 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 | ||
251 | let mut last = None; | ||
252 | // find the element that would come directly after our new import | ||
253 | let post_insert = | ||
254 | group_iter.inspect(|(_, node)| last = Some(node.clone())).find(|(path, _)| { | ||
255 | let check_segments = segment_iter(&path); | ||
256 | segments | ||
257 | .clone() | ||
258 | .zip(check_segments) | ||
259 | .flat_map(|(seg, seg2)| seg.name_ref().zip(seg2.name_ref())) | ||
260 | .all(|(l, r)| l.text() <= r.text()) | ||
261 | }); | ||
262 | match post_insert { | ||
263 | // insert our import before that element | ||
264 | Some((_, node)) => (InsertPosition::Before(node.into()), AddBlankLine::After), | ||
265 | // there is no element after our new import, so append it to the end of the group | ||
266 | None => match last { | ||
267 | Some(node) => (InsertPosition::After(node.into()), AddBlankLine::Before), | ||
268 | // the group we were looking for actually doesnt exist, so insert | ||
269 | None => { | ||
270 | // similar concept here to the `last` from above | ||
271 | let mut last = None; | ||
272 | // find the group that comes after where we want to insert | ||
273 | let post_group = path_node_iter | ||
274 | .inspect(|(_, node)| last = Some(node.clone())) | ||
275 | .find(|(p, _)| ImportGroup::new(p) > group); | ||
276 | match post_group { | ||
277 | Some((_, node)) => { | ||
278 | (InsertPosition::Before(node.into()), AddBlankLine::AfterTwice) | ||
279 | } | ||
280 | // there is no such group, so append after the last one | ||
281 | None => match last { | ||
282 | Some(node) => (InsertPosition::After(node.into()), AddBlankLine::Before), | ||
283 | // there are no imports in this file at all | ||
284 | None => (InsertPosition::First, AddBlankLine::AfterTwice), | ||
285 | }, | ||
286 | } | ||
287 | } | ||
288 | }, | ||
146 | } | 289 | } |
290 | } | ||
147 | 291 | ||
148 | fn add_nested_import( | 292 | #[cfg(test)] |
149 | common_segments: usize, | 293 | mod tests { |
150 | path_to_split: ast::Path, | 294 | use super::*; |
151 | first_segment_to_split: Option<ast::PathSegment>, | 295 | |
152 | add_self: bool, | 296 | use test_utils::assert_eq_text; |
153 | ) -> Self { | 297 | |
154 | ImportAction::AddNestedImport { | 298 | #[test] |
155 | common_segments, | 299 | fn insert_start() { |
156 | path_to_split, | 300 | check_none( |
157 | first_segment_to_split, | 301 | "std::bar::A", |
158 | add_self, | 302 | r"use std::bar::B; |
159 | } | 303 | use std::bar::D; |
304 | use std::bar::F; | ||
305 | use std::bar::G;", | ||
306 | r"use std::bar::A; | ||
307 | use std::bar::B; | ||
308 | use std::bar::D; | ||
309 | use std::bar::F; | ||
310 | use std::bar::G;", | ||
311 | ) | ||
160 | } | 312 | } |
161 | 313 | ||
162 | fn add_in_tree_list( | 314 | #[test] |
163 | common_segments: usize, | 315 | fn insert_middle() { |
164 | tree_list: ast::UseTreeList, | 316 | check_none( |
165 | add_self: bool, | 317 | "std::bar::E", |
166 | ) -> Self { | 318 | r"use std::bar::A; |
167 | ImportAction::AddInTreeList { common_segments, tree_list, add_self } | 319 | use std::bar::D; |
320 | use std::bar::F; | ||
321 | use std::bar::G;", | ||
322 | r"use std::bar::A; | ||
323 | use std::bar::D; | ||
324 | use std::bar::E; | ||
325 | use std::bar::F; | ||
326 | use std::bar::G;", | ||
327 | ) | ||
168 | } | 328 | } |
169 | 329 | ||
170 | fn better(left: ImportAction, right: ImportAction) -> ImportAction { | 330 | #[test] |
171 | if left.is_better(&right) { | 331 | fn insert_end() { |
172 | left | 332 | check_none( |
173 | } else { | 333 | "std::bar::Z", |
174 | right | 334 | r"use std::bar::A; |
175 | } | 335 | use std::bar::D; |
336 | use std::bar::F; | ||
337 | use std::bar::G;", | ||
338 | r"use std::bar::A; | ||
339 | use std::bar::D; | ||
340 | use std::bar::F; | ||
341 | use std::bar::G; | ||
342 | use std::bar::Z;", | ||
343 | ) | ||
176 | } | 344 | } |
177 | 345 | ||
178 | fn is_better(&self, other: &ImportAction) -> bool { | 346 | #[test] |
179 | match (self, other) { | 347 | fn insert_middle_pnested() { |
180 | (ImportAction::Nothing, _) => true, | 348 | check_none( |
181 | (ImportAction::AddInTreeList { .. }, ImportAction::Nothing) => false, | 349 | "std::bar::E", |
182 | ( | 350 | r"use std::bar::A; |
183 | ImportAction::AddNestedImport { common_segments: n, .. }, | 351 | use std::bar::{D, Z}; // example of weird imports due to user |
184 | ImportAction::AddInTreeList { common_segments: m, .. }, | 352 | use std::bar::F; |
185 | ) | 353 | use std::bar::G;", |
186 | | ( | 354 | r"use std::bar::A; |
187 | ImportAction::AddInTreeList { common_segments: n, .. }, | 355 | use std::bar::E; |
188 | ImportAction::AddNestedImport { common_segments: m, .. }, | 356 | use std::bar::{D, Z}; // example of weird imports due to user |
189 | ) | 357 | use std::bar::F; |
190 | | ( | 358 | use std::bar::G;", |
191 | ImportAction::AddInTreeList { common_segments: n, .. }, | 359 | ) |
192 | ImportAction::AddInTreeList { common_segments: m, .. }, | ||
193 | ) | ||
194 | | ( | ||
195 | ImportAction::AddNestedImport { common_segments: n, .. }, | ||
196 | ImportAction::AddNestedImport { common_segments: m, .. }, | ||
197 | ) => n > m, | ||
198 | (ImportAction::AddInTreeList { .. }, _) => true, | ||
199 | (ImportAction::AddNestedImport { .. }, ImportAction::Nothing) => false, | ||
200 | (ImportAction::AddNestedImport { .. }, _) => true, | ||
201 | (ImportAction::AddNewUse { .. }, _) => false, | ||
202 | } | ||
203 | } | 360 | } |
204 | } | ||
205 | 361 | ||
206 | // Find out the best ImportAction to import target path against current_use_tree. | 362 | #[test] |
207 | // If current_use_tree has a nested import the function gets called recursively on every UseTree inside a UseTreeList. | 363 | fn insert_middle_groups() { |
208 | fn walk_use_tree_for_best_action( | 364 | check_none( |
209 | current_path_segments: &mut Vec<ast::PathSegment>, // buffer containing path segments | 365 | "foo::bar::G", |
210 | current_parent_use_tree_list: Option<ast::UseTreeList>, // will be Some value if we are in a nested import | 366 | r"use std::bar::A; |
211 | current_use_tree: ast::UseTree, // the use tree we are currently examinating | 367 | use std::bar::D; |
212 | target: &[SmolStr], // the path we want to import | 368 | |
213 | ) -> ImportAction { | 369 | use foo::bar::F; |
214 | // We save the number of segments in the buffer so we can restore the correct segments | 370 | use foo::bar::H;", |
215 | // before returning. Recursive call will add segments so we need to delete them. | 371 | r"use std::bar::A; |
216 | let prev_len = current_path_segments.len(); | 372 | use std::bar::D; |
217 | 373 | ||
218 | let tree_list = current_use_tree.use_tree_list(); | 374 | use foo::bar::F; |
219 | let alias = current_use_tree.rename(); | 375 | use foo::bar::G; |
220 | 376 | use foo::bar::H;", | |
221 | let path = match current_use_tree.path() { | 377 | ) |
222 | Some(path) => path, | 378 | } |
223 | None => { | ||
224 | // If the use item don't have a path, it means it's broken (syntax error) | ||
225 | return ImportAction::add_new_use( | ||
226 | current_use_tree | ||
227 | .syntax() | ||
228 | .ancestors() | ||
229 | .find_map(ast::Use::cast) | ||
230 | .map(|it| it.syntax().clone()), | ||
231 | true, | ||
232 | ); | ||
233 | } | ||
234 | }; | ||
235 | 379 | ||
236 | // This can happen only if current_use_tree is a direct child of a UseItem | 380 | #[test] |
237 | if let Some(name) = alias.and_then(|it| it.name()) { | 381 | fn insert_first_matching_group() { |
238 | if compare_path_segment_with_name(&target[0], &name) { | 382 | check_none( |
239 | return ImportAction::Nothing; | 383 | "foo::bar::G", |
240 | } | 384 | r"use foo::bar::A; |
385 | use foo::bar::D; | ||
386 | |||
387 | use std; | ||
388 | |||
389 | use foo::bar::F; | ||
390 | use foo::bar::H;", | ||
391 | r"use foo::bar::A; | ||
392 | use foo::bar::D; | ||
393 | use foo::bar::G; | ||
394 | |||
395 | use std; | ||
396 | |||
397 | use foo::bar::F; | ||
398 | use foo::bar::H;", | ||
399 | ) | ||
241 | } | 400 | } |
242 | 401 | ||
243 | collect_path_segments_raw(current_path_segments, path.clone()); | 402 | #[test] |
244 | 403 | fn insert_missing_group() { | |
245 | // We compare only the new segments added in the line just above. | 404 | check_none( |
246 | // The first prev_len segments were already compared in 'parent' recursive calls. | 405 | "std::fmt", |
247 | let left = target.split_at(prev_len).1; | 406 | r"use foo::bar::A; |
248 | let right = current_path_segments.split_at(prev_len).1; | 407 | use foo::bar::D;", |
249 | let common = compare_path_segments(left, &right); | 408 | r"use std::fmt; |
250 | let mut action = match common { | 409 | |
251 | 0 => ImportAction::add_new_use( | 410 | use foo::bar::A; |
252 | // e.g: target is std::fmt and we can have | 411 | use foo::bar::D;", |
253 | // use foo::bar | 412 | ) |
254 | // We add a brand new use statement | 413 | } |
255 | current_use_tree | ||
256 | .syntax() | ||
257 | .ancestors() | ||
258 | .find_map(ast::Use::cast) | ||
259 | .map(|it| it.syntax().clone()), | ||
260 | true, | ||
261 | ), | ||
262 | common if common == left.len() && left.len() == right.len() => { | ||
263 | // e.g: target is std::fmt and we can have | ||
264 | // 1- use std::fmt; | ||
265 | // 2- use std::fmt::{ ... } | ||
266 | if let Some(list) = tree_list { | ||
267 | // In case 2 we need to add self to the nested list | ||
268 | // unless it's already there | ||
269 | let has_self = list.use_trees().map(|it| it.path()).any(|p| { | ||
270 | p.and_then(|it| it.segment()) | ||
271 | .and_then(|it| it.kind()) | ||
272 | .filter(|k| *k == ast::PathSegmentKind::SelfKw) | ||
273 | .is_some() | ||
274 | }); | ||
275 | |||
276 | if has_self { | ||
277 | ImportAction::Nothing | ||
278 | } else { | ||
279 | ImportAction::add_in_tree_list(current_path_segments.len(), list, true) | ||
280 | } | ||
281 | } else { | ||
282 | // Case 1 | ||
283 | ImportAction::Nothing | ||
284 | } | ||
285 | } | ||
286 | common if common != left.len() && left.len() == right.len() => { | ||
287 | // e.g: target is std::fmt and we have | ||
288 | // use std::io; | ||
289 | // We need to split. | ||
290 | let segments_to_split = current_path_segments.split_at(prev_len + common).1; | ||
291 | ImportAction::add_nested_import( | ||
292 | prev_len + common, | ||
293 | path, | ||
294 | Some(segments_to_split[0].clone()), | ||
295 | false, | ||
296 | ) | ||
297 | } | ||
298 | common if common == right.len() && left.len() > right.len() => { | ||
299 | // e.g: target is std::fmt and we can have | ||
300 | // 1- use std; | ||
301 | // 2- use std::{ ... }; | ||
302 | |||
303 | // fallback action | ||
304 | let mut better_action = ImportAction::add_new_use( | ||
305 | current_use_tree | ||
306 | .syntax() | ||
307 | .ancestors() | ||
308 | .find_map(ast::Use::cast) | ||
309 | .map(|it| it.syntax().clone()), | ||
310 | true, | ||
311 | ); | ||
312 | if let Some(list) = tree_list { | ||
313 | // Case 2, check recursively if the path is already imported in the nested list | ||
314 | for u in list.use_trees() { | ||
315 | let child_action = walk_use_tree_for_best_action( | ||
316 | current_path_segments, | ||
317 | Some(list.clone()), | ||
318 | u, | ||
319 | target, | ||
320 | ); | ||
321 | if child_action.is_better(&better_action) { | ||
322 | better_action = child_action; | ||
323 | if let ImportAction::Nothing = better_action { | ||
324 | return better_action; | ||
325 | } | ||
326 | } | ||
327 | } | ||
328 | } else { | ||
329 | // Case 1, split adding self | ||
330 | better_action = ImportAction::add_nested_import(prev_len + common, path, None, true) | ||
331 | } | ||
332 | better_action | ||
333 | } | ||
334 | common if common == left.len() && left.len() < right.len() => { | ||
335 | // e.g: target is std::fmt and we can have | ||
336 | // use std::fmt::Debug; | ||
337 | let segments_to_split = current_path_segments.split_at(prev_len + common).1; | ||
338 | ImportAction::add_nested_import( | ||
339 | prev_len + common, | ||
340 | path, | ||
341 | Some(segments_to_split[0].clone()), | ||
342 | true, | ||
343 | ) | ||
344 | } | ||
345 | common if common < left.len() && common < right.len() => { | ||
346 | // e.g: target is std::fmt::nested::Debug | ||
347 | // use std::fmt::Display | ||
348 | let segments_to_split = current_path_segments.split_at(prev_len + common).1; | ||
349 | ImportAction::add_nested_import( | ||
350 | prev_len + common, | ||
351 | path, | ||
352 | Some(segments_to_split[0].clone()), | ||
353 | false, | ||
354 | ) | ||
355 | } | ||
356 | _ => unreachable!(), | ||
357 | }; | ||
358 | 414 | ||
359 | // If we are inside a UseTreeList adding a use statement become adding to the existing | 415 | #[test] |
360 | // tree list. | 416 | fn insert_no_imports() { |
361 | action = match (current_parent_use_tree_list, action.clone()) { | 417 | check_full( |
362 | (Some(use_tree_list), ImportAction::AddNewUse { .. }) => { | 418 | "foo::bar", |
363 | ImportAction::add_in_tree_list(prev_len, use_tree_list, false) | 419 | "fn main() {}", |
364 | } | 420 | r"use foo::bar; |
365 | (_, _) => action, | ||
366 | }; | ||
367 | 421 | ||
368 | // We remove the segments added | 422 | fn main() {}", |
369 | current_path_segments.truncate(prev_len); | 423 | ) |
370 | action | 424 | } |
371 | } | ||
372 | 425 | ||
373 | fn best_action_for_target( | 426 | #[test] |
374 | container: SyntaxNode, | 427 | fn insert_empty_file() { |
375 | anchor: SyntaxNode, | 428 | // empty files will get two trailing newlines |
376 | target: &[SmolStr], | 429 | // this is due to the test case insert_no_imports above |
377 | ) -> ImportAction { | 430 | check_full( |
378 | let mut storage = Vec::with_capacity(16); // this should be the only allocation | 431 | "foo::bar", |
379 | let best_action = container | 432 | "", |
380 | .children() | 433 | r"use foo::bar; |
381 | .filter_map(ast::Use::cast) | 434 | |
382 | .filter(|u| u.visibility().is_none()) | 435 | ", |
383 | .filter_map(|it| it.use_tree()) | 436 | ) |
384 | .map(|u| walk_use_tree_for_best_action(&mut storage, None, u, target)) | 437 | } |
385 | .fold(None, |best, a| match best { | ||
386 | Some(best) => Some(ImportAction::better(best, a)), | ||
387 | None => Some(a), | ||
388 | }); | ||
389 | 438 | ||
390 | match best_action { | 439 | #[test] |
391 | Some(action) => action, | 440 | fn adds_std_group() { |
392 | None => { | 441 | check_full( |
393 | // We have no action and no UseItem was found in container so we find | 442 | "std::fmt::Debug", |
394 | // another item and we use it as anchor. | 443 | r"use stdx;", |
395 | // If there are no items above, we choose the target path itself as anchor. | 444 | r"use std::fmt::Debug; |
396 | // todo: we should include even whitespace blocks as anchor candidates | ||
397 | let anchor = container.children().next().or_else(|| Some(anchor)); | ||
398 | 445 | ||
399 | let add_after_anchor = anchor | 446 | use stdx;", |
400 | .clone() | 447 | ) |
401 | .and_then(ast::Attr::cast) | ||
402 | .map(|attr| attr.kind() == ast::AttrKind::Inner) | ||
403 | .unwrap_or(false); | ||
404 | ImportAction::add_new_use(anchor, add_after_anchor) | ||
405 | } | ||
406 | } | 448 | } |
407 | } | ||
408 | 449 | ||
409 | fn make_assist(action: &ImportAction, target: &[SmolStr], edit: &mut TextEditBuilder) { | 450 | #[test] |
410 | match action { | 451 | fn merges_groups() { |
411 | ImportAction::AddNewUse { anchor, add_after_anchor } => { | 452 | check_last("std::io", r"use std::fmt;", r"use std::{fmt, io};") |
412 | make_assist_add_new_use(anchor, *add_after_anchor, target, edit) | ||
413 | } | ||
414 | ImportAction::AddInTreeList { common_segments, tree_list, add_self } => { | ||
415 | // We know that the fist n segments already exists in the use statement we want | ||
416 | // to modify, so we want to add only the last target.len() - n segments. | ||
417 | let segments_to_add = target.split_at(*common_segments).1; | ||
418 | make_assist_add_in_tree_list(tree_list, segments_to_add, *add_self, edit) | ||
419 | } | ||
420 | ImportAction::AddNestedImport { | ||
421 | common_segments, | ||
422 | path_to_split, | ||
423 | first_segment_to_split, | ||
424 | add_self, | ||
425 | } => { | ||
426 | let segments_to_add = target.split_at(*common_segments).1; | ||
427 | make_assist_add_nested_import( | ||
428 | path_to_split, | ||
429 | first_segment_to_split, | ||
430 | segments_to_add, | ||
431 | *add_self, | ||
432 | edit, | ||
433 | ) | ||
434 | } | ||
435 | _ => {} | ||
436 | } | 453 | } |
437 | } | ||
438 | 454 | ||
439 | fn make_assist_add_new_use( | 455 | #[test] |
440 | anchor: &Option<SyntaxNode>, | 456 | fn merges_groups_last() { |
441 | after: bool, | 457 | check_last( |
442 | target: &[SmolStr], | 458 | "std::io", |
443 | edit: &mut TextEditBuilder, | 459 | r"use std::fmt::{Result, Display};", |
444 | ) { | 460 | r"use std::fmt::{Result, Display}; |
445 | if let Some(anchor) = anchor { | 461 | use std::io;", |
446 | let indent = leading_indent(anchor); | 462 | ) |
447 | let mut buf = String::new(); | ||
448 | if after { | ||
449 | buf.push_str("\n"); | ||
450 | if let Some(spaces) = &indent { | ||
451 | buf.push_str(spaces); | ||
452 | } | ||
453 | } | ||
454 | buf.push_str("use "); | ||
455 | fmt_segments_raw(target, &mut buf); | ||
456 | buf.push_str(";"); | ||
457 | if !after { | ||
458 | buf.push_str("\n\n"); | ||
459 | if let Some(spaces) = &indent { | ||
460 | buf.push_str(&spaces); | ||
461 | } | ||
462 | } | ||
463 | let position = if after { anchor.text_range().end() } else { anchor.text_range().start() }; | ||
464 | edit.insert(position, buf); | ||
465 | } | 463 | } |
466 | } | ||
467 | 464 | ||
468 | fn make_assist_add_in_tree_list( | 465 | #[test] |
469 | tree_list: &ast::UseTreeList, | 466 | fn merges_groups2() { |
470 | target: &[SmolStr], | 467 | check_full( |
471 | add_self: bool, | 468 | "std::io", |
472 | edit: &mut TextEditBuilder, | 469 | r"use std::fmt::{Result, Display};", |
473 | ) { | 470 | r"use std::{fmt::{Result, Display}, io};", |
474 | let last = tree_list.use_trees().last(); | 471 | ) |
475 | if let Some(last) = last { | ||
476 | let mut buf = String::new(); | ||
477 | let comma = last.syntax().siblings(Direction::Next).find(|n| n.kind() == T![,]); | ||
478 | let offset = if let Some(comma) = comma { | ||
479 | comma.text_range().end() | ||
480 | } else { | ||
481 | buf.push_str(","); | ||
482 | last.syntax().text_range().end() | ||
483 | }; | ||
484 | if add_self { | ||
485 | buf.push_str(" self") | ||
486 | } else { | ||
487 | buf.push_str(" "); | ||
488 | } | ||
489 | fmt_segments_raw(target, &mut buf); | ||
490 | edit.insert(offset, buf); | ||
491 | } else { | ||
492 | } | 472 | } |
493 | } | ||
494 | 473 | ||
495 | fn make_assist_add_nested_import( | 474 | #[test] |
496 | path: &ast::Path, | 475 | fn skip_merges_groups_pub() { |
497 | first_segment_to_split: &Option<ast::PathSegment>, | 476 | check_full( |
498 | target: &[SmolStr], | 477 | "std::io", |
499 | add_self: bool, | 478 | r"pub use std::fmt::{Result, Display};", |
500 | edit: &mut TextEditBuilder, | 479 | r"pub use std::fmt::{Result, Display}; |
501 | ) { | 480 | use std::io;", |
502 | let use_tree = path.syntax().ancestors().find_map(ast::UseTree::cast); | 481 | ) |
503 | if let Some(use_tree) = use_tree { | 482 | } |
504 | let (start, add_colon_colon) = if let Some(first_segment_to_split) = first_segment_to_split | ||
505 | { | ||
506 | (first_segment_to_split.syntax().text_range().start(), false) | ||
507 | } else { | ||
508 | (use_tree.syntax().text_range().end(), true) | ||
509 | }; | ||
510 | let end = use_tree.syntax().text_range().end(); | ||
511 | 483 | ||
512 | let mut buf = String::new(); | 484 | #[test] |
513 | if add_colon_colon { | 485 | fn merges_groups_self() { |
514 | buf.push_str("::"); | 486 | check_full("std::fmt::Debug", r"use std::fmt;", r"use std::fmt::{self, Debug};") |
515 | } | ||
516 | buf.push_str("{"); | ||
517 | if add_self { | ||
518 | buf.push_str("self, "); | ||
519 | } | ||
520 | fmt_segments_raw(target, &mut buf); | ||
521 | if !target.is_empty() { | ||
522 | buf.push_str(", "); | ||
523 | } | ||
524 | edit.insert(start, buf); | ||
525 | edit.insert(end, "}".to_string()); | ||
526 | } | 487 | } |
527 | } | ||
528 | 488 | ||
529 | /// If the node is on the beginning of the line, calculate indent. | 489 | fn check( |
530 | fn leading_indent(node: &SyntaxNode) -> Option<SmolStr> { | 490 | path: &str, |
531 | for token in prev_tokens(node.first_token()?) { | 491 | ra_fixture_before: &str, |
532 | if let Some(ws) = ast::Whitespace::cast(token.clone()) { | 492 | ra_fixture_after: &str, |
533 | let ws_text = ws.text(); | 493 | mb: Option<MergeBehaviour>, |
534 | if let Some(pos) = ws_text.rfind('\n') { | 494 | ) { |
535 | return Some(ws_text[pos + 1..].into()); | 495 | let file = ast::SourceFile::parse(ra_fixture_before).tree().syntax().clone(); |
536 | } | 496 | let path = ast::SourceFile::parse(&format!("use {};", path)) |
537 | } | 497 | .tree() |
538 | if token.text().contains('\n') { | 498 | .syntax() |
539 | break; | 499 | .descendants() |
540 | } | 500 | .find_map(ast::Path::cast) |
501 | .unwrap(); | ||
502 | |||
503 | let result = insert_use(file, path, mb).to_string(); | ||
504 | assert_eq_text!(&result, ra_fixture_after); | ||
505 | } | ||
506 | |||
507 | fn check_full(path: &str, ra_fixture_before: &str, ra_fixture_after: &str) { | ||
508 | check(path, ra_fixture_before, ra_fixture_after, Some(MergeBehaviour::Full)) | ||
541 | } | 509 | } |
542 | return None; | 510 | |
543 | fn prev_tokens(token: SyntaxToken) -> impl Iterator<Item = SyntaxToken> { | 511 | fn check_last(path: &str, ra_fixture_before: &str, ra_fixture_after: &str) { |
544 | successors(token.prev_token(), |token| token.prev_token()) | 512 | check(path, ra_fixture_before, ra_fixture_after, Some(MergeBehaviour::Last)) |
513 | } | ||
514 | |||
515 | fn check_none(path: &str, ra_fixture_before: &str, ra_fixture_after: &str) { | ||
516 | check(path, ra_fixture_before, ra_fixture_after, None) | ||
545 | } | 517 | } |
546 | } | 518 | } |