diff options
author | Aleksey Kladov <[email protected]> | 2019-01-08 19:17:36 +0000 |
---|---|---|
committer | Aleksey Kladov <[email protected]> | 2019-01-08 19:17:36 +0000 |
commit | 1967884d6836219ee78a754ca5c66ac781351559 (patch) | |
tree | 7594f37cd0a5200eb097b9d472c61f0223d01d05 /crates/ra_ide_api_light/src/folding_ranges.rs | |
parent | 4f4f7933b1b7ff34f8633b1686b18b2d1b994c47 (diff) |
rename ra_editor -> ra_ide_api_light
Diffstat (limited to 'crates/ra_ide_api_light/src/folding_ranges.rs')
-rw-r--r-- | crates/ra_ide_api_light/src/folding_ranges.rs | 297 |
1 files changed, 297 insertions, 0 deletions
diff --git a/crates/ra_ide_api_light/src/folding_ranges.rs b/crates/ra_ide_api_light/src/folding_ranges.rs new file mode 100644 index 000000000..6f3106889 --- /dev/null +++ b/crates/ra_ide_api_light/src/folding_ranges.rs | |||
@@ -0,0 +1,297 @@ | |||
1 | use rustc_hash::FxHashSet; | ||
2 | |||
3 | use ra_syntax::{ | ||
4 | ast, AstNode, Direction, SourceFile, SyntaxNode, TextRange, | ||
5 | SyntaxKind::{self, *}, | ||
6 | }; | ||
7 | |||
8 | #[derive(Debug, PartialEq, Eq)] | ||
9 | pub enum FoldKind { | ||
10 | Comment, | ||
11 | Imports, | ||
12 | Block, | ||
13 | } | ||
14 | |||
15 | #[derive(Debug)] | ||
16 | pub struct Fold { | ||
17 | pub range: TextRange, | ||
18 | pub kind: FoldKind, | ||
19 | } | ||
20 | |||
21 | pub fn folding_ranges(file: &SourceFile) -> Vec<Fold> { | ||
22 | let mut res = vec![]; | ||
23 | let mut visited_comments = FxHashSet::default(); | ||
24 | let mut visited_imports = FxHashSet::default(); | ||
25 | |||
26 | for node in file.syntax().descendants() { | ||
27 | // Fold items that span multiple lines | ||
28 | if let Some(kind) = fold_kind(node.kind()) { | ||
29 | if has_newline(node) { | ||
30 | res.push(Fold { | ||
31 | range: node.range(), | ||
32 | kind, | ||
33 | }); | ||
34 | } | ||
35 | } | ||
36 | |||
37 | // Fold groups of comments | ||
38 | if node.kind() == COMMENT && !visited_comments.contains(&node) { | ||
39 | if let Some(range) = contiguous_range_for_comment(node, &mut visited_comments) { | ||
40 | res.push(Fold { | ||
41 | range, | ||
42 | kind: FoldKind::Comment, | ||
43 | }) | ||
44 | } | ||
45 | } | ||
46 | |||
47 | // Fold groups of imports | ||
48 | if node.kind() == USE_ITEM && !visited_imports.contains(&node) { | ||
49 | if let Some(range) = contiguous_range_for_group(node, &mut visited_imports) { | ||
50 | res.push(Fold { | ||
51 | range, | ||
52 | kind: FoldKind::Imports, | ||
53 | }) | ||
54 | } | ||
55 | } | ||
56 | } | ||
57 | |||
58 | res | ||
59 | } | ||
60 | |||
61 | fn fold_kind(kind: SyntaxKind) -> Option<FoldKind> { | ||
62 | match kind { | ||
63 | COMMENT => Some(FoldKind::Comment), | ||
64 | USE_ITEM => Some(FoldKind::Imports), | ||
65 | NAMED_FIELD_DEF_LIST | FIELD_PAT_LIST | ITEM_LIST | EXTERN_ITEM_LIST | USE_TREE_LIST | ||
66 | | BLOCK | ENUM_VARIANT_LIST => Some(FoldKind::Block), | ||
67 | _ => None, | ||
68 | } | ||
69 | } | ||
70 | |||
71 | fn has_newline(node: &SyntaxNode) -> bool { | ||
72 | for descendant in node.descendants() { | ||
73 | if let Some(ws) = ast::Whitespace::cast(descendant) { | ||
74 | if ws.has_newlines() { | ||
75 | return true; | ||
76 | } | ||
77 | } else if let Some(comment) = ast::Comment::cast(descendant) { | ||
78 | if comment.has_newlines() { | ||
79 | return true; | ||
80 | } | ||
81 | } | ||
82 | } | ||
83 | |||
84 | false | ||
85 | } | ||
86 | |||
87 | fn contiguous_range_for_group<'a>( | ||
88 | first: &'a SyntaxNode, | ||
89 | visited: &mut FxHashSet<&'a SyntaxNode>, | ||
90 | ) -> Option<TextRange> { | ||
91 | visited.insert(first); | ||
92 | |||
93 | let mut last = first; | ||
94 | for node in first.siblings(Direction::Next) { | ||
95 | if let Some(ws) = ast::Whitespace::cast(node) { | ||
96 | // There is a blank line, which means that the group ends here | ||
97 | if ws.count_newlines_lazy().take(2).count() == 2 { | ||
98 | break; | ||
99 | } | ||
100 | |||
101 | // Ignore whitespace without blank lines | ||
102 | continue; | ||
103 | } | ||
104 | |||
105 | // Stop if we find a node that doesn't belong to the group | ||
106 | if node.kind() != first.kind() { | ||
107 | break; | ||
108 | } | ||
109 | |||
110 | visited.insert(node); | ||
111 | last = node; | ||
112 | } | ||
113 | |||
114 | if first != last { | ||
115 | Some(TextRange::from_to( | ||
116 | first.range().start(), | ||
117 | last.range().end(), | ||
118 | )) | ||
119 | } else { | ||
120 | // The group consists of only one element, therefore it cannot be folded | ||
121 | None | ||
122 | } | ||
123 | } | ||
124 | |||
125 | fn contiguous_range_for_comment<'a>( | ||
126 | first: &'a SyntaxNode, | ||
127 | visited: &mut FxHashSet<&'a SyntaxNode>, | ||
128 | ) -> Option<TextRange> { | ||
129 | visited.insert(first); | ||
130 | |||
131 | // Only fold comments of the same flavor | ||
132 | let group_flavor = ast::Comment::cast(first)?.flavor(); | ||
133 | |||
134 | let mut last = first; | ||
135 | for node in first.siblings(Direction::Next) { | ||
136 | if let Some(ws) = ast::Whitespace::cast(node) { | ||
137 | // There is a blank line, which means the group ends here | ||
138 | if ws.count_newlines_lazy().take(2).count() == 2 { | ||
139 | break; | ||
140 | } | ||
141 | |||
142 | // Ignore whitespace without blank lines | ||
143 | continue; | ||
144 | } | ||
145 | |||
146 | match ast::Comment::cast(node) { | ||
147 | Some(next_comment) if next_comment.flavor() == group_flavor => { | ||
148 | visited.insert(node); | ||
149 | last = node; | ||
150 | } | ||
151 | // The comment group ends because either: | ||
152 | // * An element of a different kind was reached | ||
153 | // * A comment of a different flavor was reached | ||
154 | _ => break, | ||
155 | } | ||
156 | } | ||
157 | |||
158 | if first != last { | ||
159 | Some(TextRange::from_to( | ||
160 | first.range().start(), | ||
161 | last.range().end(), | ||
162 | )) | ||
163 | } else { | ||
164 | // The group consists of only one element, therefore it cannot be folded | ||
165 | None | ||
166 | } | ||
167 | } | ||
168 | |||
169 | #[cfg(test)] | ||
170 | mod tests { | ||
171 | use super::*; | ||
172 | use test_utils::extract_ranges; | ||
173 | |||
174 | fn do_check(text: &str, fold_kinds: &[FoldKind]) { | ||
175 | let (ranges, text) = extract_ranges(text, "fold"); | ||
176 | let file = SourceFile::parse(&text); | ||
177 | let folds = folding_ranges(&file); | ||
178 | |||
179 | assert_eq!( | ||
180 | folds.len(), | ||
181 | ranges.len(), | ||
182 | "The amount of folds is different than the expected amount" | ||
183 | ); | ||
184 | assert_eq!( | ||
185 | folds.len(), | ||
186 | fold_kinds.len(), | ||
187 | "The amount of fold kinds is different than the expected amount" | ||
188 | ); | ||
189 | for ((fold, range), fold_kind) in folds | ||
190 | .into_iter() | ||
191 | .zip(ranges.into_iter()) | ||
192 | .zip(fold_kinds.into_iter()) | ||
193 | { | ||
194 | assert_eq!(fold.range.start(), range.start()); | ||
195 | assert_eq!(fold.range.end(), range.end()); | ||
196 | assert_eq!(&fold.kind, fold_kind); | ||
197 | } | ||
198 | } | ||
199 | |||
200 | #[test] | ||
201 | fn test_fold_comments() { | ||
202 | let text = r#" | ||
203 | <fold>// Hello | ||
204 | // this is a multiline | ||
205 | // comment | ||
206 | //</fold> | ||
207 | |||
208 | // But this is not | ||
209 | |||
210 | fn main() <fold>{ | ||
211 | <fold>// We should | ||
212 | // also | ||
213 | // fold | ||
214 | // this one.</fold> | ||
215 | <fold>//! But this one is different | ||
216 | //! because it has another flavor</fold> | ||
217 | <fold>/* As does this | ||
218 | multiline comment */</fold> | ||
219 | }</fold>"#; | ||
220 | |||
221 | let fold_kinds = &[ | ||
222 | FoldKind::Comment, | ||
223 | FoldKind::Block, | ||
224 | FoldKind::Comment, | ||
225 | FoldKind::Comment, | ||
226 | FoldKind::Comment, | ||
227 | ]; | ||
228 | do_check(text, fold_kinds); | ||
229 | } | ||
230 | |||
231 | #[test] | ||
232 | fn test_fold_imports() { | ||
233 | let text = r#" | ||
234 | <fold>use std::<fold>{ | ||
235 | str, | ||
236 | vec, | ||
237 | io as iop | ||
238 | }</fold>;</fold> | ||
239 | |||
240 | fn main() <fold>{ | ||
241 | }</fold>"#; | ||
242 | |||
243 | let folds = &[FoldKind::Imports, FoldKind::Block, FoldKind::Block]; | ||
244 | do_check(text, folds); | ||
245 | } | ||
246 | |||
247 | #[test] | ||
248 | fn test_fold_import_groups() { | ||
249 | let text = r#" | ||
250 | <fold>use std::str; | ||
251 | use std::vec; | ||
252 | use std::io as iop;</fold> | ||
253 | |||
254 | <fold>use std::mem; | ||
255 | use std::f64;</fold> | ||
256 | |||
257 | use std::collections::HashMap; | ||
258 | // Some random comment | ||
259 | use std::collections::VecDeque; | ||
260 | |||
261 | fn main() <fold>{ | ||
262 | }</fold>"#; | ||
263 | |||
264 | let folds = &[FoldKind::Imports, FoldKind::Imports, FoldKind::Block]; | ||
265 | do_check(text, folds); | ||
266 | } | ||
267 | |||
268 | #[test] | ||
269 | fn test_fold_import_and_groups() { | ||
270 | let text = r#" | ||
271 | <fold>use std::str; | ||
272 | use std::vec; | ||
273 | use std::io as iop;</fold> | ||
274 | |||
275 | <fold>use std::mem; | ||
276 | use std::f64;</fold> | ||
277 | |||
278 | <fold>use std::collections::<fold>{ | ||
279 | HashMap, | ||
280 | VecDeque, | ||
281 | }</fold>;</fold> | ||
282 | // Some random comment | ||
283 | |||
284 | fn main() <fold>{ | ||
285 | }</fold>"#; | ||
286 | |||
287 | let folds = &[ | ||
288 | FoldKind::Imports, | ||
289 | FoldKind::Imports, | ||
290 | FoldKind::Imports, | ||
291 | FoldKind::Block, | ||
292 | FoldKind::Block, | ||
293 | ]; | ||
294 | do_check(text, folds); | ||
295 | } | ||
296 | |||
297 | } | ||