diff options
author | bors[bot] <bors[bot]@users.noreply.github.com> | 2018-10-15 17:48:17 +0100 |
---|---|---|
committer | bors[bot] <bors[bot]@users.noreply.github.com> | 2018-10-15 17:48:17 +0100 |
commit | a230b438e025c5878b8d17e11d3928cbad95bb28 (patch) | |
tree | 231872915ede46874b6e7d2b3d1240d1defa5231 /crates/ra_editor/src | |
parent | e031b65f93f73164a5729cf81ff60299708bc931 (diff) | |
parent | 2bc9e9f32711047b06940c335eb5327281f8c555 (diff) |
Merge #127
127: Improve folding r=matklad a=aochagavia
I was messing around with adding support for multiline comments in folding and ended up changing a bunch of other things.
First of all, I am not convinced of folding groups of successive items. For instance, I don't see why it is worthwhile to be able to fold something like the following:
```rust
use foo;
use bar;
```
Furthermore, this causes problems if you want to fold a multiline import:
```rust
use foo::{
quux
};
use bar;
```
The problem is that now there are two possible folds at the same position: we could fold the first use or we could fold the import group. IMO, the only place where folding groups makes sense is when folding comments. Therefore I have **removed folding import groups in favor of folding multiline imports**.
Regarding folding comments, I made it a bit more robust by requiring that comments can only be folded if they have the same flavor. So if you have a bunch of `//` comments followed by `//!` comments, you will get two separate fold groups instead of a single one.
Finally, I rewrote the API in such a way that it should be trivial to add new folds. You only need to:
* Create a new FoldKind
* Add it to the `fold_kind` function that converts from `SyntaxKind` to `FoldKind`
Fixes #113
Co-authored-by: Adolfo OchagavĂa <[email protected]>
Diffstat (limited to 'crates/ra_editor/src')
-rw-r--r-- | crates/ra_editor/src/folding_ranges.rs | 177 |
1 files changed, 111 insertions, 66 deletions
diff --git a/crates/ra_editor/src/folding_ranges.rs b/crates/ra_editor/src/folding_ranges.rs index 3aabd54ae..a1699d449 100644 --- a/crates/ra_editor/src/folding_ranges.rs +++ b/crates/ra_editor/src/folding_ranges.rs | |||
@@ -1,8 +1,10 @@ | |||
1 | use rustc_hash::FxHashSet; | 1 | use rustc_hash::FxHashSet; |
2 | 2 | ||
3 | use ra_syntax::{ | 3 | use ra_syntax::{ |
4 | ast, | ||
5 | AstNode, | ||
4 | File, TextRange, SyntaxNodeRef, | 6 | File, TextRange, SyntaxNodeRef, |
5 | SyntaxKind, | 7 | SyntaxKind::{self, *}, |
6 | Direction, | 8 | Direction, |
7 | }; | 9 | }; |
8 | 10 | ||
@@ -20,67 +22,97 @@ pub struct Fold { | |||
20 | 22 | ||
21 | pub fn folding_ranges(file: &File) -> Vec<Fold> { | 23 | pub fn folding_ranges(file: &File) -> Vec<Fold> { |
22 | let mut res = vec![]; | 24 | let mut res = vec![]; |
23 | let mut visited = FxHashSet::default(); | 25 | let mut visited_comments = FxHashSet::default(); |
24 | 26 | ||
25 | for node in file.syntax().descendants() { | 27 | for node in file.syntax().descendants() { |
26 | if visited.contains(&node) { | 28 | // Fold items that span multiple lines |
29 | if let Some(kind) = fold_kind(node.kind()) { | ||
30 | if has_newline(node) { | ||
31 | res.push(Fold { range: node.range(), kind }); | ||
32 | } | ||
33 | } | ||
34 | |||
35 | // Also fold groups of comments | ||
36 | if visited_comments.contains(&node) { | ||
27 | continue; | 37 | continue; |
28 | } | 38 | } |
39 | if node.kind() == COMMENT { | ||
40 | contiguous_range_for_comment(node, &mut visited_comments) | ||
41 | .map(|range| res.push(Fold { range, kind: FoldKind::Comment })); | ||
42 | } | ||
43 | } | ||
44 | |||
45 | res | ||
46 | } | ||
29 | 47 | ||
30 | let range_and_kind = match node.kind() { | 48 | fn fold_kind(kind: SyntaxKind) -> Option<FoldKind> { |
31 | SyntaxKind::COMMENT => ( | 49 | match kind { |
32 | contiguous_range_for(SyntaxKind::COMMENT, node, &mut visited), | 50 | COMMENT => Some(FoldKind::Comment), |
33 | Some(FoldKind::Comment), | 51 | USE_ITEM => Some(FoldKind::Imports), |
34 | ), | 52 | _ => None |
35 | SyntaxKind::USE_ITEM => ( | 53 | } |
36 | contiguous_range_for(SyntaxKind::USE_ITEM, node, &mut visited), | 54 | } |
37 | Some(FoldKind::Imports), | 55 | |
38 | ), | 56 | fn has_newline( |
39 | _ => (None, None), | 57 | node: SyntaxNodeRef, |
40 | }; | 58 | ) -> bool { |
41 | 59 | for descendant in node.descendants() { | |
42 | match range_and_kind { | 60 | if let Some(ws) = ast::Whitespace::cast(descendant) { |
43 | (Some(range), Some(kind)) => { | 61 | if ws.has_newlines() { |
44 | res.push(Fold { | 62 | return true; |
45 | range: range, | 63 | } |
46 | kind: kind | 64 | } else if let Some(comment) = ast::Comment::cast(descendant) { |
47 | }); | 65 | if comment.has_newlines() { |
66 | return true; | ||
48 | } | 67 | } |
49 | _ => {} | ||
50 | } | 68 | } |
51 | } | 69 | } |
52 | 70 | ||
53 | res | 71 | false |
54 | } | 72 | } |
55 | 73 | ||
56 | fn contiguous_range_for<'a>( | 74 | fn contiguous_range_for_comment<'a>( |
57 | kind: SyntaxKind, | 75 | first: SyntaxNodeRef<'a>, |
58 | node: SyntaxNodeRef<'a>, | ||
59 | visited: &mut FxHashSet<SyntaxNodeRef<'a>>, | 76 | visited: &mut FxHashSet<SyntaxNodeRef<'a>>, |
60 | ) -> Option<TextRange> { | 77 | ) -> Option<TextRange> { |
61 | visited.insert(node); | 78 | visited.insert(first); |
62 | 79 | ||
63 | let left = node; | 80 | // Only fold comments of the same flavor |
64 | let mut right = node; | 81 | let group_flavor = ast::Comment::cast(first)?.flavor(); |
65 | for node in node.siblings(Direction::Next) { | 82 | |
66 | visited.insert(node); | 83 | let mut last = first; |
67 | match node.kind() { | 84 | for node in first.siblings(Direction::Next) { |
68 | SyntaxKind::WHITESPACE if !node.leaf_text().unwrap().as_str().contains("\n\n") => (), | 85 | if let Some(ws) = ast::Whitespace::cast(node) { |
69 | k => { | 86 | // There is a blank line, which means the group ends here |
70 | if k == kind { | 87 | if ws.count_newlines_lazy().take(2).count() == 2 { |
71 | right = node | 88 | break; |
72 | } else { | 89 | } |
73 | break; | 90 | |
74 | } | 91 | // Ignore whitespace without blank lines |
92 | continue; | ||
93 | } | ||
94 | |||
95 | match ast::Comment::cast(node) { | ||
96 | Some(next_comment) if next_comment.flavor() == group_flavor => { | ||
97 | visited.insert(node); | ||
98 | last = node; | ||
99 | } | ||
100 | // The comment group ends because either: | ||
101 | // * An element of a different kind was reached | ||
102 | // * A comment of a different flavor was reached | ||
103 | _ => { | ||
104 | break | ||
75 | } | 105 | } |
76 | } | 106 | } |
77 | } | 107 | } |
78 | if left != right { | 108 | |
109 | if first != last { | ||
79 | Some(TextRange::from_to( | 110 | Some(TextRange::from_to( |
80 | left.range().start(), | 111 | first.range().start(), |
81 | right.range().end(), | 112 | last.range().end(), |
82 | )) | 113 | )) |
83 | } else { | 114 | } else { |
115 | // The group consists of only one element, therefore it cannot be folded | ||
84 | None | 116 | None |
85 | } | 117 | } |
86 | } | 118 | } |
@@ -88,52 +120,65 @@ fn contiguous_range_for<'a>( | |||
88 | #[cfg(test)] | 120 | #[cfg(test)] |
89 | mod tests { | 121 | mod tests { |
90 | use super::*; | 122 | use super::*; |
123 | use test_utils::extract_ranges; | ||
124 | |||
125 | fn do_check(text: &str, fold_kinds: &[FoldKind]) { | ||
126 | let (ranges, text) = extract_ranges(text); | ||
127 | let file = File::parse(&text); | ||
128 | let folds = folding_ranges(&file); | ||
129 | |||
130 | assert_eq!(folds.len(), ranges.len()); | ||
131 | for ((fold, range), fold_kind) in folds.into_iter().zip(ranges.into_iter()).zip(fold_kinds.into_iter()) { | ||
132 | assert_eq!(fold.range.start(), range.start()); | ||
133 | assert_eq!(fold.range.end(), range.end()); | ||
134 | assert_eq!(&fold.kind, fold_kind); | ||
135 | } | ||
136 | } | ||
91 | 137 | ||
92 | #[test] | 138 | #[test] |
93 | fn test_fold_comments() { | 139 | fn test_fold_comments() { |
94 | let text = r#" | 140 | let text = r#" |
95 | // Hello | 141 | <|>// Hello |
96 | // this is a multiline | 142 | // this is a multiline |
97 | // comment | 143 | // comment |
98 | // | 144 | //<|> |
99 | 145 | ||
100 | // But this is not | 146 | // But this is not |
101 | 147 | ||
102 | fn main() { | 148 | fn main() { |
103 | // We should | 149 | <|>// We should |
104 | // also | 150 | // also |
105 | // fold | 151 | // fold |
106 | // this one. | 152 | // this one.<|> |
153 | <|>//! But this one is different | ||
154 | //! because it has another flavor<|> | ||
155 | <|>/* As does this | ||
156 | multiline comment */<|> | ||
107 | }"#; | 157 | }"#; |
108 | 158 | ||
109 | let file = File::parse(&text); | 159 | let fold_kinds = &[ |
110 | let folds = folding_ranges(&file); | 160 | FoldKind::Comment, |
111 | assert_eq!(folds.len(), 2); | 161 | FoldKind::Comment, |
112 | assert_eq!(folds[0].range.start(), 1.into()); | 162 | FoldKind::Comment, |
113 | assert_eq!(folds[0].range.end(), 46.into()); | 163 | FoldKind::Comment, |
114 | assert_eq!(folds[0].kind, FoldKind::Comment); | 164 | ]; |
115 | 165 | do_check(text, fold_kinds); | |
116 | assert_eq!(folds[1].range.start(), 84.into()); | ||
117 | assert_eq!(folds[1].range.end(), 137.into()); | ||
118 | assert_eq!(folds[1].kind, FoldKind::Comment); | ||
119 | } | 166 | } |
120 | 167 | ||
121 | #[test] | 168 | #[test] |
122 | fn test_fold_imports() { | 169 | fn test_fold_imports() { |
123 | let text = r#" | 170 | let text = r#" |
124 | use std::str; | 171 | <|>use std::{ |
125 | use std::vec; | 172 | str, |
126 | use std::io as iop; | 173 | vec, |
174 | io as iop | ||
175 | };<|> | ||
127 | 176 | ||
128 | fn main() { | 177 | fn main() { |
129 | }"#; | 178 | }"#; |
130 | 179 | ||
131 | let file = File::parse(&text); | 180 | let folds = &[FoldKind::Imports]; |
132 | let folds = folding_ranges(&file); | 181 | do_check(text, folds); |
133 | assert_eq!(folds.len(), 1); | ||
134 | assert_eq!(folds[0].range.start(), 1.into()); | ||
135 | assert_eq!(folds[0].range.end(), 48.into()); | ||
136 | assert_eq!(folds[0].kind, FoldKind::Imports); | ||
137 | } | 182 | } |
138 | 183 | ||
139 | 184 | ||