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