Ver Fonte

Use indentation settings in dx fmt and vscode extension

Adds the ability to specify an indent string to public autofmt methods - either
a sequence of spaces or a tab character.

Get the indentation style and size from rustfmt for dx fmt, or from the editor
settings for the vscode extension.

Closes #1595
Alex Parrill há 1 ano atrás
pai
commit
52fb080193

+ 6 - 5
packages/autofmt/src/buffer.rs

@@ -8,13 +8,14 @@ use std::fmt::{Result, Write};
 
 use dioxus_rsx::IfmtInput;
 
-use crate::write_ifmt;
+use crate::{indent::IndentOptions, write_ifmt};
 
 /// The output buffer that tracks indent and string
 #[derive(Debug, Default)]
 pub struct Buffer {
     pub buf: String,
-    pub indent: usize,
+    pub indent_level: usize,
+    pub indent: IndentOptions,
 }
 
 impl Buffer {
@@ -31,16 +32,16 @@ impl Buffer {
     }
 
     pub fn tab(&mut self) -> Result {
-        self.write_tabs(self.indent)
+        self.write_tabs(self.indent_level)
     }
 
     pub fn indented_tab(&mut self) -> Result {
-        self.write_tabs(self.indent + 1)
+        self.write_tabs(self.indent_level + 1)
     }
 
     pub fn write_tabs(&mut self, num: usize) -> std::fmt::Result {
         for _ in 0..num {
-            write!(self.buf, "    ")?
+            write!(self.buf, "{}", self.indent.indent_str())?
         }
         Ok(())
     }

+ 6 - 6
packages/autofmt/src/element.rs

@@ -66,7 +66,7 @@ impl Writer<'_> {
 
         // check if we have a lot of attributes
         let attr_len = self.is_short_attrs(attributes);
-        let is_short_attr_list = (attr_len + self.out.indent * 4) < 80;
+        let is_short_attr_list = (attr_len + self.out.indent_level * 4) < 80;
         let children_len = self.is_short_children(children);
         let is_small_children = children_len.is_some();
 
@@ -86,7 +86,7 @@ impl Writer<'_> {
 
         // if we have few children and few attributes, make it a one-liner
         if is_short_attr_list && is_small_children {
-            if children_len.unwrap() + attr_len + self.out.indent * 4 < 100 {
+            if children_len.unwrap() + attr_len + self.out.indent_level * 4 < 100 {
                 opt_level = ShortOptimization::Oneliner;
             } else {
                 opt_level = ShortOptimization::PropsOnTop;
@@ -185,11 +185,11 @@ impl Writer<'_> {
         }
 
         while let Some(attr) = attr_iter.next() {
-            self.out.indent += 1;
+            self.out.indent_level += 1;
             if !sameline {
                 self.write_comments(attr.attr.start())?;
             }
-            self.out.indent -= 1;
+            self.out.indent_level -= 1;
 
             if !sameline {
                 self.out.indented_tabbed_line()?;
@@ -398,14 +398,14 @@ impl Writer<'_> {
         for idx in start.line..end.line {
             let line = &self.src[idx];
             if line.trim().starts_with("//") {
-                for _ in 0..self.out.indent + 1 {
+                for _ in 0..self.out.indent_level + 1 {
                     write!(self.out, "    ")?
                 }
                 writeln!(self.out, "{}", line.trim()).unwrap();
             }
         }
 
-        for _ in 0..self.out.indent {
+        for _ in 0..self.out.indent_level {
             write!(self.out, "    ")?
         }
 

+ 3 - 3
packages/autofmt/src/expr.rs

@@ -29,7 +29,7 @@ impl Writer<'_> {
         let first_line = &self.src[start.line - 1];
         write!(self.out, "{}", &first_line[start.column - 1..].trim_start())?;
 
-        let prev_block_indent_level = crate::leading_whitespaces(first_line) / 4;
+        let prev_block_indent_level = self.out.indent.count_indents(first_line);
 
         for (id, line) in self.src[start.line..end.line].iter().enumerate() {
             writeln!(self.out)?;
@@ -43,9 +43,9 @@ impl Writer<'_> {
             };
 
             // trim the leading whitespace
-            let previous_indent = crate::leading_whitespaces(line) / 4;
+            let previous_indent = self.out.indent.count_indents(line);
             let offset = previous_indent.saturating_sub(prev_block_indent_level);
-            let required_indent = self.out.indent + offset;
+            let required_indent = self.out.indent_level + offset;
             self.out.write_tabs(required_indent)?;
 
             let line = line.trim_start();

+ 108 - 0
packages/autofmt/src/indent.rs

@@ -0,0 +1,108 @@
+#[derive(Clone, Copy, PartialEq, Eq, Debug)]
+pub enum IndentType {
+    Spaces,
+    Tabs,
+}
+
+#[derive(Debug, Clone)]
+pub struct IndentOptions {
+    width: usize,
+    indent_string: String,
+}
+
+impl IndentOptions {
+    pub fn new(typ: IndentType, width: usize) -> Self {
+        assert_ne!(width, 0, "Cannot have an indent width of 0");
+        Self {
+            width,
+            indent_string: match typ {
+                IndentType::Tabs => "\t".into(),
+                IndentType::Spaces => " ".repeat(width),
+            },
+        }
+    }
+
+    /// Gets a string containing one indent worth of whitespace
+    pub fn indent_str(&self) -> &str {
+        &self.indent_string
+    }
+
+    /// Computes the line length in characters, counting tabs as the indent width.
+    pub fn line_length(&self, line: &str) -> usize {
+        line.chars()
+            .map(|ch| if ch == '\t' { self.width } else { 1 })
+            .sum()
+    }
+
+    /// Estimates how many times the line has been indented.
+    pub fn count_indents(&self, mut line: &str) -> usize {
+        let mut indent = 0;
+        while !line.is_empty() {
+            // Try to count tabs
+            let num_tabs = line.chars().take_while(|ch| *ch == '\t').count();
+            if num_tabs > 0 {
+                indent += num_tabs;
+                line = &line[num_tabs..];
+                continue;
+            }
+
+            // Try to count spaces
+            let num_spaces = line.chars().take_while(|ch| *ch == ' ').count();
+            if num_spaces >= self.width {
+                // Intentionally floor here to take only the amount of space that matches an indent
+                let num_space_indents = num_spaces / self.width;
+                indent += num_space_indents;
+                line = &line[num_space_indents * self.width..];
+                continue;
+            }
+
+            // Line starts with either non-indent characters or an unevent amount of spaces,
+            // so no more indent remains.
+            break;
+        }
+        indent
+    }
+}
+
+impl Default for IndentOptions {
+    fn default() -> Self {
+        Self::new(IndentType::Spaces, 4)
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    #[test]
+    fn count_indents() {
+        assert_eq!(
+            IndentOptions::new(IndentType::Spaces, 4).count_indents("no indentation here!"),
+            0
+        );
+        assert_eq!(
+            IndentOptions::new(IndentType::Spaces, 4).count_indents("    v += 2"),
+            1
+        );
+        assert_eq!(
+            IndentOptions::new(IndentType::Spaces, 4).count_indents("        v += 2"),
+            2
+        );
+        assert_eq!(
+            IndentOptions::new(IndentType::Spaces, 4).count_indents("          v += 2"),
+            2
+        );
+        assert_eq!(
+            IndentOptions::new(IndentType::Spaces, 4).count_indents("\t\tv += 2"),
+            2
+        );
+        assert_eq!(
+            IndentOptions::new(IndentType::Spaces, 4).count_indents("\t\t  v += 2"),
+            2
+        );
+        assert_eq!(
+            IndentOptions::new(IndentType::Spaces, 2).count_indents("    v += 2"),
+            2
+        );
+    }
+}

+ 12 - 15
packages/autofmt/src/lib.rs

@@ -16,8 +16,11 @@ mod collect_macros;
 mod component;
 mod element;
 mod expr;
+mod indent;
 mod writer;
 
+pub use indent::{IndentOptions, IndentType};
+
 /// A modification to the original file to be applied by an IDE
 ///
 /// Right now this re-writes entire rsx! blocks at a time, instead of precise line-by-line changes.
@@ -47,7 +50,7 @@ pub struct FormattedBlock {
 /// back to the file precisely.
 ///
 /// Nested blocks of RSX will be handled automatically
-pub fn fmt_file(contents: &str) -> Vec<FormattedBlock> {
+pub fn fmt_file(contents: &str, indent: IndentOptions) -> Vec<FormattedBlock> {
     let mut formatted_blocks = Vec::new();
 
     let parsed = syn::parse_file(contents).unwrap();
@@ -61,6 +64,7 @@ pub fn fmt_file(contents: &str) -> Vec<FormattedBlock> {
     }
 
     let mut writer = Writer::new(contents);
+    writer.out.indent = indent;
 
     // Don't parse nested macros
     let mut end_span = LineColumn { column: 0, line: 0 };
@@ -76,7 +80,10 @@ pub fn fmt_file(contents: &str) -> Vec<FormattedBlock> {
 
         let rsx_start = macro_path.span().start();
 
-        writer.out.indent = leading_whitespaces(writer.src[rsx_start.line - 1]) / 4;
+        writer.out.indent_level = writer
+            .out
+            .indent
+            .count_indents(writer.src[rsx_start.line - 1]);
 
         write_body(&mut writer, &body);
 
@@ -159,12 +166,13 @@ pub fn fmt_block_from_expr(raw: &str, expr: ExprMacro) -> Option<String> {
     buf.consume()
 }
 
-pub fn fmt_block(block: &str, indent_level: usize) -> Option<String> {
+pub fn fmt_block(block: &str, indent_level: usize, indent: IndentOptions) -> Option<String> {
     let body = syn::parse_str::<dioxus_rsx::CallBody>(block).unwrap();
 
     let mut buf = Writer::new(block);
 
-    buf.out.indent = indent_level;
+    buf.out.indent = indent;
+    buf.out.indent_level = indent_level;
 
     write_body(&mut buf, &body);
 
@@ -230,14 +238,3 @@ pub(crate) fn write_ifmt(input: &IfmtInput, writable: &mut impl Write) -> std::f
     let display = DisplayIfmt(input);
     write!(writable, "{}", display)
 }
-
-pub fn leading_whitespaces(input: &str) -> usize {
-    input
-        .chars()
-        .map_while(|c| match c {
-            ' ' => Some(1),
-            '\t' => Some(4),
-            _ => None,
-        })
-        .sum()
-}

+ 2 - 2
packages/autofmt/src/writer.rs

@@ -96,11 +96,11 @@ impl<'a> Writer<'a> {
 
     // Push out the indent level and write each component, line by line
     pub fn write_body_indented(&mut self, children: &[BodyNode]) -> Result {
-        self.out.indent += 1;
+        self.out.indent_level += 1;
 
         self.write_body_no_indent(children)?;
 
-        self.out.indent -= 1;
+        self.out.indent_level -= 1;
         Ok(())
     }
 

+ 1 - 1
packages/autofmt/tests/samples.rs

@@ -12,7 +12,7 @@ macro_rules! twoway {
             #[test]
             fn $name() {
                 let src = include_str!(concat!("./samples/", stringify!($name), ".rsx"));
-                let formatted = dioxus_autofmt::fmt_file(src);
+                let formatted = dioxus_autofmt::fmt_file(src, Default::default());
                 let out = dioxus_autofmt::apply_formats(src, formatted);
                 // normalize line endings
                 let out = out.replace("\r", "");

+ 10 - 5
packages/autofmt/tests/wrong.rs

@@ -1,10 +1,12 @@
+use dioxus_autofmt::{IndentType, IndentOptions};
+
 macro_rules! twoway {
-    ($val:literal => $name:ident) => {
+    ($val:literal => $name:ident ($indent:expr)) => {
         #[test]
         fn $name() {
             let src_right = include_str!(concat!("./wrong/", $val, ".rsx"));
             let src_wrong = include_str!(concat!("./wrong/", $val, ".wrong.rsx"));
-            let formatted = dioxus_autofmt::fmt_file(src_wrong);
+            let formatted = dioxus_autofmt::fmt_file(src_wrong, $indent);
             let out = dioxus_autofmt::apply_formats(src_wrong, formatted);
 
             // normalize line endings
@@ -16,8 +18,11 @@ macro_rules! twoway {
     };
 }
 
-twoway!("comments" => comments);
+twoway!("comments-4sp" => comments_4sp (IndentOptions::new(IndentType::Spaces, 4)));
+twoway!("comments-tab" => comments_tab (IndentOptions::new(IndentType::Tabs, 4)));
 
-twoway!("multi" => multi);
+twoway!("multi-4sp" => multi_4sp (IndentOptions::new(IndentType::Spaces, 4)));
+twoway!("multi-tab" => multi_tab (IndentOptions::new(IndentType::Tabs, 4)));
 
-twoway!("multiexpr" => multiexpr);
+twoway!("multiexpr-4sp" => multiexpr_4sp (IndentOptions::new(IndentType::Spaces, 4)));
+twoway!("multiexpr-tab" => multiexpr_tab (IndentOptions::new(IndentType::Tabs, 4)));

+ 0 - 0
packages/autofmt/tests/wrong/comments.rsx → packages/autofmt/tests/wrong/comments-4sp.rsx


+ 0 - 0
packages/autofmt/tests/wrong/comments.wrong.rsx → packages/autofmt/tests/wrong/comments-4sp.wrong.rsx


+ 7 - 0
packages/autofmt/tests/wrong/comments-tab.rsx

@@ -0,0 +1,7 @@
+rsx! {
+	div {
+		// Comments
+		class: "asdasd",
+		"hello world"
+	}
+}

+ 5 - 0
packages/autofmt/tests/wrong/comments-tab.wrong.rsx

@@ -0,0 +1,5 @@
+rsx! {
+	div {
+		// Comments
+		class: "asdasd", "hello world" }
+}

+ 0 - 0
packages/autofmt/tests/wrong/multi.rsx → packages/autofmt/tests/wrong/multi-4sp.rsx


+ 0 - 0
packages/autofmt/tests/wrong/multi.wrong.rsx → packages/autofmt/tests/wrong/multi-4sp.wrong.rsx


+ 3 - 0
packages/autofmt/tests/wrong/multi-tab.rsx

@@ -0,0 +1,3 @@
+fn app(cx: Scope) -> Element {
+	cx.render(rsx! { div { "hello world" } })
+}

+ 5 - 0
packages/autofmt/tests/wrong/multi-tab.wrong.rsx

@@ -0,0 +1,5 @@
+fn app(cx: Scope) -> Element {
+	cx.render(rsx! {
+		div {"hello world" }
+	})
+}

+ 0 - 0
packages/autofmt/tests/wrong/multiexpr.rsx → packages/autofmt/tests/wrong/multiexpr-4sp.rsx


+ 0 - 0
packages/autofmt/tests/wrong/multiexpr.wrong.rsx → packages/autofmt/tests/wrong/multiexpr-4sp.wrong.rsx


+ 8 - 0
packages/autofmt/tests/wrong/multiexpr-tab.rsx

@@ -0,0 +1,8 @@
+fn ItWroks() {
+	cx.render(rsx! {
+		div { class: "flex flex-wrap items-center dark:text-white py-16 border-t font-light",
+			left,
+			right
+		}
+	})
+}

+ 5 - 0
packages/autofmt/tests/wrong/multiexpr-tab.wrong.rsx

@@ -0,0 +1,5 @@
+fn ItWroks() {
+	cx.render(rsx! {
+		div { class: "flex flex-wrap items-center dark:text-white py-16 border-t font-light", left, right }
+	})
+}

+ 62 - 6
packages/cli/src/cli/autoformat.rs

@@ -1,3 +1,4 @@
+use dioxus_autofmt::{IndentOptions, IndentType};
 use futures::{stream::FuturesUnordered, StreamExt};
 use std::{fs, path::Path, process::exit};
 
@@ -35,7 +36,8 @@ impl Autoformat {
         }
 
         if let Some(raw) = self.raw {
-            if let Some(inner) = dioxus_autofmt::fmt_block(&raw, 0) {
+            let indent = indentation_for(".")?;
+            if let Some(inner) = dioxus_autofmt::fmt_block(&raw, 0, indent) {
                 println!("{}", inner);
             } else {
                 // exit process with error
@@ -46,17 +48,21 @@ impl Autoformat {
 
         // Format single file
         if let Some(file) = self.file {
-            let file_content = if file == "-" {
+            let file_content;
+            let indent;
+            if file == "-" {
+                indent = indentation_for(".")?;
                 let mut contents = String::new();
                 std::io::stdin().read_to_string(&mut contents)?;
-                Ok(contents)
+                file_content = Ok(contents);
             } else {
-                fs::read_to_string(&file)
+                indent = indentation_for(".")?;
+                file_content = fs::read_to_string(&file);
             };
 
             match file_content {
                 Ok(s) => {
-                    let edits = dioxus_autofmt::fmt_file(&s);
+                    let edits = dioxus_autofmt::fmt_file(&s, indent);
                     let out = dioxus_autofmt::apply_formats(&s, edits);
                     if file == "-" {
                         print!("{}", out);
@@ -93,6 +99,12 @@ async fn autoformat_project(check: bool) -> Result<()> {
     let mut files_to_format = vec![];
     collect_rs_files(&crate_config.crate_dir, &mut files_to_format);
 
+    if files_to_format.is_empty() {
+        return Ok(());
+    }
+
+    let indent = indentation_for(&files_to_format[0])?;
+
     let counts = files_to_format
         .into_iter()
         .filter(|file| {
@@ -104,10 +116,11 @@ async fn autoformat_project(check: bool) -> Result<()> {
         })
         .map(|path| async {
             let _path = path.clone();
+            let _indent = indent.clone();
             let res = tokio::spawn(async move {
                 let contents = tokio::fs::read_to_string(&path).await?;
 
-                let edits = dioxus_autofmt::fmt_file(&contents);
+                let edits = dioxus_autofmt::fmt_file(&contents, _indent.clone());
                 let len = edits.len();
 
                 if !edits.is_empty() {
@@ -151,6 +164,49 @@ async fn autoformat_project(check: bool) -> Result<()> {
     Ok(())
 }
 
+fn indentation_for(file_or_dir: impl AsRef<Path>) -> Result<IndentOptions> {
+    let out = std::process::Command::new("cargo")
+        .args(["fmt", "--", "--print-config", "current"])
+        .arg(file_or_dir.as_ref())
+        .stdout(std::process::Stdio::piped())
+        .stderr(std::process::Stdio::inherit())
+        .output()?;
+    if !out.status.success() {
+        return Err(Error::CargoError("cargo fmt failed".into()));
+    }
+
+    let config = String::from_utf8_lossy(&out.stdout);
+
+    let hard_tabs = config
+        .lines()
+        .find(|line| line.starts_with("hard_tabs "))
+        .and_then(|line| line.split_once('='))
+        .map(|(_, value)| value.trim() == "true")
+        .ok_or_else(|| {
+            Error::RuntimeError("Could not find hard_tabs option in rustfmt config".into())
+        })?;
+    let tab_spaces = config
+        .lines()
+        .find(|line| line.starts_with("tab_spaces "))
+        .and_then(|line| line.split_once('='))
+        .map(|(_, value)| value.trim().parse::<usize>())
+        .ok_or_else(|| {
+            Error::RuntimeError("Could not find tab_spaces option in rustfmt config".into())
+        })?
+        .map_err(|_| {
+            Error::RuntimeError("Could not parse tab_spaces option in rustfmt config".into())
+        })?;
+
+    Ok(IndentOptions::new(
+        if hard_tabs {
+            IndentType::Tabs
+        } else {
+            IndentType::Spaces
+        },
+        tab_spaces,
+    ))
+}
+
 fn collect_rs_files(folder: &Path, files: &mut Vec<PathBuf>) {
     let Ok(folder) = folder.read_dir() else {
         return;

+ 39 - 7
packages/extension/src/lib.rs

@@ -1,17 +1,39 @@
 //! This file exports functions into the vscode extension
 
-use dioxus_autofmt::FormattedBlock;
+use dioxus_autofmt::{FormattedBlock, IndentOptions, IndentType};
 use wasm_bindgen::prelude::*;
 
 #[wasm_bindgen]
-pub fn format_rsx(raw: String) -> String {
-    let block = dioxus_autofmt::fmt_block(&raw, 0);
+pub fn format_rsx(raw: String, use_tabs: bool, indent_size: usize) -> String {
+    let block = dioxus_autofmt::fmt_block(
+        &raw,
+        0,
+        IndentOptions::new(
+            if use_tabs {
+                IndentType::Tabs
+            } else {
+                IndentType::Spaces
+            },
+            indent_size,
+        ),
+    );
     block.unwrap()
 }
 
 #[wasm_bindgen]
-pub fn format_selection(raw: String) -> String {
-    let block = dioxus_autofmt::fmt_block(&raw, 0);
+pub fn format_selection(raw: String, use_tabs: bool, indent_size: usize) -> String {
+    let block = dioxus_autofmt::fmt_block(
+        &raw,
+        0,
+        IndentOptions::new(
+            if use_tabs {
+                IndentType::Tabs
+            } else {
+                IndentType::Spaces
+            },
+            indent_size,
+        ),
+    );
     block.unwrap()
 }
 
@@ -35,8 +57,18 @@ impl FormatBlockInstance {
 }
 
 #[wasm_bindgen]
-pub fn format_file(contents: String) -> FormatBlockInstance {
-    let _edits = dioxus_autofmt::fmt_file(&contents);
+pub fn format_file(contents: String, use_tabs: bool, indent_size: usize) -> FormatBlockInstance {
+    let _edits = dioxus_autofmt::fmt_file(
+        &contents,
+        IndentOptions::new(
+            if use_tabs {
+                IndentType::Tabs
+            } else {
+                IndentType::Spaces
+            },
+            indent_size,
+        ),
+    );
     let out = dioxus_autofmt::apply_formats(&contents, _edits.clone());
     FormatBlockInstance { new: out, _edits }
 }

+ 1 - 1
packages/extension/src/main.ts

@@ -90,7 +90,7 @@ function fmtDocument(document: vscode.TextDocument) {
 		if (!editor) return; // Need an editor to apply text edits.
 
 		const contents = editor.document.getText();
-		const formatted = dioxus.format_file(contents);
+		const formatted = dioxus.format_file(contents, !editor.options.insertSpaces, editor.options.tabSize);
 
 		// Replace the entire text document
 		// Yes, this is a bit heavy handed, but the dioxus side doesn't know the line/col scheme that vscode is using