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.rs358
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>
+"#,
+ )
+ }
}