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