Unnamed repository; edit this file 'description' to name the repository.
Diffstat (limited to 'crates/ide/src/folding_ranges.rs')
| -rw-r--r-- | crates/ide/src/folding_ranges.rs | 293 |
1 files changed, 254 insertions, 39 deletions
diff --git a/crates/ide/src/folding_ranges.rs b/crates/ide/src/folding_ranges.rs index 3969490e8d..375e42cc83 100644 --- a/crates/ide/src/folding_ranges.rs +++ b/crates/ide/src/folding_ranges.rs @@ -1,10 +1,11 @@ use ide_db::{FxHashSet, syntax_helpers::node_ext::vis_eq}; use syntax::{ - Direction, NodeOrToken, SourceFile, - SyntaxKind::{self, *}, + Direction, NodeOrToken, SourceFile, SyntaxElement, + SyntaxKind::*, SyntaxNode, TextRange, TextSize, ast::{self, AstNode, AstToken}, match_ast, + syntax_editor::Element, }; use std::hash::Hash; @@ -31,19 +32,33 @@ pub enum FoldKind { TypeAliases, ExternCrates, // endregion: item runs + Stmt(ast::Stmt), + TailExpr(ast::Expr), } #[derive(Debug)] pub struct Fold { pub range: TextRange, pub kind: FoldKind, + pub collapsed_text: Option<String>, +} + +impl Fold { + pub fn new(range: TextRange, kind: FoldKind) -> Self { + Self { range, kind, collapsed_text: None } + } + + pub fn with_text(mut self, text: Option<String>) -> Self { + self.collapsed_text = text; + self + } } // 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> { +pub(crate) fn folding_ranges(file: &SourceFile, add_collapsed_text: bool) -> Vec<Fold> { let mut res = vec![]; let mut visited_comments = FxHashSet::default(); let mut visited_nodes = FxHashSet::default(); @@ -53,39 +68,41 @@ pub(crate) fn folding_ranges(file: &SourceFile) -> Vec<Fold> { for element in file.syntax().descendants_with_tokens() { // Fold items that span multiple lines - if let Some(kind) = fold_kind(element.kind()) { + if let Some(kind) = fold_kind(element.clone()) { let is_multiline = match &element { NodeOrToken::Node(node) => node.text().contains_char('\n'), NodeOrToken::Token(token) => token.text().contains('\n'), }; + if is_multiline { - // for the func with multiline param list - if matches!(element.kind(), FN) - && let NodeOrToken::Node(node) = &element - && let Some(fn_node) = ast::Fn::cast(node.clone()) + if let NodeOrToken::Node(node) = &element + && let Some(fn_) = ast::Fn::cast(node.clone()) { - if !fn_node + if !fn_ .param_list() .map(|param_list| param_list.syntax().text().contains_char('\n')) - .unwrap_or(false) + .unwrap_or_default() { continue; } - if fn_node.body().is_some() { + if let Some(body) = fn_.body() { // Get the actual start of the function (excluding doc comments) - let fn_start = fn_node + let fn_start = fn_ .fn_token() .map(|token| token.text_range().start()) .unwrap_or(node.text_range().start()); - res.push(Fold { - range: TextRange::new(fn_start, node.text_range().end()), - kind: FoldKind::Function, - }); + res.push(Fold::new( + TextRange::new(fn_start, body.syntax().text_range().end()), + FoldKind::Function, + )); continue; } } - res.push(Fold { range: element.text_range(), kind }); + + let collapsed_text = if add_collapsed_text { collapsed_text(&kind) } else { None }; + let fold = Fold::new(element.text_range(), kind).with_text(collapsed_text); + res.push(fold); continue; } } @@ -102,15 +119,15 @@ pub(crate) fn folding_ranges(file: &SourceFile) -> Vec<Fold> { 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, - }) + res.push(Fold::new( + TextRange::new(region, comment.syntax().text_range().end()), + FoldKind::Region, + )); } } else if let Some(range) = contiguous_range_for_comment(comment, &mut visited_comments) { - res.push(Fold { range, kind: FoldKind::Comment }) + res.push(Fold::new(range, FoldKind::Comment)); } } } @@ -123,37 +140,37 @@ pub(crate) fn folding_ranges(file: &SourceFile) -> Vec<Fold> { module, &mut visited_nodes, ) { - res.push(Fold { range, kind: FoldKind::Modules }) + res.push(Fold::new(range, FoldKind::Modules)); } }, ast::Use(use_) => { if let Some(range) = contiguous_range_for_item_group(use_, &mut visited_nodes) { - res.push(Fold { range, kind: FoldKind::Imports }) + res.push(Fold::new(range, FoldKind::Imports)); } }, ast::Const(konst) => { if let Some(range) = contiguous_range_for_item_group(konst, &mut visited_nodes) { - res.push(Fold { range, kind: FoldKind::Consts }) + res.push(Fold::new(range, FoldKind::Consts)); } }, ast::Static(statik) => { if let Some(range) = contiguous_range_for_item_group(statik, &mut visited_nodes) { - res.push(Fold { range, kind: FoldKind::Statics }) + res.push(Fold::new(range, FoldKind::Statics)); } }, ast::TypeAlias(alias) => { if let Some(range) = contiguous_range_for_item_group(alias, &mut visited_nodes) { - res.push(Fold { range, kind: FoldKind::TypeAliases }) + res.push(Fold::new(range, FoldKind::TypeAliases)); } }, ast::ExternCrate(extern_crate) => { if let Some(range) = contiguous_range_for_item_group(extern_crate, &mut visited_nodes) { - res.push(Fold { range, kind: FoldKind::ExternCrates }) + res.push(Fold::new(range, FoldKind::ExternCrates)); } }, ast::MatchArm(match_arm) => { if let Some(range) = fold_range_for_multiline_match_arm(match_arm) { - res.push(Fold {range, kind: FoldKind::MatchArm}) + res.push(Fold::new(range, FoldKind::MatchArm)); } }, _ => (), @@ -166,8 +183,66 @@ pub(crate) fn folding_ranges(file: &SourceFile) -> Vec<Fold> { res } -fn fold_kind(kind: SyntaxKind) -> Option<FoldKind> { +fn collapsed_text(kind: &FoldKind) -> Option<String> { match kind { + FoldKind::TailExpr(expr) => collapse_expr(expr.clone()), + FoldKind::Stmt(stmt) => { + match stmt { + ast::Stmt::ExprStmt(expr_stmt) => { + expr_stmt.expr().and_then(collapse_expr).map(|text| format!("{text};")) + } + ast::Stmt::LetStmt(let_stmt) => 'blk: { + if let_stmt.let_else().is_some() { + break 'blk None; + } + + let Some(expr) = let_stmt.initializer() else { + break 'blk None; + }; + + // If the `let` statement spans multiple lines, we do not collapse it. + // We use the `eq_token` to check whether the `let` statement is a single line, + // as the formatter may place the initializer on a new line for better readability. + // + // Example: + // ```rust + // let complex_pat = + // complex_expr; + // ``` + // + // In this case, we should generate the collapsed text. + let Some(eq_token) = let_stmt.eq_token() else { + break 'blk None; + }; + let eq_token_offset = + eq_token.text_range().end() - let_stmt.syntax().text_range().start(); + let text_until_eq_token = let_stmt.syntax().text().slice(..eq_token_offset); + if text_until_eq_token.contains_char('\n') { + break 'blk None; + } + + collapse_expr(expr).map(|text| format!("{text_until_eq_token} {text};")) + } + // handling `items` in external matches. + ast::Stmt::Item(_) => None, + } + } + _ => None, + } +} + +fn fold_kind(element: SyntaxElement) -> Option<FoldKind> { + // handle tail_expr + if let Some(node) = element.as_node() + // tail_expr -> stmt_list -> block + && let Some(block) = node.parent().and_then(|it| it.parent()).and_then(ast::BlockExpr::cast) + && let Some(tail_expr) = block.tail_expr() + && tail_expr.syntax() == node + { + return Some(FoldKind::TailExpr(tail_expr)); + } + + match element.kind() { COMMENT => Some(FoldKind::Comment), ARG_LIST | PARAM_LIST | GENERIC_ARG_LIST | GENERIC_PARAM_LIST => Some(FoldKind::ArgList), ARRAY_EXPR => Some(FoldKind::Array), @@ -185,10 +260,73 @@ fn fold_kind(kind: SyntaxKind) -> Option<FoldKind> { | MATCH_ARM_LIST | VARIANT_LIST | TOKEN_TREE => Some(FoldKind::Block), + EXPR_STMT | LET_STMT => Some(FoldKind::Stmt(ast::Stmt::cast(element.as_node()?.clone())?)), _ => None, } } +const COLLAPSE_EXPR_MAX_LEN: usize = 100; + +fn collapse_expr(expr: ast::Expr) -> Option<String> { + let mut text = String::with_capacity(COLLAPSE_EXPR_MAX_LEN * 2); + + let mut preorder = expr.syntax().preorder_with_tokens(); + while let Some(element) = preorder.next() { + match element { + syntax::WalkEvent::Enter(NodeOrToken::Node(node)) => { + if let Some(arg_list) = ast::ArgList::cast(node.clone()) { + let content = if arg_list.args().next().is_some() { "(…)" } else { "()" }; + text.push_str(content); + preorder.skip_subtree(); + } else if let Some(expr) = ast::Expr::cast(node) { + match expr { + ast::Expr::AwaitExpr(_) + | ast::Expr::BecomeExpr(_) + | ast::Expr::BinExpr(_) + | ast::Expr::BreakExpr(_) + | ast::Expr::CallExpr(_) + | ast::Expr::CastExpr(_) + | ast::Expr::ContinueExpr(_) + | ast::Expr::FieldExpr(_) + | ast::Expr::IndexExpr(_) + | ast::Expr::LetExpr(_) + | ast::Expr::Literal(_) + | ast::Expr::MethodCallExpr(_) + | ast::Expr::OffsetOfExpr(_) + | ast::Expr::ParenExpr(_) + | ast::Expr::PathExpr(_) + | ast::Expr::PrefixExpr(_) + | ast::Expr::RangeExpr(_) + | ast::Expr::RefExpr(_) + | ast::Expr::ReturnExpr(_) + | ast::Expr::TryExpr(_) + | ast::Expr::UnderscoreExpr(_) + | ast::Expr::YeetExpr(_) + | ast::Expr::YieldExpr(_) => {} + + // Some other exprs (e.g. `while` loop) are too complex to have a collapsed text + _ => return None, + } + } + } + syntax::WalkEvent::Enter(NodeOrToken::Token(token)) => { + if !token.kind().is_trivia() { + text.push_str(token.text()); + } + } + syntax::WalkEvent::Leave(_) => {} + } + + if text.len() > COLLAPSE_EXPR_MAX_LEN { + return None; + } + } + + text.shrink_to_fit(); + + Some(text) +} + fn contiguous_range_for_item_group<N>( first: N, visited: &mut FxHashSet<SyntaxNode>, @@ -297,7 +435,7 @@ fn contiguous_range_for_comment( } fn fold_range_for_multiline_match_arm(match_arm: ast::MatchArm) -> Option<TextRange> { - if fold_kind(match_arm.expr()?.syntax().kind()).is_some() { + if fold_kind(match_arm.expr()?.syntax().syntax_element()).is_some() { None } else if match_arm.expr()?.syntax().text().contains_char('\n') { Some(match_arm.expr()?.syntax().text_range()) @@ -314,10 +452,33 @@ mod tests { #[track_caller] fn check(#[rust_analyzer::rust_fixture] ra_fixture: &str) { + check_inner(ra_fixture, true); + } + + fn check_without_collapsed_text(#[rust_analyzer::rust_fixture] ra_fixture: &str) { + check_inner(ra_fixture, false); + } + + fn check_inner(ra_fixture: &str, enable_collapsed_text: bool) { let (ranges, text) = extract_tags(ra_fixture, "fold"); + let ranges: Vec<_> = ranges + .into_iter() + .map(|(range, text)| { + let (attr, collapsed_text) = match text { + Some(text) => match text.split_once(':') { + Some((attr, collapsed_text)) => { + (Some(attr.to_owned()), Some(collapsed_text.to_owned())) + } + None => (Some(text), None), + }, + None => (None, None), + }; + (range, attr, collapsed_text) + }) + .collect(); let parse = SourceFile::parse(&text, span::Edition::CURRENT); - let mut folds = folding_ranges(&parse.tree()); + let mut folds = folding_ranges(&parse.tree(), enable_collapsed_text); folds.sort_by_key(|fold| (fold.range.start(), fold.range.end())); assert_eq!( @@ -326,7 +487,7 @@ mod tests { "The amount of folds is different than the expected amount" ); - for (fold, (range, attr)) in folds.iter().zip(ranges.into_iter()) { + for (fold, (range, attr, collapsed_text)) 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"); @@ -346,8 +507,15 @@ mod tests { FoldKind::MatchArm => "matcharm", FoldKind::Function => "function", FoldKind::ExternCrates => "externcrates", + FoldKind::Stmt(_) => "stmt", + FoldKind::TailExpr(_) => "tailexpr", }; assert_eq!(kind, &attr.unwrap()); + if enable_collapsed_text { + assert_eq!(fold.collapsed_text, collapsed_text); + } else { + assert_eq!(fold.collapsed_text, None); + } } } @@ -511,10 +679,10 @@ macro_rules! foo <fold block>{ check( r#" fn main() <fold block>{ - match 0 <fold block>{ + <fold tailexpr>match 0 <fold block>{ 0 => 0, _ => 1, - }</fold> + }</fold></fold> }</fold> "#, ); @@ -525,7 +693,7 @@ fn main() <fold block>{ check( r#" fn main() <fold block>{ - match foo <fold block>{ + <fold tailexpr>match foo <fold block>{ block => <fold block>{ }</fold>, matcharm => <fold matcharm>some. @@ -544,7 +712,7 @@ fn main() <fold block>{ structS => <fold matcharm>StructS <fold block>{ a: 31, }</fold></fold>, - }</fold> + }</fold></fold> }</fold> "#, ) @@ -555,11 +723,11 @@ fn main() <fold block>{ check( r#" fn main() <fold block>{ - frobnicate<fold arglist>( + <fold tailexpr:frobnicate(…)>frobnicate<fold arglist>( 1, 2, 3, - )</fold> + )</fold></fold> }</fold> "#, ) @@ -698,4 +866,51 @@ type Foo<T, U> = foo<fold arglist>< "#, ); } + + #[test] + fn test_fold_tail_expr() { + check( + r#" +fn f() <fold block>{ + let x = 1; + + <fold tailexpr:some_function().chain().method()>some_function() + .chain() + .method()</fold> +}</fold> +"#, + ) + } + + #[test] + fn test_fold_let_stmt_with_chained_methods() { + check( + r#" +fn main() <fold block>{ + <fold stmt:let result = some_value.method1().method2()?.method3();>let result = some_value + .method1() + .method2()? + .method3();</fold> + + println!("{}", result); +}</fold> +"#, + ) + } + + #[test] + fn test_fold_let_stmt_with_chained_methods_without_collapsed_text() { + check_without_collapsed_text( + r#" +fn main() <fold block>{ + <fold stmt>let result = some_value + .method1() + .method2()? + .method3();</fold> + + println!("{}", result); +}</fold> +"#, + ) + } } |