diff options
author | bors[bot] <26634292+bors[bot]@users.noreply.github.com> | 2020-08-13 16:59:50 +0100 |
---|---|---|
committer | GitHub <[email protected]> | 2020-08-13 16:59:50 +0100 |
commit | 018a6cac072767dfd630c22e6d9ce134b7bb09af (patch) | |
tree | 4293492e643f9a604c5f30e051289bcea182694c /crates/ide/src/folding_ranges.rs | |
parent | 00fb411f3edea72a1a9739f7df6f21cca045730b (diff) | |
parent | 6bc2633c90cedad057c5201d1ab7f67b57247004 (diff) |
Merge #5750
5750: Rename ra_ide -> ide
r=matklad a=matklad
bors r+
🤖
Co-authored-by: Aleksey Kladov <[email protected]>
Diffstat (limited to 'crates/ide/src/folding_ranges.rs')
-rw-r--r-- | crates/ide/src/folding_ranges.rs | 422 |
1 files changed, 422 insertions, 0 deletions
diff --git a/crates/ide/src/folding_ranges.rs b/crates/ide/src/folding_ranges.rs new file mode 100644 index 000000000..7523aec55 --- /dev/null +++ b/crates/ide/src/folding_ranges.rs | |||
@@ -0,0 +1,422 @@ | |||
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, | ||
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 && !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 => Some(FoldKind::Imports), | ||
87 | ARG_LIST | PARAM_LIST => Some(FoldKind::ArgList), | ||
88 | ASSOC_ITEM_LIST | ||
89 | | RECORD_FIELD_LIST | ||
90 | | RECORD_PAT_FIELD_LIST | ||
91 | | RECORD_EXPR_FIELD_LIST | ||
92 | | ITEM_LIST | ||
93 | | EXTERN_ITEM_LIST | ||
94 | | USE_TREE_LIST | ||
95 | | BLOCK_EXPR | ||
96 | | MATCH_ARM_LIST | ||
97 | | VARIANT_LIST | ||
98 | | TOKEN_TREE => Some(FoldKind::Block), | ||
99 | _ => None, | ||
100 | } | ||
101 | } | ||
102 | |||
103 | fn has_visibility(node: &SyntaxNode) -> bool { | ||
104 | ast::Module::cast(node.clone()).and_then(|m| m.visibility()).is_some() | ||
105 | } | ||
106 | |||
107 | fn contiguous_range_for_group( | ||
108 | first: &SyntaxNode, | ||
109 | visited: &mut FxHashSet<SyntaxNode>, | ||
110 | ) -> Option<TextRange> { | ||
111 | contiguous_range_for_group_unless(first, |_| false, visited) | ||
112 | } | ||
113 | |||
114 | fn contiguous_range_for_group_unless( | ||
115 | first: &SyntaxNode, | ||
116 | unless: impl Fn(&SyntaxNode) -> bool, | ||
117 | visited: &mut FxHashSet<SyntaxNode>, | ||
118 | ) -> Option<TextRange> { | ||
119 | visited.insert(first.clone()); | ||
120 | |||
121 | let mut last = first.clone(); | ||
122 | for element in first.siblings_with_tokens(Direction::Next) { | ||
123 | let node = match element { | ||
124 | NodeOrToken::Token(token) => { | ||
125 | if let Some(ws) = ast::Whitespace::cast(token) { | ||
126 | if !ws.spans_multiple_lines() { | ||
127 | // Ignore whitespace without blank lines | ||
128 | continue; | ||
129 | } | ||
130 | } | ||
131 | // There is a blank line or another token, which means that the | ||
132 | // group ends here | ||
133 | break; | ||
134 | } | ||
135 | NodeOrToken::Node(node) => node, | ||
136 | }; | ||
137 | |||
138 | // Stop if we find a node that doesn't belong to the group | ||
139 | if node.kind() != first.kind() || unless(&node) { | ||
140 | break; | ||
141 | } | ||
142 | |||
143 | visited.insert(node.clone()); | ||
144 | last = node; | ||
145 | } | ||
146 | |||
147 | if first != &last { | ||
148 | Some(TextRange::new(first.text_range().start(), last.text_range().end())) | ||
149 | } else { | ||
150 | // The group consists of only one element, therefore it cannot be folded | ||
151 | None | ||
152 | } | ||
153 | } | ||
154 | |||
155 | fn contiguous_range_for_comment( | ||
156 | first: ast::Comment, | ||
157 | visited: &mut FxHashSet<ast::Comment>, | ||
158 | ) -> Option<TextRange> { | ||
159 | visited.insert(first.clone()); | ||
160 | |||
161 | // Only fold comments of the same flavor | ||
162 | let group_kind = first.kind(); | ||
163 | if !group_kind.shape.is_line() { | ||
164 | return None; | ||
165 | } | ||
166 | |||
167 | let mut last = first.clone(); | ||
168 | for element in first.syntax().siblings_with_tokens(Direction::Next) { | ||
169 | match element { | ||
170 | NodeOrToken::Token(token) => { | ||
171 | if let Some(ws) = ast::Whitespace::cast(token.clone()) { | ||
172 | if !ws.spans_multiple_lines() { | ||
173 | // Ignore whitespace without blank lines | ||
174 | continue; | ||
175 | } | ||
176 | } | ||
177 | if let Some(c) = ast::Comment::cast(token) { | ||
178 | if c.kind() == group_kind { | ||
179 | visited.insert(c.clone()); | ||
180 | last = c; | ||
181 | continue; | ||
182 | } | ||
183 | } | ||
184 | // The comment group ends because either: | ||
185 | // * An element of a different kind was reached | ||
186 | // * A comment of a different flavor was reached | ||
187 | break; | ||
188 | } | ||
189 | NodeOrToken::Node(_) => break, | ||
190 | }; | ||
191 | } | ||
192 | |||
193 | if first != last { | ||
194 | Some(TextRange::new(first.syntax().text_range().start(), last.syntax().text_range().end())) | ||
195 | } else { | ||
196 | // The group consists of only one element, therefore it cannot be folded | ||
197 | None | ||
198 | } | ||
199 | } | ||
200 | |||
201 | #[cfg(test)] | ||
202 | mod tests { | ||
203 | use test_utils::extract_tags; | ||
204 | |||
205 | use super::*; | ||
206 | |||
207 | fn check(ra_fixture: &str) { | ||
208 | let (ranges, text) = extract_tags(ra_fixture, "fold"); | ||
209 | |||
210 | let parse = SourceFile::parse(&text); | ||
211 | let folds = folding_ranges(&parse.tree()); | ||
212 | assert_eq!( | ||
213 | folds.len(), | ||
214 | ranges.len(), | ||
215 | "The amount of folds is different than the expected amount" | ||
216 | ); | ||
217 | |||
218 | for (fold, (range, attr)) in folds.iter().zip(ranges.into_iter()) { | ||
219 | assert_eq!(fold.range.start(), range.start()); | ||
220 | assert_eq!(fold.range.end(), range.end()); | ||
221 | |||
222 | let kind = match fold.kind { | ||
223 | FoldKind::Comment => "comment", | ||
224 | FoldKind::Imports => "imports", | ||
225 | FoldKind::Mods => "mods", | ||
226 | FoldKind::Block => "block", | ||
227 | FoldKind::ArgList => "arglist", | ||
228 | }; | ||
229 | assert_eq!(kind, &attr.unwrap()); | ||
230 | } | ||
231 | } | ||
232 | |||
233 | #[test] | ||
234 | fn test_fold_comments() { | ||
235 | check( | ||
236 | r#" | ||
237 | <fold comment>// Hello | ||
238 | // this is a multiline | ||
239 | // comment | ||
240 | //</fold> | ||
241 | |||
242 | // But this is not | ||
243 | |||
244 | fn main() <fold block>{ | ||
245 | <fold comment>// We should | ||
246 | // also | ||
247 | // fold | ||
248 | // this one.</fold> | ||
249 | <fold comment>//! But this one is different | ||
250 | //! because it has another flavor</fold> | ||
251 | <fold comment>/* As does this | ||
252 | multiline comment */</fold> | ||
253 | }</fold>"#, | ||
254 | ); | ||
255 | } | ||
256 | |||
257 | #[test] | ||
258 | fn test_fold_imports() { | ||
259 | check( | ||
260 | r#" | ||
261 | <fold imports>use std::<fold block>{ | ||
262 | str, | ||
263 | vec, | ||
264 | io as iop | ||
265 | }</fold>;</fold> | ||
266 | |||
267 | fn main() <fold block>{ | ||
268 | }</fold>"#, | ||
269 | ); | ||
270 | } | ||
271 | |||
272 | #[test] | ||
273 | fn test_fold_mods() { | ||
274 | check( | ||
275 | r#" | ||
276 | |||
277 | pub mod foo; | ||
278 | <fold mods>mod after_pub; | ||
279 | mod after_pub_next;</fold> | ||
280 | |||
281 | <fold mods>mod before_pub; | ||
282 | mod before_pub_next;</fold> | ||
283 | pub mod bar; | ||
284 | |||
285 | mod not_folding_single; | ||
286 | pub mod foobar; | ||
287 | pub not_folding_single_next; | ||
288 | |||
289 | <fold mods>#[cfg(test)] | ||
290 | mod with_attribute; | ||
291 | mod with_attribute_next;</fold> | ||
292 | |||
293 | fn main() <fold block>{ | ||
294 | }</fold>"#, | ||
295 | ); | ||
296 | } | ||
297 | |||
298 | #[test] | ||
299 | fn test_fold_import_groups() { | ||
300 | check( | ||
301 | r#" | ||
302 | <fold imports>use std::str; | ||
303 | use std::vec; | ||
304 | use std::io as iop;</fold> | ||
305 | |||
306 | <fold imports>use std::mem; | ||
307 | use std::f64;</fold> | ||
308 | |||
309 | use std::collections::HashMap; | ||
310 | // Some random comment | ||
311 | use std::collections::VecDeque; | ||
312 | |||
313 | fn main() <fold block>{ | ||
314 | }</fold>"#, | ||
315 | ); | ||
316 | } | ||
317 | |||
318 | #[test] | ||
319 | fn test_fold_import_and_groups() { | ||
320 | check( | ||
321 | r#" | ||
322 | <fold imports>use std::str; | ||
323 | use std::vec; | ||
324 | use std::io as iop;</fold> | ||
325 | |||
326 | <fold imports>use std::mem; | ||
327 | use std::f64;</fold> | ||
328 | |||
329 | <fold imports>use std::collections::<fold block>{ | ||
330 | HashMap, | ||
331 | VecDeque, | ||
332 | }</fold>;</fold> | ||
333 | // Some random comment | ||
334 | |||
335 | fn main() <fold block>{ | ||
336 | }</fold>"#, | ||
337 | ); | ||
338 | } | ||
339 | |||
340 | #[test] | ||
341 | fn test_folds_structs() { | ||
342 | check( | ||
343 | r#" | ||
344 | struct Foo <fold block>{ | ||
345 | }</fold> | ||
346 | "#, | ||
347 | ); | ||
348 | } | ||
349 | |||
350 | #[test] | ||
351 | fn test_folds_traits() { | ||
352 | check( | ||
353 | r#" | ||
354 | trait Foo <fold block>{ | ||
355 | }</fold> | ||
356 | "#, | ||
357 | ); | ||
358 | } | ||
359 | |||
360 | #[test] | ||
361 | fn test_folds_macros() { | ||
362 | check( | ||
363 | r#" | ||
364 | macro_rules! foo <fold block>{ | ||
365 | ($($tt:tt)*) => { $($tt)* } | ||
366 | }</fold> | ||
367 | "#, | ||
368 | ); | ||
369 | } | ||
370 | |||
371 | #[test] | ||
372 | fn test_fold_match_arms() { | ||
373 | check( | ||
374 | r#" | ||
375 | fn main() <fold block>{ | ||
376 | match 0 <fold block>{ | ||
377 | 0 => 0, | ||
378 | _ => 1, | ||
379 | }</fold> | ||
380 | }</fold> | ||
381 | "#, | ||
382 | ); | ||
383 | } | ||
384 | |||
385 | #[test] | ||
386 | fn fold_big_calls() { | ||
387 | check( | ||
388 | r#" | ||
389 | fn main() <fold block>{ | ||
390 | frobnicate<fold arglist>( | ||
391 | 1, | ||
392 | 2, | ||
393 | 3, | ||
394 | )</fold> | ||
395 | }</fold> | ||
396 | "#, | ||
397 | ) | ||
398 | } | ||
399 | |||
400 | #[test] | ||
401 | fn fold_record_literals() { | ||
402 | check( | ||
403 | r#" | ||
404 | const _: S = S <fold block>{ | ||
405 | |||
406 | }</fold>; | ||
407 | "#, | ||
408 | ) | ||
409 | } | ||
410 | |||
411 | #[test] | ||
412 | fn fold_multiline_params() { | ||
413 | check( | ||
414 | r#" | ||
415 | fn foo<fold arglist>( | ||
416 | x: i32, | ||
417 | y: String, | ||
418 | )</fold> {} | ||
419 | "#, | ||
420 | ) | ||
421 | } | ||
422 | } | ||