Repository: Enter-tainer/typstyle (rust)

Issue:
Custom tab size
I know that the canonical tabbing for Typst is "two spaces", but for a lot of users that's unusual. Thus, for all languages that recommend two-space, I (and a lot of programmers I know) use a four-space tabbing during development, and then turn it into two-space right before publishing, often automatically. 

Because of that, I propose having a setting for tab size. I imagine it could be easy. As a last resort, it could be crutched as 
`doNormalFormatting(text).replace("\n(  )+") { "\n" + " ".repeat(it.length / 2 * size) }` or whatever its analogue in Rust is...

Pull request description:
Add `--tab-width`, which sets number of spaces for levels of indentation in the output
This PR closes #116 and #122.

Although `typstyle` is opinionated, tab width is a common setting to expose to the user in other opinionated formatters.

Patch:
diff --git a/crates/typstyle-core/src/config.rs b/crates/typstyle-core/src/config.rs
index 609b743..5a502f6 100644
--- a/crates/typstyle-core/src/config.rs
+++ b/crates/typstyle-core/src/config.rs
@@ -30,6 +30,11 @@ impl Config {
         self
     }
 
+    pub fn with_tab_spaces(mut self, tab_spaces: usize) -> Self {
+        self.tab_spaces = tab_spaces;
+        self
+    }
+
     pub fn chain_width(&self) -> usize {
         const CHAIN_WIDTH_RATIO: f32 = 0.6;
         (self.max_width as f32 * CHAIN_WIDTH_RATIO) as usize
diff --git a/crates/typstyle/src/cli.rs b/crates/typstyle/src/cli.rs
index e5945a3..d3e49d8 100644
--- a/crates/typstyle/src/cli.rs
+++ b/crates/typstyle/src/cli.rs
@@ -74,6 +74,16 @@ pub struct StyleArgs {
         help_heading = "Format Configuration"
     )]
     pub column: usize,
+
+    /// Spaces per level of indentation in the output
+    #[arg(
+        short,
+        long,
+        default_value_t = 2,
+        global = true,
+        help_heading = "Format Configuration"
+    )]
+    pub tab_width: usize,
 }
 
 #[derive(Args)]
diff --git a/crates/typstyle/src/fmt.rs b/crates/typstyle/src/fmt.rs
index 95a3f67..4136f5c 100644
--- a/crates/typstyle/src/fmt.rs
+++ b/crates/typstyle/src/fmt.rs
@@ -70,7 +70,9 @@ pub fn format_all(directory: &Option<PathBuf>, args: &CliArguments) -> Result<Fo
         let Ok(content) = std::fs::read_to_string(entry.path()) else {
             continue;
         };
-        let cfg = Config::new().with_width(args.style.column);
+        let cfg = Config::new()
+            .with_width(args.style.column)
+            .with_tab_spaces(args.style.tab_width);
         let Ok(res) = Typstyle::new(cfg).format_content(&content) else {
             warn!("Failed to format: {}", entry.path().display());
             continue;
@@ -215,7 +217,9 @@ fn format_debug(content: String, args: &CliArguments) -> FormatResult {
         println!("{:#?}", root);
     }
 
-    let config = Config::new().with_width(args.style.column);
+    let config = Config::new()
+        .with_width(args.style.column)
+        .with_tab_spaces(args.style.tab_width);
     let res = match Typstyle::new(config).format_source_inspect(&source, |doc| {
         if args.debug.pretty_doc {
             println!("{:#?}", doc);


Test patch:
diff --git a/crates/typstyle/tests/test_format_all.rs b/crates/typstyle/tests/test_format_all.rs
index 942567b..91ebd80 100644
--- a/crates/typstyle/tests/test_format_all.rs
+++ b/crates/typstyle/tests/test_format_all.rs
@@ -201,6 +201,38 @@ fn test_all_column() {
     );
 }
 
+#[test]
+fn test_all_tab_width() {
+    let space = Workspace::new();
+    space.write(
+        "a.typ",
+        "#let f(x) = {
+for i in range(0, 5) {
+     x = x + i
+    }
+}",
+    );
+
+    typstyle_cmd_snapshot!(space.cli().arg("format-all").arg("-t=4").arg("-v"), @r"
+    success: true
+    exit_code: 0
+    ----- stdout -----
+    Successfully formatted 1 file (0 unchanged) in [DURATION]
+
+    ----- stderr -----
+    ");
+
+    assert_eq!(
+        space.read_string("a.typ"),
+        "#let f(x) = {
+    for i in range(0, 5) {
+        x = x + i
+    }
+}
+"
+    );
+}
+
 #[test]
 fn test_dir_all_check() {
     let space = Workspace::new();