diff options
Diffstat (limited to '')
-rwxr-xr-x | src/tools/rust-analyzer/crates/ide/src/folding_ranges.rs | 626 |
1 files changed, 626 insertions, 0 deletions
diff --git a/src/tools/rust-analyzer/crates/ide/src/folding_ranges.rs b/src/tools/rust-analyzer/crates/ide/src/folding_ranges.rs new file mode 100755 index 000000000..c694d95d5 --- /dev/null +++ b/src/tools/rust-analyzer/crates/ide/src/folding_ranges.rs @@ -0,0 +1,626 @@ +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<Fold> { + 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<TextSize> = 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<FoldKind> { + 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<N>(first: N, visited: &mut FxHashSet<N>) -> Option<TextRange> +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<ast::Visibility>, vis1: Option<ast::Visibility>) -> 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<ast::Comment>, +) -> Option<TextRange> { + 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<TextRange> { + 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<TextRange> { + if let Some(_) = fold_kind(match_arm.expr()?.syntax().kind()) { + return None; + } + if match_arm.expr()?.syntax().text().contains_char('\n') { + return Some(match_arm.expr()?.syntax().text_range()); + } + 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#" +<fold comment>// Hello +// this is a multiline +// comment +//</fold> + +// But this is not + +fn main() <fold block>{ + <fold comment>// We should + // also + // fold + // this one.</fold> + <fold comment>//! But this one is different + //! because it has another flavor</fold> + <fold comment>/* As does this + multiline comment */</fold> +}</fold> +"#, + ); + } + + #[test] + fn test_fold_imports() { + check( + r#" +use std::<fold block>{ + str, + vec, + io as iop +}</fold>; +"#, + ); + } + + #[test] + fn test_fold_mods() { + check( + r#" + +pub mod foo; +<fold mods>mod after_pub; +mod after_pub_next;</fold> + +<fold mods>mod before_pub; +mod before_pub_next;</fold> +pub mod bar; + +mod not_folding_single; +pub mod foobar; +pub not_folding_single_next; + +<fold mods>#[cfg(test)] +mod with_attribute; +mod with_attribute_next;</fold> + +mod inline0 {} +mod inline1 {} + +mod inline2 <fold block>{ + +}</fold> +"#, + ); + } + + #[test] + fn test_fold_import_groups() { + check( + r#" +<fold imports>use std::str; +use std::vec; +use std::io as iop;</fold> + +<fold imports>use std::mem; +use std::f64;</fold> + +<fold imports>use std::collections::HashMap; +// Some random comment +use std::collections::VecDeque;</fold> +"#, + ); + } + + #[test] + fn test_fold_import_and_groups() { + check( + r#" +<fold imports>use std::str; +use std::vec; +use std::io as iop;</fold> + +<fold imports>use std::mem; +use std::f64;</fold> + +use std::collections::<fold block>{ + HashMap, + VecDeque, +}</fold>; +// Some random comment +"#, + ); + } + + #[test] + fn test_folds_structs() { + check( + r#" +struct Foo <fold block>{ +}</fold> +"#, + ); + } + + #[test] + fn test_folds_traits() { + check( + r#" +trait Foo <fold block>{ +}</fold> +"#, + ); + } + + #[test] + fn test_folds_macros() { + check( + r#" +macro_rules! foo <fold block>{ + ($($tt:tt)*) => { $($tt)* } +}</fold> +"#, + ); + } + + #[test] + fn test_fold_match_arms() { + check( + r#" +fn main() <fold block>{ + match 0 <fold block>{ + 0 => 0, + _ => 1, + }</fold> +}</fold> +"#, + ); + } + + #[test] + fn test_fold_multiline_non_block_match_arm() { + check( + r#" + fn main() <fold block>{ + match foo <fold block>{ + block => <fold block>{ + }</fold>, + matcharm => <fold matcharm>some. + call(). + chain()</fold>, + matcharm2 + => 0, + match_expr => <fold matcharm>match foo2 <fold block>{ + bar => (), + }</fold></fold>, + array_list => <fold array>[ + 1, + 2, + 3, + ]</fold>, + strustS => <fold matcharm>StructS <fold block>{ + a: 31, + }</fold></fold>, + }</fold> + }</fold> + "#, + ) + } + + #[test] + fn fold_big_calls() { + check( + r#" +fn main() <fold block>{ + frobnicate<fold arglist>( + 1, + 2, + 3, + )</fold> +}</fold> +"#, + ) + } + + #[test] + fn fold_record_literals() { + check( + r#" +const _: S = S <fold block>{ + +}</fold>; +"#, + ) + } + + #[test] + fn fold_multiline_params() { + check( + r#" +fn foo<fold arglist>( + x: i32, + y: String, +)</fold> {} +"#, + ) + } + + #[test] + fn fold_multiline_array() { + check( + r#" +const FOO: [usize; 4] = <fold array>[ + 1, + 2, + 3, + 4, +]</fold>; +"#, + ) + } + + #[test] + fn fold_region() { + check( + r#" +// 1. some normal comment +<fold region>// region: test +// 2. some normal comment +<fold region>// region: inner +fn f() {} +// endregion</fold> +fn f2() {} +// endregion: test</fold> +"#, + ) + } + + #[test] + fn fold_consecutive_const() { + check( + r#" +<fold consts>const FIRST_CONST: &str = "first"; +const SECOND_CONST: &str = "second";</fold> +"#, + ) + } + + #[test] + fn fold_consecutive_static() { + check( + r#" +<fold statics>static FIRST_STATIC: &str = "first"; +static SECOND_STATIC: &str = "second";</fold> +"#, + ) + } + + #[test] + fn fold_where_clause() { + // fold multi-line and don't fold single line. + check( + r#" +fn foo() +where<fold whereclause> + A: Foo, + B: Foo, + C: Foo, + D: Foo,</fold> {} + +fn bar() +where + A: Bar, {} +"#, + ) + } + + #[test] + fn fold_return_type() { + check( + r#" +fn foo()<fold returntype>-> ( + bool, + bool, +)</fold> { (true, true) } + +fn bar() -> (bool, bool) { (true, true) } +"#, + ) + } +} |