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 | 358 |
1 files changed, 316 insertions, 42 deletions
diff --git a/crates/ide/src/folding_ranges.rs b/crates/ide/src/folding_ranges.rs index 3969490e8d..ebd6c274a2 100644 --- a/crates/ide/src/folding_ranges.rs +++ b/crates/ide/src/folding_ranges.rs @@ -1,10 +1,12 @@ use ide_db::{FxHashSet, syntax_helpers::node_ext::vis_eq}; +use itertools::Itertools; use syntax::{ - Direction, NodeOrToken, SourceFile, - SyntaxKind::{self, *}, + Direction, NodeOrToken, SourceFile, SyntaxElement, + SyntaxKind::*, SyntaxNode, TextRange, TextSize, - ast::{self, AstNode, AstToken}, + ast::{self, AstNode, AstToken, HasArgList, edit::AstNodeEdit}, match_ast, + syntax_editor::Element, }; use std::hash::Hash; @@ -12,7 +14,7 @@ use std::hash::Hash; const REGION_START: &str = "// region:"; const REGION_END: &str = "// endregion"; -#[derive(Debug, PartialEq, Eq)] +#[derive(Debug, PartialEq, Eq, Clone, Copy)] pub enum FoldKind { Comment, Imports, @@ -29,21 +31,36 @@ pub enum FoldKind { Consts, Statics, TypeAliases, + TraitAliases, ExternCrates, // endregion: item runs + Stmt, + TailExpr, } #[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: String) -> Self { + self.collapsed_text = Some(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, collapsed_text: bool) -> Vec<Fold> { let mut res = vec![]; let mut visited_comments = FxHashSet::default(); let mut visited_nodes = FxHashSet::default(); @@ -53,39 +70,36 @@ 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(build_fold(&element, kind, collapsed_text)); continue; } } - res.push(Fold { range: element.text_range(), kind }); + + res.push(build_fold(&element, kind, collapsed_text)); continue; } } @@ -102,15 +116,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 +137,42 @@ 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::TraitAlias(alias) => { + if let Some(range) = contiguous_range_for_item_group(alias, &mut visited_nodes) { + res.push(Fold::new(range, FoldKind::TraitAliases)); } }, 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 +185,93 @@ pub(crate) fn folding_ranges(file: &SourceFile) -> Vec<Fold> { res } -fn fold_kind(kind: SyntaxKind) -> Option<FoldKind> { - match kind { +/// Builds a fold for the given syntax element. +/// +/// This function creates a `Fold` object that represents a collapsible region in the code. +/// If `collapsed_text` is enabled, it generates a preview text for certain fold kinds that +/// shows a summarized version of the folded content. +fn build_fold(element: &SyntaxElement, kind: FoldKind, collapsed_text: bool) -> Fold { + if !collapsed_text { + return Fold::new(element.text_range(), kind); + } + + let fold_with_collapsed_text = match kind { + FoldKind::TailExpr => { + let expr = ast::Expr::cast(element.as_node().unwrap().clone()).unwrap(); + + let indent_level = expr.indent_level().0; + let indents = " ".repeat(indent_level as usize); + + let mut fold = Fold::new(element.text_range(), kind); + if let Some(collapsed_expr) = collapsed_text_from_expr(expr) { + fold = fold.with_text(format!("{indents}{collapsed_expr}")); + } + Some(fold) + } + FoldKind::Stmt => 'blk: { + let node = element.as_node().unwrap(); + + match_ast! { + match node { + ast::ExprStmt(expr) => { + let Some(expr) = expr.expr() else { + break 'blk None; + }; + + let indent_level = expr.indent_level().0; + let indents = " ".repeat(indent_level as usize); + + let mut fold = Fold::new(element.text_range(), kind); + if let Some(collapsed_expr) = collapsed_text_from_expr(expr) { + fold = fold.with_text(format!("{indents}{collapsed_expr};")); + } + Some(fold) + }, + ast::LetStmt(let_stmt) => { + if let_stmt.let_else().is_some() { + break 'blk None; + } + + let Some(expr) = let_stmt.initializer() else { + break 'blk None; + }; + + let expr_offset = + expr.syntax().text_range().start() - let_stmt.syntax().text_range().start(); + let text_before_expr = let_stmt.syntax().text().slice(..expr_offset); + if text_before_expr.contains_char('\n') { + break 'blk None; + } + + let indent_level = let_stmt.indent_level().0; + let indents = " ".repeat(indent_level as usize); + + let mut fold = Fold::new(element.text_range(), kind); + if let Some(collapsed_expr) = collapsed_text_from_expr(expr) { + fold = fold.with_text(format!("{indents}{text_before_expr}{collapsed_expr};")); + } + Some(fold) + }, + _ => None, + } + } + } + _ => None, + }; + + fold_with_collapsed_text.unwrap_or_else(|| Fold::new(element.text_range(), kind)) +} + +fn fold_kind(element: SyntaxElement) -> Option<FoldKind> { + // handle tail_expr + if let Some(node) = element.as_node() + && let Some(block) = node.parent().and_then(|it| it.parent()).and_then(ast::BlockExpr::cast) // tail_expr -> stmt_list -> block + && block.tail_expr().is_some_and(|tail| tail.syntax() == node) + { + return Some(FoldKind::TailExpr); + } + + 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 +289,105 @@ fn fold_kind(kind: SyntaxKind) -> Option<FoldKind> { | MATCH_ARM_LIST | VARIANT_LIST | TOKEN_TREE => Some(FoldKind::Block), + EXPR_STMT | LET_STMT => Some(FoldKind::Stmt), _ => None, } } +/// Generates a collapsed text representation of a chained expression. +/// +/// This function analyzes an expression and creates a concise string representation +/// that shows the structure of method chains, field accesses, and function calls. +/// It's particularly useful for folding long chained expressions like: +/// `obj.method1()?.field.method2(args)` -> `obj.method1()?.field.method2(…)` +/// +/// The function traverses the expression tree from the outermost expression inward, +/// collecting method names, field names, and call signatures. It accumulates try +/// operators (`?`) and applies them to the appropriate parts of the chain. +/// +/// # Parameters +/// - `expr`: The expression to generate collapsed text for +/// +/// # Returns +/// - `Some(String)`: A dot-separated chain representation if the expression is chainable +/// - `None`: If the expression is not suitable for collapsing (e.g., simple literals) +/// +/// # Examples +/// - `foo.bar().baz?` -> `"foo.bar().baz?"` +/// - `obj.method(arg1, arg2)` -> `"obj.method(…)"` +/// - `value?.field` -> `"value?.field"` +fn collapsed_text_from_expr(mut expr: ast::Expr) -> Option<String> { + let mut names = Vec::new(); + let mut try_marks = String::with_capacity(1); + + let fold_general_expr = |expr: ast::Expr, try_marks: &mut String| { + let text = expr.syntax().text(); + let name = if text.contains_char('\n') { + format!("<expr>{try_marks}") + } else { + format!("{text}{try_marks}") + }; + try_marks.clear(); + name + }; + + loop { + let receiver = match expr { + ast::Expr::MethodCallExpr(call) => { + let name = call + .name_ref() + .map(|name| name.text().to_owned()) + .unwrap_or_else(|| "�".into()); + if call.arg_list().and_then(|arg_list| arg_list.args().next()).is_some() { + names.push(format!("{name}(…){try_marks}")); + } else { + names.push(format!("{name}(){try_marks}")); + } + try_marks.clear(); + call.receiver() + } + ast::Expr::FieldExpr(field) => { + let name = match field.field_access() { + Some(ast::FieldKind::Name(name)) => format!("{name}{try_marks}"), + Some(ast::FieldKind::Index(index)) => format!("{index}{try_marks}"), + None => format!("�{try_marks}"), + }; + names.push(name); + try_marks.clear(); + field.expr() + } + ast::Expr::TryExpr(try_expr) => { + try_marks.push('?'); + try_expr.expr() + } + ast::Expr::CallExpr(call) => { + let name = fold_general_expr(call.expr().unwrap(), &mut try_marks); + if call.arg_list().and_then(|arg_list| arg_list.args().next()).is_some() { + names.push(format!("{name}(…){try_marks}")); + } else { + names.push(format!("{name}(){try_marks}")); + } + try_marks.clear(); + None + } + e => { + if names.is_empty() { + return None; + } + names.push(fold_general_expr(e, &mut try_marks)); + None + } + }; + if let Some(receiver) = receiver { + expr = receiver; + } else { + break; + } + } + + Some(names.iter().rev().join(".")) +} + fn contiguous_range_for_item_group<N>( first: N, visited: &mut FxHashSet<SyntaxNode>, @@ -297,7 +496,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 +513,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 = 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_vec(); 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 +548,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 +568,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 +740,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 +754,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 +773,7 @@ fn main() <fold block>{ structS => <fold matcharm>StructS <fold block>{ a: 31, }</fold></fold>, - }</fold> + }</fold></fold> }</fold> "#, ) @@ -555,11 +784,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 +927,49 @@ type Foo<T, U> = foo<fold arglist>< "#, ); } + 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> +"#, + ) + } } |