From 4b4561d0692358f33d154267e59ac0005cd6e9df Mon Sep 17 00:00:00 2001 From: Christian Schiffler Date: Fri, 5 Jun 2026 20:11:34 +0200 Subject: [PATCH 1/2] Add support for '# gd-formatter:disable' and '# gd-formatter:enable'. Implements #238. --- src/formatter.rs | 106 ++++++++++++++++++ .../reorder_code/expected/reorder_disable.gd | 17 +++ tests/reorder_code/input/reorder_disable.gd | 18 +++ 3 files changed, 141 insertions(+) create mode 100644 tests/reorder_code/expected/reorder_disable.gd create mode 100644 tests/reorder_code/input/reorder_disable.gd diff --git a/src/formatter.rs b/src/formatter.rs index 6708ec2..1f383b4 100644 --- a/src/formatter.rs +++ b/src/formatter.rs @@ -54,6 +54,9 @@ struct Formatter { tree: Tree, original_source: Option, indent_string: String, + // Original text of each # gd-formatter:disable ... # gd-formatter:enable region, + // indexed by the order they appear in the file. Used to restore regions after formatting. + disabled_regions: Vec, } impl Formatter { @@ -87,6 +90,7 @@ impl Formatter { input_tree, parser, indent_string, + disabled_regions: Vec::new(), } } @@ -153,6 +157,104 @@ impl Formatter { /// pre-applying rules that could be performance-intensive through topiary. #[inline(always)] fn preprocess(&mut self) -> &mut Self { + // Replace # gd-formatter:disable ... # gd-formatter:enable regions with placeholder + // comments so they pass through Topiary and post-processing untouched. + self.extract_disabled_regions(); + self + } + + /// Scans the content for # gd-formatter:disable / # gd-formatter:enable regions. + /// Each complete region (including the marker lines) is stored verbatim in + /// self.disabled_regions and replaced with a single placeholder comment of the + /// form `# gd-formatter:preserved-region:N`. This prevents Topiary and all + /// post-processing steps from touching the content inside those regions. + fn extract_disabled_regions(&mut self) { + const DISABLE_MARKER: &str = "# gd-formatter:disable"; + const ENABLE_MARKER: &str = "# gd-formatter:enable"; + + if !self.content.contains(DISABLE_MARKER) { + return; + } + + let mut result = String::new(); + let mut in_disabled_region = false; + let mut current_region = String::new(); + + // split_inclusive keeps the '\n' attached to each line so we never lose + // trailing newlines when we reassemble the string. + for line in self.content.split_inclusive('\n') { + let trimmed = line.trim(); + + if !in_disabled_region && trimmed == DISABLE_MARKER { + // Begin accumulating the disabled region (include the marker line itself). + in_disabled_region = true; + current_region.push_str(line); + } else if in_disabled_region && trimmed == ENABLE_MARKER { + // Close the region (include the closing marker line). + current_region.push_str(line); + in_disabled_region = false; + let region_index = self.disabled_regions.len(); + self.disabled_regions.push(current_region.clone()); + current_region.clear(); + // Emit a single placeholder comment so the rest of the pipeline sees + // one clean comment node in place of the entire disabled block. + result.push_str(&format!( + "# gd-formatter:preserved-region:{}\n", + region_index + )); + } else if in_disabled_region { + current_region.push_str(line); + } else { + result.push_str(line); + } + } + + // An unclosed disable region (no matching enable marker) is also preserved. + if in_disabled_region { + let region_index = self.disabled_regions.len(); + self.disabled_regions.push(current_region); + result.push_str(&format!( + "# gd-formatter:preserved-region:{}\n", + region_index + )); + } + + self.content = result; + // Re-parse so self.tree matches the new placeholder-containing content before + // it is handed to Topiary. + self.tree = self.parser.parse(&self.content, None).unwrap(); + } + + /// Replaces every placeholder comment emitted by extract_disabled_regions() with + /// the original region text that was saved at that time. Called as the last + /// post-processing step so all normal formatting has already been applied to the + /// surrounding code. + fn restore_disabled_regions(&mut self) -> &mut Self { + if self.disabled_regions.is_empty() { + return self; + } + + let mut result = String::new(); + + for line in self.content.split_inclusive('\n') { + // Strip leading whitespace before checking for the placeholder; Topiary + // may have adjusted indentation on comment lines. + let trimmed = line.trim(); + if let Some(rest) = trimmed.strip_prefix("# gd-formatter:preserved-region:") { + if let Ok(index) = rest.parse::() { + if let Some(original) = self.disabled_regions.get(index) { + result.push_str(original); + continue; + } + } + } + result.push_str(line); + } + + self.content = result; + // Re-parse so self.tree stays in sync with the restored content for any + // subsequent steps (validate_formatting, reorder) that rely on it. + self.tree = self.parser.parse(&self.content, Some(&self.tree)).unwrap(); self } @@ -175,6 +277,10 @@ impl Formatter { .fix_trailing_spaces() .remove_trailing_commas_from_preload() .postprocess_tree_sitter() + // Restore the original text of disabled regions after all other post-processing, + // so that the surrounding code is formatted normally while the disabled regions + // keep their exact original content. + .restore_disabled_regions() } #[inline(always)] diff --git a/tests/reorder_code/expected/reorder_disable.gd b/tests/reorder_code/expected/reorder_disable.gd new file mode 100644 index 0000000..e2c2749 --- /dev/null +++ b/tests/reorder_code/expected/reorder_disable.gd @@ -0,0 +1,17 @@ +class_name TestClass +extends Node + +# gd-formatter:disable +var vertices_preserve: PackedVector3Array = [ + Vector3(-1, 0, -1), + Vector3( 1, 0, -1), + Vector3( 1, 0, 1), + Vector3(-1, 0, 1), +] +# gd-formatter:enable +var vertices_reformat: PackedVector3Array = [ + Vector3(-1, 0, -1), + Vector3(1, 0, -1), + Vector3(1, 0, 1), + Vector3(-1, 0, 1), +] diff --git a/tests/reorder_code/input/reorder_disable.gd b/tests/reorder_code/input/reorder_disable.gd new file mode 100644 index 0000000..3c404a0 --- /dev/null +++ b/tests/reorder_code/input/reorder_disable.gd @@ -0,0 +1,18 @@ +class_name TestClass +extends Node + +# gd-formatter:disable +var vertices_preserve: PackedVector3Array = [ + Vector3(-1, 0, -1), + Vector3( 1, 0, -1), + Vector3( 1, 0, 1), + Vector3(-1, 0, 1), +] +# gd-formatter:enable + +var vertices_reformat: PackedVector3Array = [ + Vector3(-1, 0, -1), + Vector3( 1, 0, -1), + Vector3( 1, 0, 1), + Vector3(-1, 0, 1), +] From 437ce1b8f7ccc5fe45383088f5226d68b18b6b30 Mon Sep 17 00:00:00 2001 From: Nathan Lovato <12694995+NathanLovato@users.noreply.github.com> Date: Sat, 6 Jun 2026 18:49:49 +0200 Subject: [PATCH 2/2] Switch region markers from gd-formatter:disable/enable to fmt: off/on, refactor the code a little bit --- src/formatter.rs | 97 +++++++++++-------- .../reorder_code/expected/reorder_disable.gd | 4 +- tests/reorder_code/input/reorder_disable.gd | 12 +-- 3 files changed, 64 insertions(+), 49 deletions(-) diff --git a/src/formatter.rs b/src/formatter.rs index 1f383b4..1a04456 100644 --- a/src/formatter.rs +++ b/src/formatter.rs @@ -54,7 +54,7 @@ struct Formatter { tree: Tree, original_source: Option, indent_string: String, - // Original text of each # gd-formatter:disable ... # gd-formatter:enable region, + // Original text of each `# fmt: off` ... `# fmt: on` region, // indexed by the order they appear in the file. Used to restore regions after formatting. disabled_regions: Vec, } @@ -157,23 +157,41 @@ impl Formatter { /// pre-applying rules that could be performance-intensive through topiary. #[inline(always)] fn preprocess(&mut self) -> &mut Self { - // Replace # gd-formatter:disable ... # gd-formatter:enable regions with placeholder - // comments so they pass through Topiary and post-processing untouched. self.extract_disabled_regions(); self } - /// Scans the content for # gd-formatter:disable / # gd-formatter:enable regions. - /// Each complete region (including the marker lines) is stored verbatim in + /// Scans the content for `# fmt: off` / `# fmt: on` regions (ignoring whitespace). + /// Each complete region (including the marker lines) is stored literally in /// self.disabled_regions and replaced with a single placeholder comment of the - /// form `# gd-formatter:preserved-region:N`. This prevents Topiary and all + /// form `# fmt:preserved-region:N`. This prevents Topiary and all /// post-processing steps from touching the content inside those regions. fn extract_disabled_regions(&mut self) { - const DISABLE_MARKER: &str = "# gd-formatter:disable"; - const ENABLE_MARKER: &str = "# gd-formatter:enable"; + enum LineKind { + FmtOn, + FmtOff, + Other, + } - if !self.content.contains(DISABLE_MARKER) { - return; + /// Checks whether `line` is a `# fmt: off` or `# fmt: on` marker. + fn classify_line(line: &str) -> LineKind { + if !line.contains('#') { + return LineKind::Other; + } + + let line = line.trim(); + let Some(after_hash) = line.strip_prefix('#') else { + return LineKind::Other; + }; + let Some(after_fmt) = after_hash.trim_start().strip_prefix("fmt:") else { + return LineKind::Other; + }; + + match after_fmt.trim_start() { + "off" => LineKind::FmtOff, + "on" => LineKind::FmtOn, + _ => LineKind::Other, + } } let mut result = String::new(); @@ -183,29 +201,26 @@ impl Formatter { // split_inclusive keeps the '\n' attached to each line so we never lose // trailing newlines when we reassemble the string. for line in self.content.split_inclusive('\n') { - let trimmed = line.trim(); - - if !in_disabled_region && trimmed == DISABLE_MARKER { - // Begin accumulating the disabled region (include the marker line itself). - in_disabled_region = true; - current_region.push_str(line); - } else if in_disabled_region && trimmed == ENABLE_MARKER { - // Close the region (include the closing marker line). - current_region.push_str(line); - in_disabled_region = false; - let region_index = self.disabled_regions.len(); - self.disabled_regions.push(current_region.clone()); - current_region.clear(); - // Emit a single placeholder comment so the rest of the pipeline sees - // one clean comment node in place of the entire disabled block. - result.push_str(&format!( - "# gd-formatter:preserved-region:{}\n", - region_index - )); - } else if in_disabled_region { - current_region.push_str(line); - } else { - result.push_str(line); + match classify_line(line) { + LineKind::FmtOff if !in_disabled_region => { + in_disabled_region = true; + current_region.push_str(line); + } + LineKind::FmtOn if in_disabled_region => { + current_region.push_str(line); + in_disabled_region = false; + let region_index = self.disabled_regions.len(); + self.disabled_regions.push(current_region.clone()); + current_region.clear(); + result.push_str(&format!("# fmt:preserved-region:{}\n", region_index)); + } + _ => { + if in_disabled_region { + current_region.push_str(line); + } else { + result.push_str(line); + } + } } } @@ -213,16 +228,16 @@ impl Formatter { if in_disabled_region { let region_index = self.disabled_regions.len(); self.disabled_regions.push(current_region); - result.push_str(&format!( - "# gd-formatter:preserved-region:{}\n", - region_index - )); + result.push_str(&format!("# fmt:preserved-region:{}\n", region_index)); } self.content = result; - // Re-parse so self.tree matches the new placeholder-containing content before - // it is handed to Topiary. - self.tree = self.parser.parse(&self.content, None).unwrap(); + + // Reparse the tree in case we modified the source code and replaced + // some regions with disabled formatting. + if !self.disabled_regions.is_empty() { + self.tree = self.parser.parse(&self.content, None).unwrap(); + } } /// Replaces every placeholder comment emitted by extract_disabled_regions() with @@ -240,7 +255,7 @@ impl Formatter { // Strip leading whitespace before checking for the placeholder; Topiary // may have adjusted indentation on comment lines. let trimmed = line.trim(); - if let Some(rest) = trimmed.strip_prefix("# gd-formatter:preserved-region:") { + if let Some(rest) = trimmed.strip_prefix("# fmt:preserved-region:") { if let Ok(index) = rest.parse::() { if let Some(original) = self.disabled_regions.get(index) { result.push_str(original); diff --git a/tests/reorder_code/expected/reorder_disable.gd b/tests/reorder_code/expected/reorder_disable.gd index e2c2749..15344d8 100644 --- a/tests/reorder_code/expected/reorder_disable.gd +++ b/tests/reorder_code/expected/reorder_disable.gd @@ -1,14 +1,14 @@ class_name TestClass extends Node -# gd-formatter:disable +# fmt: off var vertices_preserve: PackedVector3Array = [ Vector3(-1, 0, -1), Vector3( 1, 0, -1), Vector3( 1, 0, 1), Vector3(-1, 0, 1), ] -# gd-formatter:enable +# fmt: on var vertices_reformat: PackedVector3Array = [ Vector3(-1, 0, -1), Vector3(1, 0, -1), diff --git a/tests/reorder_code/input/reorder_disable.gd b/tests/reorder_code/input/reorder_disable.gd index 3c404a0..2ce9be0 100644 --- a/tests/reorder_code/input/reorder_disable.gd +++ b/tests/reorder_code/input/reorder_disable.gd @@ -1,18 +1,18 @@ class_name TestClass extends Node -# gd-formatter:disable +# fmt: off var vertices_preserve: PackedVector3Array = [ Vector3(-1, 0, -1), Vector3( 1, 0, -1), Vector3( 1, 0, 1), Vector3(-1, 0, 1), ] -# gd-formatter:enable +# fmt: on var vertices_reformat: PackedVector3Array = [ - Vector3(-1, 0, -1), - Vector3( 1, 0, -1), - Vector3( 1, 0, 1), - Vector3(-1, 0, 1), + Vector3(-1, 0, -1), + Vector3( 1, 0, -1), + Vector3( 1, 0, 1), + Vector3(-1, 0, 1), ]