Skip to content

Commit 9cbf37f

Browse files
committed
refactor: enhance folding range handling for statements and tail expressions
1 parent b40dbf3 commit 9cbf37f

File tree

2 files changed

+122
-163
lines changed

2 files changed

+122
-163
lines changed

crates/ide/src/folding_ranges.rs

Lines changed: 113 additions & 161 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
11
use ide_db::{FxHashSet, syntax_helpers::node_ext::vis_eq};
2-
use itertools::Itertools;
32
use syntax::{
43
Direction, NodeOrToken, SourceFile, SyntaxElement,
54
SyntaxKind::*,
65
SyntaxNode, TextRange, TextSize,
7-
ast::{self, AstNode, AstToken, HasArgList, edit::AstNodeEdit},
6+
ast::{self, AstNode, AstToken},
87
match_ast,
98
syntax_editor::Element,
109
};
@@ -14,7 +13,7 @@ use std::hash::Hash;
1413
const REGION_START: &str = "// region:";
1514
const REGION_END: &str = "// endregion";
1615

17-
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
16+
#[derive(Debug, PartialEq, Eq)]
1817
pub enum FoldKind {
1918
Comment,
2019
Imports,
@@ -34,8 +33,8 @@ pub enum FoldKind {
3433
TraitAliases,
3534
ExternCrates,
3635
// endregion: item runs
37-
Stmt,
38-
TailExpr,
36+
Stmt(ast::Stmt),
37+
TailExpr(ast::Expr),
3938
}
4039

4140
#[derive(Debug)]
@@ -50,8 +49,8 @@ impl Fold {
5049
Self { range, kind, collapsed_text: None }
5150
}
5251

53-
pub fn with_text(mut self, text: String) -> Self {
54-
self.collapsed_text = Some(text);
52+
pub fn with_text(mut self, text: Option<String>) -> Self {
53+
self.collapsed_text = text;
5554
self
5655
}
5756
}
@@ -185,88 +184,73 @@ pub(crate) fn folding_ranges(file: &SourceFile, collapsed_text: bool) -> Vec<Fol
185184

186185
/// Builds a fold for the given syntax element.
187186
///
188-
/// This function creates a `Fold` object that represents a collapsible region in the code.
187+
/// This function creates a `Fold` that represents a collapsible region in the code.
189188
/// If `collapsed_text` is enabled, it generates a preview text for certain fold kinds that
190189
/// shows a summarized version of the folded content.
191190
fn build_fold(element: &SyntaxElement, kind: FoldKind, collapsed_text: bool) -> Fold {
191+
let range = element.text_range();
192192
if !collapsed_text {
193-
return Fold::new(element.text_range(), kind);
193+
return Fold::new(range, kind);
194194
}
195195

196-
let fold_with_collapsed_text = match kind {
197-
FoldKind::TailExpr => {
198-
let expr = ast::Expr::cast(element.as_node().unwrap().clone()).unwrap();
199-
200-
let indent_level = expr.indent_level().0;
201-
let indents = " ".repeat(indent_level as usize);
202-
203-
let mut fold = Fold::new(element.text_range(), kind);
204-
if let Some(collapsed_expr) = collapsed_text_from_expr(expr) {
205-
fold = fold.with_text(format!("{indents}{collapsed_expr}"));
206-
}
207-
Some(fold)
208-
}
209-
FoldKind::Stmt => 'blk: {
210-
let node = element.as_node().unwrap();
211-
212-
match_ast! {
213-
match node {
214-
ast::ExprStmt(expr) => {
215-
let Some(expr) = expr.expr() else {
216-
break 'blk None;
217-
};
218-
219-
let indent_level = expr.indent_level().0;
220-
let indents = " ".repeat(indent_level as usize);
221-
222-
let mut fold = Fold::new(element.text_range(), kind);
223-
if let Some(collapsed_expr) = collapsed_text_from_expr(expr) {
224-
fold = fold.with_text(format!("{indents}{collapsed_expr};"));
225-
}
226-
Some(fold)
227-
},
228-
ast::LetStmt(let_stmt) => {
229-
if let_stmt.let_else().is_some() {
230-
break 'blk None;
231-
}
232-
233-
let Some(expr) = let_stmt.initializer() else {
234-
break 'blk None;
235-
};
236-
237-
let expr_offset =
238-
expr.syntax().text_range().start() - let_stmt.syntax().text_range().start();
239-
let text_before_expr = let_stmt.syntax().text().slice(..expr_offset);
240-
if text_before_expr.contains_char('\n') {
241-
break 'blk None;
242-
}
196+
let collapsed_text = match &kind {
197+
FoldKind::TailExpr(expr) => collapse_expr(expr.clone()),
198+
FoldKind::Stmt(stmt) => {
199+
match stmt {
200+
ast::Stmt::ExprStmt(expr_stmt) => {
201+
expr_stmt.expr().and_then(collapse_expr).map(|text| format!("{text};"))
202+
}
203+
ast::Stmt::LetStmt(let_stmt) => 'blk: {
204+
if let_stmt.let_else().is_some() {
205+
break 'blk None;
206+
}
243207

244-
let indent_level = let_stmt.indent_level().0;
245-
let indents = " ".repeat(indent_level as usize);
208+
let Some(expr) = let_stmt.initializer() else {
209+
break 'blk None;
210+
};
211+
212+
// If the `let` statement spans multiple lines, we do not collapse it.
213+
// We use the `eq_token` to check whether the `let` statement is a single line,
214+
// as the formatter may place the initializer on a new line for better readability.
215+
//
216+
// Example:
217+
// ```rust
218+
// let complex_pat =
219+
// complex_expr;
220+
// ```
221+
//
222+
// In this case, we should generate the collapsed text.
223+
let Some(eq_token) = let_stmt.eq_token() else {
224+
break 'blk None;
225+
};
226+
let eq_token_offset =
227+
eq_token.text_range().end() - let_stmt.syntax().text_range().start();
228+
let text_until_eq_token = let_stmt.syntax().text().slice(..eq_token_offset);
229+
if text_until_eq_token.contains_char('\n') {
230+
break 'blk None;
231+
}
246232

247-
let mut fold = Fold::new(element.text_range(), kind);
248-
if let Some(collapsed_expr) = collapsed_text_from_expr(expr) {
249-
fold = fold.with_text(format!("{indents}{text_before_expr}{collapsed_expr};"));
250-
}
251-
Some(fold)
252-
},
253-
_ => None,
233+
collapse_expr(expr).map(|text| format!("{text_until_eq_token} {text};"))
254234
}
235+
// handling `items` in external matches.
236+
ast::Stmt::Item(_) => None,
255237
}
256238
}
257239
_ => None,
258240
};
259241

260-
fold_with_collapsed_text.unwrap_or_else(|| Fold::new(element.text_range(), kind))
242+
Fold::new(range, kind).with_text(collapsed_text)
261243
}
262244

263245
fn fold_kind(element: SyntaxElement) -> Option<FoldKind> {
264246
// handle tail_expr
265247
if let Some(node) = element.as_node()
266-
&& let Some(block) = node.parent().and_then(|it| it.parent()).and_then(ast::BlockExpr::cast) // tail_expr -> stmt_list -> block
267-
&& block.tail_expr().is_some_and(|tail| tail.syntax() == node)
248+
// tail_expr -> stmt_list -> block
249+
&& let Some(block) = node.parent().and_then(|it| it.parent()).and_then(ast::BlockExpr::cast)
250+
&& let Some(tail_expr) = block.tail_expr()
251+
&& tail_expr.syntax() == node
268252
{
269-
return Some(FoldKind::TailExpr);
253+
return Some(FoldKind::TailExpr(tail_expr));
270254
}
271255

272256
match element.kind() {
@@ -287,103 +271,71 @@ fn fold_kind(element: SyntaxElement) -> Option<FoldKind> {
287271
| MATCH_ARM_LIST
288272
| VARIANT_LIST
289273
| TOKEN_TREE => Some(FoldKind::Block),
290-
EXPR_STMT | LET_STMT => Some(FoldKind::Stmt),
274+
EXPR_STMT | LET_STMT => Some(FoldKind::Stmt(ast::Stmt::cast(element.as_node()?.clone())?)),
291275
_ => None,
292276
}
293277
}
294278

295-
/// Generates a collapsed text representation of a chained expression.
296-
///
297-
/// This function analyzes an expression and creates a concise string representation
298-
/// that shows the structure of method chains, field accesses, and function calls.
299-
/// It's particularly useful for folding long chained expressions like:
300-
/// `obj.method1()?.field.method2(args)` -> `obj.method1()?.field.method2(…)`
301-
///
302-
/// The function traverses the expression tree from the outermost expression inward,
303-
/// collecting method names, field names, and call signatures. It accumulates try
304-
/// operators (`?`) and applies them to the appropriate parts of the chain.
305-
///
306-
/// # Parameters
307-
/// - `expr`: The expression to generate collapsed text for
308-
///
309-
/// # Returns
310-
/// - `Some(String)`: A dot-separated chain representation if the expression is chainable
311-
/// - `None`: If the expression is not suitable for collapsing (e.g., simple literals)
312-
///
313-
/// # Examples
314-
/// - `foo.bar().baz?` -> `"foo.bar().baz?"`
315-
/// - `obj.method(arg1, arg2)` -> `"obj.method(…)"`
316-
/// - `value?.field` -> `"value?.field"`
317-
fn collapsed_text_from_expr(mut expr: ast::Expr) -> Option<String> {
318-
let mut names = Vec::new();
319-
let mut try_marks = String::with_capacity(1);
320-
321-
let fold_general_expr = |expr: ast::Expr, try_marks: &mut String| {
322-
let text = expr.syntax().text();
323-
let name = if text.contains_char('\n') {
324-
format!("<expr>{try_marks}")
325-
} else {
326-
format!("{text}{try_marks}")
327-
};
328-
try_marks.clear();
329-
name
330-
};
279+
const COLLAPSE_EXPR_MAX_LEN: usize = 100;
331280

332-
loop {
333-
let receiver = match expr {
334-
ast::Expr::MethodCallExpr(call) => {
335-
let name = call
336-
.name_ref()
337-
.map(|name| name.text().to_owned())
338-
.unwrap_or_else(|| "�".into());
339-
if call.arg_list().and_then(|arg_list| arg_list.args().next()).is_some() {
340-
names.push(format!("{name}(…){try_marks}"));
341-
} else {
342-
names.push(format!("{name}(){try_marks}"));
343-
}
344-
try_marks.clear();
345-
call.receiver()
346-
}
347-
ast::Expr::FieldExpr(field) => {
348-
let name = match field.field_access() {
349-
Some(ast::FieldKind::Name(name)) => format!("{name}{try_marks}"),
350-
Some(ast::FieldKind::Index(index)) => format!("{index}{try_marks}"),
351-
None => format!("�{try_marks}"),
352-
};
353-
names.push(name);
354-
try_marks.clear();
355-
field.expr()
356-
}
357-
ast::Expr::TryExpr(try_expr) => {
358-
try_marks.push('?');
359-
try_expr.expr()
360-
}
361-
ast::Expr::CallExpr(call) => {
362-
let name = fold_general_expr(call.expr().unwrap(), &mut try_marks);
363-
if call.arg_list().and_then(|arg_list| arg_list.args().next()).is_some() {
364-
names.push(format!("{name}(…){try_marks}"));
365-
} else {
366-
names.push(format!("{name}(){try_marks}"));
281+
fn collapse_expr(expr: ast::Expr) -> Option<String> {
282+
let mut text = String::with_capacity(COLLAPSE_EXPR_MAX_LEN * 2);
283+
284+
let mut preorder = expr.syntax().preorder_with_tokens();
285+
while let Some(element) = preorder.next() {
286+
match element {
287+
syntax::WalkEvent::Enter(NodeOrToken::Node(node)) => {
288+
if let Some(arg_list) = ast::ArgList::cast(node.clone()) {
289+
let content = if arg_list.args().next().is_some() { "(…)" } else { "()" };
290+
text.push_str(content);
291+
preorder.skip_subtree();
292+
} else if let Some(expr) = ast::Expr::cast(node) {
293+
match expr {
294+
ast::Expr::AwaitExpr(_)
295+
| ast::Expr::BecomeExpr(_)
296+
| ast::Expr::BinExpr(_)
297+
| ast::Expr::BreakExpr(_)
298+
| ast::Expr::CallExpr(_)
299+
| ast::Expr::CastExpr(_)
300+
| ast::Expr::ContinueExpr(_)
301+
| ast::Expr::FieldExpr(_)
302+
| ast::Expr::IndexExpr(_)
303+
| ast::Expr::LetExpr(_)
304+
| ast::Expr::Literal(_)
305+
| ast::Expr::MethodCallExpr(_)
306+
| ast::Expr::OffsetOfExpr(_)
307+
| ast::Expr::ParenExpr(_)
308+
| ast::Expr::PathExpr(_)
309+
| ast::Expr::PrefixExpr(_)
310+
| ast::Expr::RangeExpr(_)
311+
| ast::Expr::RefExpr(_)
312+
| ast::Expr::ReturnExpr(_)
313+
| ast::Expr::TryExpr(_)
314+
| ast::Expr::UnderscoreExpr(_)
315+
| ast::Expr::YeetExpr(_)
316+
| ast::Expr::YieldExpr(_) => {}
317+
318+
// Some other exprs (e.g. `while` loop) are too complex to have a collapsed text
319+
_ => return None,
320+
}
367321
}
368-
try_marks.clear();
369-
None
370322
}
371-
e => {
372-
if names.is_empty() {
373-
return None;
323+
syntax::WalkEvent::Enter(NodeOrToken::Token(token)) => {
324+
if !token.kind().is_trivia() {
325+
text.push_str(token.text());
374326
}
375-
names.push(fold_general_expr(e, &mut try_marks));
376-
None
377327
}
378-
};
379-
if let Some(receiver) = receiver {
380-
expr = receiver;
381-
} else {
382-
break;
328+
syntax::WalkEvent::Leave(_) => {}
329+
}
330+
331+
if text.len() > COLLAPSE_EXPR_MAX_LEN {
332+
return None;
383333
}
384334
}
385335

386-
Some(names.iter().rev().join("."))
336+
text.shrink_to_fit();
337+
338+
Some(text)
387339
}
388340

389341
fn contiguous_range_for_item_group<N>(
@@ -520,7 +472,7 @@ mod tests {
520472

521473
fn check_inner(ra_fixture: &str, enable_collapsed_text: bool) {
522474
let (ranges, text) = extract_tags(ra_fixture, "fold");
523-
let ranges = ranges
475+
let ranges: Vec<_> = ranges
524476
.into_iter()
525477
.map(|(range, text)| {
526478
let (attr, collapsed_text) = match text {
@@ -534,7 +486,7 @@ mod tests {
534486
};
535487
(range, attr, collapsed_text)
536488
})
537-
.collect_vec();
489+
.collect();
538490

539491
let parse = SourceFile::parse(&text, span::Edition::CURRENT);
540492
let mut folds = folding_ranges(&parse.tree(), enable_collapsed_text);
@@ -567,8 +519,8 @@ mod tests {
567519
FoldKind::Function => "function",
568520
FoldKind::TraitAliases => "traitaliases",
569521
FoldKind::ExternCrates => "externcrates",
570-
FoldKind::Stmt => "stmt",
571-
FoldKind::TailExpr => "tailexpr",
522+
FoldKind::Stmt(_) => "stmt",
523+
FoldKind::TailExpr(_) => "tailexpr",
572524
};
573525
assert_eq!(kind, &attr.unwrap());
574526
if enable_collapsed_text {
@@ -783,7 +735,7 @@ fn main() <fold block>{
783735
check(
784736
r#"
785737
fn main() <fold block>{
786-
<fold tailexpr: frobnicate(…)>frobnicate<fold arglist>(
738+
<fold tailexpr:frobnicate(…)>frobnicate<fold arglist>(
787739
1,
788740
2,
789741
3,
@@ -917,7 +869,7 @@ type Foo<T, U> = foo<fold arglist><
917869
fn f() <fold block>{
918870
let x = 1;
919871
920-
<fold tailexpr: some_function().chain().method()>some_function()
872+
<fold tailexpr:some_function().chain().method()>some_function()
921873
.chain()
922874
.method()</fold>
923875
}</fold>
@@ -930,7 +882,7 @@ fn f() <fold block>{
930882
check(
931883
r#"
932884
fn main() <fold block>{
933-
<fold stmt: let result = some_value.method1().method2()?.method3();>let result = some_value
885+
<fold stmt:let result = some_value.method1().method2()?.method3();>let result = some_value
934886
.method1()
935887
.method2()?
936888
.method3();</fold>

0 commit comments

Comments
 (0)