]> git.lizzy.rs Git - rust.git/blob - crates/ide/src/folding_ranges.rs
Merge #11579
[rust.git] / crates / ide / src / folding_ranges.rs
1 use ide_db::helpers::node_ext::vis_eq;
2 use rustc_hash::FxHashSet;
3
4 use syntax::{
5     ast::{self, AstNode, AstToken},
6     match_ast, Direction, NodeOrToken, SourceFile,
7     SyntaxKind::{self, *},
8     TextRange, TextSize,
9 };
10
11 use std::hash::Hash;
12
13 const REGION_START: &str = "// region:";
14 const REGION_END: &str = "// endregion";
15
16 #[derive(Debug, PartialEq, Eq)]
17 pub enum FoldKind {
18     Comment,
19     Imports,
20     Mods,
21     Block,
22     ArgList,
23     Region,
24     Consts,
25     Statics,
26     Array,
27     WhereClause,
28     ReturnType,
29 }
30
31 #[derive(Debug)]
32 pub struct Fold {
33     pub range: TextRange,
34     pub kind: FoldKind,
35 }
36
37 // Feature: Folding
38 //
39 // Defines folding regions for curly braced blocks, runs of consecutive use, mod, const or static
40 // items, and `region` / `endregion` comment markers.
41 pub(crate) fn folding_ranges(file: &SourceFile) -> Vec<Fold> {
42     let mut res = vec![];
43     let mut visited_comments = FxHashSet::default();
44     let mut visited_imports = FxHashSet::default();
45     let mut visited_mods = FxHashSet::default();
46     let mut visited_consts = FxHashSet::default();
47     let mut visited_statics = FxHashSet::default();
48
49     // regions can be nested, here is a LIFO buffer
50     let mut region_starts: Vec<TextSize> = vec![];
51
52     for element in file.syntax().descendants_with_tokens() {
53         // Fold items that span multiple lines
54         if let Some(kind) = fold_kind(element.kind()) {
55             let is_multiline = match &element {
56                 NodeOrToken::Node(node) => node.text().contains_char('\n'),
57                 NodeOrToken::Token(token) => token.text().contains('\n'),
58             };
59             if is_multiline {
60                 res.push(Fold { range: element.text_range(), kind });
61                 continue;
62             }
63         }
64
65         match element {
66             NodeOrToken::Token(token) => {
67                 // Fold groups of comments
68                 if let Some(comment) = ast::Comment::cast(token) {
69                     if visited_comments.contains(&comment) {
70                         continue;
71                     }
72                     let text = comment.text().trim_start();
73                     if text.starts_with(REGION_START) {
74                         region_starts.push(comment.syntax().text_range().start());
75                     } else if text.starts_with(REGION_END) {
76                         if let Some(region) = region_starts.pop() {
77                             res.push(Fold {
78                                 range: TextRange::new(region, comment.syntax().text_range().end()),
79                                 kind: FoldKind::Region,
80                             })
81                         }
82                     } else if let Some(range) =
83                         contiguous_range_for_comment(comment, &mut visited_comments)
84                     {
85                         res.push(Fold { range, kind: FoldKind::Comment })
86                     }
87                 }
88             }
89             NodeOrToken::Node(node) => {
90                 match_ast! {
91                     match node {
92                         ast::Module(module) => {
93                             if module.item_list().is_none() {
94                                 if let Some(range) = contiguous_range_for_item_group(
95                                     module,
96                                     &mut visited_mods,
97                                 ) {
98                                     res.push(Fold { range, kind: FoldKind::Mods })
99                                 }
100                             }
101                         },
102                         ast::Use(use_) => {
103                             if let Some(range) = contiguous_range_for_item_group(use_, &mut visited_imports) {
104                                 res.push(Fold { range, kind: FoldKind::Imports })
105                             }
106                         },
107                         ast::Const(konst) => {
108                             if let Some(range) = contiguous_range_for_item_group(konst, &mut visited_consts) {
109                                 res.push(Fold { range, kind: FoldKind::Consts })
110                             }
111                         },
112                         ast::Static(statik) => {
113                             if let Some(range) = contiguous_range_for_item_group(statik, &mut visited_statics) {
114                                 res.push(Fold { range, kind: FoldKind::Statics })
115                             }
116                         },
117                         ast::WhereClause(where_clause) => {
118                             if let Some(range) = fold_range_for_where_clause(where_clause) {
119                                 res.push(Fold { range, kind: FoldKind::WhereClause })
120                             }
121                         },
122                         _ => (),
123                     }
124                 }
125             }
126         }
127     }
128
129     res
130 }
131
132 fn fold_kind(kind: SyntaxKind) -> Option<FoldKind> {
133     match kind {
134         COMMENT => Some(FoldKind::Comment),
135         ARG_LIST | PARAM_LIST => Some(FoldKind::ArgList),
136         ARRAY_EXPR => Some(FoldKind::Array),
137         RET_TYPE => Some(FoldKind::ReturnType),
138         ASSOC_ITEM_LIST
139         | RECORD_FIELD_LIST
140         | RECORD_PAT_FIELD_LIST
141         | RECORD_EXPR_FIELD_LIST
142         | ITEM_LIST
143         | EXTERN_ITEM_LIST
144         | USE_TREE_LIST
145         | BLOCK_EXPR
146         | MATCH_ARM_LIST
147         | VARIANT_LIST
148         | TOKEN_TREE => Some(FoldKind::Block),
149         _ => None,
150     }
151 }
152
153 fn contiguous_range_for_item_group<N>(first: N, visited: &mut FxHashSet<N>) -> Option<TextRange>
154 where
155     N: ast::HasVisibility + Clone + Hash + Eq,
156 {
157     if !visited.insert(first.clone()) {
158         return None;
159     }
160
161     let (mut last, mut last_vis) = (first.clone(), first.visibility());
162     for element in first.syntax().siblings_with_tokens(Direction::Next) {
163         let node = match element {
164             NodeOrToken::Token(token) => {
165                 if let Some(ws) = ast::Whitespace::cast(token) {
166                     if !ws.spans_multiple_lines() {
167                         // Ignore whitespace without blank lines
168                         continue;
169                     }
170                 }
171                 // There is a blank line or another token, which means that the
172                 // group ends here
173                 break;
174             }
175             NodeOrToken::Node(node) => node,
176         };
177
178         if let Some(next) = N::cast(node) {
179             let next_vis = next.visibility();
180             if eq_visibility(next_vis.clone(), last_vis) {
181                 visited.insert(next.clone());
182                 last_vis = next_vis;
183                 last = next;
184                 continue;
185             }
186         }
187         // Stop if we find an item of a different kind or with a different visibility.
188         break;
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 fn eq_visibility(vis0: Option<ast::Visibility>, vis1: Option<ast::Visibility>) -> bool {
200     match (vis0, vis1) {
201         (None, None) => true,
202         (Some(vis0), Some(vis1)) => vis_eq(&vis0, &vis1),
203         _ => false,
204     }
205 }
206
207 fn contiguous_range_for_comment(
208     first: ast::Comment,
209     visited: &mut FxHashSet<ast::Comment>,
210 ) -> Option<TextRange> {
211     visited.insert(first.clone());
212
213     // Only fold comments of the same flavor
214     let group_kind = first.kind();
215     if !group_kind.shape.is_line() {
216         return None;
217     }
218
219     let mut last = first.clone();
220     for element in first.syntax().siblings_with_tokens(Direction::Next) {
221         match element {
222             NodeOrToken::Token(token) => {
223                 if let Some(ws) = ast::Whitespace::cast(token.clone()) {
224                     if !ws.spans_multiple_lines() {
225                         // Ignore whitespace without blank lines
226                         continue;
227                     }
228                 }
229                 if let Some(c) = ast::Comment::cast(token) {
230                     if c.kind() == group_kind {
231                         let text = c.text().trim_start();
232                         // regions are not real comments
233                         if !(text.starts_with(REGION_START) || text.starts_with(REGION_END)) {
234                             visited.insert(c.clone());
235                             last = c;
236                             continue;
237                         }
238                     }
239                 }
240                 // The comment group ends because either:
241                 // * An element of a different kind was reached
242                 // * A comment of a different flavor was reached
243                 break;
244             }
245             NodeOrToken::Node(_) => break,
246         };
247     }
248
249     if first != last {
250         Some(TextRange::new(first.syntax().text_range().start(), last.syntax().text_range().end()))
251     } else {
252         // The group consists of only one element, therefore it cannot be folded
253         None
254     }
255 }
256
257 fn fold_range_for_where_clause(where_clause: ast::WhereClause) -> Option<TextRange> {
258     let first_where_pred = where_clause.predicates().next();
259     let last_where_pred = where_clause.predicates().last();
260
261     if first_where_pred != last_where_pred {
262         let start = where_clause.where_token()?.text_range().end();
263         let end = where_clause.syntax().text_range().end();
264         return Some(TextRange::new(start, end));
265     }
266     None
267 }
268
269 #[cfg(test)]
270 mod tests {
271     use test_utils::extract_tags;
272
273     use super::*;
274
275     fn check(ra_fixture: &str) {
276         let (ranges, text) = extract_tags(ra_fixture, "fold");
277
278         let parse = SourceFile::parse(&text);
279         let mut folds = folding_ranges(&parse.tree());
280         folds.sort_by_key(|fold| (fold.range.start(), fold.range.end()));
281
282         assert_eq!(
283             folds.len(),
284             ranges.len(),
285             "The amount of folds is different than the expected amount"
286         );
287
288         for (fold, (range, attr)) in folds.iter().zip(ranges.into_iter()) {
289             assert_eq!(fold.range.start(), range.start(), "mismatched start of folding ranges");
290             assert_eq!(fold.range.end(), range.end(), "mismatched end of folding ranges");
291
292             let kind = match fold.kind {
293                 FoldKind::Comment => "comment",
294                 FoldKind::Imports => "imports",
295                 FoldKind::Mods => "mods",
296                 FoldKind::Block => "block",
297                 FoldKind::ArgList => "arglist",
298                 FoldKind::Region => "region",
299                 FoldKind::Consts => "consts",
300                 FoldKind::Statics => "statics",
301                 FoldKind::Array => "array",
302                 FoldKind::WhereClause => "whereclause",
303                 FoldKind::ReturnType => "returntype",
304             };
305             assert_eq!(kind, &attr.unwrap());
306         }
307     }
308
309     #[test]
310     fn test_fold_comments() {
311         check(
312             r#"
313 <fold comment>// Hello
314 // this is a multiline
315 // comment
316 //</fold>
317
318 // But this is not
319
320 fn main() <fold block>{
321     <fold comment>// We should
322     // also
323     // fold
324     // this one.</fold>
325     <fold comment>//! But this one is different
326     //! because it has another flavor</fold>
327     <fold comment>/* As does this
328     multiline comment */</fold>
329 }</fold>
330 "#,
331         );
332     }
333
334     #[test]
335     fn test_fold_imports() {
336         check(
337             r#"
338 use std::<fold block>{
339     str,
340     vec,
341     io as iop
342 }</fold>;
343 "#,
344         );
345     }
346
347     #[test]
348     fn test_fold_mods() {
349         check(
350             r#"
351
352 pub mod foo;
353 <fold mods>mod after_pub;
354 mod after_pub_next;</fold>
355
356 <fold mods>mod before_pub;
357 mod before_pub_next;</fold>
358 pub mod bar;
359
360 mod not_folding_single;
361 pub mod foobar;
362 pub not_folding_single_next;
363
364 <fold mods>#[cfg(test)]
365 mod with_attribute;
366 mod with_attribute_next;</fold>
367
368 mod inline0 {}
369 mod inline1 {}
370
371 mod inline2 <fold block>{
372
373 }</fold>
374 "#,
375         );
376     }
377
378     #[test]
379     fn test_fold_import_groups() {
380         check(
381             r#"
382 <fold imports>use std::str;
383 use std::vec;
384 use std::io as iop;</fold>
385
386 <fold imports>use std::mem;
387 use std::f64;</fold>
388
389 <fold imports>use std::collections::HashMap;
390 // Some random comment
391 use std::collections::VecDeque;</fold>
392 "#,
393         );
394     }
395
396     #[test]
397     fn test_fold_import_and_groups() {
398         check(
399             r#"
400 <fold imports>use std::str;
401 use std::vec;
402 use std::io as iop;</fold>
403
404 <fold imports>use std::mem;
405 use std::f64;</fold>
406
407 use std::collections::<fold block>{
408     HashMap,
409     VecDeque,
410 }</fold>;
411 // Some random comment
412 "#,
413         );
414     }
415
416     #[test]
417     fn test_folds_structs() {
418         check(
419             r#"
420 struct Foo <fold block>{
421 }</fold>
422 "#,
423         );
424     }
425
426     #[test]
427     fn test_folds_traits() {
428         check(
429             r#"
430 trait Foo <fold block>{
431 }</fold>
432 "#,
433         );
434     }
435
436     #[test]
437     fn test_folds_macros() {
438         check(
439             r#"
440 macro_rules! foo <fold block>{
441     ($($tt:tt)*) => { $($tt)* }
442 }</fold>
443 "#,
444         );
445     }
446
447     #[test]
448     fn test_fold_match_arms() {
449         check(
450             r#"
451 fn main() <fold block>{
452     match 0 <fold block>{
453         0 => 0,
454         _ => 1,
455     }</fold>
456 }</fold>
457 "#,
458         );
459     }
460
461     #[test]
462     fn fold_big_calls() {
463         check(
464             r#"
465 fn main() <fold block>{
466     frobnicate<fold arglist>(
467         1,
468         2,
469         3,
470     )</fold>
471 }</fold>
472 "#,
473         )
474     }
475
476     #[test]
477     fn fold_record_literals() {
478         check(
479             r#"
480 const _: S = S <fold block>{
481
482 }</fold>;
483 "#,
484         )
485     }
486
487     #[test]
488     fn fold_multiline_params() {
489         check(
490             r#"
491 fn foo<fold arglist>(
492     x: i32,
493     y: String,
494 )</fold> {}
495 "#,
496         )
497     }
498
499     #[test]
500     fn fold_multiline_array() {
501         check(
502             r#"
503 const FOO: [usize; 4] = <fold array>[
504     1,
505     2,
506     3,
507     4,
508 ]</fold>;
509 "#,
510         )
511     }
512
513     #[test]
514     fn fold_region() {
515         check(
516             r#"
517 // 1. some normal comment
518 <fold region>// region: test
519 // 2. some normal comment
520 <fold region>// region: inner
521 fn f() {}
522 // endregion</fold>
523 fn f2() {}
524 // endregion: test</fold>
525 "#,
526         )
527     }
528
529     #[test]
530     fn fold_consecutive_const() {
531         check(
532             r#"
533 <fold consts>const FIRST_CONST: &str = "first";
534 const SECOND_CONST: &str = "second";</fold>
535 "#,
536         )
537     }
538
539     #[test]
540     fn fold_consecutive_static() {
541         check(
542             r#"
543 <fold statics>static FIRST_STATIC: &str = "first";
544 static SECOND_STATIC: &str = "second";</fold>
545 "#,
546         )
547     }
548
549     #[test]
550     fn fold_where_clause() {
551         // fold multi-line and don't fold single line.
552         check(
553             r#"
554 fn foo()
555 where<fold whereclause>
556     A: Foo,
557     B: Foo,
558     C: Foo,
559     D: Foo,</fold> {}
560
561 fn bar()
562 where
563     A: Bar, {}
564 "#,
565         )
566     }
567
568     #[test]
569     fn fold_return_type() {
570         check(
571             r#"
572 fn foo()<fold returntype>-> (
573     bool,
574     bool,
575 )</fold> { (true, true) }
576
577 fn bar() -> (bool, bool) { (true, true) }
578 "#,
579         )
580     }
581 }