1use ide_db::{FxHashSet, syntax_helpers::node_ext::vis_eq};
2use syntax::{
3 Direction, NodeOrToken, SourceFile,
4 SyntaxKind::{self, *},
5 SyntaxNode, TextRange, TextSize,
6 ast::{self, AstNode, AstToken},
7 match_ast,
8};
9
10use std::hash::Hash;
11
12const REGION_START: &str = "// region:";
13const REGION_END: &str = "// endregion";
14
15#[derive(Debug, PartialEq, Eq)]
16pub enum FoldKind {
17 Comment,
18 Imports,
19 Region,
20 Block,
21 ArgList,
22 Array,
23 WhereClause,
24 ReturnType,
25 MatchArm,
26 Function,
27 Modules,
29 Consts,
30 Statics,
31 TypeAliases,
32 ExternCrates,
33 }
35
36#[derive(Debug)]
37pub struct Fold {
38 pub range: TextRange,
39 pub kind: FoldKind,
40}
41
42pub(crate) fn folding_ranges(file: &SourceFile) -> Vec<Fold> {
47 let mut res = vec![];
48 let mut visited_comments = FxHashSet::default();
49 let mut visited_nodes = FxHashSet::default();
50
51 let mut region_starts: Vec<TextSize> = vec![];
53
54 for element in file.syntax().descendants_with_tokens() {
55 if let Some(kind) = fold_kind(element.kind()) {
57 let is_multiline = match &element {
58 NodeOrToken::Node(node) => node.text().contains_char('\n'),
59 NodeOrToken::Token(token) => token.text().contains('\n'),
60 };
61 if is_multiline {
62 if matches!(element.kind(), FN)
64 && let NodeOrToken::Node(node) = &element
65 && let Some(fn_node) = ast::Fn::cast(node.clone())
66 {
67 if !fn_node
68 .param_list()
69 .map(|param_list| param_list.syntax().text().contains_char('\n'))
70 .unwrap_or(false)
71 {
72 continue;
73 }
74
75 if fn_node.body().is_some() {
76 let fn_start = fn_node
78 .fn_token()
79 .map(|token| token.text_range().start())
80 .unwrap_or(node.text_range().start());
81 res.push(Fold {
82 range: TextRange::new(fn_start, node.text_range().end()),
83 kind: FoldKind::Function,
84 });
85 continue;
86 }
87 }
88 res.push(Fold { range: element.text_range(), kind });
89 continue;
90 }
91 }
92
93 match element {
94 NodeOrToken::Token(token) => {
95 if let Some(comment) = ast::Comment::cast(token) {
97 if visited_comments.contains(&comment) {
98 continue;
99 }
100 let text = comment.text().trim_start();
101 if text.starts_with(REGION_START) {
102 region_starts.push(comment.syntax().text_range().start());
103 } else if text.starts_with(REGION_END) {
104 if let Some(region) = region_starts.pop() {
105 res.push(Fold {
106 range: TextRange::new(region, comment.syntax().text_range().end()),
107 kind: FoldKind::Region,
108 })
109 }
110 } else if let Some(range) =
111 contiguous_range_for_comment(comment, &mut visited_comments)
112 {
113 res.push(Fold { range, kind: FoldKind::Comment })
114 }
115 }
116 }
117 NodeOrToken::Node(node) => {
118 match_ast! {
119 match node {
120 ast::Module(module) => {
121 if module.item_list().is_none()
122 && let Some(range) = contiguous_range_for_item_group(
123 module,
124 &mut visited_nodes,
125 ) {
126 res.push(Fold { range, kind: FoldKind::Modules })
127 }
128 },
129 ast::Use(use_) => {
130 if let Some(range) = contiguous_range_for_item_group(use_, &mut visited_nodes) {
131 res.push(Fold { range, kind: FoldKind::Imports })
132 }
133 },
134 ast::Const(konst) => {
135 if let Some(range) = contiguous_range_for_item_group(konst, &mut visited_nodes) {
136 res.push(Fold { range, kind: FoldKind::Consts })
137 }
138 },
139 ast::Static(statik) => {
140 if let Some(range) = contiguous_range_for_item_group(statik, &mut visited_nodes) {
141 res.push(Fold { range, kind: FoldKind::Statics })
142 }
143 },
144 ast::TypeAlias(alias) => {
145 if let Some(range) = contiguous_range_for_item_group(alias, &mut visited_nodes) {
146 res.push(Fold { range, kind: FoldKind::TypeAliases })
147 }
148 },
149 ast::ExternCrate(extern_crate) => {
150 if let Some(range) = contiguous_range_for_item_group(extern_crate, &mut visited_nodes) {
151 res.push(Fold { range, kind: FoldKind::ExternCrates })
152 }
153 },
154 ast::MatchArm(match_arm) => {
155 if let Some(range) = fold_range_for_multiline_match_arm(match_arm) {
156 res.push(Fold {range, kind: FoldKind::MatchArm})
157 }
158 },
159 _ => (),
160 }
161 }
162 }
163 }
164 }
165
166 res
167}
168
169fn fold_kind(kind: SyntaxKind) -> Option<FoldKind> {
170 match kind {
171 COMMENT => Some(FoldKind::Comment),
172 ARG_LIST | PARAM_LIST | GENERIC_ARG_LIST | GENERIC_PARAM_LIST => Some(FoldKind::ArgList),
173 ARRAY_EXPR => Some(FoldKind::Array),
174 RET_TYPE => Some(FoldKind::ReturnType),
175 FN => Some(FoldKind::Function),
176 WHERE_CLAUSE => Some(FoldKind::WhereClause),
177 ASSOC_ITEM_LIST
178 | RECORD_FIELD_LIST
179 | RECORD_PAT_FIELD_LIST
180 | RECORD_EXPR_FIELD_LIST
181 | ITEM_LIST
182 | EXTERN_ITEM_LIST
183 | USE_TREE_LIST
184 | BLOCK_EXPR
185 | MATCH_ARM_LIST
186 | VARIANT_LIST
187 | TOKEN_TREE => Some(FoldKind::Block),
188 _ => None,
189 }
190}
191
192fn contiguous_range_for_item_group<N>(
193 first: N,
194 visited: &mut FxHashSet<SyntaxNode>,
195) -> Option<TextRange>
196where
197 N: ast::HasVisibility + Clone + Hash + Eq,
198{
199 if !visited.insert(first.syntax().clone()) {
200 return None;
201 }
202
203 let (mut last, mut last_vis) = (first.clone(), first.visibility());
204 for element in first.syntax().siblings_with_tokens(Direction::Next) {
205 let node = match element {
206 NodeOrToken::Token(token) => {
207 if let Some(ws) = ast::Whitespace::cast(token)
208 && !ws.spans_multiple_lines()
209 {
210 continue;
212 }
213 break;
216 }
217 NodeOrToken::Node(node) => node,
218 };
219
220 if let Some(next) = N::cast(node) {
221 let next_vis = next.visibility();
222 if eq_visibility(next_vis.clone(), last_vis) {
223 visited.insert(next.syntax().clone());
224 last_vis = next_vis;
225 last = next;
226 continue;
227 }
228 }
229 break;
231 }
232
233 if first != last {
234 Some(TextRange::new(first.syntax().text_range().start(), last.syntax().text_range().end()))
235 } else {
236 None
238 }
239}
240
241fn eq_visibility(vis0: Option<ast::Visibility>, vis1: Option<ast::Visibility>) -> bool {
242 match (vis0, vis1) {
243 (None, None) => true,
244 (Some(vis0), Some(vis1)) => vis_eq(&vis0, &vis1),
245 _ => false,
246 }
247}
248
249fn contiguous_range_for_comment(
250 first: ast::Comment,
251 visited: &mut FxHashSet<ast::Comment>,
252) -> Option<TextRange> {
253 visited.insert(first.clone());
254
255 let group_kind = first.kind();
257 if !group_kind.shape.is_line() {
258 return None;
259 }
260
261 let mut last = first.clone();
262 for element in first.syntax().siblings_with_tokens(Direction::Next) {
263 match element {
264 NodeOrToken::Token(token) => {
265 if let Some(ws) = ast::Whitespace::cast(token.clone())
266 && !ws.spans_multiple_lines()
267 {
268 continue;
270 }
271 if let Some(c) = ast::Comment::cast(token)
272 && c.kind() == group_kind
273 {
274 let text = c.text().trim_start();
275 if !(text.starts_with(REGION_START) || text.starts_with(REGION_END)) {
277 visited.insert(c.clone());
278 last = c;
279 continue;
280 }
281 }
282 break;
286 }
287 NodeOrToken::Node(_) => break,
288 };
289 }
290
291 if first != last {
292 Some(TextRange::new(first.syntax().text_range().start(), last.syntax().text_range().end()))
293 } else {
294 None
296 }
297}
298
299fn fold_range_for_multiline_match_arm(match_arm: ast::MatchArm) -> Option<TextRange> {
300 if fold_kind(match_arm.expr()?.syntax().kind()).is_some() {
301 None
302 } else if match_arm.expr()?.syntax().text().contains_char('\n') {
303 Some(match_arm.expr()?.syntax().text_range())
304 } else {
305 None
306 }
307}
308
309#[cfg(test)]
310mod tests {
311 use test_utils::extract_tags;
312
313 use super::*;
314
315 #[track_caller]
316 fn check(#[rust_analyzer::rust_fixture] ra_fixture: &str) {
317 let (ranges, text) = extract_tags(ra_fixture, "fold");
318
319 let parse = SourceFile::parse(&text, span::Edition::CURRENT);
320 let mut folds = folding_ranges(&parse.tree());
321 folds.sort_by_key(|fold| (fold.range.start(), fold.range.end()));
322
323 assert_eq!(
324 folds.len(),
325 ranges.len(),
326 "The amount of folds is different than the expected amount"
327 );
328
329 for (fold, (range, attr)) in folds.iter().zip(ranges.into_iter()) {
330 assert_eq!(fold.range.start(), range.start(), "mismatched start of folding ranges");
331 assert_eq!(fold.range.end(), range.end(), "mismatched end of folding ranges");
332
333 let kind = match fold.kind {
334 FoldKind::Comment => "comment",
335 FoldKind::Imports => "imports",
336 FoldKind::Modules => "mods",
337 FoldKind::Block => "block",
338 FoldKind::ArgList => "arglist",
339 FoldKind::Region => "region",
340 FoldKind::Consts => "consts",
341 FoldKind::Statics => "statics",
342 FoldKind::TypeAliases => "typealiases",
343 FoldKind::Array => "array",
344 FoldKind::WhereClause => "whereclause",
345 FoldKind::ReturnType => "returntype",
346 FoldKind::MatchArm => "matcharm",
347 FoldKind::Function => "function",
348 FoldKind::ExternCrates => "externcrates",
349 };
350 assert_eq!(kind, &attr.unwrap());
351 }
352 }
353
354 #[test]
355 fn test_fold_func_with_multiline_param_list() {
356 check(
357 r#"
358<fold function>fn func<fold arglist>(
359 a: i32,
360 b: i32,
361 c: i32,
362)</fold> <fold block>{
363
364
365
366}</fold></fold>
367"#,
368 );
369 }
370
371 #[test]
372 fn test_fold_comments() {
373 check(
374 r#"
375<fold comment>// Hello
376// this is a multiline
377// comment
378//</fold>
379
380// But this is not
381
382fn main() <fold block>{
383 <fold comment>// We should
384 // also
385 // fold
386 // this one.</fold>
387 <fold comment>//! But this one is different
388 //! because it has another flavor</fold>
389 <fold comment>/* As does this
390 multiline comment */</fold>
391}</fold>
392"#,
393 );
394 }
395
396 #[test]
397 fn test_fold_imports() {
398 check(
399 r#"
400use std::<fold block>{
401 str,
402 vec,
403 io as iop
404}</fold>;
405"#,
406 );
407 }
408
409 #[test]
410 fn test_fold_mods() {
411 check(
412 r#"
413
414pub mod foo;
415<fold mods>mod after_pub;
416mod after_pub_next;</fold>
417
418<fold mods>mod before_pub;
419mod before_pub_next;</fold>
420pub mod bar;
421
422mod not_folding_single;
423pub mod foobar;
424pub not_folding_single_next;
425
426<fold mods>#[cfg(test)]
427mod with_attribute;
428mod with_attribute_next;</fold>
429
430mod inline0 {}
431mod inline1 {}
432
433mod inline2 <fold block>{
434
435}</fold>
436"#,
437 );
438 }
439
440 #[test]
441 fn test_fold_import_groups() {
442 check(
443 r#"
444<fold imports>use std::str;
445use std::vec;
446use std::io as iop;</fold>
447
448<fold imports>use std::mem;
449use std::f64;</fold>
450
451<fold imports>use std::collections::HashMap;
452// Some random comment
453use std::collections::VecDeque;</fold>
454"#,
455 );
456 }
457
458 #[test]
459 fn test_fold_import_and_groups() {
460 check(
461 r#"
462<fold imports>use std::str;
463use std::vec;
464use std::io as iop;</fold>
465
466<fold imports>use std::mem;
467use std::f64;</fold>
468
469use std::collections::<fold block>{
470 HashMap,
471 VecDeque,
472}</fold>;
473// Some random comment
474"#,
475 );
476 }
477
478 #[test]
479 fn test_folds_structs() {
480 check(
481 r#"
482struct Foo <fold block>{
483}</fold>
484"#,
485 );
486 }
487
488 #[test]
489 fn test_folds_traits() {
490 check(
491 r#"
492trait Foo <fold block>{
493}</fold>
494"#,
495 );
496 }
497
498 #[test]
499 fn test_folds_macros() {
500 check(
501 r#"
502macro_rules! foo <fold block>{
503 ($($tt:tt)*) => { $($tt)* }
504}</fold>
505"#,
506 );
507 }
508
509 #[test]
510 fn test_fold_match_arms() {
511 check(
512 r#"
513fn main() <fold block>{
514 match 0 <fold block>{
515 0 => 0,
516 _ => 1,
517 }</fold>
518}</fold>
519"#,
520 );
521 }
522
523 #[test]
524 fn test_fold_multiline_non_block_match_arm() {
525 check(
526 r#"
527 fn main() <fold block>{
528 match foo <fold block>{
529 block => <fold block>{
530 }</fold>,
531 matcharm => <fold matcharm>some.
532 call().
533 chain()</fold>,
534 matcharm2
535 => 0,
536 match_expr => <fold matcharm>match foo2 <fold block>{
537 bar => (),
538 }</fold></fold>,
539 array_list => <fold array>[
540 1,
541 2,
542 3,
543 ]</fold>,
544 structS => <fold matcharm>StructS <fold block>{
545 a: 31,
546 }</fold></fold>,
547 }</fold>
548 }</fold>
549 "#,
550 )
551 }
552
553 #[test]
554 fn fold_big_calls() {
555 check(
556 r#"
557fn main() <fold block>{
558 frobnicate<fold arglist>(
559 1,
560 2,
561 3,
562 )</fold>
563}</fold>
564"#,
565 )
566 }
567
568 #[test]
569 fn fold_record_literals() {
570 check(
571 r#"
572const _: S = S <fold block>{
573
574}</fold>;
575"#,
576 )
577 }
578
579 #[test]
580 fn fold_multiline_params() {
581 check(
582 r#"
583<fold function>fn foo<fold arglist>(
584 x: i32,
585 y: String,
586)</fold> {}</fold>
587"#,
588 )
589 }
590
591 #[test]
592 fn fold_multiline_array() {
593 check(
594 r#"
595const FOO: [usize; 4] = <fold array>[
596 1,
597 2,
598 3,
599 4,
600]</fold>;
601"#,
602 )
603 }
604
605 #[test]
606 fn fold_region() {
607 check(
608 r#"
609// 1. some normal comment
610<fold region>// region: test
611// 2. some normal comment
612<fold region>// region: inner
613fn f() {}
614// endregion</fold>
615fn f2() {}
616// endregion: test</fold>
617"#,
618 )
619 }
620
621 #[test]
622 fn fold_consecutive_const() {
623 check(
624 r#"
625<fold consts>const FIRST_CONST: &str = "first";
626const SECOND_CONST: &str = "second";</fold>
627"#,
628 )
629 }
630
631 #[test]
632 fn fold_consecutive_static() {
633 check(
634 r#"
635<fold statics>static FIRST_STATIC: &str = "first";
636static SECOND_STATIC: &str = "second";</fold>
637"#,
638 )
639 }
640
641 #[test]
642 fn fold_where_clause() {
643 check(
644 r#"
645fn foo()
646<fold whereclause>where
647 A: Foo,
648 B: Foo,
649 C: Foo,
650 D: Foo,</fold> {}
651
652fn bar()
653<fold whereclause>where
654 A: Bar,</fold> {}
655"#,
656 )
657 }
658
659 #[test]
660 fn fold_return_type() {
661 check(
662 r#"
663fn foo()<fold returntype>-> (
664 bool,
665 bool,
666)</fold> { (true, true) }
667
668fn bar() -> (bool, bool) { (true, true) }
669"#,
670 )
671 }
672
673 #[test]
674 fn fold_generics() {
675 check(
676 r#"
677type Foo<T, U> = foo<fold arglist><
678 T,
679 U,
680></fold>;
681"#,
682 )
683 }
684
685 #[test]
686 fn test_fold_doc_comments_with_multiline_paramlist_function() {
687 check(
688 r#"
689<fold comment>/// A very very very very very very very very very very very very very very very
690/// very very very long description</fold>
691<fold function>fn foo<fold arglist>(
692 very_long_parameter_name: u32,
693 another_very_long_parameter_name: u32,
694 third_very_long_param: u32,
695)</fold> <fold block>{
696 todo!()
697}</fold></fold>
698"#,
699 );
700 }
701}