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