11use ide_db:: { FxHashSet , syntax_helpers:: node_ext:: vis_eq} ;
2- use itertools:: Itertools ;
32use 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;
1413const REGION_START : & str = "// region:" ;
1514const REGION_END : & str = "// endregion" ;
1615
17- #[ derive( Debug , PartialEq , Eq , Clone , Copy ) ]
16+ #[ derive( Debug , PartialEq , Eq ) ]
1817pub 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.
191190fn 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
263245fn 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
389341fn 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#"
785737fn 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><
917869fn 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#"
932884fn 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