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