diff --git a/crates/ide/src/folding_ranges.rs b/crates/ide/src/folding_ranges.rs index 3969490e8dcf..375e42cc833e 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, +} + +impl Fold { + pub fn new(range: TextRange, kind: FoldKind) -> Self { + Self { range, kind, collapsed_text: None } + } + + pub fn with_text(mut self, text: Option) -> 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 { +pub(crate) fn folding_ranges(file: &SourceFile, add_collapsed_text: bool) -> Vec { 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 { 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 { 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 { 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 { res } -fn fold_kind(kind: SyntaxKind) -> Option { +fn collapsed_text(kind: &FoldKind) -> Option { 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 { + // 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 { | 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 { + 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( first: N, visited: &mut FxHashSet, @@ -297,7 +435,7 @@ fn contiguous_range_for_comment( } fn fold_range_for_multiline_match_arm(match_arm: ast::MatchArm) -> Option { - 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 { check( r#" fn main() { - match 0 { + match 0 { 0 => 0, _ => 1, - } + } } "#, ); @@ -525,7 +693,7 @@ fn main() { check( r#" fn main() { - match foo { + match foo { block => { }, matcharm => some. @@ -544,7 +712,7 @@ fn main() { structS => StructS { a: 31, }, - } + } } "#, ) @@ -555,11 +723,11 @@ fn main() { check( r#" fn main() { - frobnicate( + frobnicate( 1, 2, 3, - ) + ) } "#, ) @@ -698,4 +866,51 @@ type Foo = foo< "#, ); } + + #[test] + fn test_fold_tail_expr() { + check( + r#" +fn f() { + let x = 1; + + some_function() + .chain() + .method() +} +"#, + ) + } + + #[test] + fn test_fold_let_stmt_with_chained_methods() { + check( + r#" +fn main() { + let result = some_value + .method1() + .method2()? + .method3(); + + println!("{}", result); +} +"#, + ) + } + + #[test] + fn test_fold_let_stmt_with_chained_methods_without_collapsed_text() { + check_without_collapsed_text( + r#" +fn main() { + let result = some_value + .method1() + .method2()? + .method3(); + + println!("{}", result); +} +"#, + ) + } } diff --git a/crates/ide/src/lib.rs b/crates/ide/src/lib.rs index 930eaf2262d9..be0b96d7832d 100644 --- a/crates/ide/src/lib.rs +++ b/crates/ide/src/lib.rs @@ -501,12 +501,15 @@ impl Analysis { } /// Returns the set of folding ranges. - pub fn folding_ranges(&self, file_id: FileId) -> Cancellable> { + pub fn folding_ranges(&self, file_id: FileId, collapsed_text: bool) -> Cancellable> { self.with_db(|db| { let editioned_file_id_wrapper = EditionedFileId::current_edition_guess_origin(&self.db, file_id); - folding_ranges::folding_ranges(&db.parse(editioned_file_id_wrapper).tree()) + folding_ranges::folding_ranges( + &db.parse(editioned_file_id_wrapper).tree(), + collapsed_text, + ) }) } diff --git a/crates/ide/src/static_index.rs b/crates/ide/src/static_index.rs index aba6b64f977a..6dd73e4e26c5 100644 --- a/crates/ide/src/static_index.rs +++ b/crates/ide/src/static_index.rs @@ -159,7 +159,7 @@ pub enum VendoredLibrariesConfig<'a> { impl StaticIndex<'_> { fn add_file(&mut self, file_id: FileId) { let current_crate = crates_for(self.db, file_id).pop().map(Into::into); - let folds = self.analysis.folding_ranges(file_id).unwrap(); + let folds = self.analysis.folding_ranges(file_id, true).unwrap(); let inlay_hints = self .analysis .inlay_hints( diff --git a/crates/rust-analyzer/src/handlers/request.rs b/crates/rust-analyzer/src/handlers/request.rs index ad07da77597d..2cb7825f8ef3 100644 --- a/crates/rust-analyzer/src/handlers/request.rs +++ b/crates/rust-analyzer/src/handlers/request.rs @@ -1264,11 +1264,15 @@ pub(crate) fn handle_folding_range( params: FoldingRangeParams, ) -> anyhow::Result>> { let _p = tracing::info_span!("handle_folding_range").entered(); + let file_id = try_default!(from_proto::file_id(&snap, ¶ms.text_document.uri)?); - let folds = snap.analysis.folding_ranges(file_id)?; + let collapsed_text = snap.config.folding_range_collapsed_text(); + let folds = snap.analysis.folding_ranges(file_id, collapsed_text)?; + let text = snap.analysis.file_text(file_id)?; let line_index = snap.file_line_index(file_id)?; let line_folding_only = snap.config.line_folding_only(); + let res = folds .into_iter() .map(|it| to_proto::folding_range(&text, &line_index, line_folding_only, it)) diff --git a/crates/rust-analyzer/src/lsp/capabilities.rs b/crates/rust-analyzer/src/lsp/capabilities.rs index d6a694be9121..3ad4cb70b419 100644 --- a/crates/rust-analyzer/src/lsp/capabilities.rs +++ b/crates/rust-analyzer/src/lsp/capabilities.rs @@ -335,6 +335,20 @@ impl ClientCapabilities { .unwrap_or_default() } + pub fn folding_range_collapsed_text(&self) -> bool { + (|| -> _ { + self.0 + .text_document + .as_ref()? + .folding_range + .as_ref()? + .folding_range + .as_ref()? + .collapsed_text + })() + .unwrap_or_default() + } + pub fn hierarchical_symbols(&self) -> bool { (|| -> _ { self.0 diff --git a/crates/rust-analyzer/src/lsp/to_proto.rs b/crates/rust-analyzer/src/lsp/to_proto.rs index 6f0f57725fc7..f6c16c8fde48 100644 --- a/crates/rust-analyzer/src/lsp/to_proto.rs +++ b/crates/rust-analyzer/src/lsp/to_proto.rs @@ -907,9 +907,9 @@ pub(crate) fn folding_range( text: &str, line_index: &LineIndex, line_folding_only: bool, - fold: Fold, + Fold { range: text_range, kind, collapsed_text }: Fold, ) -> lsp_types::FoldingRange { - let kind = match fold.kind { + let kind = match kind { FoldKind::Comment => Some(lsp_types::FoldingRangeKind::Comment), FoldKind::Imports => Some(lsp_types::FoldingRangeKind::Imports), FoldKind::Region => Some(lsp_types::FoldingRangeKind::Region), @@ -924,17 +924,19 @@ pub(crate) fn folding_range( | FoldKind::Array | FoldKind::ExternCrates | FoldKind::MatchArm - | FoldKind::Function => None, + | FoldKind::Function + | FoldKind::Stmt(_) + | FoldKind::TailExpr(_) => None, }; - let range = range(line_index, fold.range); + let range = range(line_index, text_range); if line_folding_only { // Clients with line_folding_only == true (such as VSCode) will fold the whole end line // even if it contains text not in the folding range. To prevent that we exclude // range.end.line from the folding region if there is more text after range.end // on the same line. - let has_more_text_on_end_line = text[TextRange::new(fold.range.end(), TextSize::of(text))] + let has_more_text_on_end_line = text[TextRange::new(text_range.end(), TextSize::of(text))] .chars() .take_while(|it| *it != '\n') .any(|it| !it.is_whitespace()); @@ -945,13 +947,20 @@ pub(crate) fn folding_range( range.end.line }; + let collapsed_text = collapsed_text.map(|collapsed_text| { + let range_start = text_range.start(); + let line_start = range_start - TextSize::from(range.start.character); + let text_before_range = &text[TextRange::new(line_start, range_start)]; + format!("{text_before_range}{collapsed_text}") + }); + lsp_types::FoldingRange { start_line: range.start.line, start_character: None, end_line, end_character: None, kind, - collapsed_text: None, + collapsed_text, } } else { lsp_types::FoldingRange { @@ -960,7 +969,7 @@ pub(crate) fn folding_range( end_line: range.end.line, end_character: Some(range.end.character), kind, - collapsed_text: None, + collapsed_text, } } } @@ -2031,8 +2040,8 @@ fn main() { }"#; let (analysis, file_id) = Analysis::from_single_file(text.to_owned()); - let folds = analysis.folding_ranges(file_id).unwrap(); - assert_eq!(folds.len(), 4); + let folds = analysis.folding_ranges(file_id, true).unwrap(); + assert_eq!(folds.len(), 5); let line_index = LineIndex { index: Arc::new(ide::LineIndex::new(text)), @@ -2042,7 +2051,7 @@ fn main() { let converted: Vec = folds.into_iter().map(|it| folding_range(text, &line_index, true, it)).collect(); - let expected_lines = [(0, 2), (4, 10), (5, 6), (7, 9)]; + let expected_lines = [(0, 2), (4, 10), (5, 9), (5, 6), (7, 9)]; assert_eq!(converted.len(), expected_lines.len()); for (folding_range, (start_line, end_line)) in converted.iter().zip(expected_lines.iter()) { assert_eq!(folding_range.start_line, *start_line);