]> git.lizzy.rs Git - rust.git/blob - crates/ide/src/folding_ranges.rs
added region folding
[rust.git] / crates / ide / src / folding_ranges.rs
1 //! FIXME: write short doc here
2
3 use rustc_hash::FxHashSet;
4
5 use syntax::{
6     ast::{self, AstNode, AstToken, VisibilityOwner},
7     Direction, NodeOrToken, SourceFile,
8     SyntaxKind::{self, *},
9     SyntaxNode, TextRange, TextSize,
10 };
11
12 use lazy_static::lazy_static;
13
14 #[derive(Debug, PartialEq, Eq)]
15 pub enum FoldKind {
16     Comment,
17     Imports,
18     Mods,
19     Block,
20     ArgList,
21     Region,
22 }
23
24 #[derive(Debug)]
25 pub struct Fold {
26     pub range: TextRange,
27     pub kind: FoldKind,
28 }
29
30 pub(crate) fn folding_ranges(file: &SourceFile) -> Vec<Fold> {
31     let mut res = vec![];
32     let mut visited_comments = FxHashSet::default();
33     let mut visited_imports = FxHashSet::default();
34     let mut visited_mods = FxHashSet::default();
35     // regions can be nested, here is a LIFO buffer
36     let mut regions_starts: Vec<TextSize> = vec![];
37
38     for element in file.syntax().descendants_with_tokens() {
39         // Fold items that span multiple lines
40         if let Some(kind) = fold_kind(element.kind()) {
41             let is_multiline = match &element {
42                 NodeOrToken::Node(node) => node.text().contains_char('\n'),
43                 NodeOrToken::Token(token) => token.text().contains('\n'),
44             };
45             if is_multiline {
46                 res.push(Fold { range: element.text_range(), kind });
47                 continue;
48             }
49         }
50
51         match element {
52             NodeOrToken::Token(token) => {
53                 // Fold groups of comments
54                 if let Some(comment) = ast::Comment::cast(token) {
55                     if !visited_comments.contains(&comment) {
56                         // regions are not really comments
57                         use regex::Regex;
58                         lazy_static! {
59                             static ref RE_START: Regex =
60                                 Regex::new(r"^\s*//\s*#?region\b").unwrap();
61                             static ref RE_END: Regex =
62                                 Regex::new(r"^\s*//\s*#?endregion\b").unwrap();
63                         }
64                         if RE_START.is_match(comment.text()) {
65                             regions_starts.push(comment.syntax().text_range().start());
66                         } else if RE_END.is_match(comment.text()) {
67                             if !regions_starts.is_empty() {
68                                 res.push(Fold {
69                                     range: TextRange::new(
70                                         regions_starts.pop().unwrap(),
71                                         comment.syntax().text_range().end(),
72                                     ),
73                                     kind: FoldKind::Region,
74                                 })
75                             }
76                         } else {
77                             if let Some(range) =
78                                 contiguous_range_for_comment(comment, &mut visited_comments)
79                             {
80                                 res.push(Fold { range, kind: FoldKind::Comment })
81                             }
82                         }
83                     }
84                 }
85             }
86             NodeOrToken::Node(node) => {
87                 // Fold groups of imports
88                 if node.kind() == USE && !visited_imports.contains(&node) {
89                     if let Some(range) = contiguous_range_for_group(&node, &mut visited_imports) {
90                         res.push(Fold { range, kind: FoldKind::Imports })
91                     }
92                 }
93
94                 // Fold groups of mods
95                 if node.kind() == MODULE && !has_visibility(&node) && !visited_mods.contains(&node)
96                 {
97                     if let Some(range) =
98                         contiguous_range_for_group_unless(&node, has_visibility, &mut visited_mods)
99                     {
100                         res.push(Fold { range, kind: FoldKind::Mods })
101                     }
102                 }
103             }
104         }
105     }
106
107     res
108 }
109
110 fn fold_kind(kind: SyntaxKind) -> Option<FoldKind> {
111     match kind {
112         COMMENT => Some(FoldKind::Comment),
113         ARG_LIST | PARAM_LIST => Some(FoldKind::ArgList),
114         ASSOC_ITEM_LIST
115         | RECORD_FIELD_LIST
116         | RECORD_PAT_FIELD_LIST
117         | RECORD_EXPR_FIELD_LIST
118         | ITEM_LIST
119         | EXTERN_ITEM_LIST
120         | USE_TREE_LIST
121         | BLOCK_EXPR
122         | MATCH_ARM_LIST
123         | VARIANT_LIST
124         | TOKEN_TREE => Some(FoldKind::Block),
125         _ => None,
126     }
127 }
128
129 fn has_visibility(node: &SyntaxNode) -> bool {
130     ast::Module::cast(node.clone()).and_then(|m| m.visibility()).is_some()
131 }
132
133 fn contiguous_range_for_group(
134     first: &SyntaxNode,
135     visited: &mut FxHashSet<SyntaxNode>,
136 ) -> Option<TextRange> {
137     contiguous_range_for_group_unless(first, |_| false, visited)
138 }
139
140 fn contiguous_range_for_group_unless(
141     first: &SyntaxNode,
142     unless: impl Fn(&SyntaxNode) -> bool,
143     visited: &mut FxHashSet<SyntaxNode>,
144 ) -> Option<TextRange> {
145     visited.insert(first.clone());
146
147     let mut last = first.clone();
148     for element in first.siblings_with_tokens(Direction::Next) {
149         let node = match element {
150             NodeOrToken::Token(token) => {
151                 if let Some(ws) = ast::Whitespace::cast(token) {
152                     if !ws.spans_multiple_lines() {
153                         // Ignore whitespace without blank lines
154                         continue;
155                     }
156                 }
157                 // There is a blank line or another token, which means that the
158                 // group ends here
159                 break;
160             }
161             NodeOrToken::Node(node) => node,
162         };
163
164         // Stop if we find a node that doesn't belong to the group
165         if node.kind() != first.kind() || unless(&node) {
166             break;
167         }
168
169         visited.insert(node.clone());
170         last = node;
171     }
172
173     if first != &last {
174         Some(TextRange::new(first.text_range().start(), last.text_range().end()))
175     } else {
176         // The group consists of only one element, therefore it cannot be folded
177         None
178     }
179 }
180
181 fn contiguous_range_for_comment(
182     first: ast::Comment,
183     visited: &mut FxHashSet<ast::Comment>,
184 ) -> Option<TextRange> {
185     visited.insert(first.clone());
186
187     // Only fold comments of the same flavor
188     let group_kind = first.kind();
189     if !group_kind.shape.is_line() {
190         return None;
191     }
192
193     let mut last = first.clone();
194     for element in first.syntax().siblings_with_tokens(Direction::Next) {
195         match element {
196             NodeOrToken::Token(token) => {
197                 if let Some(ws) = ast::Whitespace::cast(token.clone()) {
198                     if !ws.spans_multiple_lines() {
199                         // Ignore whitespace without blank lines
200                         continue;
201                     }
202                 }
203                 if let Some(c) = ast::Comment::cast(token) {
204                     if c.kind() == group_kind {
205                         // regions are not really comments
206                         use regex::Regex;
207                         lazy_static! {
208                             static ref RE_START: Regex =
209                                 Regex::new(r"^\s*//\s*#?region\b").unwrap();
210                             static ref RE_END: Regex =
211                                 Regex::new(r"^\s*//\s*#?endregion\b").unwrap();
212                         }
213                         if RE_START.is_match(c.text()) || RE_END.is_match(c.text()) {
214                             break;
215                         } else {
216                             visited.insert(c.clone());
217                             last = c;
218                             continue;
219                         }
220                     }
221                 }
222                 // The comment group ends because either:
223                 // * An element of a different kind was reached
224                 // * A comment of a different flavor was reached
225                 break;
226             }
227             NodeOrToken::Node(_) => break,
228         };
229     }
230
231     if first != last {
232         Some(TextRange::new(first.syntax().text_range().start(), last.syntax().text_range().end()))
233     } else {
234         // The group consists of only one element, therefore it cannot be folded
235         None
236     }
237 }
238
239 #[cfg(test)]
240 mod tests {
241     use test_utils::extract_tags;
242
243     use super::*;
244
245     fn check(ra_fixture: &str) {
246         let (ranges, text) = extract_tags(ra_fixture, "fold");
247
248         let parse = SourceFile::parse(&text);
249         let folds = folding_ranges(&parse.tree());
250         assert_eq!(
251             folds.len(),
252             ranges.len(),
253             "The amount of folds is different than the expected amount"
254         );
255
256         for (fold, (range, attr)) in folds.iter().zip(ranges.into_iter()) {
257             assert_eq!(fold.range.start(), range.start());
258             assert_eq!(fold.range.end(), range.end());
259
260             let kind = match fold.kind {
261                 FoldKind::Comment => "comment",
262                 FoldKind::Imports => "imports",
263                 FoldKind::Mods => "mods",
264                 FoldKind::Block => "block",
265                 FoldKind::ArgList => "arglist",
266                 FoldKind::Region => "region",
267             };
268             assert_eq!(kind, &attr.unwrap());
269         }
270     }
271
272     #[test]
273     fn test_fold_comments() {
274         check(
275             r#"
276 <fold comment>// Hello
277 // this is a multiline
278 // comment
279 //</fold>
280
281 // But this is not
282
283 fn main() <fold block>{
284     <fold comment>// We should
285     // also
286     // fold
287     // this one.</fold>
288     <fold comment>//! But this one is different
289     //! because it has another flavor</fold>
290     <fold comment>/* As does this
291     multiline comment */</fold>
292 }</fold>"#,
293         );
294     }
295
296     #[test]
297     fn test_fold_imports() {
298         check(
299             r#"
300 use std::<fold block>{
301     str,
302     vec,
303     io as iop
304 }</fold>;
305
306 fn main() <fold block>{
307 }</fold>"#,
308         );
309     }
310
311     #[test]
312     fn test_fold_mods() {
313         check(
314             r#"
315
316 pub mod foo;
317 <fold mods>mod after_pub;
318 mod after_pub_next;</fold>
319
320 <fold mods>mod before_pub;
321 mod before_pub_next;</fold>
322 pub mod bar;
323
324 mod not_folding_single;
325 pub mod foobar;
326 pub not_folding_single_next;
327
328 <fold mods>#[cfg(test)]
329 mod with_attribute;
330 mod with_attribute_next;</fold>
331
332 fn main() <fold block>{
333 }</fold>"#,
334         );
335     }
336
337     #[test]
338     fn test_fold_import_groups() {
339         check(
340             r#"
341 <fold imports>use std::str;
342 use std::vec;
343 use std::io as iop;</fold>
344
345 <fold imports>use std::mem;
346 use std::f64;</fold>
347
348 <fold imports>use std::collections::HashMap;
349 // Some random comment
350 use std::collections::VecDeque;</fold>
351
352 fn main() <fold block>{
353 }</fold>"#,
354         );
355     }
356
357     #[test]
358     fn test_fold_import_and_groups() {
359         check(
360             r#"
361 <fold imports>use std::str;
362 use std::vec;
363 use std::io as iop;</fold>
364
365 <fold imports>use std::mem;
366 use std::f64;</fold>
367
368 use std::collections::<fold block>{
369     HashMap,
370     VecDeque,
371 }</fold>;
372 // Some random comment
373
374 fn main() <fold block>{
375 }</fold>"#,
376         );
377     }
378
379     #[test]
380     fn test_folds_structs() {
381         check(
382             r#"
383 struct Foo <fold block>{
384 }</fold>
385 "#,
386         );
387     }
388
389     #[test]
390     fn test_folds_traits() {
391         check(
392             r#"
393 trait Foo <fold block>{
394 }</fold>
395 "#,
396         );
397     }
398
399     #[test]
400     fn test_folds_macros() {
401         check(
402             r#"
403 macro_rules! foo <fold block>{
404     ($($tt:tt)*) => { $($tt)* }
405 }</fold>
406 "#,
407         );
408     }
409
410     #[test]
411     fn test_fold_match_arms() {
412         check(
413             r#"
414 fn main() <fold block>{
415     match 0 <fold block>{
416         0 => 0,
417         _ => 1,
418     }</fold>
419 }</fold>
420 "#,
421         );
422     }
423
424     #[test]
425     fn fold_big_calls() {
426         check(
427             r#"
428 fn main() <fold block>{
429     frobnicate<fold arglist>(
430         1,
431         2,
432         3,
433     )</fold>
434 }</fold>
435 "#,
436         )
437     }
438
439     #[test]
440     fn fold_record_literals() {
441         check(
442             r#"
443 const _: S = S <fold block>{
444
445 }</fold>;
446 "#,
447         )
448     }
449
450     #[test]
451     fn fold_multiline_params() {
452         check(
453             r#"
454 fn foo<fold arglist>(
455     x: i32,
456     y: String,
457 )</fold> {}
458 "#,
459         )
460     }
461
462     #[test]
463     fn fold_region() {
464         log_init_for_test_debug();
465         // only error level log is printed on the terminal
466         log::error!("test fold_region");
467         check(
468             r#"
469 // 1. some normal comment
470 <fold region>// region: test
471 // 2. some normal comment
472 calling_function(x,y);
473 // endregion: test</fold>
474 "#,
475         )
476     }
477
478     fn log_init_for_test_debug() {
479         let _ = env_logger::builder().is_test(true).try_init();
480     }
481 }