Skip to content

Commit 7de2a30

Browse files
committed
Fix import insertion breaking nested modules
1 parent 98e2f67 commit 7de2a30

File tree

5 files changed

+117
-62
lines changed

5 files changed

+117
-62
lines changed

crates/assists/src/handlers/auto_import.rs

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
use std::collections::BTreeSet;
22

3+
use ast::make;
34
use either::Either;
45
use hir::{
56
AsAssocItem, AssocItemContainer, ModPath, Module, ModuleDef, PathResolution, Semantics, Trait,
67
Type,
78
};
89
use ide_db::{imports_locator, RootDatabase};
10+
use insert_use::ImportScope;
911
use rustc_hash::FxHashSet;
1012
use syntax::{
1113
ast::{self, AstNode},
@@ -16,8 +18,6 @@ use crate::{
1618
utils::{insert_use, MergeBehaviour},
1719
AssistContext, AssistId, AssistKind, Assists, GroupLabel,
1820
};
19-
use ast::make;
20-
use insert_use::find_insert_use_container;
2121

2222
// Assist: auto_import
2323
//
@@ -47,8 +47,9 @@ pub(crate) fn auto_import(acc: &mut Assists, ctx: &AssistContext) -> Option<()>
4747

4848
let range = ctx.sema.original_range(&auto_import_assets.syntax_under_caret).range;
4949
let group = auto_import_assets.get_import_group_message();
50-
let container = find_insert_use_container(&auto_import_assets.syntax_under_caret, ctx)?;
51-
let syntax = container.either(|l| l.syntax().clone(), |r| r.syntax().clone());
50+
let scope =
51+
ImportScope::find_insert_use_container(&auto_import_assets.syntax_under_caret, ctx)?;
52+
let syntax = scope.as_syntax_node();
5253
for import in proposed_imports {
5354
acc.add_group(
5455
&group,
@@ -57,7 +58,7 @@ pub(crate) fn auto_import(acc: &mut Assists, ctx: &AssistContext) -> Option<()>
5758
range,
5859
|builder| {
5960
let new_syntax = insert_use(
60-
&syntax,
61+
&scope,
6162
make::path_from_text(&import.to_string()),
6263
Some(MergeBehaviour::Full),
6364
);

crates/assists/src/handlers/extract_struct_from_enum_variant.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ use crate::{
1515
AssistContext, AssistId, AssistKind, Assists,
1616
};
1717
use ast::make;
18-
use insert_use::find_insert_use_container;
18+
use insert_use::ImportScope;
1919

2020
// Assist: extract_struct_from_enum_variant
2121
//
@@ -110,11 +110,11 @@ fn insert_import(
110110
if let Some(mut mod_path) = mod_path {
111111
mod_path.segments.pop();
112112
mod_path.segments.push(variant_hir_name.clone());
113-
let container = find_insert_use_container(path.syntax(), ctx)?;
114-
let syntax = container.either(|l| l.syntax().clone(), |r| r.syntax().clone());
113+
let scope = ImportScope::find_insert_use_container(path.syntax(), ctx)?;
114+
let syntax = scope.as_syntax_node();
115115

116116
let new_syntax = insert_use(
117-
&syntax,
117+
&scope,
118118
make::path_from_text(&mod_path.to_string()),
119119
Some(MergeBehaviour::Full),
120120
);

crates/assists/src/handlers/replace_qualified_name_with_use.rs

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ use syntax::{algo::SyntaxRewriter, ast, match_ast, AstNode, SyntaxNode, TextRang
22
use test_utils::mark;
33

44
use crate::{
5-
utils::{find_insert_use_container, insert_use, MergeBehaviour},
5+
utils::{insert_use, ImportScope, MergeBehaviour},
66
AssistContext, AssistId, AssistKind, Assists,
77
};
88
use ast::make;
@@ -44,8 +44,8 @@ pub(crate) fn replace_qualified_name_with_use(
4444
};
4545

4646
let target = path.syntax().text_range();
47-
let container = find_insert_use_container(path.syntax(), ctx)?;
48-
let syntax = container.either(|l| l.syntax().clone(), |r| r.syntax().clone());
47+
let scope = ImportScope::find_insert_use_container(path.syntax(), ctx)?;
48+
let syntax = scope.as_syntax_node();
4949
acc.add(
5050
AssistId("replace_qualified_name_with_use", AssistKind::RefactorRewrite),
5151
"Replace qualified path with use",
@@ -56,12 +56,14 @@ pub(crate) fn replace_qualified_name_with_use(
5656
let mut rewriter = SyntaxRewriter::default();
5757
shorten_paths(&mut rewriter, syntax.clone(), path);
5858
let rewritten_syntax = rewriter.rewrite(&syntax);
59-
let new_syntax = insert_use(
60-
&rewritten_syntax,
61-
make::path_from_text(path_to_import),
62-
Some(MergeBehaviour::Full),
63-
);
64-
builder.replace(syntax.text_range(), new_syntax.to_string())
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+
}
6567
},
6668
)
6769
}

crates/assists/src/utils.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ use syntax::{
1616

1717
use crate::assist_config::SnippetCap;
1818

19-
pub(crate) use insert_use::{find_insert_use_container, insert_use, MergeBehaviour};
19+
pub(crate) use insert_use::{insert_use, ImportScope, MergeBehaviour};
2020

2121
pub(crate) fn unwrap_trivial_block(block: ast::BlockExpr) -> ast::Expr {
2222
extract_trivial_expression(&block)

crates/assists/src/utils/insert_use.rs

Lines changed: 95 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
use std::iter::{self, successors};
22

33
use algo::skip_trivia_token;
4-
use ast::{edit::AstNodeEdit, PathSegmentKind, VisibilityOwner};
5-
use either::Either;
4+
use ast::{
5+
edit::{AstNodeEdit, IndentLevel},
6+
PathSegmentKind, VisibilityOwner,
7+
};
68
use syntax::{
79
algo,
810
ast::{self, make, AstNode},
@@ -11,64 +13,129 @@ use syntax::{
1113

1214
use test_utils::mark;
1315

14-
/// Determines the containing syntax node in which to insert a `use` statement affecting `position`.
15-
pub(crate) fn find_insert_use_container(
16-
position: &SyntaxNode,
17-
ctx: &crate::assist_context::AssistContext,
18-
) -> Option<Either<ast::ItemList, ast::SourceFile>> {
19-
ctx.sema.ancestors_with_macros(position.clone()).find_map(|n| {
20-
if let Some(module) = ast::Module::cast(n.clone()) {
21-
module.item_list().map(Either::Left)
16+
#[derive(Debug)]
17+
pub enum ImportScope {
18+
File(ast::SourceFile),
19+
Module(ast::ItemList),
20+
}
21+
22+
impl ImportScope {
23+
pub fn from(syntax: SyntaxNode) -> Option<Self> {
24+
if let Some(module) = ast::Module::cast(syntax.clone()) {
25+
module.item_list().map(ImportScope::Module)
26+
} else if let this @ Some(_) = ast::SourceFile::cast(syntax.clone()) {
27+
this.map(ImportScope::File)
2228
} else {
23-
Some(Either::Right(ast::SourceFile::cast(n)?))
29+
ast::ItemList::cast(syntax).map(ImportScope::Module)
2430
}
25-
})
31+
}
32+
33+
/// Determines the containing syntax node in which to insert a `use` statement affecting `position`.
34+
pub(crate) fn find_insert_use_container(
35+
position: &SyntaxNode,
36+
ctx: &crate::assist_context::AssistContext,
37+
) -> Option<Self> {
38+
ctx.sema.ancestors_with_macros(position.clone()).find_map(Self::from)
39+
}
40+
41+
pub(crate) fn as_syntax_node(&self) -> &SyntaxNode {
42+
match self {
43+
ImportScope::File(file) => file.syntax(),
44+
ImportScope::Module(item_list) => item_list.syntax(),
45+
}
46+
}
47+
48+
fn indent_level(&self) -> IndentLevel {
49+
match self {
50+
ImportScope::File(file) => file.indent_level(),
51+
ImportScope::Module(item_list) => item_list.indent_level() + 1,
52+
}
53+
}
54+
55+
fn first_insert_pos(&self) -> (InsertPosition<SyntaxElement>, AddBlankLine) {
56+
match self {
57+
ImportScope::File(_) => (InsertPosition::First, AddBlankLine::AfterTwice),
58+
// don't insert the impotrs before the item lists curly brace
59+
ImportScope::Module(item_list) => item_list
60+
.l_curly_token()
61+
.map(|b| (InsertPosition::After(b.into()), AddBlankLine::Around))
62+
.unwrap_or((InsertPosition::First, AddBlankLine::AfterTwice)),
63+
}
64+
}
65+
66+
fn insert_pos_after_inner_attribute(&self) -> (InsertPosition<SyntaxElement>, AddBlankLine) {
67+
// check if the scope has a inner attributes, we dont want to insert in front of it
68+
match self
69+
.as_syntax_node()
70+
.children()
71+
// no flat_map here cause we want to short circuit the iterator
72+
.map(ast::Attr::cast)
73+
.take_while(|attr| {
74+
attr.as_ref().map(|attr| attr.kind() == ast::AttrKind::Inner).unwrap_or(false)
75+
})
76+
.last()
77+
.flatten()
78+
{
79+
Some(attr) => {
80+
(InsertPosition::After(attr.syntax().clone().into()), AddBlankLine::BeforeTwice)
81+
}
82+
None => self.first_insert_pos(),
83+
}
84+
}
2685
}
2786

2887
/// Insert an import path into the given file/node. A `merge` value of none indicates that no import merging is allowed to occur.
29-
pub fn insert_use(
30-
where_: &SyntaxNode,
88+
pub(crate) fn insert_use(
89+
scope: &ImportScope,
3190
path: ast::Path,
3291
merge: Option<MergeBehaviour>,
3392
) -> SyntaxNode {
3493
let use_item = make::use_(make::use_tree(path.clone(), None, None, false));
3594
// merge into existing imports if possible
3695
if let Some(mb) = merge {
37-
for existing_use in where_.children().filter_map(ast::Use::cast) {
96+
for existing_use in scope.as_syntax_node().children().filter_map(ast::Use::cast) {
3897
if let Some(merged) = try_merge_imports(&existing_use, &use_item, mb) {
3998
let to_delete: SyntaxElement = existing_use.syntax().clone().into();
4099
let to_delete = to_delete.clone()..=to_delete;
41100
let to_insert = iter::once(merged.syntax().clone().into());
42-
return algo::replace_children(where_, to_delete, to_insert);
101+
return algo::replace_children(scope.as_syntax_node(), to_delete, to_insert);
43102
}
44103
}
45104
}
46105

47106
// either we weren't allowed to merge or there is no import that fits the merge conditions
48107
// so look for the place we have to insert to
49-
let (insert_position, add_blank) = find_insert_position(where_, path);
108+
let (insert_position, add_blank) = find_insert_position(scope, path);
50109

51110
let to_insert: Vec<SyntaxElement> = {
52111
let mut buf = Vec::new();
53112

54113
match add_blank {
55-
AddBlankLine::Before => buf.push(make::tokens::single_newline().into()),
114+
AddBlankLine::Before | AddBlankLine::Around => {
115+
buf.push(make::tokens::single_newline().into())
116+
}
56117
AddBlankLine::BeforeTwice => buf.push(make::tokens::blank_line().into()),
57118
_ => (),
58119
}
59120

121+
if let ident_level @ 1..=usize::MAX = scope.indent_level().0 as usize {
122+
// TODO: this alone doesnt properly re-align all cases
123+
buf.push(make::tokens::whitespace(&" ".repeat(4 * ident_level)).into());
124+
}
60125
buf.push(use_item.syntax().clone().into());
61126

62127
match add_blank {
63-
AddBlankLine::After => buf.push(make::tokens::single_newline().into()),
128+
AddBlankLine::After | AddBlankLine::Around => {
129+
buf.push(make::tokens::single_newline().into())
130+
}
64131
AddBlankLine::AfterTwice => buf.push(make::tokens::blank_line().into()),
65132
_ => (),
66133
}
67134

68135
buf
69136
};
70137

71-
algo::insert_children(where_, insert_position, to_insert)
138+
algo::insert_children(scope.as_syntax_node(), insert_position, to_insert)
72139
}
73140

74141
fn try_merge_imports(
@@ -218,16 +285,18 @@ fn segment_iter(path: &ast::Path) -> impl Iterator<Item = ast::PathSegment> + Cl
218285
enum AddBlankLine {
219286
Before,
220287
BeforeTwice,
288+
Around,
221289
After,
222290
AfterTwice,
223291
}
224292

225293
fn find_insert_position(
226-
scope: &SyntaxNode,
294+
scope: &ImportScope,
227295
insert_path: ast::Path,
228296
) -> (InsertPosition<SyntaxElement>, AddBlankLine) {
229297
let group = ImportGroup::new(&insert_path);
230298
let path_node_iter = scope
299+
.as_syntax_node()
231300
.children()
232301
.filter_map(|node| ast::Use::cast(node.clone()).zip(Some(node)))
233302
.flat_map(|(use_, node)| use_.use_tree().and_then(|tree| tree.path()).zip(Some(node)));
@@ -275,27 +344,7 @@ fn find_insert_position(
275344
(InsertPosition::After(node.into()), AddBlankLine::BeforeTwice)
276345
}
277346
// there are no imports in this file at all
278-
None => {
279-
// check if the scope has a inner attributes, we dont want to insert in front of it
280-
match scope
281-
.children()
282-
// no flat_map here cause we want to short circuit the iterator
283-
.map(ast::Attr::cast)
284-
.take_while(|attr| {
285-
attr.as_ref()
286-
.map(|attr| attr.kind() == ast::AttrKind::Inner)
287-
.unwrap_or(false)
288-
})
289-
.last()
290-
.flatten()
291-
{
292-
Some(attr) => (
293-
InsertPosition::After(attr.syntax().clone().into()),
294-
AddBlankLine::BeforeTwice,
295-
),
296-
None => (InsertPosition::First, AddBlankLine::AfterTwice),
297-
}
298-
}
347+
None => scope.insert_pos_after_inner_attribute(),
299348
},
300349
}
301350
}
@@ -640,7 +689,10 @@ use foo::bar;",
640689
ra_fixture_after: &str,
641690
mb: Option<MergeBehaviour>,
642691
) {
643-
let file = ast::SourceFile::parse(ra_fixture_before).tree().syntax().clone();
692+
let file = super::ImportScope::from(
693+
ast::SourceFile::parse(ra_fixture_before).tree().syntax().clone(),
694+
)
695+
.unwrap();
644696
let path = ast::SourceFile::parse(&format!("use {};", path))
645697
.tree()
646698
.syntax()

0 commit comments

Comments
 (0)