ide/
folding_ranges.rs

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