From 2ea3d8d6ce4c272939ca16b76c8fb7139f8a7f48 Mon Sep 17 00:00:00 2001 From: "David M. Lary" Date: Sun, 5 Apr 2026 23:51:46 -0500 Subject: [PATCH 1/2] Indent backslash continuation lines according to style guide From gdscript style guide[^1]: > Use 2 indent levels to distinguish continuation lines from regular code blocks. Currently the formatter indents continuation lines to the same level as the first line: ```gdscript func test(thing): thing.calling(1) \ .a(12314) \ .long() \ .chain("a string breaks things") \ .of() \ .functions() ``` With this change, continuation lines (joined with a backslash) are formatted with double indention: ```gdscript func test(thing): thing.calling(1) \ .a(12314) \ .long() \ .chain("a string breaks things") \ .of() \ .functions() ``` fixes #180 [^1]: https://docs.godotengine.org/en/stable/tutorials/scripting/gdscript/gdscript_styleguide.html#indentation --- src/formatter.rs | 40 +++++++++++++++++----- tests/expected/line_continuation.gd | 4 +-- tests/expected/line_continuation_indent.gd | 15 ++++++++ tests/input/line_continuation_indent.gd | 15 ++++++++ 4 files changed, 63 insertions(+), 11 deletions(-) create mode 100644 tests/expected/line_continuation_indent.gd create mode 100644 tests/input/line_continuation_indent.gd diff --git a/src/formatter.rs b/src/formatter.rs index 1c07baf..7a844ff 100644 --- a/src/formatter.rs +++ b/src/formatter.rs @@ -53,6 +53,7 @@ struct Formatter { input_tree: GdTree, tree: Tree, original_source: Option, + indent_string: String, } impl Formatter { @@ -72,6 +73,11 @@ impl Formatter { } else { None }; + let indent_string = if config.use_spaces { + " ".repeat(config.indent_size) + } else { + "\t".to_string() + }; Self { original_source, @@ -80,22 +86,17 @@ impl Formatter { tree, input_tree, parser, + indent_string, } } #[inline(always)] fn format(&mut self) -> Result<&mut Self, Box> { - let indent_string = if self.config.use_spaces { - " ".repeat(self.config.indent_size) - } else { - "\t".to_string() - }; - let language = Language { name: "gdscript".to_owned(), query: TopiaryQuery::new(&tree_sitter_gdscript::LANGUAGE.into(), QUERY).unwrap(), grammar: tree_sitter_gdscript::LANGUAGE.into(), - indent: Some(indent_string), + indent: Some(self.indent_string.clone()), }; let mut output = Vec::new(); @@ -475,12 +476,31 @@ impl Formatter { self } + /// This function indents lines following a line continuation by two + /// levels to match style guide. Note that this function will only work + /// with an up-to-date tree-sitter tree. + #[inline(always)] + fn fix_line_continuation_indentation(&mut self) -> &mut Self { + let re = RegexBuilder::new(r"\\\r?\n") + .build() + .expect("line continuation regex should compile"); + + self.regex_replace_all_outside_strings( + re, + format!("$0{}{}", self.indent_string, self.indent_string), + ); + + self + } + /// This function runs postprocess passes that uses tree-sitter. #[inline(always)] fn postprocess_tree_sitter(&mut self) -> &mut Self { self.tree = self.parser.parse(&self.content, None).unwrap(); - self.handle_two_blank_line() + self.fix_line_continuation_indentation() + .handle_two_blank_line() + } /// Replaces every match of regex `re` with `rep`, but only if the match is @@ -509,7 +529,9 @@ impl Formatter { .root_node() .descendant_for_byte_range(start_byte, start_byte) .unwrap(); - if node.kind() == "string" { + // String nodes may also contain escape_sequence nodes. These are + // found when a backslash is present within a string. + if node.kind() == "string" || node.kind() == "escape_sequence" { continue; } diff --git a/tests/expected/line_continuation.gd b/tests/expected/line_continuation.gd index 6b5890e..b36d879 100644 --- a/tests/expected/line_continuation.gd +++ b/tests/expected/line_continuation.gd @@ -1,4 +1,4 @@ func _handles(resource: Resource) -> bool: return resource is NoiseTexture2D \ - or resource is GradientTexture1D \ - or resource is GradientTexture2D + or resource is GradientTexture1D \ + or resource is GradientTexture2D diff --git a/tests/expected/line_continuation_indent.gd b/tests/expected/line_continuation_indent.gd new file mode 100644 index 0000000..5a67351 --- /dev/null +++ b/tests/expected/line_continuation_indent.gd @@ -0,0 +1,15 @@ +var thing = \ + my_long_function() + +thing \ + .calling(1) \ + .a(12314) \ + .long() \ + .chain("a string breaks things") \ + .of() \ + .functions() + +var string = "\ + this is a really long \ + string, but we don't want \ + to change the indentation." diff --git a/tests/input/line_continuation_indent.gd b/tests/input/line_continuation_indent.gd new file mode 100644 index 0000000..ba2e8af --- /dev/null +++ b/tests/input/line_continuation_indent.gd @@ -0,0 +1,15 @@ +var thing = \ +my_long_function() + +thing \ +.calling(1) \ +.a(12314) \ +.long() \ +.chain("a string breaks things") \ +.of() \ +.functions() + +var string = "\ + this is a really long \ + string, but we don't want \ + to change the indentation." From e8ab7a3953baea036caadb05f011f9f50282bb65 Mon Sep 17 00:00:00 2001 From: Nathan Lovato <12694995+NathanLovato@users.noreply.github.com> Date: Sat, 11 Apr 2026 14:35:30 +0200 Subject: [PATCH 2/2] Use queries to narrow and address the indent backslash continuation line issue --- queries/gdscript.scm | 7 ++++ src/formatter.rs | 21 +--------- tests/expected/line_continuation_indent.gd | 46 +++++++++++++++------- tests/input/line_continuation_indent.gd | 46 +++++++++++++++------- 4 files changed, 70 insertions(+), 50 deletions(-) diff --git a/queries/gdscript.scm b/queries/gdscript.scm index b9aab5d..9d6d853 100644 --- a/queries/gdscript.scm +++ b/queries/gdscript.scm @@ -246,3 +246,10 @@ (get_node) @leaf (line_continuation) @prepend_space @append_antispace @append_hardline +; Indent nodes that immediately follows a line continuation ("\") by two levels, +; following the GDScript style guide. The line_continuation token only appears +; as a direct child of these three node types. We're leaving arrays, dicts, and +; enums out because they should have a single indent level inside their bodies +(attribute (line_continuation) @append_indent_start @append_indent_start . (_) @append_indent_end @append_indent_end) +(binary_operator (line_continuation) @append_indent_start @append_indent_start . (_) @append_indent_end @append_indent_end) +(variable_statement (line_continuation) @append_indent_start @append_indent_start . (_) @append_indent_end @append_indent_end) diff --git a/src/formatter.rs b/src/formatter.rs index 7a844ff..3e42380 100644 --- a/src/formatter.rs +++ b/src/formatter.rs @@ -476,31 +476,12 @@ impl Formatter { self } - /// This function indents lines following a line continuation by two - /// levels to match style guide. Note that this function will only work - /// with an up-to-date tree-sitter tree. - #[inline(always)] - fn fix_line_continuation_indentation(&mut self) -> &mut Self { - let re = RegexBuilder::new(r"\\\r?\n") - .build() - .expect("line continuation regex should compile"); - - self.regex_replace_all_outside_strings( - re, - format!("$0{}{}", self.indent_string, self.indent_string), - ); - - self - } - /// This function runs postprocess passes that uses tree-sitter. #[inline(always)] fn postprocess_tree_sitter(&mut self) -> &mut Self { self.tree = self.parser.parse(&self.content, None).unwrap(); - self.fix_line_continuation_indentation() - .handle_two_blank_line() - + self.handle_two_blank_line() } /// Replaces every match of regex `re` with `rep`, but only if the match is diff --git a/tests/expected/line_continuation_indent.gd b/tests/expected/line_continuation_indent.gd index 5a67351..17e51ac 100644 --- a/tests/expected/line_continuation_indent.gd +++ b/tests/expected/line_continuation_indent.gd @@ -1,15 +1,31 @@ -var thing = \ - my_long_function() - -thing \ - .calling(1) \ - .a(12314) \ - .long() \ - .chain("a string breaks things") \ - .of() \ - .functions() - -var string = "\ - this is a really long \ - string, but we don't want \ - to change the indentation." +var is_valid: bool = some_condition \ + and another_condition + + +func _handles(resource: Resource) -> bool: + return resource is NoiseTexture2D \ + or resource is GradientTexture1D + + +func _process(delta: float) -> void: + if is_on_floor() \ + and not is_jumping: + apply_gravity(delta) + + +func _calculate() -> float: + var x: float = some_long_value \ + + another_value + return x + + +func _ready() -> void: + node.set_position(Vector2.ZERO) \ + .rotated(PI) + + +# string with \ must not change indentation +func _get_message() -> String: + var greeting: String = "\ + Hello world" + return greeting diff --git a/tests/input/line_continuation_indent.gd b/tests/input/line_continuation_indent.gd index ba2e8af..e786ec2 100644 --- a/tests/input/line_continuation_indent.gd +++ b/tests/input/line_continuation_indent.gd @@ -1,15 +1,31 @@ -var thing = \ -my_long_function() - -thing \ -.calling(1) \ -.a(12314) \ -.long() \ -.chain("a string breaks things") \ -.of() \ -.functions() - -var string = "\ - this is a really long \ - string, but we don't want \ - to change the indentation." +var is_valid: bool = some_condition \ +and another_condition + + +func _handles(resource: Resource) -> bool: + return resource is NoiseTexture2D \ + or resource is GradientTexture1D + + +func _process(delta: float) -> void: + if is_on_floor() \ + and not is_jumping: + apply_gravity(delta) + + +func _calculate() -> float: + var x: float = some_long_value \ + + another_value + return x + + +func _ready() -> void: + node.set_position(Vector2.ZERO) \ + .rotated(PI) + + +# string with \ must not change indentation +func _get_message() -> String: + var greeting: String = "\ + Hello world" + return greeting