aboutsummaryrefslogtreecommitdiff
path: root/crates/ra_ide_api_light/src/folding_ranges.rs
diff options
context:
space:
mode:
authorAleksey Kladov <[email protected]>2019-01-08 19:17:36 +0000
committerAleksey Kladov <[email protected]>2019-01-08 19:17:36 +0000
commit1967884d6836219ee78a754ca5c66ac781351559 (patch)
tree7594f37cd0a5200eb097b9d472c61f0223d01d05 /crates/ra_ide_api_light/src/folding_ranges.rs
parent4f4f7933b1b7ff34f8633b1686b18b2d1b994c47 (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.rs297
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 @@
1use rustc_hash::FxHashSet;
2
3use ra_syntax::{
4 ast, AstNode, Direction, SourceFile, SyntaxNode, TextRange,
5 SyntaxKind::{self, *},
6};
7
8#[derive(Debug, PartialEq, Eq)]
9pub enum FoldKind {
10 Comment,
11 Imports,
12 Block,
13}
14
15#[derive(Debug)]
16pub struct Fold {
17 pub range: TextRange,
18 pub kind: FoldKind,
19}
20
21pub 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
61fn 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
71fn 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
87fn 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
125fn 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)]
170mod 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
210fn 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
240fn 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;
251use std::vec;
252use std::io as iop;</fold>
253
254<fold>use std::mem;
255use std::f64;</fold>
256
257use std::collections::HashMap;
258// Some random comment
259use std::collections::VecDeque;
260
261fn 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;
272use std::vec;
273use std::io as iop;</fold>
274
275<fold>use std::mem;
276use std::f64;</fold>
277
278<fold>use std::collections::<fold>{
279 HashMap,
280 VecDeque,
281}</fold>;</fold>
282// Some random comment
283
284fn 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}