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::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 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 | 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 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::AnyComment,
395 visited: &mut FxHashSet<ast::AnyComment>,
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 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 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 if !(text.starts_with(REGION_START) || text.starts_with(REGION_END)) {
420 visited.insert(c.clone());
421 last = c;
422 continue;
423 }
424 }
425 break;
429 }
430
431 if first != last {
432 Some(TextRange::new(first.syntax().text_range().start(), last.syntax().text_range().end()))
433 } else {
434 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}