Skip to main content

ide/
folding_ranges.rs

1use ide_db::{FxHashSet, syntax_helpers::node_ext::vis_eq};
2use syntax::{
3    Direction, NodeOrToken, SourceFile, SyntaxElement,
4    SyntaxKind::*,
5    SyntaxNode, TextRange, TextSize,
6    ast::{self, AstNode, AstToken},
7    match_ast,
8    syntax_editor::Element,
9};
10
11use std::hash::Hash;
12
13const REGION_START: &str = "// region:";
14const REGION_END: &str = "// endregion";
15
16#[derive(Copy, Clone, Debug, PartialEq, Eq)]
17pub enum FoldKind {
18    Comment,
19    Imports,
20    Region,
21    Block,
22    ArgList,
23    Array,
24    WhereClause,
25    ReturnType,
26    MatchArm,
27    Function,
28    // region: item runs
29    Modules,
30    Consts,
31    Statics,
32    TypeAliases,
33    ExternCrates,
34    // endregion: item runs
35    Stmt,
36    TailExpr,
37}
38
39#[derive(Debug)]
40pub struct Fold {
41    pub range: TextRange,
42    pub kind: FoldKind,
43    pub collapsed_text: Option<String>,
44}
45
46impl Fold {
47    pub fn new(range: TextRange, kind: FoldKind) -> Self {
48        Self { range, kind, collapsed_text: None }
49    }
50
51    pub fn with_text(mut self, text: Option<String>) -> Self {
52        self.collapsed_text = text;
53        self
54    }
55}
56
57// Feature: Folding
58//
59// Defines folding regions for curly braced blocks, runs of consecutive use, mod, const or static
60// items, and `region` / `endregion` comment markers.
61pub(crate) fn folding_ranges(file: &SourceFile, add_collapsed_text: bool) -> Vec<Fold> {
62    let mut res = vec![];
63    let mut visited_comments = FxHashSet::default();
64    let mut visited_nodes = FxHashSet::default();
65
66    // regions can be nested, here is a LIFO buffer
67    let mut region_starts: Vec<TextSize> = vec![];
68
69    for element in file.syntax().descendants_with_tokens() {
70        // Fold items that span multiple lines
71        if let Some((kind, collapsed_text)) = fold_kind(element.clone(), add_collapsed_text) {
72            let is_multiline = match &element {
73                NodeOrToken::Node(node) => node.text().contains_char('\n'),
74                NodeOrToken::Token(token) => token.text().contains('\n'),
75            };
76
77            if is_multiline {
78                if let NodeOrToken::Node(node) = &element
79                    && let Some(fn_) = ast::Fn::cast(node.clone())
80                {
81                    if !fn_
82                        .param_list()
83                        .map(|param_list| param_list.syntax().text().contains_char('\n'))
84                        .unwrap_or_default()
85                    {
86                        continue;
87                    }
88
89                    if let Some(body) = fn_.body() {
90                        // Get the actual start of the function (excluding doc comments)
91                        let fn_start = fn_
92                            .fn_token()
93                            .map(|token| token.text_range().start())
94                            .unwrap_or(node.text_range().start());
95                        res.push(Fold::new(
96                            TextRange::new(fn_start, body.syntax().text_range().end()),
97                            FoldKind::Function,
98                        ));
99                        continue;
100                    }
101                }
102
103                let fold = Fold::new(element.text_range(), kind).with_text(collapsed_text);
104                res.push(fold);
105                continue;
106            }
107        }
108
109        match element {
110            NodeOrToken::Token(token) => {
111                // Fold groups of comments
112                if let Some(comment) = ast::Comment::cast(token) {
113                    if visited_comments.contains(&comment) {
114                        continue;
115                    }
116                    let text = comment.text().trim_start();
117                    if text.starts_with(REGION_START) {
118                        region_starts.push(comment.syntax().text_range().start());
119                    } else if text.starts_with(REGION_END) {
120                        if let Some(region) = region_starts.pop() {
121                            res.push(Fold::new(
122                                TextRange::new(region, comment.syntax().text_range().end()),
123                                FoldKind::Region,
124                            ));
125                        }
126                    } else if let Some(range) =
127                        contiguous_range_for_comment(comment, &mut visited_comments)
128                    {
129                        res.push(Fold::new(range, FoldKind::Comment));
130                    }
131                }
132            }
133            NodeOrToken::Node(node) => {
134                match_ast! {
135                    match node {
136                        ast::Module(module) => {
137                            if module.item_list().is_none()
138                                && let Some(range) = contiguous_range_for_item_group(
139                                    module,
140                                    &mut visited_nodes,
141                                ) {
142                                    res.push(Fold::new(range, FoldKind::Modules));
143                                }
144                        },
145                        ast::Use(use_) => {
146                            if let Some(range) = contiguous_range_for_item_group(use_, &mut visited_nodes) {
147                                res.push(Fold::new(range, FoldKind::Imports));
148                            }
149                        },
150                        ast::Const(konst) => {
151                            if let Some(range) = contiguous_range_for_item_group(konst, &mut visited_nodes) {
152                                res.push(Fold::new(range, FoldKind::Consts));
153                            }
154                        },
155                        ast::Static(statik) => {
156                            if let Some(range) = contiguous_range_for_item_group(statik, &mut visited_nodes) {
157                                res.push(Fold::new(range, FoldKind::Statics));
158                            }
159                        },
160                        ast::TypeAlias(alias) => {
161                            if let Some(range) = contiguous_range_for_item_group(alias, &mut visited_nodes) {
162                                res.push(Fold::new(range, FoldKind::TypeAliases));
163                            }
164                        },
165                        ast::ExternCrate(extern_crate) => {
166                            if let Some(range) = contiguous_range_for_item_group(extern_crate, &mut visited_nodes) {
167                                res.push(Fold::new(range, FoldKind::ExternCrates));
168                            }
169                        },
170                        ast::MatchArm(match_arm) => {
171                            if let Some(range) = fold_range_for_multiline_match_arm(match_arm) {
172                                res.push(Fold::new(range, FoldKind::MatchArm));
173                            }
174                        },
175                        _ => (),
176                    }
177                }
178            }
179        }
180    }
181
182    res
183}
184
185fn fold_kind(
186    element: SyntaxElement,
187    add_collapsed_text: bool,
188) -> Option<(FoldKind, Option<String>)> {
189    // handle tail_expr
190    if let Some(node) = element.as_node()
191        // tail_expr -> stmt_list -> block
192        && let Some(block) = node.parent().and_then(|it| it.parent()).and_then(ast::BlockExpr::cast)
193        && let Some(tail_expr) = block.tail_expr()
194        && tail_expr.syntax() == node
195    {
196        return Some((
197            FoldKind::TailExpr,
198            add_collapsed_text.then(|| collapse_expr(tail_expr)).flatten(),
199        ));
200    }
201
202    match element.kind() {
203        COMMENT => Some(FoldKind::Comment),
204        ARG_LIST | PARAM_LIST | GENERIC_ARG_LIST | GENERIC_PARAM_LIST => Some(FoldKind::ArgList),
205        ARRAY_EXPR => Some(FoldKind::Array),
206        RET_TYPE => Some(FoldKind::ReturnType),
207        FN => Some(FoldKind::Function),
208        WHERE_CLAUSE => Some(FoldKind::WhereClause),
209        ASSOC_ITEM_LIST
210        | RECORD_FIELD_LIST
211        | RECORD_PAT_FIELD_LIST
212        | RECORD_EXPR_FIELD_LIST
213        | ITEM_LIST
214        | EXTERN_ITEM_LIST
215        | USE_TREE_LIST
216        | BLOCK_EXPR
217        | MATCH_ARM_LIST
218        | VARIANT_LIST
219        | TOKEN_TREE => Some(FoldKind::Block),
220        EXPR_STMT | LET_STMT => {
221            return Some((
222                FoldKind::Stmt,
223                add_collapsed_text
224                    .then(|| collapsed_stmt(ast::Stmt::cast(element.as_node()?.clone())?))
225                    .flatten(),
226            ));
227        }
228        _ => None,
229    }
230    .zip(Some(None))
231}
232
233fn collapsed_stmt(stmt: ast::Stmt) -> Option<String> {
234    match stmt {
235        ast::Stmt::ExprStmt(expr_stmt) => {
236            expr_stmt.expr().and_then(collapse_expr).map(|text| format!("{text};"))
237        }
238        ast::Stmt::LetStmt(let_stmt) => 'blk: {
239            if let_stmt.let_else().is_some() {
240                break 'blk None;
241            }
242
243            let Some(expr) = let_stmt.initializer() else {
244                break 'blk None;
245            };
246
247            // If the `let` statement spans multiple lines, we do not collapse it.
248            // We use the `eq_token` to check whether the `let` statement is a single line,
249            // as the formatter may place the initializer on a new line for better readability.
250            //
251            // Example:
252            // ```rust
253            // let complex_pat =
254            //     complex_expr;
255            // ```
256            //
257            // In this case, we should generate the collapsed text.
258            let Some(eq_token) = let_stmt.eq_token() else {
259                break 'blk None;
260            };
261            let eq_token_offset =
262                eq_token.text_range().end() - let_stmt.syntax().text_range().start();
263            let text_until_eq_token = let_stmt.syntax().text().slice(..eq_token_offset);
264            if text_until_eq_token.contains_char('\n') {
265                break 'blk None;
266            }
267
268            collapse_expr(expr).map(|text| format!("{text_until_eq_token} {text};"))
269        }
270        // handling `items` in external matches.
271        ast::Stmt::Item(_) => None,
272    }
273}
274
275fn collapse_expr(expr: ast::Expr) -> Option<String> {
276    const COLLAPSE_EXPR_MAX_LEN: usize = 100;
277    let mut text = String::with_capacity(COLLAPSE_EXPR_MAX_LEN * 2);
278
279    let mut preorder = expr.syntax().preorder_with_tokens();
280    while let Some(element) = preorder.next() {
281        match element {
282            syntax::WalkEvent::Enter(NodeOrToken::Node(node)) => {
283                if let Some(arg_list) = ast::ArgList::cast(node.clone()) {
284                    let content = if arg_list.args().next().is_some() { "(…)" } else { "()" };
285                    text.push_str(content);
286                    preorder.skip_subtree();
287                } else if let Some(expr) = ast::Expr::cast(node) {
288                    match expr {
289                        ast::Expr::AwaitExpr(_)
290                        | ast::Expr::BecomeExpr(_)
291                        | ast::Expr::BinExpr(_)
292                        | ast::Expr::BreakExpr(_)
293                        | ast::Expr::CallExpr(_)
294                        | ast::Expr::CastExpr(_)
295                        | ast::Expr::ContinueExpr(_)
296                        | ast::Expr::FieldExpr(_)
297                        | ast::Expr::IndexExpr(_)
298                        | ast::Expr::LetExpr(_)
299                        | ast::Expr::Literal(_)
300                        | ast::Expr::MethodCallExpr(_)
301                        | ast::Expr::OffsetOfExpr(_)
302                        | ast::Expr::ParenExpr(_)
303                        | ast::Expr::PathExpr(_)
304                        | ast::Expr::PrefixExpr(_)
305                        | ast::Expr::RangeExpr(_)
306                        | ast::Expr::RefExpr(_)
307                        | ast::Expr::ReturnExpr(_)
308                        | ast::Expr::TryExpr(_)
309                        | ast::Expr::UnderscoreExpr(_)
310                        | ast::Expr::YeetExpr(_)
311                        | ast::Expr::YieldExpr(_) => {}
312
313                        // Some other exprs (e.g. `while` loop) are too complex to have a collapsed text
314                        _ => return None,
315                    }
316                }
317            }
318            syntax::WalkEvent::Enter(NodeOrToken::Token(token)) => {
319                if !token.kind().is_trivia() {
320                    text.push_str(token.text());
321                }
322            }
323            syntax::WalkEvent::Leave(_) => {}
324        }
325
326        if text.len() > COLLAPSE_EXPR_MAX_LEN {
327            return None;
328        }
329    }
330
331    text.shrink_to_fit();
332
333    Some(text)
334}
335
336fn contiguous_range_for_item_group<N>(
337    first: N,
338    visited: &mut FxHashSet<SyntaxNode>,
339) -> Option<TextRange>
340where
341    N: ast::HasVisibility + Clone + Hash + Eq,
342{
343    if !visited.insert(first.syntax().clone()) {
344        return None;
345    }
346
347    let (mut last, mut last_vis) = (first.clone(), first.visibility());
348    for element in first.syntax().siblings_with_tokens(Direction::Next) {
349        let node = match element {
350            NodeOrToken::Token(token) => {
351                if let Some(ws) = ast::Whitespace::cast(token)
352                    && !ws.spans_multiple_lines()
353                {
354                    // Ignore whitespace without blank lines
355                    continue;
356                }
357                // There is a blank line or another token, which means that the
358                // group ends here
359                break;
360            }
361            NodeOrToken::Node(node) => node,
362        };
363
364        if let Some(next) = N::cast(node) {
365            let next_vis = next.visibility();
366            if eq_visibility(next_vis.clone(), last_vis) {
367                visited.insert(next.syntax().clone());
368                last_vis = next_vis;
369                last = next;
370                continue;
371            }
372        }
373        // Stop if we find an item of a different kind or with a different visibility.
374        break;
375    }
376
377    if first != last {
378        Some(TextRange::new(first.syntax().text_range().start(), last.syntax().text_range().end()))
379    } else {
380        // The group consists of only one element, therefore it cannot be folded
381        None
382    }
383}
384
385fn eq_visibility(vis0: Option<ast::Visibility>, vis1: Option<ast::Visibility>) -> bool {
386    match (vis0, vis1) {
387        (None, None) => true,
388        (Some(vis0), Some(vis1)) => vis_eq(&vis0, &vis1),
389        _ => false,
390    }
391}
392
393fn contiguous_range_for_comment(
394    first: ast::Comment,
395    visited: &mut FxHashSet<ast::Comment>,
396) -> Option<TextRange> {
397    visited.insert(first.clone());
398
399    // Only fold comments of the same flavor
400    let group_kind = first.kind();
401    if !group_kind.shape.is_line() {
402        return None;
403    }
404
405    let mut last = first.clone();
406    for element in first.syntax().siblings_with_tokens(Direction::Next) {
407        match element {
408            NodeOrToken::Token(token) => {
409                if let Some(ws) = ast::Whitespace::cast(token.clone())
410                    && !ws.spans_multiple_lines()
411                {
412                    // Ignore whitespace without blank lines
413                    continue;
414                }
415                if let Some(c) = ast::Comment::cast(token)
416                    && c.kind() == group_kind
417                {
418                    let text = c.text().trim_start();
419                    // regions are not real comments
420                    if !(text.starts_with(REGION_START) || text.starts_with(REGION_END)) {
421                        visited.insert(c.clone());
422                        last = c;
423                        continue;
424                    }
425                }
426                // The comment group ends because either:
427                // * An element of a different kind was reached
428                // * A comment of a different flavor was reached
429                break;
430            }
431            NodeOrToken::Node(_) => break,
432        };
433    }
434
435    if first != last {
436        Some(TextRange::new(first.syntax().text_range().start(), last.syntax().text_range().end()))
437    } else {
438        // The group consists of only one element, therefore it cannot be folded
439        None
440    }
441}
442
443fn fold_range_for_multiline_match_arm(match_arm: ast::MatchArm) -> Option<TextRange> {
444    if fold_kind(match_arm.expr()?.syntax().syntax_element(), false).is_some() {
445        None
446    } else if match_arm.expr()?.syntax().text().contains_char('\n') {
447        Some(match_arm.expr()?.syntax().text_range())
448    } else {
449        None
450    }
451}
452
453#[cfg(test)]
454mod tests {
455    use test_utils::extract_tags;
456
457    use super::*;
458
459    #[track_caller]
460    fn check(#[rust_analyzer::rust_fixture] ra_fixture: &str) {
461        check_inner(ra_fixture, true);
462    }
463
464    fn check_without_collapsed_text(#[rust_analyzer::rust_fixture] ra_fixture: &str) {
465        check_inner(ra_fixture, false);
466    }
467
468    fn check_inner(ra_fixture: &str, enable_collapsed_text: bool) {
469        let (ranges, text) = extract_tags(ra_fixture, "fold");
470        let ranges: Vec<_> = ranges
471            .into_iter()
472            .map(|(range, text)| {
473                let (attr, collapsed_text) = match text {
474                    Some(text) => match text.split_once(':') {
475                        Some((attr, collapsed_text)) => {
476                            (Some(attr.to_owned()), Some(collapsed_text.to_owned()))
477                        }
478                        None => (Some(text), None),
479                    },
480                    None => (None, None),
481                };
482                (range, attr, collapsed_text)
483            })
484            .collect();
485
486        let parse = SourceFile::parse(&text, span::Edition::CURRENT);
487        let mut folds = folding_ranges(&parse.tree(), enable_collapsed_text);
488        folds.sort_by_key(|fold| (fold.range.start(), fold.range.end()));
489
490        assert_eq!(
491            folds.len(),
492            ranges.len(),
493            "The amount of folds is different than the expected amount"
494        );
495
496        for (fold, (range, attr, collapsed_text)) in folds.iter().zip(ranges) {
497            assert_eq!(fold.range.start(), range.start(), "mismatched start of folding ranges");
498            assert_eq!(fold.range.end(), range.end(), "mismatched end of folding ranges");
499
500            let kind = match fold.kind {
501                FoldKind::Comment => "comment",
502                FoldKind::Imports => "imports",
503                FoldKind::Modules => "mods",
504                FoldKind::Block => "block",
505                FoldKind::ArgList => "arglist",
506                FoldKind::Region => "region",
507                FoldKind::Consts => "consts",
508                FoldKind::Statics => "statics",
509                FoldKind::TypeAliases => "typealiases",
510                FoldKind::Array => "array",
511                FoldKind::WhereClause => "whereclause",
512                FoldKind::ReturnType => "returntype",
513                FoldKind::MatchArm => "matcharm",
514                FoldKind::Function => "function",
515                FoldKind::ExternCrates => "externcrates",
516                FoldKind::Stmt => "stmt",
517                FoldKind::TailExpr => "tailexpr",
518            };
519            assert_eq!(kind, &attr.unwrap());
520            if enable_collapsed_text {
521                assert_eq!(fold.collapsed_text, collapsed_text);
522            } else {
523                assert_eq!(fold.collapsed_text, None);
524            }
525        }
526    }
527
528    #[test]
529    fn test_fold_func_with_multiline_param_list() {
530        check(
531            r#"
532<fold function>fn func<fold arglist>(
533    a: i32,
534    b: i32,
535    c: i32,
536)</fold> <fold block>{
537
538
539
540}</fold></fold>
541"#,
542        );
543    }
544
545    #[test]
546    fn test_fold_comments() {
547        check(
548            r#"
549<fold comment>// Hello
550// this is a multiline
551// comment
552//</fold>
553
554// But this is not
555
556fn main() <fold block>{
557    <fold comment>// We should
558    // also
559    // fold
560    // this one.</fold>
561    <fold comment>//! But this one is different
562    //! because it has another flavor</fold>
563    <fold comment>/* As does this
564    multiline comment */</fold>
565}</fold>
566"#,
567        );
568    }
569
570    #[test]
571    fn test_fold_imports() {
572        check(
573            r#"
574use std::<fold block>{
575    str,
576    vec,
577    io as iop
578}</fold>;
579"#,
580        );
581    }
582
583    #[test]
584    fn test_fold_mods() {
585        check(
586            r#"
587
588pub mod foo;
589<fold mods>mod after_pub;
590mod after_pub_next;</fold>
591
592<fold mods>mod before_pub;
593mod before_pub_next;</fold>
594pub mod bar;
595
596mod not_folding_single;
597pub mod foobar;
598pub not_folding_single_next;
599
600<fold mods>#[cfg(test)]
601mod with_attribute;
602mod with_attribute_next;</fold>
603
604mod inline0 {}
605mod inline1 {}
606
607mod inline2 <fold block>{
608
609}</fold>
610"#,
611        );
612    }
613
614    #[test]
615    fn test_fold_import_groups() {
616        check(
617            r#"
618<fold imports>use std::str;
619use std::vec;
620use std::io as iop;</fold>
621
622<fold imports>use std::mem;
623use std::f64;</fold>
624
625<fold imports>use std::collections::HashMap;
626// Some random comment
627use std::collections::VecDeque;</fold>
628"#,
629        );
630    }
631
632    #[test]
633    fn test_fold_import_and_groups() {
634        check(
635            r#"
636<fold imports>use std::str;
637use std::vec;
638use std::io as iop;</fold>
639
640<fold imports>use std::mem;
641use std::f64;</fold>
642
643use std::collections::<fold block>{
644    HashMap,
645    VecDeque,
646}</fold>;
647// Some random comment
648"#,
649        );
650    }
651
652    #[test]
653    fn test_folds_structs() {
654        check(
655            r#"
656struct Foo <fold block>{
657}</fold>
658"#,
659        );
660    }
661
662    #[test]
663    fn test_folds_traits() {
664        check(
665            r#"
666trait Foo <fold block>{
667}</fold>
668"#,
669        );
670    }
671
672    #[test]
673    fn test_folds_macros() {
674        check(
675            r#"
676macro_rules! foo <fold block>{
677    ($($tt:tt)*) => { $($tt)* }
678}</fold>
679"#,
680        );
681    }
682
683    #[test]
684    fn test_fold_match_arms() {
685        check(
686            r#"
687fn main() <fold block>{
688    <fold tailexpr>match 0 <fold block>{
689        0 => 0,
690        _ => 1,
691    }</fold></fold>
692}</fold>
693"#,
694        );
695    }
696
697    #[test]
698    fn test_fold_multiline_non_block_match_arm() {
699        check(
700            r#"
701            fn main() <fold block>{
702                <fold tailexpr>match foo <fold block>{
703                    block => <fold block>{
704                    }</fold>,
705                    matcharm => <fold matcharm>some.
706                        call().
707                        chain()</fold>,
708                    matcharm2
709                        => 0,
710                    match_expr => <fold matcharm>match foo2 <fold block>{
711                        bar => (),
712                    }</fold></fold>,
713                    array_list => <fold array>[
714                        1,
715                        2,
716                        3,
717                    ]</fold>,
718                    structS => <fold matcharm>StructS <fold block>{
719                        a: 31,
720                    }</fold></fold>,
721                }</fold></fold>
722            }</fold>
723            "#,
724        )
725    }
726
727    #[test]
728    fn fold_big_calls() {
729        check(
730            r#"
731fn main() <fold block>{
732    <fold tailexpr:frobnicate(…)>frobnicate<fold arglist>(
733        1,
734        2,
735        3,
736    )</fold></fold>
737}</fold>
738"#,
739        )
740    }
741
742    #[test]
743    fn fold_record_literals() {
744        check(
745            r#"
746const _: S = S <fold block>{
747
748}</fold>;
749"#,
750        )
751    }
752
753    #[test]
754    fn fold_multiline_params() {
755        check(
756            r#"
757<fold function>fn foo<fold arglist>(
758    x: i32,
759    y: String,
760)</fold> {}</fold>
761"#,
762        )
763    }
764
765    #[test]
766    fn fold_multiline_array() {
767        check(
768            r#"
769const FOO: [usize; 4] = <fold array>[
770    1,
771    2,
772    3,
773    4,
774]</fold>;
775"#,
776        )
777    }
778
779    #[test]
780    fn fold_region() {
781        check(
782            r#"
783// 1. some normal comment
784<fold region>// region: test
785// 2. some normal comment
786<fold region>// region: inner
787fn f() {}
788// endregion</fold>
789fn f2() {}
790// endregion: test</fold>
791"#,
792        )
793    }
794
795    #[test]
796    fn fold_consecutive_const() {
797        check(
798            r#"
799<fold consts>const FIRST_CONST: &str = "first";
800const SECOND_CONST: &str = "second";</fold>
801"#,
802        )
803    }
804
805    #[test]
806    fn fold_consecutive_static() {
807        check(
808            r#"
809<fold statics>static FIRST_STATIC: &str = "first";
810static SECOND_STATIC: &str = "second";</fold>
811"#,
812        )
813    }
814
815    #[test]
816    fn fold_where_clause() {
817        check(
818            r#"
819fn foo()
820<fold whereclause>where
821    A: Foo,
822    B: Foo,
823    C: Foo,
824    D: Foo,</fold> {}
825
826fn bar()
827<fold whereclause>where
828    A: Bar,</fold> {}
829"#,
830        )
831    }
832
833    #[test]
834    fn fold_return_type() {
835        check(
836            r#"
837fn foo()<fold returntype>-> (
838    bool,
839    bool,
840)</fold> { (true, true) }
841
842fn bar() -> (bool, bool) { (true, true) }
843"#,
844        )
845    }
846
847    #[test]
848    fn fold_generics() {
849        check(
850            r#"
851type Foo<T, U> = foo<fold arglist><
852    T,
853    U,
854></fold>;
855"#,
856        )
857    }
858
859    #[test]
860    fn test_fold_doc_comments_with_multiline_paramlist_function() {
861        check(
862            r#"
863<fold comment>/// A very very very very very very very very very very very very very very very
864/// very very very long description</fold>
865<fold function>fn foo<fold arglist>(
866    very_long_parameter_name: u32,
867    another_very_long_parameter_name: u32,
868    third_very_long_param: u32,
869)</fold> <fold block>{
870    todo!()
871}</fold></fold>
872"#,
873        );
874    }
875
876    #[test]
877    fn test_fold_tail_expr() {
878        check(
879            r#"
880fn f() <fold block>{
881    let x = 1;
882
883    <fold tailexpr:some_function().chain().method()>some_function()
884        .chain()
885        .method()</fold>
886}</fold>
887"#,
888        )
889    }
890
891    #[test]
892    fn test_fold_let_stmt_with_chained_methods() {
893        check(
894            r#"
895fn main() <fold block>{
896    <fold stmt:let result = some_value.method1().method2()?.method3();>let result = some_value
897        .method1()
898        .method2()?
899        .method3();</fold>
900
901    println!("{}", result);
902}</fold>
903"#,
904        )
905    }
906
907    #[test]
908    fn test_fold_let_stmt_with_chained_methods_without_collapsed_text() {
909        check_without_collapsed_text(
910            r#"
911fn main() <fold block>{
912    <fold stmt>let result = some_value
913        .method1()
914        .method2()?
915        .method3();</fold>
916
917    println!("{}", result);
918}</fold>
919"#,
920        )
921    }
922}