diff options
author | bors[bot] <26634292+bors[bot]@users.noreply.github.com> | 2020-09-04 14:28:05 +0100 |
---|---|---|
committer | GitHub <[email protected]> | 2020-09-04 14:28:05 +0100 |
commit | 9dfa69a44ac5054b5c970fdd13ade4d50f4c097d (patch) | |
tree | 58b1f9069d022c985ef57a74212fcefd03a877c3 /crates | |
parent | 8a21b2c96acb351baa81422f6633462b5db2298a (diff) | |
parent | 82f61e6629f709d7f347fd801ef5c31f476ff29e (diff) |
Merge #5935
5935: Rewrite import insertion r=matklad a=Veykril
This is my attempt at refactoring the import insertion #3947. I hope what I created here is somewhat in line with what was requested, it wouldn't surprise me .
`common_prefix` is a copy from `merge_imports.rs` so those should be unified somewhere, `try_merge_trees` is also copied from there but slighly modified to take the `MergeBehaviour` enum into account.
`MergeBehaviour` should in the end become a configuration option, and the order if `ImportGroup` probably as well?
I'm not too familiar with the assist stuff and the like which is why I dont know what i have to do with `insert_use_statement` and `find_insert_use_container` for now.
I will most likely add more test cases in the end as well as I currently only tried to hit every path in `find_insert_position`.
Some of the merge tests also fail atm due to them not sorting what they insert. There is also this test case I'm not sure if we want to support it. I would assume we want to? https://github.com/rust-analyzer/rust-analyzer/pull/5935/files#diff-6923916dd8bdd2f1ab4b984adacd265fR540-R547
The entire module was rewritten so looking at the the file itself is probably better than looking at the diff.
Regarding the sub issues of #3947:
- #3301: This is fixed with the rewrite, what this implementation does is that it scans through the first occurence of groupings and picks the appropriate one out. This means the user can actually rearrange the groupings on a per file basis to their liking. If a group isnt being found it is inserted according to the `ImportGroup` variant order(Would be nice if this was configurable I imagine).
- #3831: This should be fixed with the introduced `MergeBehaviour` enum and it's `Last` variant.
- #3946: This should also be [fixed](https://github.com/rust-analyzer/rust-analyzer/pull/5935/files#diff-6923916dd8bdd2f1ab4b984adacd265fR87)
- #5795: This is fixed in the sense that the grouping search picks the first group that is of the same kind as the import that is being added. So if there is a random import in the middle of the program it should only be considered if there is no group of the same kind in the file already present.
- the last point in the list I havent checked yet, tho I got the feeling that it's not gonna be too simple as that will require knowledge of whether in this example `ast` is a crate or the module that is already imported.
Co-authored-by: Lukas Wirth <[email protected]>
Diffstat (limited to 'crates')
-rw-r--r-- | crates/assists/src/handlers/auto_import.rs | 20 | ||||
-rw-r--r-- | crates/assists/src/handlers/extract_struct_from_enum_variant.rs | 33 | ||||
-rw-r--r-- | crates/assists/src/handlers/replace_qualified_name_with_use.rs | 68 | ||||
-rw-r--r-- | crates/assists/src/utils.rs | 2 | ||||
-rw-r--r-- | crates/assists/src/utils/insert_use.rs | 1144 | ||||
-rw-r--r-- | crates/syntax/src/ast/make.rs | 12 |
6 files changed, 744 insertions, 535 deletions
diff --git a/crates/assists/src/handlers/auto_import.rs b/crates/assists/src/handlers/auto_import.rs index c4770f336..66e819154 100644 --- a/crates/assists/src/handlers/auto_import.rs +++ b/crates/assists/src/handlers/auto_import.rs | |||
@@ -1,11 +1,13 @@ | |||
1 | use std::collections::BTreeSet; | 1 | use std::collections::BTreeSet; |
2 | 2 | ||
3 | use ast::make; | ||
3 | use either::Either; | 4 | use either::Either; |
4 | use hir::{ | 5 | use hir::{ |
5 | AsAssocItem, AssocItemContainer, ModPath, Module, ModuleDef, PathResolution, Semantics, Trait, | 6 | AsAssocItem, AssocItemContainer, ModPath, Module, ModuleDef, PathResolution, Semantics, Trait, |
6 | Type, | 7 | Type, |
7 | }; | 8 | }; |
8 | use ide_db::{imports_locator, RootDatabase}; | 9 | use ide_db::{imports_locator, RootDatabase}; |
10 | use insert_use::ImportScope; | ||
9 | use rustc_hash::FxHashSet; | 11 | use rustc_hash::FxHashSet; |
10 | use syntax::{ | 12 | use syntax::{ |
11 | ast::{self, AstNode}, | 13 | ast::{self, AstNode}, |
@@ -13,7 +15,8 @@ use syntax::{ | |||
13 | }; | 15 | }; |
14 | 16 | ||
15 | use crate::{ | 17 | use crate::{ |
16 | utils::insert_use_statement, AssistContext, AssistId, AssistKind, Assists, GroupLabel, | 18 | utils::{insert_use, MergeBehaviour}, |
19 | AssistContext, AssistId, AssistKind, Assists, GroupLabel, | ||
17 | }; | 20 | }; |
18 | 21 | ||
19 | // Assist: auto_import | 22 | // Assist: auto_import |
@@ -44,6 +47,9 @@ pub(crate) fn auto_import(acc: &mut Assists, ctx: &AssistContext) -> Option<()> | |||
44 | 47 | ||
45 | let range = ctx.sema.original_range(&auto_import_assets.syntax_under_caret).range; | 48 | let range = ctx.sema.original_range(&auto_import_assets.syntax_under_caret).range; |
46 | let group = auto_import_assets.get_import_group_message(); | 49 | let group = auto_import_assets.get_import_group_message(); |
50 | let scope = | ||
51 | ImportScope::find_insert_use_container(&auto_import_assets.syntax_under_caret, ctx)?; | ||
52 | let syntax = scope.as_syntax_node(); | ||
47 | for import in proposed_imports { | 53 | for import in proposed_imports { |
48 | acc.add_group( | 54 | acc.add_group( |
49 | &group, | 55 | &group, |
@@ -51,12 +57,12 @@ pub(crate) fn auto_import(acc: &mut Assists, ctx: &AssistContext) -> Option<()> | |||
51 | format!("Import `{}`", &import), | 57 | format!("Import `{}`", &import), |
52 | range, | 58 | range, |
53 | |builder| { | 59 | |builder| { |
54 | insert_use_statement( | 60 | let new_syntax = insert_use( |
55 | &auto_import_assets.syntax_under_caret, | 61 | &scope, |
56 | &import.to_string(), | 62 | make::path_from_text(&import.to_string()), |
57 | ctx, | 63 | Some(MergeBehaviour::Full), |
58 | builder.text_edit_builder(), | ||
59 | ); | 64 | ); |
65 | builder.replace(syntax.text_range(), new_syntax.to_string()) | ||
60 | }, | 66 | }, |
61 | ); | 67 | ); |
62 | } | 68 | } |
@@ -358,7 +364,7 @@ mod tests { | |||
358 | } | 364 | } |
359 | ", | 365 | ", |
360 | r" | 366 | r" |
361 | use PubMod::{PubStruct2, PubStruct1}; | 367 | use PubMod::{PubStruct1, PubStruct2}; |
362 | 368 | ||
363 | struct Test { | 369 | struct Test { |
364 | test: PubStruct2<u8>, | 370 | test: PubStruct2<u8>, |
diff --git a/crates/assists/src/handlers/extract_struct_from_enum_variant.rs b/crates/assists/src/handlers/extract_struct_from_enum_variant.rs index 8ac20210a..80c62d8bb 100644 --- a/crates/assists/src/handlers/extract_struct_from_enum_variant.rs +++ b/crates/assists/src/handlers/extract_struct_from_enum_variant.rs | |||
@@ -10,9 +10,12 @@ use syntax::{ | |||
10 | }; | 10 | }; |
11 | 11 | ||
12 | use crate::{ | 12 | use crate::{ |
13 | assist_context::AssistBuilder, utils::insert_use_statement, AssistContext, AssistId, | 13 | assist_context::AssistBuilder, |
14 | AssistKind, Assists, | 14 | utils::{insert_use, MergeBehaviour}, |
15 | AssistContext, AssistId, AssistKind, Assists, | ||
15 | }; | 16 | }; |
17 | use ast::make; | ||
18 | use insert_use::ImportScope; | ||
16 | 19 | ||
17 | // Assist: extract_struct_from_enum_variant | 20 | // Assist: extract_struct_from_enum_variant |
18 | // | 21 | // |
@@ -94,6 +97,7 @@ fn existing_struct_def(db: &RootDatabase, variant_name: &str, variant: &EnumVari | |||
94 | .any(|(name, _)| name.to_string() == variant_name.to_string()) | 97 | .any(|(name, _)| name.to_string() == variant_name.to_string()) |
95 | } | 98 | } |
96 | 99 | ||
100 | #[allow(dead_code)] | ||
97 | fn insert_import( | 101 | fn insert_import( |
98 | ctx: &AssistContext, | 102 | ctx: &AssistContext, |
99 | builder: &mut AssistBuilder, | 103 | builder: &mut AssistBuilder, |
@@ -107,12 +111,16 @@ fn insert_import( | |||
107 | if let Some(mut mod_path) = mod_path { | 111 | if let Some(mut mod_path) = mod_path { |
108 | mod_path.segments.pop(); | 112 | mod_path.segments.pop(); |
109 | mod_path.segments.push(variant_hir_name.clone()); | 113 | mod_path.segments.push(variant_hir_name.clone()); |
110 | insert_use_statement( | 114 | let scope = ImportScope::find_insert_use_container(path.syntax(), ctx)?; |
111 | path.syntax(), | 115 | let syntax = scope.as_syntax_node(); |
112 | &mod_path.to_string(), | 116 | |
113 | ctx, | 117 | let new_syntax = insert_use( |
114 | builder.text_edit_builder(), | 118 | &scope, |
119 | make::path_from_text(&mod_path.to_string()), | ||
120 | Some(MergeBehaviour::Full), | ||
115 | ); | 121 | ); |
122 | // FIXME: this will currently panic as multiple imports will have overlapping text ranges | ||
123 | builder.replace(syntax.text_range(), new_syntax.to_string()) | ||
116 | } | 124 | } |
117 | Some(()) | 125 | Some(()) |
118 | } | 126 | } |
@@ -167,9 +175,9 @@ fn update_reference( | |||
167 | builder: &mut AssistBuilder, | 175 | builder: &mut AssistBuilder, |
168 | reference: Reference, | 176 | reference: Reference, |
169 | source_file: &SourceFile, | 177 | source_file: &SourceFile, |
170 | enum_module_def: &ModuleDef, | 178 | _enum_module_def: &ModuleDef, |
171 | variant_hir_name: &Name, | 179 | _variant_hir_name: &Name, |
172 | visited_modules_set: &mut FxHashSet<Module>, | 180 | _visited_modules_set: &mut FxHashSet<Module>, |
173 | ) -> Option<()> { | 181 | ) -> Option<()> { |
174 | let path_expr: ast::PathExpr = find_node_at_offset::<ast::PathExpr>( | 182 | let path_expr: ast::PathExpr = find_node_at_offset::<ast::PathExpr>( |
175 | source_file.syntax(), | 183 | source_file.syntax(), |
@@ -178,13 +186,14 @@ fn update_reference( | |||
178 | let call = path_expr.syntax().parent().and_then(ast::CallExpr::cast)?; | 186 | let call = path_expr.syntax().parent().and_then(ast::CallExpr::cast)?; |
179 | let list = call.arg_list()?; | 187 | let list = call.arg_list()?; |
180 | let segment = path_expr.path()?.segment()?; | 188 | let segment = path_expr.path()?.segment()?; |
181 | let module = ctx.sema.scope(&path_expr.syntax()).module()?; | 189 | let _module = ctx.sema.scope(&path_expr.syntax()).module()?; |
182 | let list_range = list.syntax().text_range(); | 190 | let list_range = list.syntax().text_range(); |
183 | let inside_list_range = TextRange::new( | 191 | let inside_list_range = TextRange::new( |
184 | list_range.start().checked_add(TextSize::from(1))?, | 192 | list_range.start().checked_add(TextSize::from(1))?, |
185 | list_range.end().checked_sub(TextSize::from(1))?, | 193 | list_range.end().checked_sub(TextSize::from(1))?, |
186 | ); | 194 | ); |
187 | builder.edit_file(reference.file_range.file_id); | 195 | builder.edit_file(reference.file_range.file_id); |
196 | /* FIXME: this most likely requires AST-based editing, see `insert_import` | ||
188 | if !visited_modules_set.contains(&module) { | 197 | if !visited_modules_set.contains(&module) { |
189 | if insert_import(ctx, builder, &path_expr, &module, enum_module_def, variant_hir_name) | 198 | if insert_import(ctx, builder, &path_expr, &module, enum_module_def, variant_hir_name) |
190 | .is_some() | 199 | .is_some() |
@@ -192,6 +201,7 @@ fn update_reference( | |||
192 | visited_modules_set.insert(module); | 201 | visited_modules_set.insert(module); |
193 | } | 202 | } |
194 | } | 203 | } |
204 | */ | ||
195 | builder.replace(inside_list_range, format!("{}{}", segment, list)); | 205 | builder.replace(inside_list_range, format!("{}{}", segment, list)); |
196 | Some(()) | 206 | Some(()) |
197 | } | 207 | } |
@@ -250,6 +260,7 @@ pub enum A { One(One) }"#, | |||
250 | } | 260 | } |
251 | 261 | ||
252 | #[test] | 262 | #[test] |
263 | #[ignore] // FIXME: this currently panics if `insert_import` is used | ||
253 | fn test_extract_struct_with_complex_imports() { | 264 | fn test_extract_struct_with_complex_imports() { |
254 | check_assist( | 265 | check_assist( |
255 | extract_struct_from_enum_variant, | 266 | extract_struct_from_enum_variant, |
diff --git a/crates/assists/src/handlers/replace_qualified_name_with_use.rs b/crates/assists/src/handlers/replace_qualified_name_with_use.rs index 470e5f8ff..85c70d16b 100644 --- a/crates/assists/src/handlers/replace_qualified_name_with_use.rs +++ b/crates/assists/src/handlers/replace_qualified_name_with_use.rs | |||
@@ -2,9 +2,10 @@ use syntax::{algo::SyntaxRewriter, ast, match_ast, AstNode, SyntaxNode, TextRang | |||
2 | use test_utils::mark; | 2 | use test_utils::mark; |
3 | 3 | ||
4 | use crate::{ | 4 | use crate::{ |
5 | utils::{find_insert_use_container, insert_use_statement}, | 5 | utils::{insert_use, ImportScope, MergeBehaviour}, |
6 | AssistContext, AssistId, AssistKind, Assists, | 6 | AssistContext, AssistId, AssistKind, Assists, |
7 | }; | 7 | }; |
8 | use ast::make; | ||
8 | 9 | ||
9 | // Assist: replace_qualified_name_with_use | 10 | // Assist: replace_qualified_name_with_use |
10 | // | 11 | // |
@@ -32,7 +33,7 @@ pub(crate) fn replace_qualified_name_with_use( | |||
32 | mark::hit!(dont_import_trivial_paths); | 33 | mark::hit!(dont_import_trivial_paths); |
33 | return None; | 34 | return None; |
34 | } | 35 | } |
35 | let path_to_import = path.to_string().clone(); | 36 | let path_to_import = path.to_string(); |
36 | let path_to_import = match path.segment()?.generic_arg_list() { | 37 | let path_to_import = match path.segment()?.generic_arg_list() { |
37 | Some(generic_args) => { | 38 | Some(generic_args) => { |
38 | let generic_args_start = | 39 | let generic_args_start = |
@@ -43,28 +44,26 @@ pub(crate) fn replace_qualified_name_with_use( | |||
43 | }; | 44 | }; |
44 | 45 | ||
45 | let target = path.syntax().text_range(); | 46 | let target = path.syntax().text_range(); |
47 | let scope = ImportScope::find_insert_use_container(path.syntax(), ctx)?; | ||
48 | let syntax = scope.as_syntax_node(); | ||
46 | acc.add( | 49 | acc.add( |
47 | AssistId("replace_qualified_name_with_use", AssistKind::RefactorRewrite), | 50 | AssistId("replace_qualified_name_with_use", AssistKind::RefactorRewrite), |
48 | "Replace qualified path with use", | 51 | "Replace qualified path with use", |
49 | target, | 52 | target, |
50 | |builder| { | 53 | |builder| { |
51 | let container = match find_insert_use_container(path.syntax(), ctx) { | ||
52 | Some(c) => c, | ||
53 | None => return, | ||
54 | }; | ||
55 | insert_use_statement( | ||
56 | path.syntax(), | ||
57 | &path_to_import.to_string(), | ||
58 | ctx, | ||
59 | builder.text_edit_builder(), | ||
60 | ); | ||
61 | |||
62 | // Now that we've brought the name into scope, re-qualify all paths that could be | 54 | // Now that we've brought the name into scope, re-qualify all paths that could be |
63 | // affected (that is, all paths inside the node we added the `use` to). | 55 | // affected (that is, all paths inside the node we added the `use` to). |
64 | let mut rewriter = SyntaxRewriter::default(); | 56 | let mut rewriter = SyntaxRewriter::default(); |
65 | let syntax = container.either(|l| l.syntax().clone(), |r| r.syntax().clone()); | 57 | shorten_paths(&mut rewriter, syntax.clone(), path); |
66 | shorten_paths(&mut rewriter, syntax, path); | 58 | let rewritten_syntax = rewriter.rewrite(&syntax); |
67 | builder.rewrite(rewriter); | 59 | if let Some(ref import_scope) = ImportScope::from(rewritten_syntax) { |
60 | let new_syntax = insert_use( | ||
61 | import_scope, | ||
62 | make::path_from_text(path_to_import), | ||
63 | Some(MergeBehaviour::Full), | ||
64 | ); | ||
65 | builder.replace(syntax.text_range(), new_syntax.to_string()) | ||
66 | } | ||
68 | }, | 67 | }, |
69 | ) | 68 | ) |
70 | } | 69 | } |
@@ -220,9 +219,10 @@ impl std::fmt::Debug<|> for Foo { | |||
220 | } | 219 | } |
221 | ", | 220 | ", |
222 | r" | 221 | r" |
223 | use stdx; | ||
224 | use std::fmt::Debug; | 222 | use std::fmt::Debug; |
225 | 223 | ||
224 | use stdx; | ||
225 | |||
226 | impl Debug for Foo { | 226 | impl Debug for Foo { |
227 | } | 227 | } |
228 | ", | 228 | ", |
@@ -274,7 +274,7 @@ impl std::io<|> for Foo { | |||
274 | } | 274 | } |
275 | ", | 275 | ", |
276 | r" | 276 | r" |
277 | use std::{io, fmt}; | 277 | use std::{fmt, io}; |
278 | 278 | ||
279 | impl io for Foo { | 279 | impl io for Foo { |
280 | } | 280 | } |
@@ -293,7 +293,7 @@ impl std::fmt::Debug<|> for Foo { | |||
293 | } | 293 | } |
294 | ", | 294 | ", |
295 | r" | 295 | r" |
296 | use std::fmt::{self, Debug, }; | 296 | use std::fmt::{self, Debug}; |
297 | 297 | ||
298 | impl Debug for Foo { | 298 | impl Debug for Foo { |
299 | } | 299 | } |
@@ -312,7 +312,7 @@ impl std::fmt<|> for Foo { | |||
312 | } | 312 | } |
313 | ", | 313 | ", |
314 | r" | 314 | r" |
315 | use std::fmt::{self, Debug}; | 315 | use std::fmt::{Debug, self}; |
316 | 316 | ||
317 | impl fmt for Foo { | 317 | impl fmt for Foo { |
318 | } | 318 | } |
@@ -330,8 +330,9 @@ use std::fmt::{Debug, nested::{Display}}; | |||
330 | impl std::fmt::nested<|> for Foo { | 330 | impl std::fmt::nested<|> for Foo { |
331 | } | 331 | } |
332 | ", | 332 | ", |
333 | // FIXME(veykril): should be nested::{self, Display} here | ||
333 | r" | 334 | r" |
334 | use std::fmt::{Debug, nested::{Display, self}}; | 335 | use std::fmt::{Debug, nested::{Display}, nested}; |
335 | 336 | ||
336 | impl nested for Foo { | 337 | impl nested for Foo { |
337 | } | 338 | } |
@@ -349,8 +350,9 @@ use std::fmt::{Debug, nested::{self, Display}}; | |||
349 | impl std::fmt::nested<|> for Foo { | 350 | impl std::fmt::nested<|> for Foo { |
350 | } | 351 | } |
351 | ", | 352 | ", |
353 | // FIXME(veykril): nested is duplicated now | ||
352 | r" | 354 | r" |
353 | use std::fmt::{Debug, nested::{self, Display}}; | 355 | use std::fmt::{Debug, nested::{self, Display}, nested}; |
354 | 356 | ||
355 | impl nested for Foo { | 357 | impl nested for Foo { |
356 | } | 358 | } |
@@ -369,7 +371,7 @@ impl std::fmt::nested::Debug<|> for Foo { | |||
369 | } | 371 | } |
370 | ", | 372 | ", |
371 | r" | 373 | r" |
372 | use std::fmt::{Debug, nested::{Display, Debug}}; | 374 | use std::fmt::{Debug, nested::{Display}, nested::Debug}; |
373 | 375 | ||
374 | impl Debug for Foo { | 376 | impl Debug for Foo { |
375 | } | 377 | } |
@@ -388,7 +390,7 @@ impl std::fmt::nested::Display<|> for Foo { | |||
388 | } | 390 | } |
389 | ", | 391 | ", |
390 | r" | 392 | r" |
391 | use std::fmt::{nested::Display, Debug}; | 393 | use std::fmt::{Debug, nested::Display}; |
392 | 394 | ||
393 | impl Display for Foo { | 395 | impl Display for Foo { |
394 | } | 396 | } |
@@ -407,7 +409,7 @@ impl std::fmt::Display<|> for Foo { | |||
407 | } | 409 | } |
408 | ", | 410 | ", |
409 | r" | 411 | r" |
410 | use std::fmt::{Display, nested::Debug}; | 412 | use std::fmt::{nested::Debug, Display}; |
411 | 413 | ||
412 | impl Display for Foo { | 414 | impl Display for Foo { |
413 | } | 415 | } |
@@ -427,11 +429,12 @@ use crate::{ | |||
427 | 429 | ||
428 | fn foo() { crate::ty::lower<|>::trait_env() } | 430 | fn foo() { crate::ty::lower<|>::trait_env() } |
429 | ", | 431 | ", |
432 | // FIXME(veykril): formatting broke here | ||
430 | r" | 433 | r" |
431 | use crate::{ | 434 | use crate::{ |
432 | ty::{Substs, Ty, lower}, | 435 | ty::{Substs, Ty}, |
433 | AssocItem, | 436 | AssocItem, |
434 | }; | 437 | ty::lower}; |
435 | 438 | ||
436 | fn foo() { lower::trait_env() } | 439 | fn foo() { lower::trait_env() } |
437 | ", | 440 | ", |
@@ -451,6 +454,8 @@ impl foo::Debug<|> for Foo { | |||
451 | r" | 454 | r" |
452 | use std::fmt as foo; | 455 | use std::fmt as foo; |
453 | 456 | ||
457 | use foo::Debug; | ||
458 | |||
454 | impl Debug for Foo { | 459 | impl Debug for Foo { |
455 | } | 460 | } |
456 | ", | 461 | ", |
@@ -515,6 +520,7 @@ fn main() { | |||
515 | ", | 520 | ", |
516 | r" | 521 | r" |
517 | #![allow(dead_code)] | 522 | #![allow(dead_code)] |
523 | |||
518 | use std::fmt::Debug; | 524 | use std::fmt::Debug; |
519 | 525 | ||
520 | fn main() { | 526 | fn main() { |
@@ -627,7 +633,7 @@ fn main() { | |||
627 | } | 633 | } |
628 | ", | 634 | ", |
629 | r" | 635 | r" |
630 | use std::fmt::{self, Display}; | 636 | use std::fmt::{Display, self}; |
631 | 637 | ||
632 | fn main() { | 638 | fn main() { |
633 | fmt; | 639 | fmt; |
@@ -647,9 +653,8 @@ impl std::io<|> for Foo { | |||
647 | } | 653 | } |
648 | ", | 654 | ", |
649 | r" | 655 | r" |
650 | use std::io; | ||
651 | |||
652 | pub use std::fmt; | 656 | pub use std::fmt; |
657 | use std::io; | ||
653 | 658 | ||
654 | impl io for Foo { | 659 | impl io for Foo { |
655 | } | 660 | } |
@@ -668,9 +673,8 @@ impl std::io<|> for Foo { | |||
668 | } | 673 | } |
669 | ", | 674 | ", |
670 | r" | 675 | r" |
671 | use std::io; | ||
672 | |||
673 | pub(crate) use std::fmt; | 676 | pub(crate) use std::fmt; |
677 | use std::io; | ||
674 | 678 | ||
675 | impl io for Foo { | 679 | impl io for Foo { |
676 | } | 680 | } |
diff --git a/crates/assists/src/utils.rs b/crates/assists/src/utils.rs index daa7b64f7..7559ddd63 100644 --- a/crates/assists/src/utils.rs +++ b/crates/assists/src/utils.rs | |||
@@ -16,7 +16,7 @@ use syntax::{ | |||
16 | 16 | ||
17 | use crate::assist_config::SnippetCap; | 17 | use crate::assist_config::SnippetCap; |
18 | 18 | ||
19 | pub(crate) use insert_use::{find_insert_use_container, insert_use_statement}; | 19 | pub(crate) use insert_use::{insert_use, ImportScope, MergeBehaviour}; |
20 | 20 | ||
21 | pub(crate) fn unwrap_trivial_block(block: ast::BlockExpr) -> ast::Expr { | 21 | pub(crate) fn unwrap_trivial_block(block: ast::BlockExpr) -> ast::Expr { |
22 | extract_trivial_expression(&block) | 22 | extract_trivial_expression(&block) |
diff --git a/crates/assists/src/utils/insert_use.rs b/crates/assists/src/utils/insert_use.rs index 49096a67c..8a4c8520d 100644 --- a/crates/assists/src/utils/insert_use.rs +++ b/crates/assists/src/utils/insert_use.rs | |||
@@ -1,546 +1,724 @@ | |||
1 | //! Handle syntactic aspects of inserting a new `use`. | 1 | //! Handle syntactic aspects of inserting a new `use`. |
2 | // FIXME: rewrite according to the plan, outlined in | 2 | use std::iter::{self, successors}; |
3 | // https://github.com/rust-analyzer/rust-analyzer/issues/3301#issuecomment-592931553 | ||
4 | 3 | ||
5 | use std::iter::successors; | 4 | use algo::skip_trivia_token; |
6 | 5 | use ast::{ | |
7 | use either::Either; | 6 | edit::{AstNodeEdit, IndentLevel}, |
7 | PathSegmentKind, VisibilityOwner, | ||
8 | }; | ||
8 | use syntax::{ | 9 | use syntax::{ |
9 | ast::{self, NameOwner, VisibilityOwner}, | 10 | algo, |
10 | AstNode, AstToken, Direction, SmolStr, | 11 | ast::{self, make, AstNode}, |
11 | SyntaxKind::{PATH, PATH_SEGMENT}, | 12 | Direction, InsertPosition, SyntaxElement, SyntaxNode, T, |
12 | SyntaxNode, SyntaxToken, T, | ||
13 | }; | 13 | }; |
14 | use text_edit::TextEditBuilder; | 14 | use test_utils::mark; |
15 | |||
16 | use crate::assist_context::AssistContext; | ||
17 | |||
18 | /// Determines the containing syntax node in which to insert a `use` statement affecting `position`. | ||
19 | pub(crate) fn find_insert_use_container( | ||
20 | position: &SyntaxNode, | ||
21 | ctx: &AssistContext, | ||
22 | ) -> Option<Either<ast::ItemList, ast::SourceFile>> { | ||
23 | ctx.sema.ancestors_with_macros(position.clone()).find_map(|n| { | ||
24 | if let Some(module) = ast::Module::cast(n.clone()) { | ||
25 | return module.item_list().map(|it| Either::Left(it)); | ||
26 | } | ||
27 | Some(Either::Right(ast::SourceFile::cast(n)?)) | ||
28 | }) | ||
29 | } | ||
30 | 15 | ||
31 | /// Creates and inserts a use statement for the given path to import. | 16 | #[derive(Debug)] |
32 | /// The use statement is inserted in the scope most appropriate to the | 17 | pub enum ImportScope { |
33 | /// the cursor position given, additionally merged with the existing use imports. | 18 | File(ast::SourceFile), |
34 | pub(crate) fn insert_use_statement( | 19 | Module(ast::ItemList), |
35 | // Ideally the position of the cursor, used to | ||
36 | position: &SyntaxNode, | ||
37 | path_to_import: &str, | ||
38 | ctx: &AssistContext, | ||
39 | builder: &mut TextEditBuilder, | ||
40 | ) { | ||
41 | let target = path_to_import.split("::").map(SmolStr::new).collect::<Vec<_>>(); | ||
42 | let container = find_insert_use_container(position, ctx); | ||
43 | |||
44 | if let Some(container) = container { | ||
45 | let syntax = container.either(|l| l.syntax().clone(), |r| r.syntax().clone()); | ||
46 | let action = best_action_for_target(syntax, position.clone(), &target); | ||
47 | make_assist(&action, &target, builder); | ||
48 | } | ||
49 | } | 20 | } |
50 | 21 | ||
51 | fn collect_path_segments_raw( | 22 | impl ImportScope { |
52 | segments: &mut Vec<ast::PathSegment>, | 23 | pub fn from(syntax: SyntaxNode) -> Option<Self> { |
53 | mut path: ast::Path, | 24 | if let Some(module) = ast::Module::cast(syntax.clone()) { |
54 | ) -> Option<usize> { | 25 | module.item_list().map(ImportScope::Module) |
55 | let oldlen = segments.len(); | 26 | } else if let this @ Some(_) = ast::SourceFile::cast(syntax.clone()) { |
56 | loop { | 27 | this.map(ImportScope::File) |
57 | let mut children = path.syntax().children_with_tokens(); | 28 | } else { |
58 | let (first, second, third) = ( | 29 | ast::ItemList::cast(syntax).map(ImportScope::Module) |
59 | children.next().map(|n| (n.clone(), n.kind())), | ||
60 | children.next().map(|n| (n.clone(), n.kind())), | ||
61 | children.next().map(|n| (n.clone(), n.kind())), | ||
62 | ); | ||
63 | match (first, second, third) { | ||
64 | (Some((subpath, PATH)), Some((_, T![::])), Some((segment, PATH_SEGMENT))) => { | ||
65 | path = ast::Path::cast(subpath.as_node()?.clone())?; | ||
66 | segments.push(ast::PathSegment::cast(segment.as_node()?.clone())?); | ||
67 | } | ||
68 | (Some((segment, PATH_SEGMENT)), _, _) => { | ||
69 | segments.push(ast::PathSegment::cast(segment.as_node()?.clone())?); | ||
70 | break; | ||
71 | } | ||
72 | (_, _, _) => return None, | ||
73 | } | 30 | } |
74 | } | 31 | } |
75 | // We need to reverse only the new added segments | ||
76 | let only_new_segments = segments.split_at_mut(oldlen).1; | ||
77 | only_new_segments.reverse(); | ||
78 | Some(segments.len() - oldlen) | ||
79 | } | ||
80 | 32 | ||
81 | fn fmt_segments_raw(segments: &[SmolStr], buf: &mut String) { | 33 | /// Determines the containing syntax node in which to insert a `use` statement affecting `position`. |
82 | let mut iter = segments.iter(); | 34 | pub(crate) fn find_insert_use_container( |
83 | if let Some(s) = iter.next() { | 35 | position: &SyntaxNode, |
84 | buf.push_str(s); | 36 | ctx: &crate::assist_context::AssistContext, |
85 | } | 37 | ) -> Option<Self> { |
86 | for s in iter { | 38 | ctx.sema.ancestors_with_macros(position.clone()).find_map(Self::from) |
87 | buf.push_str("::"); | ||
88 | buf.push_str(s); | ||
89 | } | 39 | } |
90 | } | ||
91 | |||
92 | /// Returns the number of common segments. | ||
93 | fn compare_path_segments(left: &[SmolStr], right: &[ast::PathSegment]) -> usize { | ||
94 | left.iter().zip(right).take_while(|(l, r)| compare_path_segment(l, r)).count() | ||
95 | } | ||
96 | 40 | ||
97 | fn compare_path_segment(a: &SmolStr, b: &ast::PathSegment) -> bool { | 41 | pub(crate) fn as_syntax_node(&self) -> &SyntaxNode { |
98 | if let Some(kb) = b.kind() { | 42 | match self { |
99 | match kb { | 43 | ImportScope::File(file) => file.syntax(), |
100 | ast::PathSegmentKind::Name(nameref_b) => a == nameref_b.text(), | 44 | ImportScope::Module(item_list) => item_list.syntax(), |
101 | ast::PathSegmentKind::SelfKw => a == "self", | ||
102 | ast::PathSegmentKind::SuperKw => a == "super", | ||
103 | ast::PathSegmentKind::CrateKw => a == "crate", | ||
104 | ast::PathSegmentKind::Type { .. } => false, // not allowed in imports | ||
105 | } | 45 | } |
106 | } else { | ||
107 | false | ||
108 | } | 46 | } |
109 | } | ||
110 | |||
111 | fn compare_path_segment_with_name(a: &SmolStr, b: &ast::Name) -> bool { | ||
112 | a == b.text() | ||
113 | } | ||
114 | |||
115 | #[derive(Clone, Debug)] | ||
116 | enum ImportAction { | ||
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 | } | ||
142 | 47 | ||
143 | impl ImportAction { | 48 | fn indent_level(&self) -> IndentLevel { |
144 | fn add_new_use(anchor: Option<SyntaxNode>, add_after_anchor: bool) -> Self { | 49 | match self { |
145 | ImportAction::AddNewUse { anchor, add_after_anchor } | 50 | ImportScope::File(file) => file.indent_level(), |
146 | } | 51 | ImportScope::Module(item_list) => item_list.indent_level() + 1, |
147 | |||
148 | fn add_nested_import( | ||
149 | common_segments: usize, | ||
150 | path_to_split: ast::Path, | ||
151 | first_segment_to_split: Option<ast::PathSegment>, | ||
152 | add_self: bool, | ||
153 | ) -> Self { | ||
154 | ImportAction::AddNestedImport { | ||
155 | common_segments, | ||
156 | path_to_split, | ||
157 | first_segment_to_split, | ||
158 | add_self, | ||
159 | } | 52 | } |
160 | } | 53 | } |
161 | 54 | ||
162 | fn add_in_tree_list( | 55 | fn first_insert_pos(&self) -> (InsertPosition<SyntaxElement>, AddBlankLine) { |
163 | common_segments: usize, | 56 | match self { |
164 | tree_list: ast::UseTreeList, | 57 | ImportScope::File(_) => (InsertPosition::First, AddBlankLine::AfterTwice), |
165 | add_self: bool, | 58 | // don't insert the imports before the item list's opening curly brace |
166 | ) -> Self { | 59 | ImportScope::Module(item_list) => item_list |
167 | ImportAction::AddInTreeList { common_segments, tree_list, add_self } | 60 | .l_curly_token() |
168 | } | 61 | .map(|b| (InsertPosition::After(b.into()), AddBlankLine::Around)) |
169 | 62 | .unwrap_or((InsertPosition::First, AddBlankLine::AfterTwice)), | |
170 | fn better(left: ImportAction, right: ImportAction) -> ImportAction { | ||
171 | if left.is_better(&right) { | ||
172 | left | ||
173 | } else { | ||
174 | right | ||
175 | } | 63 | } |
176 | } | 64 | } |
177 | 65 | ||
178 | fn is_better(&self, other: &ImportAction) -> bool { | 66 | fn insert_pos_after_inner_attribute(&self) -> (InsertPosition<SyntaxElement>, AddBlankLine) { |
179 | match (self, other) { | 67 | // check if the scope has inner attributes, we dont want to insert in front of them |
180 | (ImportAction::Nothing, _) => true, | 68 | match self |
181 | (ImportAction::AddInTreeList { .. }, ImportAction::Nothing) => false, | 69 | .as_syntax_node() |
182 | ( | 70 | .children() |
183 | ImportAction::AddNestedImport { common_segments: n, .. }, | 71 | // no flat_map here cause we want to short circuit the iterator |
184 | ImportAction::AddInTreeList { common_segments: m, .. }, | 72 | .map(ast::Attr::cast) |
185 | ) | 73 | .take_while(|attr| { |
186 | | ( | 74 | attr.as_ref().map(|attr| attr.kind() == ast::AttrKind::Inner).unwrap_or(false) |
187 | ImportAction::AddInTreeList { common_segments: n, .. }, | 75 | }) |
188 | ImportAction::AddNestedImport { common_segments: m, .. }, | 76 | .last() |
189 | ) | 77 | .flatten() |
190 | | ( | 78 | { |
191 | ImportAction::AddInTreeList { common_segments: n, .. }, | 79 | Some(attr) => { |
192 | ImportAction::AddInTreeList { common_segments: m, .. }, | 80 | (InsertPosition::After(attr.syntax().clone().into()), AddBlankLine::BeforeTwice) |
193 | ) | 81 | } |
194 | | ( | 82 | None => self.first_insert_pos(), |
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 | } | 83 | } |
203 | } | 84 | } |
204 | } | 85 | } |
205 | 86 | ||
206 | // Find out the best ImportAction to import target path against current_use_tree. | 87 | /// Insert an import path into the given file/node. A `merge` value of none indicates that no import merging is allowed to occur. |
207 | // If current_use_tree has a nested import the function gets called recursively on every UseTree inside a UseTreeList. | 88 | pub(crate) fn insert_use( |
208 | fn walk_use_tree_for_best_action( | 89 | scope: &ImportScope, |
209 | current_path_segments: &mut Vec<ast::PathSegment>, // buffer containing path segments | 90 | path: ast::Path, |
210 | current_parent_use_tree_list: Option<ast::UseTreeList>, // will be Some value if we are in a nested import | 91 | merge: Option<MergeBehaviour>, |
211 | current_use_tree: ast::UseTree, // the use tree we are currently examinating | 92 | ) -> SyntaxNode { |
212 | target: &[SmolStr], // the path we want to import | 93 | let use_item = make::use_(make::use_tree(path.clone(), None, None, false)); |
213 | ) -> ImportAction { | 94 | // merge into existing imports if possible |
214 | // We save the number of segments in the buffer so we can restore the correct segments | 95 | if let Some(mb) = merge { |
215 | // before returning. Recursive call will add segments so we need to delete them. | 96 | for existing_use in scope.as_syntax_node().children().filter_map(ast::Use::cast) { |
216 | let prev_len = current_path_segments.len(); | 97 | if let Some(merged) = try_merge_imports(&existing_use, &use_item, mb) { |
217 | 98 | let to_delete: SyntaxElement = existing_use.syntax().clone().into(); | |
218 | let tree_list = current_use_tree.use_tree_list(); | 99 | let to_delete = to_delete.clone()..=to_delete; |
219 | let alias = current_use_tree.rename(); | 100 | let to_insert = iter::once(merged.syntax().clone().into()); |
220 | 101 | return algo::replace_children(scope.as_syntax_node(), to_delete, to_insert); | |
221 | let path = match current_use_tree.path() { | 102 | } |
222 | Some(path) => path, | ||
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 | |||
236 | // This can happen only if current_use_tree is a direct child of a UseItem | ||
237 | if let Some(name) = alias.and_then(|it| it.name()) { | ||
238 | if compare_path_segment_with_name(&target[0], &name) { | ||
239 | return ImportAction::Nothing; | ||
240 | } | 103 | } |
241 | } | 104 | } |
242 | 105 | ||
243 | collect_path_segments_raw(current_path_segments, path.clone()); | 106 | // either we weren't allowed to merge or there is no import that fits the merge conditions |
244 | 107 | // so look for the place we have to insert to | |
245 | // We compare only the new segments added in the line just above. | 108 | let (insert_position, add_blank) = find_insert_position(scope, path); |
246 | // The first prev_len segments were already compared in 'parent' recursive calls. | 109 | |
247 | let left = target.split_at(prev_len).1; | 110 | let to_insert: Vec<SyntaxElement> = { |
248 | let right = current_path_segments.split_at(prev_len).1; | 111 | let mut buf = Vec::new(); |
249 | let common = compare_path_segments(left, &right); | 112 | |
250 | let mut action = match common { | 113 | match add_blank { |
251 | 0 => ImportAction::add_new_use( | 114 | AddBlankLine::Before | AddBlankLine::Around => { |
252 | // e.g: target is std::fmt and we can have | 115 | buf.push(make::tokens::single_newline().into()) |
253 | // use foo::bar | ||
254 | // We add a brand new use statement | ||
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 | } | 116 | } |
117 | AddBlankLine::BeforeTwice => buf.push(make::tokens::blank_line().into()), | ||
118 | _ => (), | ||
285 | } | 119 | } |
286 | common if common != left.len() && left.len() == right.len() => { | 120 | |
287 | // e.g: target is std::fmt and we have | 121 | if let ident_level @ 1..=usize::MAX = scope.indent_level().0 as usize { |
288 | // use std::io; | 122 | // FIXME: this alone doesnt properly re-align all cases |
289 | // We need to split. | 123 | buf.push(make::tokens::whitespace(&" ".repeat(4 * ident_level)).into()); |
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 | } | 124 | } |
298 | common if common == right.len() && left.len() > right.len() => { | 125 | buf.push(use_item.syntax().clone().into()); |
299 | // e.g: target is std::fmt and we can have | 126 | |
300 | // 1- use std; | 127 | match add_blank { |
301 | // 2- use std::{ ... }; | 128 | AddBlankLine::After | AddBlankLine::Around => { |
302 | 129 | buf.push(make::tokens::single_newline().into()) | |
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 | } | 130 | } |
332 | better_action | 131 | AddBlankLine::AfterTwice => buf.push(make::tokens::blank_line().into()), |
333 | } | 132 | _ => (), |
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 | } | 133 | } |
356 | _ => unreachable!(), | ||
357 | }; | ||
358 | 134 | ||
359 | // If we are inside a UseTreeList adding a use statement become adding to the existing | 135 | buf |
360 | // tree list. | ||
361 | action = match (current_parent_use_tree_list, action.clone()) { | ||
362 | (Some(use_tree_list), ImportAction::AddNewUse { .. }) => { | ||
363 | ImportAction::add_in_tree_list(prev_len, use_tree_list, false) | ||
364 | } | ||
365 | (_, _) => action, | ||
366 | }; | 136 | }; |
367 | 137 | ||
368 | // We remove the segments added | 138 | algo::insert_children(scope.as_syntax_node(), insert_position, to_insert) |
369 | current_path_segments.truncate(prev_len); | ||
370 | action | ||
371 | } | 139 | } |
372 | 140 | ||
373 | fn best_action_for_target( | 141 | fn try_merge_imports( |
374 | container: SyntaxNode, | 142 | old: &ast::Use, |
375 | anchor: SyntaxNode, | 143 | new: &ast::Use, |
376 | target: &[SmolStr], | 144 | merge_behaviour: MergeBehaviour, |
377 | ) -> ImportAction { | 145 | ) -> Option<ast::Use> { |
378 | let mut storage = Vec::with_capacity(16); // this should be the only allocation | 146 | // don't merge into re-exports |
379 | let best_action = container | 147 | if old.visibility().and_then(|vis| vis.pub_token()).is_some() { |
380 | .children() | 148 | return None; |
381 | .filter_map(ast::Use::cast) | 149 | } |
382 | .filter(|u| u.visibility().is_none()) | 150 | let old_tree = old.use_tree()?; |
383 | .filter_map(|it| it.use_tree()) | 151 | let new_tree = new.use_tree()?; |
384 | .map(|u| walk_use_tree_for_best_action(&mut storage, None, u, target)) | 152 | let merged = try_merge_trees(&old_tree, &new_tree, merge_behaviour)?; |
385 | .fold(None, |best, a| match best { | 153 | Some(old.with_use_tree(merged)) |
386 | Some(best) => Some(ImportAction::better(best, a)), | 154 | } |
387 | None => Some(a), | ||
388 | }); | ||
389 | 155 | ||
390 | match best_action { | 156 | /// Simple function that checks if a UseTreeList is deeper than one level |
391 | Some(action) => action, | 157 | fn use_tree_list_is_nested(tl: &ast::UseTreeList) -> bool { |
392 | None => { | 158 | tl.use_trees().any(|use_tree| { |
393 | // We have no action and no UseItem was found in container so we find | 159 | use_tree.use_tree_list().is_some() || use_tree.path().and_then(|p| p.qualifier()).is_some() |
394 | // another item and we use it as anchor. | 160 | }) |
395 | // If there are no items above, we choose the target path itself as anchor. | 161 | } |
396 | // todo: we should include even whitespace blocks as anchor candidates | ||
397 | let anchor = container.children().next().or_else(|| Some(anchor)); | ||
398 | 162 | ||
399 | let add_after_anchor = anchor | 163 | // FIXME: currently this merely prepends the new tree into old, ideally it would insert the items in a sorted fashion |
400 | .clone() | 164 | pub fn try_merge_trees( |
401 | .and_then(ast::Attr::cast) | 165 | old: &ast::UseTree, |
402 | .map(|attr| attr.kind() == ast::AttrKind::Inner) | 166 | new: &ast::UseTree, |
403 | .unwrap_or(false); | 167 | merge_behaviour: MergeBehaviour, |
404 | ImportAction::add_new_use(anchor, add_after_anchor) | 168 | ) -> Option<ast::UseTree> { |
405 | } | 169 | let lhs_path = old.path()?; |
170 | let rhs_path = new.path()?; | ||
171 | |||
172 | let (lhs_prefix, rhs_prefix) = common_prefix(&lhs_path, &rhs_path)?; | ||
173 | let lhs = old.split_prefix(&lhs_prefix); | ||
174 | let rhs = new.split_prefix(&rhs_prefix); | ||
175 | let lhs_tl = lhs.use_tree_list()?; | ||
176 | let rhs_tl = rhs.use_tree_list()?; | ||
177 | |||
178 | // if we are only allowed to merge the last level check if the split off paths are only one level deep | ||
179 | if merge_behaviour == MergeBehaviour::Last | ||
180 | && (use_tree_list_is_nested(&lhs_tl) || use_tree_list_is_nested(&rhs_tl)) | ||
181 | { | ||
182 | mark::hit!(test_last_merge_too_long); | ||
183 | return None; | ||
406 | } | 184 | } |
407 | } | ||
408 | 185 | ||
409 | fn make_assist(action: &ImportAction, target: &[SmolStr], edit: &mut TextEditBuilder) { | 186 | let should_insert_comma = lhs_tl |
410 | match action { | 187 | .r_curly_token() |
411 | ImportAction::AddNewUse { anchor, add_after_anchor } => { | 188 | .and_then(|it| skip_trivia_token(it.prev_token()?, Direction::Prev)) |
412 | make_assist_add_new_use(anchor, *add_after_anchor, target, edit) | 189 | .map(|it| it.kind()) |
413 | } | 190 | != Some(T![,]); |
414 | ImportAction::AddInTreeList { common_segments, tree_list, add_self } => { | 191 | let mut to_insert: Vec<SyntaxElement> = Vec::new(); |
415 | // We know that the fist n segments already exists in the use statement we want | 192 | if should_insert_comma { |
416 | // to modify, so we want to add only the last target.len() - n segments. | 193 | to_insert.push(make::token(T![,]).into()); |
417 | let segments_to_add = target.split_at(*common_segments).1; | 194 | to_insert.push(make::tokens::single_space().into()); |
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 | } | 195 | } |
196 | to_insert.extend( | ||
197 | rhs_tl.syntax().children_with_tokens().filter(|it| !matches!(it.kind(), T!['{'] | T!['}'])), | ||
198 | ); | ||
199 | let pos = InsertPosition::Before(lhs_tl.r_curly_token()?.into()); | ||
200 | let use_tree_list = lhs_tl.insert_children(pos, to_insert); | ||
201 | Some(lhs.with_use_tree_list(use_tree_list)) | ||
437 | } | 202 | } |
438 | 203 | ||
439 | fn make_assist_add_new_use( | 204 | /// Traverses both paths until they differ, returning the common prefix of both. |
440 | anchor: &Option<SyntaxNode>, | 205 | fn common_prefix(lhs: &ast::Path, rhs: &ast::Path) -> Option<(ast::Path, ast::Path)> { |
441 | after: bool, | 206 | let mut res = None; |
442 | target: &[SmolStr], | 207 | let mut lhs_curr = first_path(&lhs); |
443 | edit: &mut TextEditBuilder, | 208 | let mut rhs_curr = first_path(&rhs); |
444 | ) { | 209 | loop { |
445 | if let Some(anchor) = anchor { | 210 | match (lhs_curr.segment(), rhs_curr.segment()) { |
446 | let indent = leading_indent(anchor); | 211 | (Some(lhs), Some(rhs)) if lhs.syntax().text() == rhs.syntax().text() => (), |
447 | let mut buf = String::new(); | 212 | _ => break, |
448 | if after { | ||
449 | buf.push_str("\n"); | ||
450 | if let Some(spaces) = &indent { | ||
451 | buf.push_str(spaces); | ||
452 | } | ||
453 | } | 213 | } |
454 | buf.push_str("use "); | 214 | res = Some((lhs_curr.clone(), rhs_curr.clone())); |
455 | fmt_segments_raw(target, &mut buf); | 215 | |
456 | buf.push_str(";"); | 216 | match lhs_curr.parent_path().zip(rhs_curr.parent_path()) { |
457 | if !after { | 217 | Some((lhs, rhs)) => { |
458 | buf.push_str("\n\n"); | 218 | lhs_curr = lhs; |
459 | if let Some(spaces) = &indent { | 219 | rhs_curr = rhs; |
460 | buf.push_str(&spaces); | ||
461 | } | 220 | } |
221 | _ => break, | ||
462 | } | 222 | } |
463 | let position = if after { anchor.text_range().end() } else { anchor.text_range().start() }; | ||
464 | edit.insert(position, buf); | ||
465 | } | 223 | } |
224 | |||
225 | res | ||
466 | } | 226 | } |
467 | 227 | ||
468 | fn make_assist_add_in_tree_list( | 228 | /// What type of merges are allowed. |
469 | tree_list: &ast::UseTreeList, | 229 | #[derive(Copy, Clone, PartialEq, Eq)] |
470 | target: &[SmolStr], | 230 | pub enum MergeBehaviour { |
471 | add_self: bool, | 231 | /// Merge everything together creating deeply nested imports. |
472 | edit: &mut TextEditBuilder, | 232 | Full, |
473 | ) { | 233 | /// Only merge the last import level, doesn't allow import nesting. |
474 | let last = tree_list.use_trees().last(); | 234 | Last, |
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 | } | ||
493 | } | 235 | } |
494 | 236 | ||
495 | fn make_assist_add_nested_import( | 237 | #[derive(Eq, PartialEq, PartialOrd, Ord)] |
496 | path: &ast::Path, | 238 | enum ImportGroup { |
497 | first_segment_to_split: &Option<ast::PathSegment>, | 239 | // the order here defines the order of new group inserts |
498 | target: &[SmolStr], | 240 | Std, |
499 | add_self: bool, | 241 | ExternCrate, |
500 | edit: &mut TextEditBuilder, | 242 | ThisCrate, |
501 | ) { | 243 | ThisModule, |
502 | let use_tree = path.syntax().ancestors().find_map(ast::UseTree::cast); | 244 | SuperModule, |
503 | if let Some(use_tree) = use_tree { | 245 | } |
504 | let (start, add_colon_colon) = if let Some(first_segment_to_split) = first_segment_to_split | 246 | |
505 | { | 247 | impl ImportGroup { |
506 | (first_segment_to_split.syntax().text_range().start(), false) | 248 | fn new(path: &ast::Path) -> ImportGroup { |
507 | } else { | 249 | let default = ImportGroup::ExternCrate; |
508 | (use_tree.syntax().text_range().end(), true) | 250 | |
251 | let first_segment = match first_segment(path) { | ||
252 | Some(it) => it, | ||
253 | None => return default, | ||
509 | }; | 254 | }; |
510 | let end = use_tree.syntax().text_range().end(); | ||
511 | 255 | ||
512 | let mut buf = String::new(); | 256 | let kind = first_segment.kind().unwrap_or(PathSegmentKind::SelfKw); |
513 | if add_colon_colon { | 257 | match kind { |
514 | buf.push_str("::"); | 258 | PathSegmentKind::SelfKw => ImportGroup::ThisModule, |
515 | } | 259 | PathSegmentKind::SuperKw => ImportGroup::SuperModule, |
516 | buf.push_str("{"); | 260 | PathSegmentKind::CrateKw => ImportGroup::ThisCrate, |
517 | if add_self { | 261 | PathSegmentKind::Name(name) => match name.text().as_str() { |
518 | buf.push_str("self, "); | 262 | "std" => ImportGroup::Std, |
519 | } | 263 | "core" => ImportGroup::Std, |
520 | fmt_segments_raw(target, &mut buf); | 264 | // FIXME: can be ThisModule as well |
521 | if !target.is_empty() { | 265 | _ => ImportGroup::ExternCrate, |
522 | buf.push_str(", "); | 266 | }, |
267 | PathSegmentKind::Type { .. } => unreachable!(), | ||
523 | } | 268 | } |
524 | edit.insert(start, buf); | ||
525 | edit.insert(end, "}".to_string()); | ||
526 | } | 269 | } |
527 | } | 270 | } |
528 | 271 | ||
529 | /// If the node is on the beginning of the line, calculate indent. | 272 | fn first_segment(path: &ast::Path) -> Option<ast::PathSegment> { |
530 | fn leading_indent(node: &SyntaxNode) -> Option<SmolStr> { | 273 | first_path(path).segment() |
531 | for token in prev_tokens(node.first_token()?) { | 274 | } |
532 | if let Some(ws) = ast::Whitespace::cast(token.clone()) { | 275 | |
533 | let ws_text = ws.text(); | 276 | fn first_path(path: &ast::Path) -> ast::Path { |
534 | if let Some(pos) = ws_text.rfind('\n') { | 277 | successors(Some(path.clone()), ast::Path::qualifier).last().unwrap() |
535 | return Some(ws_text[pos + 1..].into()); | 278 | } |
279 | |||
280 | fn segment_iter(path: &ast::Path) -> impl Iterator<Item = ast::PathSegment> + Clone { | ||
281 | path.syntax().children().flat_map(ast::PathSegment::cast) | ||
282 | } | ||
283 | |||
284 | #[derive(PartialEq, Eq)] | ||
285 | enum AddBlankLine { | ||
286 | Before, | ||
287 | BeforeTwice, | ||
288 | Around, | ||
289 | After, | ||
290 | AfterTwice, | ||
291 | } | ||
292 | |||
293 | fn find_insert_position( | ||
294 | scope: &ImportScope, | ||
295 | insert_path: ast::Path, | ||
296 | ) -> (InsertPosition<SyntaxElement>, AddBlankLine) { | ||
297 | let group = ImportGroup::new(&insert_path); | ||
298 | let path_node_iter = scope | ||
299 | .as_syntax_node() | ||
300 | .children() | ||
301 | .filter_map(|node| ast::Use::cast(node.clone()).zip(Some(node))) | ||
302 | .flat_map(|(use_, node)| use_.use_tree().and_then(|tree| tree.path()).zip(Some(node))); | ||
303 | // Iterator that discards anything thats not in the required grouping | ||
304 | // This implementation allows the user to rearrange their import groups as this only takes the first group that fits | ||
305 | let group_iter = path_node_iter | ||
306 | .clone() | ||
307 | .skip_while(|(path, _)| ImportGroup::new(path) != group) | ||
308 | .take_while(|(path, _)| ImportGroup::new(path) == group); | ||
309 | |||
310 | let segments = segment_iter(&insert_path); | ||
311 | // 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 | ||
312 | let mut last = None; | ||
313 | // find the element that would come directly after our new import | ||
314 | let post_insert = | ||
315 | group_iter.inspect(|(_, node)| last = Some(node.clone())).find(|(path, _)| { | ||
316 | let check_segments = segment_iter(&path); | ||
317 | segments | ||
318 | .clone() | ||
319 | .zip(check_segments) | ||
320 | .flat_map(|(seg, seg2)| seg.name_ref().zip(seg2.name_ref())) | ||
321 | .all(|(l, r)| l.text() <= r.text()) | ||
322 | }); | ||
323 | match post_insert { | ||
324 | // insert our import before that element | ||
325 | Some((_, node)) => (InsertPosition::Before(node.into()), AddBlankLine::After), | ||
326 | // there is no element after our new import, so append it to the end of the group | ||
327 | None => match last { | ||
328 | Some(node) => (InsertPosition::After(node.into()), AddBlankLine::Before), | ||
329 | // the group we were looking for actually doesnt exist, so insert | ||
330 | None => { | ||
331 | // similar concept here to the `last` from above | ||
332 | let mut last = None; | ||
333 | // find the group that comes after where we want to insert | ||
334 | let post_group = path_node_iter | ||
335 | .inspect(|(_, node)| last = Some(node.clone())) | ||
336 | .find(|(p, _)| ImportGroup::new(p) > group); | ||
337 | match post_group { | ||
338 | Some((_, node)) => { | ||
339 | (InsertPosition::Before(node.into()), AddBlankLine::AfterTwice) | ||
340 | } | ||
341 | // there is no such group, so append after the last one | ||
342 | None => match last { | ||
343 | Some(node) => { | ||
344 | (InsertPosition::After(node.into()), AddBlankLine::BeforeTwice) | ||
345 | } | ||
346 | // there are no imports in this file at all | ||
347 | None => scope.insert_pos_after_inner_attribute(), | ||
348 | }, | ||
349 | } | ||
536 | } | 350 | } |
537 | } | 351 | }, |
538 | if token.text().contains('\n') { | 352 | } |
539 | break; | 353 | } |
540 | } | 354 | |
355 | #[cfg(test)] | ||
356 | mod tests { | ||
357 | use super::*; | ||
358 | |||
359 | use test_utils::assert_eq_text; | ||
360 | |||
361 | #[test] | ||
362 | fn insert_start() { | ||
363 | check_none( | ||
364 | "std::bar::AA", | ||
365 | r" | ||
366 | use std::bar::B; | ||
367 | use std::bar::D; | ||
368 | use std::bar::F; | ||
369 | use std::bar::G;", | ||
370 | r" | ||
371 | use std::bar::AA; | ||
372 | use std::bar::B; | ||
373 | use std::bar::D; | ||
374 | use std::bar::F; | ||
375 | use std::bar::G;", | ||
376 | ) | ||
377 | } | ||
378 | |||
379 | #[test] | ||
380 | fn insert_middle() { | ||
381 | check_none( | ||
382 | "std::bar::EE", | ||
383 | r" | ||
384 | use std::bar::A; | ||
385 | use std::bar::D; | ||
386 | use std::bar::F; | ||
387 | use std::bar::G;", | ||
388 | r" | ||
389 | use std::bar::A; | ||
390 | use std::bar::D; | ||
391 | use std::bar::EE; | ||
392 | use std::bar::F; | ||
393 | use std::bar::G;", | ||
394 | ) | ||
395 | } | ||
396 | |||
397 | #[test] | ||
398 | fn insert_end() { | ||
399 | check_none( | ||
400 | "std::bar::ZZ", | ||
401 | r" | ||
402 | use std::bar::A; | ||
403 | use std::bar::D; | ||
404 | use std::bar::F; | ||
405 | use std::bar::G;", | ||
406 | r" | ||
407 | use std::bar::A; | ||
408 | use std::bar::D; | ||
409 | use std::bar::F; | ||
410 | use std::bar::G; | ||
411 | use std::bar::ZZ;", | ||
412 | ) | ||
413 | } | ||
414 | |||
415 | #[test] | ||
416 | fn insert_middle_nested() { | ||
417 | check_none( | ||
418 | "std::bar::EE", | ||
419 | r" | ||
420 | use std::bar::A; | ||
421 | use std::bar::{D, Z}; // example of weird imports due to user | ||
422 | use std::bar::F; | ||
423 | use std::bar::G;", | ||
424 | r" | ||
425 | use std::bar::A; | ||
426 | use std::bar::EE; | ||
427 | use std::bar::{D, Z}; // example of weird imports due to user | ||
428 | use std::bar::F; | ||
429 | use std::bar::G;", | ||
430 | ) | ||
431 | } | ||
432 | |||
433 | #[test] | ||
434 | fn insert_middle_groups() { | ||
435 | check_none( | ||
436 | "foo::bar::GG", | ||
437 | r" | ||
438 | use std::bar::A; | ||
439 | use std::bar::D; | ||
440 | |||
441 | use foo::bar::F; | ||
442 | use foo::bar::H;", | ||
443 | r" | ||
444 | use std::bar::A; | ||
445 | use std::bar::D; | ||
446 | |||
447 | use foo::bar::F; | ||
448 | use foo::bar::GG; | ||
449 | use foo::bar::H;", | ||
450 | ) | ||
451 | } | ||
452 | |||
453 | #[test] | ||
454 | fn insert_first_matching_group() { | ||
455 | check_none( | ||
456 | "foo::bar::GG", | ||
457 | r" | ||
458 | use foo::bar::A; | ||
459 | use foo::bar::D; | ||
460 | |||
461 | use std; | ||
462 | |||
463 | use foo::bar::F; | ||
464 | use foo::bar::H;", | ||
465 | r" | ||
466 | use foo::bar::A; | ||
467 | use foo::bar::D; | ||
468 | use foo::bar::GG; | ||
469 | |||
470 | use std; | ||
471 | |||
472 | use foo::bar::F; | ||
473 | use foo::bar::H;", | ||
474 | ) | ||
475 | } | ||
476 | |||
477 | #[test] | ||
478 | fn insert_missing_group_std() { | ||
479 | check_none( | ||
480 | "std::fmt", | ||
481 | r" | ||
482 | use foo::bar::A; | ||
483 | use foo::bar::D;", | ||
484 | r" | ||
485 | use std::fmt; | ||
486 | |||
487 | use foo::bar::A; | ||
488 | use foo::bar::D;", | ||
489 | ) | ||
490 | } | ||
491 | |||
492 | #[test] | ||
493 | fn insert_missing_group_self() { | ||
494 | check_none( | ||
495 | "self::fmt", | ||
496 | r" | ||
497 | use foo::bar::A; | ||
498 | use foo::bar::D;", | ||
499 | r" | ||
500 | use foo::bar::A; | ||
501 | use foo::bar::D; | ||
502 | |||
503 | use self::fmt;", | ||
504 | ) | ||
505 | } | ||
506 | |||
507 | #[test] | ||
508 | fn insert_no_imports() { | ||
509 | check_full( | ||
510 | "foo::bar", | ||
511 | "fn main() {}", | ||
512 | r"use foo::bar; | ||
513 | |||
514 | fn main() {}", | ||
515 | ) | ||
516 | } | ||
517 | |||
518 | #[test] | ||
519 | fn insert_empty_file() { | ||
520 | // empty files will get two trailing newlines | ||
521 | // this is due to the test case insert_no_imports above | ||
522 | check_full( | ||
523 | "foo::bar", | ||
524 | "", | ||
525 | r"use foo::bar; | ||
526 | |||
527 | ", | ||
528 | ) | ||
529 | } | ||
530 | |||
531 | #[test] | ||
532 | fn insert_after_inner_attr() { | ||
533 | check_full( | ||
534 | "foo::bar", | ||
535 | r"#![allow(unused_imports)]", | ||
536 | r"#![allow(unused_imports)] | ||
537 | |||
538 | use foo::bar;", | ||
539 | ) | ||
541 | } | 540 | } |
542 | return None; | 541 | |
543 | fn prev_tokens(token: SyntaxToken) -> impl Iterator<Item = SyntaxToken> { | 542 | #[test] |
544 | successors(token.prev_token(), |token| token.prev_token()) | 543 | fn insert_after_inner_attr2() { |
544 | check_full( | ||
545 | "foo::bar", | ||
546 | r"#![allow(unused_imports)] | ||
547 | |||
548 | fn main() {}", | ||
549 | r"#![allow(unused_imports)] | ||
550 | |||
551 | use foo::bar; | ||
552 | |||
553 | fn main() {}", | ||
554 | ) | ||
555 | } | ||
556 | |||
557 | #[test] | ||
558 | fn merge_groups() { | ||
559 | check_last("std::io", r"use std::fmt;", r"use std::{fmt, io};") | ||
560 | } | ||
561 | |||
562 | #[test] | ||
563 | fn merge_groups_last() { | ||
564 | check_last( | ||
565 | "std::io", | ||
566 | r"use std::fmt::{Result, Display};", | ||
567 | r"use std::fmt::{Result, Display}; | ||
568 | use std::io;", | ||
569 | ) | ||
570 | } | ||
571 | |||
572 | #[test] | ||
573 | fn merge_groups_full() { | ||
574 | check_full( | ||
575 | "std::io", | ||
576 | r"use std::fmt::{Result, Display};", | ||
577 | r"use std::{fmt::{Result, Display}, io};", | ||
578 | ) | ||
579 | } | ||
580 | |||
581 | #[test] | ||
582 | fn merge_groups_long_full() { | ||
583 | check_full( | ||
584 | "std::foo::bar::Baz", | ||
585 | r"use std::foo::bar::Qux;", | ||
586 | r"use std::foo::bar::{Qux, Baz};", | ||
587 | ) | ||
588 | } | ||
589 | |||
590 | #[test] | ||
591 | fn merge_groups_long_last() { | ||
592 | check_last( | ||
593 | "std::foo::bar::Baz", | ||
594 | r"use std::foo::bar::Qux;", | ||
595 | r"use std::foo::bar::{Qux, Baz};", | ||
596 | ) | ||
597 | } | ||
598 | |||
599 | #[test] | ||
600 | fn merge_groups_long_full_list() { | ||
601 | check_full( | ||
602 | "std::foo::bar::Baz", | ||
603 | r"use std::foo::bar::{Qux, Quux};", | ||
604 | r"use std::foo::bar::{Qux, Quux, Baz};", | ||
605 | ) | ||
606 | } | ||
607 | |||
608 | #[test] | ||
609 | fn merge_groups_long_last_list() { | ||
610 | check_last( | ||
611 | "std::foo::bar::Baz", | ||
612 | r"use std::foo::bar::{Qux, Quux};", | ||
613 | r"use std::foo::bar::{Qux, Quux, Baz};", | ||
614 | ) | ||
615 | } | ||
616 | |||
617 | #[test] | ||
618 | fn merge_groups_long_full_nested() { | ||
619 | check_full( | ||
620 | "std::foo::bar::Baz", | ||
621 | r"use std::foo::bar::{Qux, quux::{Fez, Fizz}};", | ||
622 | r"use std::foo::bar::{Qux, quux::{Fez, Fizz}, Baz};", | ||
623 | ) | ||
624 | } | ||
625 | |||
626 | #[test] | ||
627 | fn merge_groups_long_last_nested() { | ||
628 | check_last( | ||
629 | "std::foo::bar::Baz", | ||
630 | r"use std::foo::bar::{Qux, quux::{Fez, Fizz}};", | ||
631 | r"use std::foo::bar::Baz; | ||
632 | use std::foo::bar::{Qux, quux::{Fez, Fizz}};", | ||
633 | ) | ||
634 | } | ||
635 | |||
636 | #[test] | ||
637 | fn merge_groups_skip_pub() { | ||
638 | check_full( | ||
639 | "std::io", | ||
640 | r"pub use std::fmt::{Result, Display};", | ||
641 | r"pub use std::fmt::{Result, Display}; | ||
642 | use std::io;", | ||
643 | ) | ||
644 | } | ||
645 | |||
646 | #[test] | ||
647 | fn merge_groups_skip_pub_crate() { | ||
648 | check_full( | ||
649 | "std::io", | ||
650 | r"pub(crate) use std::fmt::{Result, Display};", | ||
651 | r"pub(crate) use std::fmt::{Result, Display}; | ||
652 | use std::io;", | ||
653 | ) | ||
654 | } | ||
655 | |||
656 | #[test] | ||
657 | #[ignore] // FIXME: Support this | ||
658 | fn split_out_merge() { | ||
659 | check_last( | ||
660 | "std::fmt::Result", | ||
661 | r"use std::{fmt, io};", | ||
662 | r"use std::{self, fmt::Result}; | ||
663 | use std::io;", | ||
664 | ) | ||
665 | } | ||
666 | |||
667 | #[test] | ||
668 | fn merge_groups_self() { | ||
669 | check_full("std::fmt::Debug", r"use std::fmt;", r"use std::fmt::{self, Debug};") | ||
670 | } | ||
671 | |||
672 | #[test] | ||
673 | fn merge_self_glob() { | ||
674 | check_full( | ||
675 | "token::TokenKind", | ||
676 | r"use token::TokenKind::*;", | ||
677 | r"use token::TokenKind::{self::*, self};", | ||
678 | ) | ||
679 | } | ||
680 | |||
681 | #[test] | ||
682 | fn merge_last_too_long() { | ||
683 | mark::check!(test_last_merge_too_long); | ||
684 | check_last( | ||
685 | "foo::bar", | ||
686 | r"use foo::bar::baz::Qux;", | ||
687 | r"use foo::bar::baz::Qux; | ||
688 | use foo::bar;", | ||
689 | ); | ||
690 | } | ||
691 | |||
692 | fn check( | ||
693 | path: &str, | ||
694 | ra_fixture_before: &str, | ||
695 | ra_fixture_after: &str, | ||
696 | mb: Option<MergeBehaviour>, | ||
697 | ) { | ||
698 | let file = super::ImportScope::from( | ||
699 | ast::SourceFile::parse(ra_fixture_before).tree().syntax().clone(), | ||
700 | ) | ||
701 | .unwrap(); | ||
702 | let path = ast::SourceFile::parse(&format!("use {};", path)) | ||
703 | .tree() | ||
704 | .syntax() | ||
705 | .descendants() | ||
706 | .find_map(ast::Path::cast) | ||
707 | .unwrap(); | ||
708 | |||
709 | let result = insert_use(&file, path, mb).to_string(); | ||
710 | assert_eq_text!(&result, ra_fixture_after); | ||
711 | } | ||
712 | |||
713 | fn check_full(path: &str, ra_fixture_before: &str, ra_fixture_after: &str) { | ||
714 | check(path, ra_fixture_before, ra_fixture_after, Some(MergeBehaviour::Full)) | ||
715 | } | ||
716 | |||
717 | fn check_last(path: &str, ra_fixture_before: &str, ra_fixture_after: &str) { | ||
718 | check(path, ra_fixture_before, ra_fixture_after, Some(MergeBehaviour::Last)) | ||
719 | } | ||
720 | |||
721 | fn check_none(path: &str, ra_fixture_before: &str, ra_fixture_after: &str) { | ||
722 | check(path, ra_fixture_before, ra_fixture_after, None) | ||
545 | } | 723 | } |
546 | } | 724 | } |
diff --git a/crates/syntax/src/ast/make.rs b/crates/syntax/src/ast/make.rs index c2c938ad1..33f1ad7b3 100644 --- a/crates/syntax/src/ast/make.rs +++ b/crates/syntax/src/ast/make.rs | |||
@@ -339,7 +339,7 @@ pub mod tokens { | |||
339 | use crate::{ast, AstNode, Parse, SourceFile, SyntaxKind::*, SyntaxToken}; | 339 | use crate::{ast, AstNode, Parse, SourceFile, SyntaxKind::*, SyntaxToken}; |
340 | 340 | ||
341 | pub(super) static SOURCE_FILE: Lazy<Parse<SourceFile>> = | 341 | pub(super) static SOURCE_FILE: Lazy<Parse<SourceFile>> = |
342 | Lazy::new(|| SourceFile::parse("const C: <()>::Item = (1 != 1, 2 == 2, !true)\n;")); | 342 | Lazy::new(|| SourceFile::parse("const C: <()>::Item = (1 != 1, 2 == 2, !true)\n;\n\n")); |
343 | 343 | ||
344 | pub fn single_space() -> SyntaxToken { | 344 | pub fn single_space() -> SyntaxToken { |
345 | SOURCE_FILE | 345 | SOURCE_FILE |
@@ -379,6 +379,16 @@ pub mod tokens { | |||
379 | .unwrap() | 379 | .unwrap() |
380 | } | 380 | } |
381 | 381 | ||
382 | pub fn blank_line() -> SyntaxToken { | ||
383 | SOURCE_FILE | ||
384 | .tree() | ||
385 | .syntax() | ||
386 | .descendants_with_tokens() | ||
387 | .filter_map(|it| it.into_token()) | ||
388 | .find(|it| it.kind() == WHITESPACE && it.text().as_str() == "\n\n") | ||
389 | .unwrap() | ||
390 | } | ||
391 | |||
382 | pub struct WsBuilder(SourceFile); | 392 | pub struct WsBuilder(SourceFile); |
383 | 393 | ||
384 | impl WsBuilder { | 394 | impl WsBuilder { |