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 #[derive(Debug, PartialEq, Eq)]
28 pub(crate) fn folding_ranges(file: &SourceFile) -> Vec<Fold> {
30 let mut visited_comments = FxHashSet::default();
31 let mut visited_imports = FxHashSet::default();
32 let mut visited_mods = FxHashSet::default();
33 // regions can be nested, here is a LIFO buffer
34 let mut regions_starts: Vec<TextSize> = vec![];
36 for element in file.syntax().descendants_with_tokens() {
37 // Fold items that span multiple lines
38 if let Some(kind) = fold_kind(element.kind()) {
39 let is_multiline = match &element {
40 NodeOrToken::Node(node) => node.text().contains_char('\n'),
41 NodeOrToken::Token(token) => token.text().contains('\n'),
44 res.push(Fold { range: element.text_range(), kind });
50 NodeOrToken::Token(token) => {
51 // Fold groups of comments
52 if let Some(comment) = ast::Comment::cast(token) {
53 if !visited_comments.contains(&comment) {
54 // regions are not real comments
55 if comment.text().trim().starts_with("// region:") {
56 regions_starts.push(comment.syntax().text_range().start());
57 } else if comment.text().trim().starts_with("// endregion") {
58 if let Some(region) = regions_starts.pop() {
60 range: TextRange::new(
62 comment.syntax().text_range().end(),
64 kind: FoldKind::Region,
69 contiguous_range_for_comment(comment, &mut visited_comments)
71 res.push(Fold { range, kind: FoldKind::Comment })
77 NodeOrToken::Node(node) => {
78 // Fold groups of imports
79 if node.kind() == USE && !visited_imports.contains(&node) {
80 if let Some(range) = contiguous_range_for_group(&node, &mut visited_imports) {
81 res.push(Fold { range, kind: FoldKind::Imports })
85 // Fold groups of mods
86 if node.kind() == MODULE && !has_visibility(&node) && !visited_mods.contains(&node)
89 contiguous_range_for_group_unless(&node, has_visibility, &mut visited_mods)
91 res.push(Fold { range, kind: FoldKind::Mods })
101 fn fold_kind(kind: SyntaxKind) -> Option<FoldKind> {
103 COMMENT => Some(FoldKind::Comment),
104 ARG_LIST | PARAM_LIST => Some(FoldKind::ArgList),
107 | RECORD_PAT_FIELD_LIST
108 | RECORD_EXPR_FIELD_LIST
115 | TOKEN_TREE => Some(FoldKind::Block),
120 fn has_visibility(node: &SyntaxNode) -> bool {
121 ast::Module::cast(node.clone()).and_then(|m| m.visibility()).is_some()
124 fn contiguous_range_for_group(
126 visited: &mut FxHashSet<SyntaxNode>,
127 ) -> Option<TextRange> {
128 contiguous_range_for_group_unless(first, |_| false, visited)
131 fn contiguous_range_for_group_unless(
133 unless: impl Fn(&SyntaxNode) -> bool,
134 visited: &mut FxHashSet<SyntaxNode>,
135 ) -> Option<TextRange> {
136 visited.insert(first.clone());
138 let mut last = first.clone();
139 for element in first.siblings_with_tokens(Direction::Next) {
140 let node = match element {
141 NodeOrToken::Token(token) => {
142 if let Some(ws) = ast::Whitespace::cast(token) {
143 if !ws.spans_multiple_lines() {
144 // Ignore whitespace without blank lines
148 // There is a blank line or another token, which means that the
152 NodeOrToken::Node(node) => node,
155 // Stop if we find a node that doesn't belong to the group
156 if node.kind() != first.kind() || unless(&node) {
160 visited.insert(node.clone());
165 Some(TextRange::new(first.text_range().start(), last.text_range().end()))
167 // The group consists of only one element, therefore it cannot be folded
172 fn contiguous_range_for_comment(
174 visited: &mut FxHashSet<ast::Comment>,
175 ) -> Option<TextRange> {
176 visited.insert(first.clone());
178 // Only fold comments of the same flavor
179 let group_kind = first.kind();
180 if !group_kind.shape.is_line() {
184 let mut last = first.clone();
185 for element in first.syntax().siblings_with_tokens(Direction::Next) {
187 NodeOrToken::Token(token) => {
188 if let Some(ws) = ast::Whitespace::cast(token.clone()) {
189 if !ws.spans_multiple_lines() {
190 // Ignore whitespace without blank lines
194 if let Some(c) = ast::Comment::cast(token) {
195 if c.kind() == group_kind {
196 // regions are not real comments
197 if c.text().trim().starts_with("// region:")
198 || c.text().trim().starts_with("// endregion")
202 visited.insert(c.clone());
208 // The comment group ends because either:
209 // * An element of a different kind was reached
210 // * A comment of a different flavor was reached
213 NodeOrToken::Node(_) => break,
218 Some(TextRange::new(first.syntax().text_range().start(), last.syntax().text_range().end()))
220 // The group consists of only one element, therefore it cannot be folded
227 use test_utils::extract_tags;
231 fn check(ra_fixture: &str) {
232 let (ranges, text) = extract_tags(ra_fixture, "fold");
234 let parse = SourceFile::parse(&text);
235 let folds = folding_ranges(&parse.tree());
239 "The amount of folds is different than the expected amount"
242 for (fold, (range, attr)) in folds.iter().zip(ranges.into_iter()) {
243 assert_eq!(fold.range.start(), range.start());
244 assert_eq!(fold.range.end(), range.end());
246 let kind = match fold.kind {
247 FoldKind::Comment => "comment",
248 FoldKind::Imports => "imports",
249 FoldKind::Mods => "mods",
250 FoldKind::Block => "block",
251 FoldKind::ArgList => "arglist",
252 FoldKind::Region => "region",
254 assert_eq!(kind, &attr.unwrap());
259 fn test_fold_comments() {
262 <fold comment>// Hello
263 // this is a multiline
269 fn main() <fold block>{
270 <fold comment>// We should
274 <fold comment>//! But this one is different
275 //! because it has another flavor</fold>
276 <fold comment>/* As does this
277 multiline comment */</fold>
283 fn test_fold_imports() {
286 use std::<fold block>{
292 fn main() <fold block>{
298 fn test_fold_mods() {
303 <fold mods>mod after_pub;
304 mod after_pub_next;</fold>
306 <fold mods>mod before_pub;
307 mod before_pub_next;</fold>
310 mod not_folding_single;
312 pub not_folding_single_next;
314 <fold mods>#[cfg(test)]
316 mod with_attribute_next;</fold>
318 fn main() <fold block>{
324 fn test_fold_import_groups() {
327 <fold imports>use std::str;
329 use std::io as iop;</fold>
331 <fold imports>use std::mem;
334 <fold imports>use std::collections::HashMap;
335 // Some random comment
336 use std::collections::VecDeque;</fold>
338 fn main() <fold block>{
344 fn test_fold_import_and_groups() {
347 <fold imports>use std::str;
349 use std::io as iop;</fold>
351 <fold imports>use std::mem;
354 use std::collections::<fold block>{
358 // Some random comment
360 fn main() <fold block>{
366 fn test_folds_structs() {
369 struct Foo <fold block>{
376 fn test_folds_traits() {
379 trait Foo <fold block>{
386 fn test_folds_macros() {
389 macro_rules! foo <fold block>{
390 ($($tt:tt)*) => { $($tt)* }
397 fn test_fold_match_arms() {
400 fn main() <fold block>{
401 match 0 <fold block>{
411 fn fold_big_calls() {
414 fn main() <fold block>{
415 frobnicate<fold arglist>(
426 fn fold_record_literals() {
429 const _: S = S <fold block>{
437 fn fold_multiline_params() {
440 fn foo<fold arglist>(
452 // 1. some normal comment
453 <fold region>// region: test
454 // 2. some normal comment
455 calling_function(x,y);
456 // endregion: test</fold>