Skip to content

Commit 31f4313

Browse files
committed
Handle comments in binary expressions and fix operator detection
Fix #277 Fix #278
1 parent 2ca7a8d commit 31f4313

6 files changed

Lines changed: 165 additions & 10 deletions

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ This file documents the changes made to the formatter with each release.
2121
- Fixed --reorder-code detaches trailing comments from top-level declarations and re-attaches them above the following declaration (#271)
2222
- Fix empty lines removed between conditional blocks (#276)
2323
- Fix editorconfig ignored when running from stdin (#275)
24+
- Fix code getting modified/comments getting mangled with operators in multiline chains of binary operator expressions (#278)
25+
- Fix Extra space is added after ! in if statements (#277)
2426

2527
## Release 0.21.0 (2026-07-16)
2628

src/formatter.rs

Lines changed: 95 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1667,6 +1667,52 @@ fn process_parenthesized_expression(
16671667
finish_group(render_elements, group_index);
16681668
}
16691669

1670+
/// Finds and returns the unnamed operator token between a binary expression's
1671+
/// operands.
1672+
///
1673+
/// Tree-sitter gives every `binary_operator` node a named `left` field and a
1674+
/// named `right` field. Each field contains the parsed expression on that side
1675+
/// of the operator and the operator is an unnamed token child of the enclosing
1676+
/// binary expression. For example:
1677+
///
1678+
/// ```gdscript
1679+
/// (
1680+
/// a
1681+
/// # Explain the next condition.
1682+
/// || b
1683+
/// ):
1684+
/// ```
1685+
///
1686+
/// The AST for this may look like:
1687+
///
1688+
/// (parenthesized_expression
1689+
/// (binary_operator
1690+
/// left: (identifier) ; a
1691+
/// (comment) ; # Explain the next condition.
1692+
/// right: (identifier))) ; b
1693+
/// ```
1694+
///
1695+
/// Here, left is just a and right is b. The comment is before the || operator,
1696+
/// but we could also have cases where it's after the || operator. That's why we
1697+
/// need to look for the operator itself.
1698+
fn binary_operator_token(node: tree_sitter::Node) -> Option<tree_sitter::Node> {
1699+
let left = node.child_by_field_name("left")?;
1700+
let right = node.child_by_field_name("right")?;
1701+
let mut child_index = 0;
1702+
while child_index < node.child_count() {
1703+
if let Some(child) = node.child(child_index as u32) {
1704+
if child.start_byte() >= left.end_byte()
1705+
&& child.end_byte() <= right.start_byte()
1706+
&& GDScriptNodeKind::get_kind_from_ast_node(child) != GDScriptNodeKind::Comment
1707+
{
1708+
return Some(child);
1709+
}
1710+
}
1711+
child_index += 1;
1712+
}
1713+
None
1714+
}
1715+
16701716
/// Formats BinaryOperator nodes. Homogeneous operator chains use balanced
16711717
/// groups to distribute operands and wrap before operators. Standalone boolean
16721718
/// expressions gain parentheses when they wrap, as GDScript otherwise has no
@@ -1687,7 +1733,7 @@ fn process_binary_operator(
16871733
return;
16881734
}
16891735

1690-
let operator_text = if let Some(operator) = node.child(1) {
1736+
let operator_text = if let Some(operator) = binary_operator_token(node) {
16911737
&input.source[operator.start_byte()..operator.end_byte()]
16921738
} else {
16931739
""
@@ -1744,9 +1790,9 @@ fn process_binary_operator(
17441790
let mut segments = Vec::with_capacity(child_count);
17451791
let mut levels: Vec<tree_sitter::Node> = Vec::with_capacity(child_count);
17461792
let mut current_node = node;
1747-
while let Some(left) = current_node.child(0) {
1793+
while let Some(left) = current_node.child_by_field_name("left") {
17481794
if GDScriptNodeKind::get_kind_from_ast_node(left) == GDScriptNodeKind::BinaryOperator {
1749-
let left_operator_text = if let Some(operator) = left.child(1) {
1795+
let left_operator_text = if let Some(operator) = binary_operator_token(left) {
17501796
&input.source[operator.start_byte()..operator.end_byte()]
17511797
} else {
17521798
""
@@ -1759,24 +1805,52 @@ fn process_binary_operator(
17591805
}
17601806
break;
17611807
}
1762-
if let Some(left) = current_node.child(0) {
1808+
if let Some(left) = current_node.child_by_field_name("left") {
17631809
segments.push(BinaryChainSegment {
17641810
operator: None,
17651811
expression: left,
17661812
});
17671813
}
1768-
if let Some(right) = current_node.child(2) {
1814+
let mut has_comment = false;
1815+
let mut child_index = 0;
1816+
while child_index < current_node.child_count() {
1817+
if let Some(child) = current_node.child(child_index as u32) {
1818+
if GDScriptNodeKind::get_kind_from_ast_node(child) == GDScriptNodeKind::Comment {
1819+
segments.push(BinaryChainSegment {
1820+
operator: None,
1821+
expression: child,
1822+
});
1823+
has_comment = true;
1824+
}
1825+
}
1826+
child_index += 1;
1827+
}
1828+
if let Some(right) = current_node.child_by_field_name("right") {
17691829
segments.push(BinaryChainSegment {
1770-
operator: current_node.child(1),
1830+
operator: binary_operator_token(current_node),
17711831
expression: right,
17721832
});
17731833
}
17741834
let mut level_index = levels.len();
17751835
while level_index > 0 {
17761836
level_index -= 1;
1777-
if let Some(right) = levels[level_index].child(2) {
1837+
let level = levels[level_index];
1838+
let mut child_index = 0;
1839+
while child_index < level.child_count() {
1840+
if let Some(child) = level.child(child_index as u32) {
1841+
if GDScriptNodeKind::get_kind_from_ast_node(child) == GDScriptNodeKind::Comment {
1842+
segments.push(BinaryChainSegment {
1843+
operator: None,
1844+
expression: child,
1845+
});
1846+
has_comment = true;
1847+
}
1848+
}
1849+
child_index += 1;
1850+
}
1851+
if let Some(right) = level.child_by_field_name("right") {
17781852
segments.push(BinaryChainSegment {
1779-
operator: levels[level_index].child(1),
1853+
operator: binary_operator_token(level),
17801854
expression: right,
17811855
});
17821856
}
@@ -1812,8 +1886,17 @@ fn process_binary_operator(
18121886
let mut segment_index = 0;
18131887
while segment_index < segments.len() {
18141888
let segment = &segments[segment_index];
1889+
let is_comment = GDScriptNodeKind::get_kind_from_ast_node(segment.expression)
1890+
== GDScriptNodeKind::Comment;
1891+
if is_comment && segment_index > 0 {
1892+
render_elements.push(RenderElement::HardLine);
1893+
}
18151894
if let Some(operator) = segment.operator {
1816-
render_elements.push(RenderElement::BalancedLine);
1895+
if has_comment {
1896+
render_elements.push(RenderElement::HardLine);
1897+
} else {
1898+
render_elements.push(RenderElement::BalancedLine);
1899+
}
18171900
process_node(input, operator, render_elements);
18181901
render_elements.push(RenderElement::Space);
18191902
}
@@ -2557,7 +2640,9 @@ fn process_separator_between_sibling_nodes(
25572640
}
25582641

25592642
if parent_kind == GDScriptNodeKind::UnaryOperator
2560-
&& (previous_child.kind() == "~" || previous_kind == GDScriptNodeKind::Operator)
2643+
&& (previous_child.kind() == "~"
2644+
|| previous_child.kind() == "!"
2645+
|| previous_kind == GDScriptNodeKind::Operator)
25612646
{
25622647
return;
25632648
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
extends Control
2+
3+
4+
func _gui_input(event: InputEvent) -> void:
5+
if event is InputEventKey:
6+
if (
7+
# Stop propagation of the ` key since it's used to open/close the console
8+
event.keycode == KEY_QUOTELEFT
9+
# Stop propagation of the escape key since we don't want to unfocus the input
10+
|| event.keycode == KEY_ESCAPE
11+
# Stop propagation of the up/down keys since they're used to navigate the command history
12+
|| event.keycode == KEY_UP
13+
|| event.keycode == KEY_DOWN
14+
# Stop propagation of the left, right, home, end, page up, and page down keys so that we can handle caret navigation ourselves
15+
|| event.keycode == KEY_LEFT
16+
|| event.keycode == KEY_RIGHT
17+
|| event.keycode == KEY_HOME
18+
|| event.keycode == KEY_END
19+
|| event.keycode == KEY_PAGEUP
20+
|| event.keycode == KEY_PAGEDOWN
21+
# Stop propagation of the tab key so we can use it for autocomplete
22+
|| event.keycode == KEY_TAB
23+
):
24+
accept_event()

tests/expected/operators_in_context.gd

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,15 @@ func foo():
55
a = [1 + 2 + 3]
66
a = (1 + 2)
77
a = (1 | 2 | 3)
8+
a = (1 & 2 & 3)
9+
a = (1 ^ 2 ^ 3)
10+
a = (1 << 2 << 3)
11+
a = (8 >> 2 >> 1)
812
a = false && true
913
a = ~1
14+
a = +1
15+
a = -1
16+
if not valid:
17+
pass
18+
if !valid:
19+
pass
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
extends Control
2+
3+
4+
func _gui_input(event: InputEvent) -> void:
5+
if event is InputEventKey:
6+
if (
7+
# Stop propagation of the ` key since it's used to open/close the console
8+
event.keycode == KEY_QUOTELEFT
9+
# Stop propagation of the escape key since we don't want to unfocus the input
10+
|| event.keycode == KEY_ESCAPE
11+
# Stop propagation of the up/down keys since they're used to navigate the command history
12+
|| event.keycode == KEY_UP
13+
|| event.keycode == KEY_DOWN
14+
# Stop propagation of the left, right, home, end, page up, and page down keys so that we can handle caret navigation ourselves
15+
|| event.keycode == KEY_LEFT
16+
|| event.keycode == KEY_RIGHT
17+
|| event.keycode == KEY_HOME
18+
|| event.keycode == KEY_END
19+
|| event.keycode == KEY_PAGEUP
20+
|| event.keycode == KEY_PAGEDOWN
21+
# Stop propagation of the tab key so we can use it for autocomplete
22+
|| event.keycode == KEY_TAB
23+
):
24+
accept_event()

tests/input/operators_in_context.gd

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,15 @@ func foo():
55
a = [1+2+3]
66
a = ( 1+2 )
77
a = ( 1 | 2|3 )
8+
a = ( 1 & 2&3 )
9+
a = ( 1 ^ 2^3 )
10+
a = ( 1 << 2<<3 )
11+
a = ( 8 >> 2>>1 )
812
a = false&& true
913
a = ~ 1
14+
a = + 1
15+
a = - 1
16+
if not valid:
17+
pass
18+
if ! valid:
19+
pass

0 commit comments

Comments
 (0)