aboutsummaryrefslogtreecommitdiff
path: root/crates/ide_db/src/helpers/insert_use.rs
blob: 4852121a1d34e0bc533729eabe168805a970c037 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
//! Handle syntactic aspects of inserting a new `use`.
use std::cmp::Ordering;

use hir::Semantics;
use syntax::{
    algo,
    ast::{self, make, AstNode, ModuleItemOwner, PathSegmentKind},
    ted, AstToken, Direction, NodeOrToken, SyntaxNode, SyntaxToken,
};

use crate::{
    helpers::merge_imports::{try_merge_imports, use_tree_path_cmp, MergeBehavior},
    RootDatabase,
};

pub use hir::PrefixKind;

/// How imports should be grouped into use statements.
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum ImportGranularity {
    /// Do not change the granularity of any imports and preserve the original structure written by the developer.
    Preserve,
    /// Merge imports from the same crate into a single use statement.
    Crate,
    /// Merge imports from the same module into a single use statement.
    Module,
    /// Flatten imports so that each has its own use statement.
    Item,
}

impl ImportGranularity {
    pub fn merge_behavior(self) -> Option<MergeBehavior> {
        match self {
            ImportGranularity::Crate => Some(MergeBehavior::Crate),
            ImportGranularity::Module => Some(MergeBehavior::Module),
            ImportGranularity::Preserve | ImportGranularity::Item => None,
        }
    }
}

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct InsertUseConfig {
    pub granularity: ImportGranularity,
    pub prefix_kind: PrefixKind,
    pub group: bool,
}

#[derive(Debug, Clone)]
pub enum ImportScope {
    File(ast::SourceFile),
    Module(ast::ItemList),
}

impl ImportScope {
    pub fn from(syntax: SyntaxNode) -> Option<Self> {
        if let Some(module) = ast::Module::cast(syntax.clone()) {
            module.item_list().map(ImportScope::Module)
        } else if let this @ Some(_) = ast::SourceFile::cast(syntax.clone()) {
            this.map(ImportScope::File)
        } else {
            ast::ItemList::cast(syntax).map(ImportScope::Module)
        }
    }

    /// Determines the containing syntax node in which to insert a `use` statement affecting `position`.
    pub fn find_insert_use_container_with_macros(
        position: &SyntaxNode,
        sema: &Semantics<'_, RootDatabase>,
    ) -> Option<Self> {
        sema.ancestors_with_macros(position.clone()).find_map(Self::from)
    }

    /// Determines the containing syntax node in which to insert a `use` statement affecting `position`.
    pub fn find_insert_use_container(position: &SyntaxNode) -> Option<Self> {
        std::iter::successors(Some(position.clone()), SyntaxNode::parent).find_map(Self::from)
    }

    pub fn as_syntax_node(&self) -> &SyntaxNode {
        match self {
            ImportScope::File(file) => file.syntax(),
            ImportScope::Module(item_list) => item_list.syntax(),
        }
    }

    pub fn clone_for_update(&self) -> Self {
        match self {
            ImportScope::File(file) => ImportScope::File(file.clone_for_update()),
            ImportScope::Module(item_list) => ImportScope::Module(item_list.clone_for_update()),
        }
    }

    fn guess_merge_behavior_from_scope(&self) -> Option<MergeBehavior> {
        let use_stmt = |item| match item {
            ast::Item::Use(use_) => use_.use_tree(),
            _ => None,
        };
        let use_stmts = match self {
            ImportScope::File(f) => f.items(),
            ImportScope::Module(m) => m.items(),
        }
        .filter_map(use_stmt);
        let mut res = None;
        for tree in use_stmts {
            if let Some(list) = tree.use_tree_list() {
                if list.use_trees().any(|tree| tree.use_tree_list().is_some()) {
                    // double nested tree list, can only be a crate style import at this point
                    return Some(MergeBehavior::Crate);
                }
                // has to be at least a module style based import, might be crate style tho so look further
                res = Some(MergeBehavior::Module);
            }
        }
        res
    }
}

/// Insert an import path into the given file/node. A `merge` value of none indicates that no import merging is allowed to occur.
pub fn insert_use<'a>(scope: &ImportScope, path: ast::Path, cfg: InsertUseConfig) {
    let _p = profile::span("insert_use");
    let mb = match cfg.granularity {
        ImportGranularity::Preserve => scope.guess_merge_behavior_from_scope(),
        ImportGranularity::Crate => Some(MergeBehavior::Crate),
        ImportGranularity::Module => Some(MergeBehavior::Module),
        ImportGranularity::Item => None,
    };

    let use_item =
        make::use_(None, make::use_tree(path.clone(), None, None, false)).clone_for_update();
    // merge into existing imports if possible
    if let Some(mb) = mb {
        for existing_use in scope.as_syntax_node().children().filter_map(ast::Use::cast) {
            if let Some(merged) = try_merge_imports(&existing_use, &use_item, mb) {
                ted::replace(existing_use.syntax(), merged.syntax());
                return;
            }
        }
    }

    // either we weren't allowed to merge or there is no import that fits the merge conditions
    // so look for the place we have to insert to
    insert_use_(scope, path, cfg.group, use_item);
}

#[derive(Eq, PartialEq, PartialOrd, Ord)]
enum ImportGroup {
    // the order here defines the order of new group inserts
    Std,
    ExternCrate,
    ThisCrate,
    ThisModule,
    SuperModule,
}

impl ImportGroup {
    fn new(path: &ast::Path) -> ImportGroup {
        let default = ImportGroup::ExternCrate;

        let first_segment = match path.first_segment() {
            Some(it) => it,
            None => return default,
        };

        let kind = first_segment.kind().unwrap_or(PathSegmentKind::SelfKw);
        match kind {
            PathSegmentKind::SelfKw => ImportGroup::ThisModule,
            PathSegmentKind::SuperKw => ImportGroup::SuperModule,
            PathSegmentKind::CrateKw => ImportGroup::ThisCrate,
            PathSegmentKind::Name(name) => match name.text().as_str() {
                "std" => ImportGroup::Std,
                "core" => ImportGroup::Std,
                _ => ImportGroup::ExternCrate,
            },
            PathSegmentKind::Type { .. } => unreachable!(),
        }
    }
}

fn insert_use_(
    scope: &ImportScope,
    insert_path: ast::Path,
    group_imports: bool,
    use_item: ast::Use,
) {
    let scope_syntax = scope.as_syntax_node();
    let group = ImportGroup::new(&insert_path);
    let path_node_iter = scope_syntax
        .children()
        .filter_map(|node| ast::Use::cast(node.clone()).zip(Some(node)))
        .flat_map(|(use_, node)| {
            let tree = use_.use_tree()?;
            let path = tree.path()?;
            let has_tl = tree.use_tree_list().is_some();
            Some((path, has_tl, node))
        });

    if !group_imports {
        if let Some((_, _, node)) = path_node_iter.last() {
            cov_mark::hit!(insert_no_grouping_last);
            ted::insert(ted::Position::after(node), use_item.syntax());
        } else {
            cov_mark::hit!(insert_no_grouping_last2);
            ted::insert(ted::Position::first_child_of(scope_syntax), make::tokens::blank_line());
            ted::insert(ted::Position::first_child_of(scope_syntax), use_item.syntax());
        }
        return;
    }

    // Iterator that discards anything thats not in the required grouping
    // This implementation allows the user to rearrange their import groups as this only takes the first group that fits
    let group_iter = path_node_iter
        .clone()
        .skip_while(|(path, ..)| ImportGroup::new(path) != group)
        .take_while(|(path, ..)| ImportGroup::new(path) == group);

    // 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
    let mut last = None;
    // find the element that would come directly after our new import
    let post_insert: Option<(_, _, SyntaxNode)> = group_iter
        .inspect(|(.., node)| last = Some(node.clone()))
        .find(|&(ref path, has_tl, _)| {
            use_tree_path_cmp(&insert_path, false, path, has_tl) != Ordering::Greater
        });

    if let Some((.., node)) = post_insert {
        cov_mark::hit!(insert_group);
        // insert our import before that element
        return ted::insert(ted::Position::before(node), use_item.syntax());
    }
    if let Some(node) = last {
        cov_mark::hit!(insert_group_last);
        // there is no element after our new import, so append it to the end of the group
        return ted::insert(ted::Position::after(node), use_item.syntax());
    }

    // the group we were looking for actually doesn't exist, so insert

    let mut last = None;
    // find the group that comes after where we want to insert
    let post_group = path_node_iter
        .inspect(|(.., node)| last = Some(node.clone()))
        .find(|(p, ..)| ImportGroup::new(p) > group);
    if let Some((.., node)) = post_group {
        cov_mark::hit!(insert_group_new_group);
        ted::insert(ted::Position::before(&node), use_item.syntax());
        if let Some(node) = algo::non_trivia_sibling(node.into(), Direction::Prev) {
            ted::insert(ted::Position::after(node), make::tokens::single_newline());
        }
        return;
    }
    // there is no such group, so append after the last one
    if let Some(node) = last {
        cov_mark::hit!(insert_group_no_group);
        ted::insert(ted::Position::after(&node), use_item.syntax());
        ted::insert(ted::Position::after(node), make::tokens::single_newline());
        return;
    }
    // there are no imports in this file at all
    if let Some(last_inner_element) = scope_syntax
        .children_with_tokens()
        .filter(|child| match child {
            NodeOrToken::Node(node) => is_inner_attribute(node.clone()),
            NodeOrToken::Token(token) => is_inner_comment(token.clone()),
        })
        .last()
    {
        cov_mark::hit!(insert_group_empty_inner_attr);
        ted::insert(ted::Position::after(&last_inner_element), use_item.syntax());
        ted::insert(ted::Position::after(last_inner_element), make::tokens::single_newline());
        return;
    }
    match scope {
        ImportScope::File(_) => {
            cov_mark::hit!(insert_group_empty_file);
            ted::insert(ted::Position::first_child_of(scope_syntax), make::tokens::blank_line());
            ted::insert(ted::Position::first_child_of(scope_syntax), use_item.syntax())
        }
        // don't insert the imports before the item list's opening curly brace
        ImportScope::Module(item_list) => match item_list.l_curly_token() {
            Some(b) => {
                cov_mark::hit!(insert_group_empty_module);
                ted::insert(ted::Position::after(&b), make::tokens::single_newline());
                ted::insert(ted::Position::after(&b), use_item.syntax());
            }
            None => {
                // This should never happens, broken module syntax node
                ted::insert(
                    ted::Position::first_child_of(scope_syntax),
                    make::tokens::blank_line(),
                );
                ted::insert(ted::Position::first_child_of(scope_syntax), use_item.syntax());
            }
        },
    }
}

fn is_inner_attribute(node: SyntaxNode) -> bool {
    ast::Attr::cast(node).map(|attr| attr.kind()) == Some(ast::AttrKind::Inner)
}

fn is_inner_comment(token: SyntaxToken) -> bool {
    ast::Comment::cast(token).and_then(|comment| comment.kind().doc)
        == Some(ast::CommentPlacement::Inner)
}
#[cfg(test)]
mod tests;