diff --git a/README.md b/README.md index ab61180..cd933a6 100644 --- a/README.md +++ b/README.md @@ -59,16 +59,15 @@ edition = "2021" You can configure all settings through a `leptosfmt.toml` file. ```toml -max_width = 100 -tab_spaces = 4 +max_width = 100 # Maximum width of each line +tab_spaces = 4 # Number of spaces per tab +indentation_style = "Auto" # "Tabs", "Spaces" or "Auto" +newline_style = "Auto" # "Unix", "Windows" or "Auto" attr_value_brace_style = "WhenRequired" # "Always", "AlwaysUnlessLit", "WhenRequired" or "Preserve" - ``` To see what each setting does, the see [configuration docs](./docs/configuration.md) - - ## Examples **Single file** diff --git a/formatter/src/collect.rs b/formatter/src/collect.rs index c77a1af..7f5495e 100644 --- a/formatter/src/collect.rs +++ b/formatter/src/collect.rs @@ -5,7 +5,7 @@ use syn::{ File, Macro, }; -use crate::ViewMacro; +use crate::{ParentIdent, ViewMacro}; struct ViewMacroVisitor<'ast> { macros: Vec>, @@ -16,14 +16,17 @@ impl<'ast> Visit<'ast> for ViewMacroVisitor<'ast> { fn visit_macro(&mut self, node: &'ast Macro) { if node.path.is_ident("view") { let span_line = node.span().start().line; - let indent = self - .source - .line(span_line - 1) + let line = self.source.line(span_line - 1); + + let indent_chars: Vec<_> = line .chars() - .take_while(|&c| c == ' ') - .count(); + .take_while(|&c| c == ' ' || c == '\t') + .collect(); + + let tabs = indent_chars.iter().filter(|&&c| c == '\t').count(); + let spaces = indent_chars.iter().filter(|&&c| c == ' ').count(); - if let Some(view_mac) = ViewMacro::try_parse(Some(indent), node) { + if let Some(view_mac) = ViewMacro::try_parse(ParentIdent { tabs, spaces }, node) { self.macros.push(view_mac); } } diff --git a/formatter/src/formatter/mac.rs b/formatter/src/formatter/mac.rs index 8701ac7..84cfaa7 100644 --- a/formatter/src/formatter/mac.rs +++ b/formatter/src/formatter/mac.rs @@ -1,5 +1,5 @@ use crop::Rope; -use leptosfmt_pretty_printer::{Printer, PrinterSettings}; +use leptosfmt_pretty_printer::Printer; use proc_macro2::{token_stream, Span, TokenStream, TokenTree}; use rstml::node::Node; use syn::{spanned::Spanned, Macro}; @@ -7,7 +7,7 @@ use syn::{spanned::Spanned, Macro}; use super::{Formatter, FormatterSettings}; pub struct ViewMacro<'a> { - pub parent_ident: Option, + pub parent_ident: ParentIdent, pub cx: Option, pub global_class: Option, pub nodes: Vec, @@ -16,8 +16,14 @@ pub struct ViewMacro<'a> { pub comma: Option, } +#[derive(Default)] +pub struct ParentIdent { + pub tabs: usize, + pub spaces: usize, +} + impl<'a> ViewMacro<'a> { - pub fn try_parse(parent_ident: Option, mac: &'a Macro) -> Option { + pub fn try_parse(parent_ident: ParentIdent, mac: &'a Macro) -> Option { let mut tokens = mac.tokens.clone().into_iter(); let (cx, comma) = (tokens.next(), tokens.next()); @@ -75,7 +81,8 @@ impl Formatter<'_> { .. } = view_mac; - self.printer.cbox(parent_indent.unwrap_or(0) as isize); + self.printer + .cbox((parent_indent.tabs * self.settings.tab_spaces + parent_indent.spaces) as isize); self.flush_comments(cx.span().start().line - 1); self.printer.word("view! {"); @@ -155,7 +162,7 @@ pub fn format_macro( settings: &FormatterSettings, source: Option<&Rope>, ) -> String { - let mut printer: Printer; + let mut printer = Printer::new(settings.to_printer_settings(source)); let mut formatter = match source { Some(source) => { let whitespace = crate::collect_comments::extract_whitespace_and_comments( @@ -163,22 +170,9 @@ pub fn format_macro( mac.mac.tokens.clone(), ); - let crlf_line_endings = source - .raw_lines() - .next() - .map(|raw_line| raw_line.to_string().ends_with("\r\n")) - .unwrap_or_default(); - - printer = Printer::new(PrinterSettings { - crlf_line_endings, - ..settings.into() - }); Formatter::with_source(*settings, &mut printer, source, whitespace) } - None => { - printer = Printer::new(settings.into()); - Formatter::new(*settings, &mut printer) - } + None => Formatter::new(*settings, &mut printer), }; formatter.view_macro(mac); @@ -195,7 +189,7 @@ mod tests { macro_rules! view_macro { ($($tt:tt)*) => {{ let mac: Macro = syn::parse2(quote! { $($tt)* }).unwrap(); - format_macro(&ViewMacro::try_parse(None, &mac).unwrap(), &Default::default(), None) + format_macro(&ViewMacro::try_parse(Default::default(), &mac).unwrap(), &Default::default(), None) }} } diff --git a/formatter/src/formatter/mod.rs b/formatter/src/formatter/mod.rs index 28182cf..8271ccc 100644 --- a/formatter/src/formatter/mod.rs +++ b/formatter/src/formatter/mod.rs @@ -12,7 +12,7 @@ mod mac; mod node; pub use mac::format_macro; -pub use mac::ViewMacro; +pub use mac::{ParentIdent, ViewMacro}; use serde::Deserialize; use serde::Serialize; @@ -25,6 +25,21 @@ pub enum AttributeValueBraceStyle { Preserve, } +#[derive(Clone, Copy, Debug, PartialEq, Deserialize, Serialize)] +pub enum IndentationStyle { + Auto, + Spaces, + Tabs, +} + +#[derive(Clone, Copy, Debug, PartialEq, Deserialize, Serialize)] +pub enum NewlineStyle { + Auto, + Native, + Unix, + Windows, +} + #[derive(Clone, Copy, Debug, Deserialize, Serialize)] #[serde(default)] pub struct FormatterSettings { @@ -34,6 +49,12 @@ pub struct FormatterSettings { // Number of spaces per tab pub tab_spaces: usize, + // Determines indentation style (tabs or spaces) + pub indentation_style: IndentationStyle, + + // Determines line ending (unix or windows) + pub newline_style: NewlineStyle, + // Determines placement of braces around single expression attribute values pub attr_value_brace_style: AttributeValueBraceStyle, } @@ -44,17 +65,45 @@ impl Default for FormatterSettings { max_width: 100, tab_spaces: 4, attr_value_brace_style: AttributeValueBraceStyle::WhenRequired, + indentation_style: IndentationStyle::Auto, + newline_style: NewlineStyle::Auto, } } } -impl From<&FormatterSettings> for PrinterSettings { - fn from(value: &FormatterSettings) -> Self { - Self { - margin: value.max_width as isize, - indent: value.tab_spaces as isize, +fn uses_crlf_line_ending(source: &Rope) -> bool { + source + .raw_lines() + .next() + .map(|raw_line| raw_line.to_string().ends_with("\r\n")) + .unwrap_or_default() +} + +fn uses_tabs_for_indentation(source: &Rope) -> bool { + source + .lines() + .find(|line| matches!(line.chars().next(), Some('\t') | Some(' '))) + .map(|line| matches!(line.chars().next(), Some('\t'))) + .unwrap_or_default() +} + +impl FormatterSettings { + pub fn to_printer_settings(&self, source: Option<&Rope>) -> PrinterSettings { + PrinterSettings { + margin: self.max_width as isize, + spaces: self.tab_spaces as isize, min_space: 60, - crlf_line_endings: false, + crlf_line_endings: match self.newline_style { + NewlineStyle::Auto => source.map(uses_crlf_line_ending).unwrap_or_default(), + NewlineStyle::Native => cfg!(windows), + NewlineStyle::Unix => false, + NewlineStyle::Windows => true, + }, + hard_tabs: match self.indentation_style { + IndentationStyle::Auto => source.map(uses_tabs_for_indentation).unwrap_or_default(), + IndentationStyle::Spaces => false, + IndentationStyle::Tabs => true, + }, } } } diff --git a/formatter/src/source_file.rs b/formatter/src/source_file.rs index 9633074..438f77e 100644 --- a/formatter/src/source_file.rs +++ b/formatter/src/source_file.rs @@ -79,6 +79,8 @@ fn format_source( mod tests { use indoc::indoc; + use crate::IndentationStyle; + use super::*; #[test] @@ -438,4 +440,108 @@ mod tests { } "#); } + + #[test] + fn indent_with_tabs() { + let source = indoc! {" + fn main() { + \u{0020}view! { cx, +
+
Example
+
+ } + } + "}; + + let result = format_file_source( + source, + FormatterSettings { + tab_spaces: 1, + indentation_style: IndentationStyle::Tabs, + ..Default::default() + }, + ) + .unwrap(); + + let expected = indoc! {" + fn main() { + \u{0020}view! { cx, + \t\t
+ \t\t\t
Example
+ \t\t
+ \t} + } + "}; + + assert_eq!(result, expected); + } + + #[test] + fn auto_detect_tabs() { + let source = indoc! {" + fn main() { + \tview! { cx, +
+
Example
+
+ } + } + "}; + + let result = format_file_source( + source, + FormatterSettings { + indentation_style: IndentationStyle::Auto, + ..Default::default() + }, + ) + .unwrap(); + + let expected = indoc! {" + fn main() { + \tview! { cx, + \t\t
+ \t\t\t
Example
+ \t\t
+ \t} + } + "}; + + assert_eq!(result, expected); + } + + #[test] + fn auto_detect_spaces() { + let source = indoc! {" + fn main() { + \u{0020}view! { cx, +
+
Example
+
+ } + } + "}; + + let result = format_file_source( + source, + FormatterSettings { + tab_spaces: 1, + indentation_style: IndentationStyle::Auto, + ..Default::default() + }, + ) + .unwrap(); + + let expected = indoc! {" + fn main() { + \u{0020}view! { cx, + \u{0020}\u{0020}
+ \u{0020}\u{0020}\u{0020}
Example
+ \u{0020}\u{0020}
+ \u{0020}} + } + "}; + + assert_eq!(result, expected); + } } diff --git a/formatter/src/test_helpers.rs b/formatter/src/test_helpers.rs index c2e9af8..aa77dac 100644 --- a/formatter/src/test_helpers.rs +++ b/formatter/src/test_helpers.rs @@ -118,8 +118,8 @@ pub fn format_with_source( source: &str, run: impl FnOnce(&mut Formatter), ) -> String { - let mut printer = Printer::new((&settings).into()); let rope = Rope::from_str(source).unwrap(); + let mut printer = Printer::new(settings.to_printer_settings(Some(&rope))); let tokens = ::from_str(source).unwrap(); let whitespace = crate::collect_comments::extract_whitespace_and_comments(&rope, tokens); let mut formatter = Formatter::with_source(settings, &mut printer, &rope, whitespace); @@ -128,7 +128,7 @@ pub fn format_with_source( } pub fn format_with(settings: FormatterSettings, run: impl FnOnce(&mut Formatter)) -> String { - let mut printer = Printer::new((&settings).into()); + let mut printer = Printer::new(settings.to_printer_settings(None)); let mut formatter = Formatter::new(settings, &mut printer); run(&mut formatter); printer.eof() diff --git a/formatter/src/view_macro.rs b/formatter/src/view_macro.rs index 71d4223..106c542 100644 --- a/formatter/src/view_macro.rs +++ b/formatter/src/view_macro.rs @@ -34,7 +34,7 @@ impl MacroFormatter for ViewMacroFormatter<'_> { return false; } - let Some(m) = ViewMacro::try_parse(None, mac) else { + let Some(m) = ViewMacro::try_parse(Default::default(), mac) else { return false; }; let mut formatter = Formatter { diff --git a/printer/src/algorithm.rs b/printer/src/algorithm.rs index 704fab5..8b1bb07 100644 --- a/printer/src/algorithm.rs +++ b/printer/src/algorithm.rs @@ -50,11 +50,13 @@ pub struct PrinterSettings { // Target line width. pub margin: isize, // Number of spaces incement at each level of block indentation. - pub indent: isize, + pub spaces: isize, // Every line is allowed at least this much space, even if highly indented. pub min_space: isize, // Print CRLF line ending instead of LF pub crlf_line_endings: bool, + // Whether to use tab characters instead of spaces + pub hard_tabs: bool, } pub struct Printer { @@ -356,7 +358,16 @@ impl Printer { } fn print_indent(&mut self) { - self.out.reserve(self.pending_indentation); + if !self.settings.hard_tabs { + self.out.reserve(self.pending_indentation); + } else { + let tabs = self.pending_indentation / self.settings.spaces as usize; + let remaining_spaces = self.pending_indentation % self.settings.spaces as usize; + self.out.reserve(tabs + remaining_spaces); + self.out.extend(iter::repeat('\t').take(tabs)); + self.pending_indentation = remaining_spaces + } + self.out .extend(iter::repeat(' ').take(self.pending_indentation)); self.pending_indentation = 0; diff --git a/printer/src/convenience.rs b/printer/src/convenience.rs index 46591c1..aa41cac 100644 --- a/printer/src/convenience.rs +++ b/printer/src/convenience.rs @@ -3,11 +3,11 @@ use std::borrow::Cow; impl Printer { pub fn ibox_indent(&mut self) { - self.ibox(self.settings.indent); + self.ibox(self.settings.spaces); } pub fn ibox_dedent(&mut self) { - self.ibox(-self.settings.indent); + self.ibox(-self.settings.spaces); } pub fn ibox(&mut self, indent: isize) { @@ -18,11 +18,11 @@ impl Printer { } pub fn cbox_indent(&mut self) { - self.cbox(self.settings.indent); + self.cbox(self.settings.spaces); } pub fn cbox_dedent(&mut self) { - self.cbox(-self.settings.indent); + self.cbox(-self.settings.spaces); } pub fn cbox(&mut self, indent: isize) { @@ -33,7 +33,7 @@ impl Printer { } pub fn end_dedent(&mut self) { - self.offset(-self.settings.indent); + self.offset(-self.settings.spaces); self.end(); }