1 //! FIXME: write short doc here
3 use rustc_hash::FxHashSet;
6 ast::{self, AstNode, AstToken, VisibilityOwner},
7 Direction, NodeOrToken, SourceFile,
9 SyntaxNode, TextRange, TextSize,
12 use lazy_static::lazy_static;
14 #[derive(Debug, PartialEq, Eq)]
30 pub(crate) fn folding_ranges(file: &SourceFile) -> Vec<Fold> {
32 let mut visited_comments = FxHashSet::default();
33 let mut visited_imports = FxHashSet::default();
34 let mut visited_mods = FxHashSet::default();
35 // regions can be nested, here is a LIFO buffer
36 let mut regions_starts: Vec<TextSize> = vec![];
38 for element in file.syntax().descendants_with_tokens() {
39 // Fold items that span multiple lines
40 if let Some(kind) = fold_kind(element.kind()) {
41 let is_multiline = match &element {
42 NodeOrToken::Node(node) => node.text().contains_char('\n'),
43 NodeOrToken::Token(token) => token.text().contains('\n'),
46 res.push(Fold { range: element.text_range(), kind });
52 NodeOrToken::Token(token) => {
53 // Fold groups of comments
54 if let Some(comment) = ast::Comment::cast(token) {
55 if !visited_comments.contains(&comment) {
56 // regions are not really comments
59 static ref RE_START: Regex =
60 Regex::new(r"^\s*//\s*#?region\b").unwrap();
61 static ref RE_END: Regex =
62 Regex::new(r"^\s*//\s*#?endregion\b").unwrap();
64 if RE_START.is_match(comment.text()) {
65 regions_starts.push(comment.syntax().text_range().start());
66 } else if RE_END.is_match(comment.text()) {
67 if !regions_starts.is_empty() {
69 range: TextRange::new(
70 regions_starts.pop().unwrap(),
71 comment.syntax().text_range().end(),
73 kind: FoldKind::Region,
78 contiguous_range_for_comment(comment, &mut visited_comments)
80 res.push(Fold { range, kind: FoldKind::Comment })
86 NodeOrToken::Node(node) => {
87 // Fold groups of imports
88 if node.kind() == USE && !visited_imports.contains(&node) {
89 if let Some(range) = contiguous_range_for_group(&node, &mut visited_imports) {
90 res.push(Fold { range, kind: FoldKind::Imports })
94 // Fold groups of mods
95 if node.kind() == MODULE && !has_visibility(&node) && !visited_mods.contains(&node)
98 contiguous_range_for_group_unless(&node, has_visibility, &mut visited_mods)
100 res.push(Fold { range, kind: FoldKind::Mods })
110 fn fold_kind(kind: SyntaxKind) -> Option<FoldKind> {
112 COMMENT => Some(FoldKind::Comment),
113 ARG_LIST | PARAM_LIST => Some(FoldKind::ArgList),
116 | RECORD_PAT_FIELD_LIST
117 | RECORD_EXPR_FIELD_LIST
124 | TOKEN_TREE => Some(FoldKind::Block),
129 fn has_visibility(node: &SyntaxNode) -> bool {
130 ast::Module::cast(node.clone()).and_then(|m| m.visibility()).is_some()
133 fn contiguous_range_for_group(
135 visited: &mut FxHashSet<SyntaxNode>,
136 ) -> Option<TextRange> {
137 contiguous_range_for_group_unless(first, |_| false, visited)
140 fn contiguous_range_for_group_unless(
142 unless: impl Fn(&SyntaxNode) -> bool,
143 visited: &mut FxHashSet<SyntaxNode>,
144 ) -> Option<TextRange> {
145 visited.insert(first.clone());
147 let mut last = first.clone();
148 for element in first.siblings_with_tokens(Direction::Next) {
149 let node = match element {
150 NodeOrToken::Token(token) => {
151 if let Some(ws) = ast::Whitespace::cast(token) {
152 if !ws.spans_multiple_lines() {
153 // Ignore whitespace without blank lines
157 // There is a blank line or another token, which means that the
161 NodeOrToken::Node(node) => node,
164 // Stop if we find a node that doesn't belong to the group
165 if node.kind() != first.kind() || unless(&node) {
169 visited.insert(node.clone());
174 Some(TextRange::new(first.text_range().start(), last.text_range().end()))
176 // The group consists of only one element, therefore it cannot be folded
181 fn contiguous_range_for_comment(
183 visited: &mut FxHashSet<ast::Comment>,
184 ) -> Option<TextRange> {
185 visited.insert(first.clone());
187 // Only fold comments of the same flavor
188 let group_kind = first.kind();
189 if !group_kind.shape.is_line() {
193 let mut last = first.clone();
194 for element in first.syntax().siblings_with_tokens(Direction::Next) {
196 NodeOrToken::Token(token) => {
197 if let Some(ws) = ast::Whitespace::cast(token.clone()) {
198 if !ws.spans_multiple_lines() {
199 // Ignore whitespace without blank lines
203 if let Some(c) = ast::Comment::cast(token) {
204 if c.kind() == group_kind {
205 // regions are not really comments
208 static ref RE_START: Regex =
209 Regex::new(r"^\s*//\s*#?region\b").unwrap();
210 static ref RE_END: Regex =
211 Regex::new(r"^\s*//\s*#?endregion\b").unwrap();
213 if RE_START.is_match(c.text()) || RE_END.is_match(c.text()) {
216 visited.insert(c.clone());
222 // The comment group ends because either:
223 // * An element of a different kind was reached
224 // * A comment of a different flavor was reached
227 NodeOrToken::Node(_) => break,
232 Some(TextRange::new(first.syntax().text_range().start(), last.syntax().text_range().end()))
234 // The group consists of only one element, therefore it cannot be folded
241 use test_utils::extract_tags;
245 fn check(ra_fixture: &str) {
246 let (ranges, text) = extract_tags(ra_fixture, "fold");
248 let parse = SourceFile::parse(&text);
249 let folds = folding_ranges(&parse.tree());
253 "The amount of folds is different than the expected amount"
256 for (fold, (range, attr)) in folds.iter().zip(ranges.into_iter()) {
257 assert_eq!(fold.range.start(), range.start());
258 assert_eq!(fold.range.end(), range.end());
260 let kind = match fold.kind {
261 FoldKind::Comment => "comment",
262 FoldKind::Imports => "imports",
263 FoldKind::Mods => "mods",
264 FoldKind::Block => "block",
265 FoldKind::ArgList => "arglist",
266 FoldKind::Region => "region",
268 assert_eq!(kind, &attr.unwrap());
273 fn test_fold_comments() {
276 <fold comment>// Hello
277 // this is a multiline
283 fn main() <fold block>{
284 <fold comment>// We should
288 <fold comment>//! But this one is different
289 //! because it has another flavor</fold>
290 <fold comment>/* As does this
291 multiline comment */</fold>
297 fn test_fold_imports() {
300 use std::<fold block>{
306 fn main() <fold block>{
312 fn test_fold_mods() {
317 <fold mods>mod after_pub;
318 mod after_pub_next;</fold>
320 <fold mods>mod before_pub;
321 mod before_pub_next;</fold>
324 mod not_folding_single;
326 pub not_folding_single_next;
328 <fold mods>#[cfg(test)]
330 mod with_attribute_next;</fold>
332 fn main() <fold block>{
338 fn test_fold_import_groups() {
341 <fold imports>use std::str;
343 use std::io as iop;</fold>
345 <fold imports>use std::mem;
348 <fold imports>use std::collections::HashMap;
349 // Some random comment
350 use std::collections::VecDeque;</fold>
352 fn main() <fold block>{
358 fn test_fold_import_and_groups() {
361 <fold imports>use std::str;
363 use std::io as iop;</fold>
365 <fold imports>use std::mem;
368 use std::collections::<fold block>{
372 // Some random comment
374 fn main() <fold block>{
380 fn test_folds_structs() {
383 struct Foo <fold block>{
390 fn test_folds_traits() {
393 trait Foo <fold block>{
400 fn test_folds_macros() {
403 macro_rules! foo <fold block>{
404 ($($tt:tt)*) => { $($tt)* }
411 fn test_fold_match_arms() {
414 fn main() <fold block>{
415 match 0 <fold block>{
425 fn fold_big_calls() {
428 fn main() <fold block>{
429 frobnicate<fold arglist>(
440 fn fold_record_literals() {
443 const _: S = S <fold block>{
451 fn fold_multiline_params() {
454 fn foo<fold arglist>(
464 log_init_for_test_debug();
465 // only error level log is printed on the terminal
466 log::error!("test fold_region");
469 // 1. some normal comment
470 <fold region>// region: test
471 // 2. some normal comment
472 calling_function(x,y);
473 // endregion: test</fold>
478 fn log_init_for_test_debug() {
479 let _ = env_logger::builder().is_test(true).try_init();