]> git.lizzy.rs Git - rust.git/blob - crates/ra_ide/src/folding_ranges.rs
5cec689f8bad11f12a632265f82f079ee7d8e9d3
[rust.git] / crates / ra_ide / src / folding_ranges.rs
1 //! FIXME: write short doc here
2
3 use rustc_hash::FxHashSet;
4
5 use ra_syntax::{
6     ast::{self, AstNode, AstToken, VisibilityOwner},
7     Direction, NodeOrToken, SourceFile,
8     SyntaxKind::{self, *},
9     SyntaxNode, TextRange,
10 };
11
12 #[derive(Debug, PartialEq, Eq)]
13 pub enum FoldKind {
14     Comment,
15     Imports,
16     Mods,
17     Block,
18     ArgList,
19 }
20
21 #[derive(Debug)]
22 pub struct Fold {
23     pub range: TextRange,
24     pub kind: FoldKind,
25 }
26
27 pub(crate) fn folding_ranges(file: &SourceFile) -> Vec<Fold> {
28     let mut res = vec![];
29     let mut visited_comments = FxHashSet::default();
30     let mut visited_imports = FxHashSet::default();
31     let mut visited_mods = FxHashSet::default();
32
33     for element in file.syntax().descendants_with_tokens() {
34         // Fold items that span multiple lines
35         if let Some(kind) = fold_kind(element.kind()) {
36             let is_multiline = match &element {
37                 NodeOrToken::Node(node) => node.text().contains_char('\n'),
38                 NodeOrToken::Token(token) => token.text().contains('\n'),
39             };
40             if is_multiline {
41                 res.push(Fold { range: element.text_range(), kind });
42                 continue;
43             }
44         }
45
46         match element {
47             NodeOrToken::Token(token) => {
48                 // Fold groups of comments
49                 if let Some(comment) = ast::Comment::cast(token) {
50                     if !visited_comments.contains(&comment) {
51                         if let Some(range) =
52                             contiguous_range_for_comment(comment, &mut visited_comments)
53                         {
54                             res.push(Fold { range, kind: FoldKind::Comment })
55                         }
56                     }
57                 }
58             }
59             NodeOrToken::Node(node) => {
60                 // Fold groups of imports
61                 if node.kind() == USE_ITEM && !visited_imports.contains(&node) {
62                     if let Some(range) = contiguous_range_for_group(&node, &mut visited_imports) {
63                         res.push(Fold { range, kind: FoldKind::Imports })
64                     }
65                 }
66
67                 // Fold groups of mods
68                 if node.kind() == MODULE && !has_visibility(&node) && !visited_mods.contains(&node)
69                 {
70                     if let Some(range) =
71                         contiguous_range_for_group_unless(&node, has_visibility, &mut visited_mods)
72                     {
73                         res.push(Fold { range, kind: FoldKind::Mods })
74                     }
75                 }
76             }
77         }
78     }
79
80     res
81 }
82
83 fn fold_kind(kind: SyntaxKind) -> Option<FoldKind> {
84     match kind {
85         COMMENT => Some(FoldKind::Comment),
86         USE_ITEM => Some(FoldKind::Imports),
87         ARG_LIST => Some(FoldKind::ArgList),
88         RECORD_FIELD_DEF_LIST
89         | RECORD_FIELD_PAT_LIST
90         | ITEM_LIST
91         | EXTERN_ITEM_LIST
92         | USE_TREE_LIST
93         | BLOCK_EXPR
94         | MATCH_ARM_LIST
95         | ENUM_VARIANT_LIST
96         | TOKEN_TREE => Some(FoldKind::Block),
97         _ => None,
98     }
99 }
100
101 fn has_visibility(node: &SyntaxNode) -> bool {
102     ast::Module::cast(node.clone()).and_then(|m| m.visibility()).is_some()
103 }
104
105 fn contiguous_range_for_group(
106     first: &SyntaxNode,
107     visited: &mut FxHashSet<SyntaxNode>,
108 ) -> Option<TextRange> {
109     contiguous_range_for_group_unless(first, |_| false, visited)
110 }
111
112 fn contiguous_range_for_group_unless(
113     first: &SyntaxNode,
114     unless: impl Fn(&SyntaxNode) -> bool,
115     visited: &mut FxHashSet<SyntaxNode>,
116 ) -> Option<TextRange> {
117     visited.insert(first.clone());
118
119     let mut last = first.clone();
120     for element in first.siblings_with_tokens(Direction::Next) {
121         let node = match element {
122             NodeOrToken::Token(token) => {
123                 if let Some(ws) = ast::Whitespace::cast(token) {
124                     if !ws.spans_multiple_lines() {
125                         // Ignore whitespace without blank lines
126                         continue;
127                     }
128                 }
129                 // There is a blank line or another token, which means that the
130                 // group ends here
131                 break;
132             }
133             NodeOrToken::Node(node) => node,
134         };
135
136         // Stop if we find a node that doesn't belong to the group
137         if node.kind() != first.kind() || unless(&node) {
138             break;
139         }
140
141         visited.insert(node.clone());
142         last = node;
143     }
144
145     if first != &last {
146         Some(TextRange::new(first.text_range().start(), last.text_range().end()))
147     } else {
148         // The group consists of only one element, therefore it cannot be folded
149         None
150     }
151 }
152
153 fn contiguous_range_for_comment(
154     first: ast::Comment,
155     visited: &mut FxHashSet<ast::Comment>,
156 ) -> Option<TextRange> {
157     visited.insert(first.clone());
158
159     // Only fold comments of the same flavor
160     let group_kind = first.kind();
161     if !group_kind.shape.is_line() {
162         return None;
163     }
164
165     let mut last = first.clone();
166     for element in first.syntax().siblings_with_tokens(Direction::Next) {
167         match element {
168             NodeOrToken::Token(token) => {
169                 if let Some(ws) = ast::Whitespace::cast(token.clone()) {
170                     if !ws.spans_multiple_lines() {
171                         // Ignore whitespace without blank lines
172                         continue;
173                     }
174                 }
175                 if let Some(c) = ast::Comment::cast(token) {
176                     if c.kind() == group_kind {
177                         visited.insert(c.clone());
178                         last = c;
179                         continue;
180                     }
181                 }
182                 // The comment group ends because either:
183                 // * An element of a different kind was reached
184                 // * A comment of a different flavor was reached
185                 break;
186             }
187             NodeOrToken::Node(_) => break,
188         };
189     }
190
191     if first != last {
192         Some(TextRange::new(first.syntax().text_range().start(), last.syntax().text_range().end()))
193     } else {
194         // The group consists of only one element, therefore it cannot be folded
195         None
196     }
197 }
198
199 #[cfg(test)]
200 mod tests {
201     use test_utils::extract_tags;
202
203     use super::*;
204
205     fn check(ra_fixture: &str) {
206         let (ranges, text) = extract_tags(ra_fixture, "fold");
207
208         let parse = SourceFile::parse(&text);
209         let folds = folding_ranges(&parse.tree());
210         assert_eq!(
211             folds.len(),
212             ranges.len(),
213             "The amount of folds is different than the expected amount"
214         );
215
216         for (fold, (range, attr)) in folds.iter().zip(ranges.into_iter()) {
217             assert_eq!(fold.range.start(), range.start());
218             assert_eq!(fold.range.end(), range.end());
219
220             let kind = match fold.kind {
221                 FoldKind::Comment => "comment",
222                 FoldKind::Imports => "imports",
223                 FoldKind::Mods => "mods",
224                 FoldKind::Block => "block",
225                 FoldKind::ArgList => "arglist",
226             };
227             assert_eq!(kind, &attr.unwrap());
228         }
229     }
230
231     #[test]
232     fn test_fold_comments() {
233         check(
234             r#"
235 <fold comment>// Hello
236 // this is a multiline
237 // comment
238 //</fold>
239
240 // But this is not
241
242 fn main() <fold block>{
243     <fold comment>// We should
244     // also
245     // fold
246     // this one.</fold>
247     <fold comment>//! But this one is different
248     //! because it has another flavor</fold>
249     <fold comment>/* As does this
250     multiline comment */</fold>
251 }</fold>"#,
252         );
253     }
254
255     #[test]
256     fn test_fold_imports() {
257         check(
258             r#"
259 <fold imports>use std::<fold block>{
260     str,
261     vec,
262     io as iop
263 }</fold>;</fold>
264
265 fn main() <fold block>{
266 }</fold>"#,
267         );
268     }
269
270     #[test]
271     fn test_fold_mods() {
272         check(
273             r#"
274
275 pub mod foo;
276 <fold mods>mod after_pub;
277 mod after_pub_next;</fold>
278
279 <fold mods>mod before_pub;
280 mod before_pub_next;</fold>
281 pub mod bar;
282
283 mod not_folding_single;
284 pub mod foobar;
285 pub not_folding_single_next;
286
287 <fold mods>#[cfg(test)]
288 mod with_attribute;
289 mod with_attribute_next;</fold>
290
291 fn main() <fold block>{
292 }</fold>"#,
293         );
294     }
295
296     #[test]
297     fn test_fold_import_groups() {
298         check(
299             r#"
300 <fold imports>use std::str;
301 use std::vec;
302 use std::io as iop;</fold>
303
304 <fold imports>use std::mem;
305 use std::f64;</fold>
306
307 use std::collections::HashMap;
308 // Some random comment
309 use std::collections::VecDeque;
310
311 fn main() <fold block>{
312 }</fold>"#,
313         );
314     }
315
316     #[test]
317     fn test_fold_import_and_groups() {
318         check(
319             r#"
320 <fold imports>use std::str;
321 use std::vec;
322 use std::io as iop;</fold>
323
324 <fold imports>use std::mem;
325 use std::f64;</fold>
326
327 <fold imports>use std::collections::<fold block>{
328     HashMap,
329     VecDeque,
330 }</fold>;</fold>
331 // Some random comment
332
333 fn main() <fold block>{
334 }</fold>"#,
335         );
336     }
337
338     #[test]
339     fn test_folds_macros() {
340         check(
341             r#"
342 macro_rules! foo <fold block>{
343     ($($tt:tt)*) => { $($tt)* }
344 }</fold>
345 "#,
346         );
347     }
348
349     #[test]
350     fn test_fold_match_arms() {
351         check(
352             r#"
353 fn main() <fold block>{
354     match 0 <fold block>{
355         0 => 0,
356         _ => 1,
357     }</fold>
358 }</fold>"#,
359         );
360     }
361
362     #[test]
363     fn fold_big_calls() {
364         check(
365             r#"
366 fn main() <fold block>{
367     frobnicate<fold arglist>(
368         1,
369         2,
370         3,
371     )</fold>
372 }</fold>
373         "#,
374         )
375     }
376 }