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 Modules,
30 Consts,
31 Statics,
32 TypeAliases,
33 ExternCrates,
34 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
57pub(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 let mut region_starts: Vec<TextSize> = vec![];
68
69 for element in file.syntax().descendants_with_tokens() {
70 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 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 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 if let Some(node) = element.as_node()
191 && 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 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 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 _ => 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 continue;
356 }
357 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 break;
375 }
376
377 if first != last {
378 Some(TextRange::new(first.syntax().text_range().start(), last.syntax().text_range().end()))
379 } else {
380 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 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 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 if !(text.starts_with(REGION_START) || text.starts_with(REGION_END)) {
421 visited.insert(c.clone());
422 last = c;
423 continue;
424 }
425 }
426 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 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}