@@ -162,6 +162,7 @@ impl Formatter {
162162 self . add_newlines_after_extends_statement ( )
163163 . fix_dangling_semicolons ( )
164164 . fix_dangling_commas ( )
165+ . fix_nested_parenthesized_lambda_indentation ( )
165166 . fix_trailing_spaces ( )
166167 . remove_trailing_commas_from_preload ( )
167168 . postprocess_tree_sitter ( )
@@ -186,12 +187,9 @@ impl Formatter {
186187
187188 #[ inline( always) ]
188189 fn ensure_safe_reorder ( & mut self ) -> Result < ( ) , Box < dyn std:: error:: Error > > {
189- let original_source = self
190- . original_source
191- . as_deref ( )
192- . ok_or_else ( || {
193- "Safe mode requires the original source to verify reordered code" . to_string ( )
194- } ) ?;
190+ let original_source = self . original_source . as_deref ( ) . ok_or_else ( || {
191+ "Safe mode requires the original source to verify reordered code" . to_string ( )
192+ } ) ?;
195193
196194 self . tree = self . parser . parse ( & self . content , None ) . unwrap ( ) ;
197195 ensure_top_level_tokens_match ( original_source, & self . tree , & self . content ) ?;
@@ -263,9 +261,10 @@ impl Formatter {
263261 // separate arguments or elements in arrays and use inline comments to
264262 // describe the elements
265263 // This is done in the Godot Nakama repository for example.
266- let comment_re = RegexBuilder :: new ( r"(?m)(?P<before>[^\n\r]*?)(?P<comment>#[^\n\r]*)\n\s+," )
267- . build ( )
268- . expect ( "dangling comma with comment regex should compile" ) ;
264+ let comment_re =
265+ RegexBuilder :: new ( r"(?m)(?P<before>[^\n\r]*?)(?P<comment>#[^\n\r]*)\n\s+," )
266+ . build ( )
267+ . expect ( "dangling comma with comment regex should compile" ) ;
269268
270269 self . regex_replace_all_outside_strings ( comment_re, |caps : & regex:: Captures | {
271270 let before = caps. name ( "before" ) . unwrap ( ) . as_str ( ) ;
@@ -298,6 +297,162 @@ impl Formatter {
298297 self
299298 }
300299
300+ /// This function removes duplicate indentation caused by lambdas wrapped in nested
301+ /// parenthesized expressions (e.g. a multiline lambda inside a ternary expression).
302+ /// Topiary applies an indent for each parenthesis level and another indent for the
303+ /// lambda body. That causes the lambda to have too much indentation and
304+ /// causes a GDScript parse error.
305+ #[ inline( always) ]
306+ fn fix_nested_parenthesized_lambda_indentation ( & mut self ) -> & mut Self {
307+ let mut captures = Vec :: new ( ) ;
308+ let mut stack = vec ! [ self . tree. root_node( ) ] ;
309+
310+ while let Some ( node) = stack. pop ( ) {
311+ if node. kind ( ) == "lambda" {
312+ if let Some ( body) = node. child_by_field_name ( "body" ) {
313+ if body. end_position ( ) . row > node. start_position ( ) . row {
314+ captures. push ( ( node, body) ) ;
315+ }
316+ }
317+ }
318+
319+ let mut cursor = node. walk ( ) ;
320+ for child in node. children ( & mut cursor) {
321+ stack. push ( child) ;
322+ }
323+ }
324+
325+ if captures. is_empty ( ) {
326+ return self ;
327+ }
328+
329+ captures. sort_by_key ( |( lambda, _) | lambda. start_position ( ) . row ) ;
330+
331+ let mut lines: Vec < String > = self
332+ . content
333+ . split ( '\n' )
334+ . map ( |line| line. to_string ( ) )
335+ . collect ( ) ;
336+ let indent_size = self . config . indent_size . max ( 1 ) ;
337+ // We might need to pull the closing parenthesis up to the lambda's last line
338+ // to fix the indentation.
339+ // GDScript doesn't parse multi-line lambdas when there is a newline between the body
340+ // and the closing parenthesis (parenthesized expression, ternary, ...).
341+ let mut closing_merges: Vec < ( usize , usize ) > = Vec :: new ( ) ;
342+
343+ for ( lambda, body) in captures {
344+ let header_row = lambda. start_position ( ) . row as usize ;
345+ let mut first_row = body. start_position ( ) . row as usize ;
346+ let last_row = body. end_position ( ) . row as usize ;
347+
348+ if first_row == header_row {
349+ first_row = first_row. saturating_add ( 1 ) ;
350+ }
351+
352+ if header_row >= lines. len ( )
353+ || first_row >= lines. len ( )
354+ || last_row >= lines. len ( )
355+ || first_row > last_row
356+ {
357+ continue ;
358+ }
359+
360+ // Skip empty lines at the top of the lambda body. They shouldn't count when
361+ // we inspect indentation or decide whether the lambda is multi-line.
362+ while first_row <= last_row
363+ && first_row < lines. len ( )
364+ && lines[ first_row] . trim ( ) . is_empty ( )
365+ {
366+ first_row += 1 ;
367+ }
368+ if first_row > last_row || first_row >= lines. len ( ) {
369+ continue ;
370+ }
371+
372+ let header_indent = calculate_indent_info ( & lines[ header_row] , indent_size) ;
373+ let first_indent = calculate_indent_info ( & lines[ first_row] , indent_size) ;
374+ let target_spaces = header_indent. spaces + indent_size;
375+ if first_indent. spaces <= target_spaces {
376+ continue ;
377+ }
378+ let delta_spaces = first_indent. spaces - target_spaces;
379+
380+ let suffix = lines[ first_row] [ first_indent. column ..] . to_string ( ) ;
381+ lines[ first_row] = format ! (
382+ "{}{suffix}" ,
383+ render_indent(
384+ & IndentInfo {
385+ spaces: target_spaces,
386+ column: first_indent. column,
387+ } ,
388+ indent_size,
389+ self . config. use_spaces
390+ )
391+ ) ;
392+
393+ for row in ( first_row + 1 ) ..=last_row {
394+ if row >= lines. len ( ) || lines[ row] . trim ( ) . is_empty ( ) {
395+ continue ;
396+ }
397+ let current_indent = calculate_indent_info ( & lines[ row] , indent_size) ;
398+ if current_indent. spaces <= delta_spaces {
399+ continue ;
400+ }
401+ let new_spaces = current_indent. spaces - delta_spaces;
402+ let suffix = lines[ row] [ current_indent. column ..] . to_string ( ) ;
403+ lines[ row] = format ! (
404+ "{}{}" ,
405+ render_indent(
406+ & IndentInfo {
407+ spaces: new_spaces,
408+ column: current_indent. column,
409+ } ,
410+ indent_size,
411+ self . config. use_spaces
412+ ) ,
413+ suffix
414+ ) ;
415+ }
416+ if let Some ( parent) = lambda. parent ( ) {
417+ // When a lambda sits inside an expression wrapped with
418+ // parentheses, the GDScript parser needs the closing ")" to
419+ // immediately follow the lambda body (no blank line, no line
420+ // return at the end of the lambda body).
421+ // We look for the next non-empty line and, if it's just a
422+ // closing parenthesis, we merge it back onto the lambda body.
423+ if parent. kind ( ) == "parenthesized_expression"
424+ && !lines[ last_row] . trim_end ( ) . ends_with ( ')' )
425+ {
426+ let mut closing_row = last_row + 1 ;
427+ while closing_row < lines. len ( ) && lines[ closing_row] . trim ( ) . is_empty ( ) {
428+ closing_row += 1 ;
429+ }
430+ if closing_row < lines. len ( ) && lines[ closing_row] . trim ( ) == ")" {
431+ closing_merges. push ( ( closing_row, last_row) ) ;
432+ }
433+ }
434+ }
435+ }
436+
437+ closing_merges. sort_by ( |a, b| b. 0 . cmp ( & a. 0 ) ) ;
438+ for ( closing_row, body_row) in closing_merges {
439+ if closing_row >= lines. len ( ) || body_row >= lines. len ( ) {
440+ continue ;
441+ }
442+ if lines[ closing_row] . trim ( ) != ")" {
443+ continue ;
444+ }
445+ let trimmed_body = lines[ body_row] . trim_end ( ) . to_string ( ) ;
446+ lines[ body_row] = format ! ( "{trimmed_body})" ) ;
447+ lines. remove ( closing_row) ;
448+ }
449+
450+ self . content = lines. join ( "\n " ) ;
451+ self . tree = self . parser . parse ( & self . content , Some ( & self . tree ) ) . unwrap ( ) ;
452+
453+ self
454+ }
455+
301456 /// This function removes trailing spaces at the end of lines.
302457 #[ inline( always) ]
303458 fn fix_trailing_spaces ( & mut self ) -> & mut Self {
@@ -941,6 +1096,53 @@ struct GdTreeNode {
9411096 children : Vec < usize > ,
9421097}
9431098
1099+ /// Represents a line's indentation using two metrics:
1100+ /// - `spaces`: the total indentation width measured in space units;
1101+ /// - `column`: the byte index of the first non-whitespace character in the line.
1102+ struct IndentInfo {
1103+ spaces : usize ,
1104+ column : usize ,
1105+ }
1106+
1107+ /// Calculates indentation information for `line`, interpreting tabs using
1108+ /// `indent_size`.
1109+ fn calculate_indent_info ( line : & str , indent_size : usize ) -> IndentInfo {
1110+ let mut spaces = 0usize ;
1111+
1112+ for ( idx, ch) in line. char_indices ( ) {
1113+ match ch {
1114+ '\t' => spaces += indent_size,
1115+ ' ' => spaces += 1 ,
1116+ _ => {
1117+ return IndentInfo {
1118+ spaces,
1119+ column : idx,
1120+ } ;
1121+ }
1122+ }
1123+ }
1124+
1125+ IndentInfo {
1126+ spaces,
1127+ column : line. len ( ) ,
1128+ }
1129+ }
1130+
1131+ /// Renders an indentation string with width `indent.spaces` according to the
1132+ /// formatter's configuration.
1133+ fn render_indent ( indent : & IndentInfo , indent_size : usize , use_spaces : bool ) -> String {
1134+ if use_spaces {
1135+ return " " . repeat ( indent. spaces ) ;
1136+ }
1137+
1138+ let tabs = indent. spaces / indent_size;
1139+ let remainder = indent. spaces % indent_size;
1140+ let mut result = String :: with_capacity ( tabs + remainder) ;
1141+ result. push_str ( & "\t " . repeat ( tabs) ) ;
1142+ result. push_str ( & " " . repeat ( remainder) ) ;
1143+ result
1144+ }
1145+
9441146/// Calculates end position of the `slice` counting from `start`
9451147fn calculate_end_position ( mut start : Point , slice : & str ) -> Point {
9461148 for b in slice. as_bytes ( ) {
0 commit comments