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