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::AnyComment::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 | INNER_DOC_COMMENT | OUTER_DOC_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::AnyComment,
395    visited: &mut FxHashSet<ast::AnyComment>,
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    let next_comments = std::iter::successors(Some(first.syntax().clone()), |it| it.next_token());
407    for token in next_comments {
408        if let Some(ws) = ast::Whitespace::cast(token.clone())
409            && !ws.spans_multiple_lines()
410        {
411            // Ignore whitespace without blank lines
412            continue;
413        }
414        if let Some(c) = ast::AnyComment::cast(token)
415            && c.kind() == group_kind
416        {
417            let text = c.text().trim_start();
418            // regions are not real comments
419            if !(text.starts_with(REGION_START) || text.starts_with(REGION_END)) {
420                visited.insert(c.clone());
421                last = c;
422                continue;
423            }
424        }
425        // The comment group ends because either:
426        // * An element of a different kind was reached
427        // * A comment of a different flavor was reached
428        break;
429    }
430
431    if first != last {
432        Some(TextRange::new(first.syntax().text_range().start(), last.syntax().text_range().end()))
433    } else {
434        // The group consists of only one element, therefore it cannot be folded
435        None
436    }
437}
438
439fn fold_range_for_multiline_match_arm(match_arm: ast::MatchArm) -> Option<TextRange> {
440    if fold_kind(match_arm.expr()?.syntax().syntax_element(), false).is_some() {
441        None
442    } else if match_arm.expr()?.syntax().text().contains_char('\n') {
443        Some(match_arm.expr()?.syntax().text_range())
444    } else {
445        None
446    }
447}
448
449#[cfg(test)]
450mod tests {
451    use test_utils::extract_tags;
452
453    use super::*;
454
455    #[track_caller]
456    fn check(#[rust_analyzer::rust_fixture] ra_fixture: &str) {
457        check_inner(ra_fixture, true);
458    }
459
460    fn check_without_collapsed_text(#[rust_analyzer::rust_fixture] ra_fixture: &str) {
461        check_inner(ra_fixture, false);
462    }
463
464    fn check_inner(ra_fixture: &str, enable_collapsed_text: bool) {
465        let (ranges, text) = extract_tags(ra_fixture, "fold");
466        let ranges: Vec<_> = ranges
467            .into_iter()
468            .map(|(range, text)| {
469                let (attr, collapsed_text) = match text {
470                    Some(text) => match text.split_once(':') {
471                        Some((attr, collapsed_text)) => {
472                            (Some(attr.to_owned()), Some(collapsed_text.to_owned()))
473                        }
474                        None => (Some(text), None),
475                    },
476                    None => (None, None),
477                };
478                (range, attr, collapsed_text)
479            })
480            .collect();
481
482        let parse = SourceFile::parse(&text, span::Edition::CURRENT);
483        let mut folds = folding_ranges(&parse.tree(), enable_collapsed_text);
484        folds.sort_by_key(|fold| (fold.range.start(), fold.range.end()));
485
486        assert_eq!(
487            folds.len(),
488            ranges.len(),
489            "The amount of folds is different than the expected amount"
490        );
491
492        for (fold, (range, attr, collapsed_text)) in folds.iter().zip(ranges) {
493            assert_eq!(fold.range.start(), range.start(), "mismatched start of folding ranges");
494            assert_eq!(fold.range.end(), range.end(), "mismatched end of folding ranges");
495
496            let kind = match fold.kind {
497                FoldKind::Comment => "comment",
498                FoldKind::Imports => "imports",
499                FoldKind::Modules => "mods",
500                FoldKind::Block => "block",
501                FoldKind::ArgList => "arglist",
502                FoldKind::Region => "region",
503                FoldKind::Consts => "consts",
504                FoldKind::Statics => "statics",
505                FoldKind::TypeAliases => "typealiases",
506                FoldKind::Array => "array",
507                FoldKind::WhereClause => "whereclause",
508                FoldKind::ReturnType => "returntype",
509                FoldKind::MatchArm => "matcharm",
510                FoldKind::Function => "function",
511                FoldKind::ExternCrates => "externcrates",
512                FoldKind::Stmt => "stmt",
513                FoldKind::TailExpr => "tailexpr",
514            };
515            assert_eq!(kind, &attr.unwrap());
516            if enable_collapsed_text {
517                assert_eq!(fold.collapsed_text, collapsed_text);
518            } else {
519                assert_eq!(fold.collapsed_text, None);
520            }
521        }
522    }
523
524    #[test]
525    fn test_fold_func_with_multiline_param_list() {
526        check(
527            r#"
528<fold function>fn func<fold arglist>(
529    a: i32,
530    b: i32,
531    c: i32,
532)</fold> <fold block>{
533
534
535
536}</fold></fold>
537"#,
538        );
539    }
540
541    #[test]
542    fn test_fold_comments() {
543        check(
544            r#"
545<fold comment>// Hello
546// this is a multiline
547// comment
548//</fold>
549
550// But this is not
551
552fn main() <fold block>{
553    <fold comment>// We should
554    // also
555    // fold
556    // this one.</fold>
557    <fold comment>//! But this one is different
558    //! because it has another flavor</fold>
559    <fold comment>/* As does this
560    multiline comment */</fold>
561}</fold>
562"#,
563        );
564    }
565
566    #[test]
567    fn test_fold_imports() {
568        check(
569            r#"
570use std::<fold block>{
571    str,
572    vec,
573    io as iop
574}</fold>;
575"#,
576        );
577    }
578
579    #[test]
580    fn test_fold_mods() {
581        check(
582            r#"
583
584pub mod foo;
585<fold mods>mod after_pub;
586mod after_pub_next;</fold>
587
588<fold mods>mod before_pub;
589mod before_pub_next;</fold>
590pub mod bar;
591
592mod not_folding_single;
593pub mod foobar;
594pub not_folding_single_next;
595
596<fold mods>#[cfg(test)]
597mod with_attribute;
598mod with_attribute_next;</fold>
599
600mod inline0 {}
601mod inline1 {}
602
603mod inline2 <fold block>{
604
605}</fold>
606"#,
607        );
608    }
609
610    #[test]
611    fn test_fold_import_groups() {
612        check(
613            r#"
614<fold imports>use std::str;
615use std::vec;
616use std::io as iop;</fold>
617
618<fold imports>use std::mem;
619use std::f64;</fold>
620
621<fold imports>use std::collections::HashMap;
622// Some random comment
623use std::collections::VecDeque;</fold>
624"#,
625        );
626    }
627
628    #[test]
629    fn test_fold_import_and_groups() {
630        check(
631            r#"
632<fold imports>use std::str;
633use std::vec;
634use std::io as iop;</fold>
635
636<fold imports>use std::mem;
637use std::f64;</fold>
638
639use std::collections::<fold block>{
640    HashMap,
641    VecDeque,
642}</fold>;
643// Some random comment
644"#,
645        );
646    }
647
648    #[test]
649    fn test_folds_structs() {
650        check(
651            r#"
652struct Foo <fold block>{
653}</fold>
654"#,
655        );
656    }
657
658    #[test]
659    fn test_folds_traits() {
660        check(
661            r#"
662trait Foo <fold block>{
663}</fold>
664"#,
665        );
666    }
667
668    #[test]
669    fn test_folds_macros() {
670        check(
671            r#"
672macro_rules! foo <fold block>{
673    ($($tt:tt)*) => { $($tt)* }
674}</fold>
675"#,
676        );
677    }
678
679    #[test]
680    fn test_fold_match_arms() {
681        check(
682            r#"
683fn main() <fold block>{
684    <fold tailexpr>match 0 <fold block>{
685        0 => 0,
686        _ => 1,
687    }</fold></fold>
688}</fold>
689"#,
690        );
691    }
692
693    #[test]
694    fn test_fold_multiline_non_block_match_arm() {
695        check(
696            r#"
697            fn main() <fold block>{
698                <fold tailexpr>match foo <fold block>{
699                    block => <fold block>{
700                    }</fold>,
701                    matcharm => <fold matcharm>some.
702                        call().
703                        chain()</fold>,
704                    matcharm2
705                        => 0,
706                    match_expr => <fold matcharm>match foo2 <fold block>{
707                        bar => (),
708                    }</fold></fold>,
709                    array_list => <fold array>[
710                        1,
711                        2,
712                        3,
713                    ]</fold>,
714                    structS => <fold matcharm>StructS <fold block>{
715                        a: 31,
716                    }</fold></fold>,
717                }</fold></fold>
718            }</fold>
719            "#,
720        )
721    }
722
723    #[test]
724    fn fold_big_calls() {
725        check(
726            r#"
727fn main() <fold block>{
728    <fold tailexpr:frobnicate(…)>frobnicate<fold arglist>(
729        1,
730        2,
731        3,
732    )</fold></fold>
733}</fold>
734"#,
735        )
736    }
737
738    #[test]
739    fn fold_record_literals() {
740        check(
741            r#"
742const _: S = S <fold block>{
743
744}</fold>;
745"#,
746        )
747    }
748
749    #[test]
750    fn fold_multiline_params() {
751        check(
752            r#"
753<fold function>fn foo<fold arglist>(
754    x: i32,
755    y: String,
756)</fold> {}</fold>
757"#,
758        )
759    }
760
761    #[test]
762    fn fold_multiline_array() {
763        check(
764            r#"
765const FOO: [usize; 4] = <fold array>[
766    1,
767    2,
768    3,
769    4,
770]</fold>;
771"#,
772        )
773    }
774
775    #[test]
776    fn fold_region() {
777        check(
778            r#"
779// 1. some normal comment
780<fold region>// region: test
781// 2. some normal comment
782<fold region>// region: inner
783fn f() {}
784// endregion</fold>
785fn f2() {}
786// endregion: test</fold>
787"#,
788        )
789    }
790
791    #[test]
792    fn fold_consecutive_const() {
793        check(
794            r#"
795<fold consts>const FIRST_CONST: &str = "first";
796const SECOND_CONST: &str = "second";</fold>
797"#,
798        )
799    }
800
801    #[test]
802    fn fold_consecutive_static() {
803        check(
804            r#"
805<fold statics>static FIRST_STATIC: &str = "first";
806static SECOND_STATIC: &str = "second";</fold>
807"#,
808        )
809    }
810
811    #[test]
812    fn fold_where_clause() {
813        check(
814            r#"
815fn foo()
816<fold whereclause>where
817    A: Foo,
818    B: Foo,
819    C: Foo,
820    D: Foo,</fold> {}
821
822fn bar()
823<fold whereclause>where
824    A: Bar,</fold> {}
825"#,
826        )
827    }
828
829    #[test]
830    fn fold_return_type() {
831        check(
832            r#"
833fn foo()<fold returntype>-> (
834    bool,
835    bool,
836)</fold> { (true, true) }
837
838fn bar() -> (bool, bool) { (true, true) }
839"#,
840        )
841    }
842
843    #[test]
844    fn fold_generics() {
845        check(
846            r#"
847type Foo<T, U> = foo<fold arglist><
848    T,
849    U,
850></fold>;
851"#,
852        )
853    }
854
855    #[test]
856    fn test_fold_doc_comments_with_multiline_paramlist_function() {
857        check(
858            r#"
859<fold comment>/// A very very very very very very very very very very very very very very very
860/// very very very long description</fold>
861<fold function>fn foo<fold arglist>(
862    very_long_parameter_name: u32,
863    another_very_long_parameter_name: u32,
864    third_very_long_param: u32,
865)</fold> <fold block>{
866    todo!()
867}</fold></fold>
868"#,
869        );
870    }
871
872    #[test]
873    fn test_fold_tail_expr() {
874        check(
875            r#"
876fn f() <fold block>{
877    let x = 1;
878
879    <fold tailexpr:some_function().chain().method()>some_function()
880        .chain()
881        .method()</fold>
882}</fold>
883"#,
884        )
885    }
886
887    #[test]
888    fn test_fold_let_stmt_with_chained_methods() {
889        check(
890            r#"
891fn main() <fold block>{
892    <fold stmt:let result = some_value.method1().method2()?.method3();>let result = some_value
893        .method1()
894        .method2()?
895        .method3();</fold>
896
897    println!("{}", result);
898}</fold>
899"#,
900        )
901    }
902
903    #[test]
904    fn test_fold_let_stmt_with_chained_methods_without_collapsed_text() {
905        check_without_collapsed_text(
906            r#"
907fn main() <fold block>{
908    <fold stmt>let result = some_value
909        .method1()
910        .method2()?
911        .method3();</fold>
912
913    println!("{}", result);
914}</fold>
915"#,
916        )
917    }
918}