diff --git a/queries/gdscript.scm b/queries/gdscript.scm index b4ba6ef..d417f40 100644 --- a/queries/gdscript.scm +++ b/queries/gdscript.scm @@ -85,11 +85,8 @@ (class_definition) @prepend_hardline (class_definition extends: (extends_statement "extends" @prepend_space)) -(source - (class_name_statement - extends: (extends_statement) @append_delimiter @append_hardline (#delimiter! "\n"))) -(source - (extends_statement) @append_delimiter @append_hardline (#delimiter! "\n")) +(source (class_name_statement extends: (extends_statement) @append_hardline)) +(source (extends_statement) @append_hardline) (source (class_name_statement (name) @append_hardline)) ; CONST DEFINITIONS diff --git a/src/formatter.rs b/src/formatter.rs index 17e2883..41dfecf 100644 --- a/src/formatter.rs +++ b/src/formatter.rs @@ -14,7 +14,7 @@ //! rules have too much performance overhead when applied through Topiary. use std::{collections::VecDeque, io::BufWriter}; -use regex::{Regex, RegexBuilder}; +use regex::{Regex, RegexBuilder, Replacer}; use topiary_core::{Language, Operation, TopiaryQuery, formatter_tree}; use tree_sitter::{Parser, Point, Query, QueryCursor, StreamingIterator, Tree}; @@ -126,14 +126,15 @@ impl Formatter { /// pre-applying rules that could be performance-intensive through topiary. #[inline(always)] fn preprocess(&mut self) -> &mut Self { - self.remove_newlines_after_extends_statement() + self } /// This function runs over the content after going through topiary. We use it /// to clean up/balance out the output. #[inline(always)] fn postprocess(&mut self) -> &mut Self { - self.fix_dangling_semicolons() + self.add_newlines_after_extends_statement() + .fix_dangling_semicolons() .fix_dangling_commas() .remove_trailing_commas_from_preload() .postprocess_tree_sitter() @@ -154,23 +155,40 @@ impl Formatter { Ok(self.content) } - /// This function removes additional new line characters after `extends_statement`. + /// This function adds additional new line characters after `extends_statement`. #[inline(always)] - fn remove_newlines_after_extends_statement(&mut self) -> &mut Self { + fn add_newlines_after_extends_statement(&mut self) -> &mut Self { // This regex matches substrings which: - // - must NOT contain "#" or "\n" characters between new line and "extends" keyword - // - must end with at least one new line character + // - must start wtih "extends" keyword // - must contain `extends_name` character sequence that satisfies one of the following conditions: // - consists out of alphanumeric characters // - consists out of any characters (except new lines) between double quotes + // - must contain at least one new line character between `extends_name` and optional doc comment + // - may contain multiple doc comment lines that starts with `##` and ends with a new line character let re = RegexBuilder::new( - r#"(?P^[^#\n]*extends )(?P([a-zA-Z0-9]+|".*?"))\n(\n*)"#, + r#"(?P^extends )(?P([a-zA-Z0-9]+|".*?"))\n+(?P(?:^##.*\n)*)(?P\z)?"#, ) .multi_line(true) .build() .expect("regex should compile"); - self.regex_replace_all_outside_strings(re, "$extends_line$extends_name\n"); + self.regex_replace_all_outside_strings(re, |caps: ®ex::Captures| { + let extends_line = caps.name("extends_line").unwrap().as_str(); + let extends_name = caps.name("extends_name").unwrap().as_str(); + let doc = caps + .name("doc") + .map(|m| m.as_str()) + .unwrap_or_default() + .trim_end(); // remove last new line from doc comment because we add a new line manually + // insert new line only if we are not at the end of file + let blank_new_line = if caps.name("EOF").is_some() { "" } else { "\n" }; + + format!( + "{}{}\n{}{}", + extends_line, extends_name, doc, blank_new_line + ) + }); + self } @@ -233,7 +251,7 @@ impl Formatter { /// outside of strings (simple or multiline). /// Use this to make post-processing changes needed for formatting but that /// shouldn't affect strings in the source code. - fn regex_replace_all_outside_strings(&mut self, re: Regex, rep: &str) { + fn regex_replace_all_outside_strings(&mut self, re: Regex, mut rep: R) { let mut iter = re.captures_iter(&self.content).peekable(); if iter.peek().is_none() { return; @@ -260,7 +278,7 @@ impl Formatter { } let mut replacement = String::new(); - capture.expand(rep, &mut replacement); + rep.replace_append(&capture, &mut replacement); let new_end_byte = start_byte + replacement.len(); diff --git a/tests/expected/class_doc_comment.gd b/tests/expected/class_doc_comment.gd new file mode 100644 index 0000000..62c9584 --- /dev/null +++ b/tests/expected/class_doc_comment.gd @@ -0,0 +1,24 @@ +class_name Player +extends CharacterBody2D +## A brief description of the class's role and functionality. +## +## The description of script +class_name Player +extends CharacterBody2D +## A brief description of the class's role and functionality. +## +## The description of script +class_name Player +extends CharacterBody2D +## A brief description of the class's role and functionality. +## +## The description of script + +var a = 10 +class_name Player +extends CharacterBody2D +## A brief description of the class's role and functionality. +## +## The description of script + +var a = 10 diff --git a/tests/input/class_doc_comment.gd b/tests/input/class_doc_comment.gd new file mode 100644 index 0000000..7c66146 --- /dev/null +++ b/tests/input/class_doc_comment.gd @@ -0,0 +1,23 @@ +class_name Player extends CharacterBody2D + +## A brief description of the class's role and functionality. +## +## The description of script +class_name Player extends CharacterBody2D +## A brief description of the class's role and functionality. +## +## The description of script +class_name Player extends CharacterBody2D +## A brief description of the class's role and functionality. +## +## The description of script + +var a = 10 +class_name Player +extends CharacterBody2D + +## A brief description of the class's role and functionality. +## +## The description of script + +var a = 10