aboutsummaryrefslogtreecommitdiff
path: root/crates/ra_ide_api
diff options
context:
space:
mode:
Diffstat (limited to 'crates/ra_ide_api')
-rw-r--r--crates/ra_ide_api/src/folding_ranges.rs326
-rw-r--r--crates/ra_ide_api/src/lib.rs6
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 @@
1use rustc_hash::FxHashSet;
2
3use ra_syntax::{
4 AstNode, Direction, SourceFile, SyntaxNode, TextRange,
5 SyntaxKind::{self, *},
6 ast::{self, VisibilityOwner},
7};
8
9#[derive(Debug, PartialEq, Eq)]
10pub enum FoldKind {
11 Comment,
12 Imports,
13 Mods,
14 Block,
15}
16
17#[derive(Debug)]
18pub struct Fold {
19 pub range: TextRange,
20 pub kind: FoldKind,
21}
22
23pub(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
64fn 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
74fn has_visibility(node: &SyntaxNode) -> bool {
75 ast::Module::cast(node).and_then(|m| m.visibility()).is_some()
76}
77
78fn 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
85fn 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
121fn 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)]
163mod 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
201fn 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
231fn 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
242pub mod foo;
243<fold>mod after_pub;
244mod after_pub_next;</fold>
245
246<fold>mod before_pub;
247mod before_pub_next;</fold>
248pub mod bar;
249
250mod not_folding_single;
251pub mod foobar;
252pub not_folding_single_next;
253
254<fold>#[cfg(test)]
255mod with_attribute;
256mod with_attribute_next;</fold>
257
258fn 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;
269use std::vec;
270use std::io as iop;</fold>
271
272<fold>use std::mem;
273use std::f64;</fold>
274
275use std::collections::HashMap;
276// Some random comment
277use std::collections::VecDeque;
278
279fn 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;
290use std::vec;
291use std::io as iop;</fold>
292
293<fold>use std::mem;
294use std::f64;</fold>
295
296<fold>use std::collections::<fold>{
297 HashMap,
298 VecDeque,
299}</fold>;</fold>
300// Some random comment
301
302fn 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#"
318macro_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;
34mod diagnostics; 34mod diagnostics;
35mod syntax_tree; 35mod syntax_tree;
36mod line_index; 36mod line_index;
37mod folding_ranges;
37mod line_index_utils; 38mod 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};
68pub use ra_ide_api_light::{ 70pub use ra_ide_api_light::{
69 Fold, FoldKind, HighlightedRange, Severity, StructureNode, LocalEdit, 71 HighlightedRange, Severity, StructureNode, LocalEdit,
70}; 72};
71pub use ra_db::{ 73pub 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.