use ide_db::{syntax_helpers::node_ext::vis_eq, FxHashSet}; use syntax::{ ast::{self, AstNode, AstToken}, match_ast, Direction, NodeOrToken, SourceFile, SyntaxKind::{self, *}, TextRange, TextSize, }; use std::hash::Hash; const REGION_START: &str = "// region:"; const REGION_END: &str = "// endregion"; #[derive(Debug, PartialEq, Eq)] pub enum FoldKind { Comment, Imports, Mods, Block, ArgList, Region, Consts, Statics, Array, WhereClause, ReturnType, MatchArm, } #[derive(Debug)] pub struct Fold { pub range: TextRange, pub kind: FoldKind, } // Feature: Folding // // Defines folding regions for curly braced blocks, runs of consecutive use, mod, const or static // items, and `region` / `endregion` comment markers. pub(crate) fn folding_ranges(file: &SourceFile) -> Vec { let mut res = vec![]; let mut visited_comments = FxHashSet::default(); let mut visited_imports = FxHashSet::default(); let mut visited_mods = FxHashSet::default(); let mut visited_consts = FxHashSet::default(); let mut visited_statics = FxHashSet::default(); // regions can be nested, here is a LIFO buffer let mut region_starts: Vec = vec![]; for element in file.syntax().descendants_with_tokens() { // Fold items that span multiple lines if let Some(kind) = fold_kind(element.kind()) { let is_multiline = match &element { NodeOrToken::Node(node) => node.text().contains_char('\n'), NodeOrToken::Token(token) => token.text().contains('\n'), }; if is_multiline { res.push(Fold { range: element.text_range(), kind }); continue; } } match element { NodeOrToken::Token(token) => { // Fold groups of comments if let Some(comment) = ast::Comment::cast(token) { if visited_comments.contains(&comment) { continue; } let text = comment.text().trim_start(); if text.starts_with(REGION_START) { region_starts.push(comment.syntax().text_range().start()); } else if text.starts_with(REGION_END) { if let Some(region) = region_starts.pop() { res.push(Fold { range: TextRange::new(region, comment.syntax().text_range().end()), kind: FoldKind::Region, }) } } else if let Some(range) = contiguous_range_for_comment(comment, &mut visited_comments) { res.push(Fold { range, kind: FoldKind::Comment }) } } } NodeOrToken::Node(node) => { match_ast! { match node { ast::Module(module) => { if module.item_list().is_none() { if let Some(range) = contiguous_range_for_item_group( module, &mut visited_mods, ) { res.push(Fold { range, kind: FoldKind::Mods }) } } }, ast::Use(use_) => { if let Some(range) = contiguous_range_for_item_group(use_, &mut visited_imports) { res.push(Fold { range, kind: FoldKind::Imports }) } }, ast::Const(konst) => { if let Some(range) = contiguous_range_for_item_group(konst, &mut visited_consts) { res.push(Fold { range, kind: FoldKind::Consts }) } }, ast::Static(statik) => { if let Some(range) = contiguous_range_for_item_group(statik, &mut visited_statics) { res.push(Fold { range, kind: FoldKind::Statics }) } }, ast::WhereClause(where_clause) => { if let Some(range) = fold_range_for_where_clause(where_clause) { res.push(Fold { range, kind: FoldKind::WhereClause }) } }, ast::MatchArm(match_arm) => { if let Some(range) = fold_range_for_multiline_match_arm(match_arm) { res.push(Fold {range, kind: FoldKind::MatchArm}) } }, _ => (), } } } } } res } fn fold_kind(kind: SyntaxKind) -> Option { match kind { COMMENT => Some(FoldKind::Comment), ARG_LIST | PARAM_LIST => Some(FoldKind::ArgList), ARRAY_EXPR => Some(FoldKind::Array), RET_TYPE => Some(FoldKind::ReturnType), ASSOC_ITEM_LIST | RECORD_FIELD_LIST | RECORD_PAT_FIELD_LIST | RECORD_EXPR_FIELD_LIST | ITEM_LIST | EXTERN_ITEM_LIST | USE_TREE_LIST | BLOCK_EXPR | MATCH_ARM_LIST | VARIANT_LIST | TOKEN_TREE => Some(FoldKind::Block), _ => None, } } fn contiguous_range_for_item_group(first: N, visited: &mut FxHashSet) -> Option where N: ast::HasVisibility + Clone + Hash + Eq, { if !visited.insert(first.clone()) { return None; } let (mut last, mut last_vis) = (first.clone(), first.visibility()); for element in first.syntax().siblings_with_tokens(Direction::Next) { let node = match element { NodeOrToken::Token(token) => { if let Some(ws) = ast::Whitespace::cast(token) { if !ws.spans_multiple_lines() { // Ignore whitespace without blank lines continue; } } // There is a blank line or another token, which means that the // group ends here break; } NodeOrToken::Node(node) => node, }; if let Some(next) = N::cast(node) { let next_vis = next.visibility(); if eq_visibility(next_vis.clone(), last_vis) { visited.insert(next.clone()); last_vis = next_vis; last = next; continue; } } // Stop if we find an item of a different kind or with a different visibility. break; } if first != last { Some(TextRange::new(first.syntax().text_range().start(), last.syntax().text_range().end())) } else { // The group consists of only one element, therefore it cannot be folded None } } fn eq_visibility(vis0: Option, vis1: Option) -> bool { match (vis0, vis1) { (None, None) => true, (Some(vis0), Some(vis1)) => vis_eq(&vis0, &vis1), _ => false, } } fn contiguous_range_for_comment( first: ast::Comment, visited: &mut FxHashSet, ) -> Option { visited.insert(first.clone()); // Only fold comments of the same flavor let group_kind = first.kind(); if !group_kind.shape.is_line() { return None; } let mut last = first.clone(); for element in first.syntax().siblings_with_tokens(Direction::Next) { match element { NodeOrToken::Token(token) => { if let Some(ws) = ast::Whitespace::cast(token.clone()) { if !ws.spans_multiple_lines() { // Ignore whitespace without blank lines continue; } } if let Some(c) = ast::Comment::cast(token) { if c.kind() == group_kind { let text = c.text().trim_start(); // regions are not real comments if !(text.starts_with(REGION_START) || text.starts_with(REGION_END)) { visited.insert(c.clone()); last = c; continue; } } } // The comment group ends because either: // * An element of a different kind was reached // * A comment of a different flavor was reached break; } NodeOrToken::Node(_) => break, }; } if first != last { Some(TextRange::new(first.syntax().text_range().start(), last.syntax().text_range().end())) } else { // The group consists of only one element, therefore it cannot be folded None } } fn fold_range_for_where_clause(where_clause: ast::WhereClause) -> Option { let first_where_pred = where_clause.predicates().next(); let last_where_pred = where_clause.predicates().last(); if first_where_pred != last_where_pred { let start = where_clause.where_token()?.text_range().end(); let end = where_clause.syntax().text_range().end(); return Some(TextRange::new(start, end)); } None } fn fold_range_for_multiline_match_arm(match_arm: ast::MatchArm) -> Option { if fold_kind(match_arm.expr()?.syntax().kind()).is_some() { None } else if match_arm.expr()?.syntax().text().contains_char('\n') { Some(match_arm.expr()?.syntax().text_range()) } else { None } } #[cfg(test)] mod tests { use test_utils::extract_tags; use super::*; fn check(ra_fixture: &str) { let (ranges, text) = extract_tags(ra_fixture, "fold"); let parse = SourceFile::parse(&text); let mut folds = folding_ranges(&parse.tree()); folds.sort_by_key(|fold| (fold.range.start(), fold.range.end())); assert_eq!( folds.len(), ranges.len(), "The amount of folds is different than the expected amount" ); for (fold, (range, attr)) in folds.iter().zip(ranges.into_iter()) { assert_eq!(fold.range.start(), range.start(), "mismatched start of folding ranges"); assert_eq!(fold.range.end(), range.end(), "mismatched end of folding ranges"); let kind = match fold.kind { FoldKind::Comment => "comment", FoldKind::Imports => "imports", FoldKind::Mods => "mods", FoldKind::Block => "block", FoldKind::ArgList => "arglist", FoldKind::Region => "region", FoldKind::Consts => "consts", FoldKind::Statics => "statics", FoldKind::Array => "array", FoldKind::WhereClause => "whereclause", FoldKind::ReturnType => "returntype", FoldKind::MatchArm => "matcharm", }; assert_eq!(kind, &attr.unwrap()); } } #[test] fn test_fold_comments() { check( r#" // Hello // this is a multiline // comment // // But this is not fn main() { // We should // also // fold // this one. //! But this one is different //! because it has another flavor /* As does this multiline comment */ } "#, ); } #[test] fn test_fold_imports() { check( r#" use std::{ str, vec, io as iop }; "#, ); } #[test] fn test_fold_mods() { check( r#" pub mod foo; mod after_pub; mod after_pub_next; mod before_pub; mod before_pub_next; pub mod bar; mod not_folding_single; pub mod foobar; pub not_folding_single_next; #[cfg(test)] mod with_attribute; mod with_attribute_next; mod inline0 {} mod inline1 {} mod inline2 { } "#, ); } #[test] fn test_fold_import_groups() { check( r#" use std::str; use std::vec; use std::io as iop; use std::mem; use std::f64; use std::collections::HashMap; // Some random comment use std::collections::VecDeque; "#, ); } #[test] fn test_fold_import_and_groups() { check( r#" use std::str; use std::vec; use std::io as iop; use std::mem; use std::f64; use std::collections::{ HashMap, VecDeque, }; // Some random comment "#, ); } #[test] fn test_folds_structs() { check( r#" struct Foo { } "#, ); } #[test] fn test_folds_traits() { check( r#" trait Foo { } "#, ); } #[test] fn test_folds_macros() { check( r#" macro_rules! foo { ($($tt:tt)*) => { $($tt)* } } "#, ); } #[test] fn test_fold_match_arms() { check( r#" fn main() { match 0 { 0 => 0, _ => 1, } } "#, ); } #[test] fn test_fold_multiline_non_block_match_arm() { check( r#" fn main() { match foo { block => { }, matcharm => some. call(). chain(), matcharm2 => 0, match_expr => match foo2 { bar => (), }, array_list => [ 1, 2, 3, ], structS => StructS { a: 31, }, } } "#, ) } #[test] fn fold_big_calls() { check( r#" fn main() { frobnicate( 1, 2, 3, ) } "#, ) } #[test] fn fold_record_literals() { check( r#" const _: S = S { }; "#, ) } #[test] fn fold_multiline_params() { check( r#" fn foo( x: i32, y: String, ) {} "#, ) } #[test] fn fold_multiline_array() { check( r#" const FOO: [usize; 4] = [ 1, 2, 3, 4, ]; "#, ) } #[test] fn fold_region() { check( r#" // 1. some normal comment // region: test // 2. some normal comment // region: inner fn f() {} // endregion fn f2() {} // endregion: test "#, ) } #[test] fn fold_consecutive_const() { check( r#" const FIRST_CONST: &str = "first"; const SECOND_CONST: &str = "second"; "#, ) } #[test] fn fold_consecutive_static() { check( r#" static FIRST_STATIC: &str = "first"; static SECOND_STATIC: &str = "second"; "#, ) } #[test] fn fold_where_clause() { // fold multi-line and don't fold single line. check( r#" fn foo() where A: Foo, B: Foo, C: Foo, D: Foo, {} fn bar() where A: Bar, {} "#, ) } #[test] fn fold_return_type() { check( r#" fn foo()-> ( bool, bool, ) { (true, true) } fn bar() -> (bool, bool) { (true, true) } "#, ) } }