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