feat(rome_formatter): `align > indent` nesting with `IndentStyle::Space`
MODIFY
crates/rome_formatter/src/builders.rs
crates/rome_formatter/src/builders.rs
@@ -747,6 +747,9 @@ impl<Context> std::fmt::Debug for Dedent<'_, Context> {
/// Using [indent] is preferred in all other situations as it respects the users preferred indent character.
///
/// # Examples
+///
+/// ## Tab indention
+///
/// ```
/// use std::num::NonZeroU8;
/// use rome_formatter::{format, format_args};
@@ -779,9 +782,58 @@ impl<Context> std::fmt::Debug for Dedent<'_, Context> {
/// );
/// ```
///
-/// You can see that the printer indents the function's `}` by two spaces as specified by the IR and not with a tab.
-/// You can further see that alignments behave the same as an `indent` if you nest `indent` inside an align.
+/// You can see that:
+///
+/// * the printer indents the function's `}` by two spaces because it is inside of an `align`.
+/// * the block `console.log` gets indented by two tabs.
+/// This is because `align` increases the indention level by one (same as `indent`)
+/// if you nest an `indent` inside an `align`.
+/// Meaning that, `align > ... > indent` results in the same indention as `indent > ... > indent`.
+///
+/// ## Spaces indention
+///
+/// ```
+/// use std::num::NonZeroU8;
+/// use rome_formatter::{format, format_args, IndentStyle};
+/// use rome_formatter::prelude::*;
+///
+/// let context = SimpleFormatContext {
+/// indent_style: IndentStyle::Space(4),
+/// ..SimpleFormatContext::default()
+/// };
+///
+/// let block = format!(context, [
+/// text("a"),
+/// hard_line_break(),
+/// text("?"),
+/// space(),
+/// align(2, &format_args![
+/// text("function () {"),
+/// hard_line_break(),
+/// text("}"),
+/// ]),
+/// hard_line_break(),
+/// text(":"),
+/// space(),
+/// align(2, &format_args![
+/// text("function () {"),
+/// block_indent(&text("console.log('test');")),
+/// text("}"),
+/// ]),
+/// text(";")
+/// ]).unwrap();
+///
+/// assert_eq!(
+/// "a\n? function () {\n }\n: function () {\n console.log('test');\n };",
+/// block.print().as_code()
+/// );
+/// ```
+///
+/// The printing of `align` differs if using spaces as indention sequence *and* it contains an `indent`.
+/// You can see the difference when comparing the indention of the `console.log(...)` expression to the previous example:
///
+/// * tab indention: Printer indents the expression with two tabs because the `align` increases the indention level.
+/// * space indention: Printer indents the expression by 4 spaces (one indention level) **and** 2 spaces for the align.
pub fn align<Content, Context>(count: u8, content: &Content) -> Align<Context>
where
Content: Format<Context>,

MODIFY
crates/rome_formatter/src/format_element.rs
crates/rome_formatter/src/format_element.rs
@@ -396,6 +396,10 @@ impl std::fmt::Debug for BestFitting {
pub struct Interned(Rc<FormatElement>);
impl Interned {
+ pub(crate) fn new(element: FormatElement) -> Self {
+ Self(Rc::new(element))
+ }
+
pub(crate) fn try_unwrap(this: Interned) -> Result<FormatElement, Interned> {
Rc::try_unwrap(this.0).map_err(Interned)
}
@@ -697,16 +701,6 @@ impl FormatElement {
_ => Some(self),
}
}
-
- /// Interns a format element.
- ///
- /// Returns `self` for an already interned element.
- pub fn intern(self) -> Interned {
- match self {
- FormatElement::Interned(interned) => interned,
- element => Interned(Rc::new(element)),
- }
- }
}
impl From<Text> for FormatElement {
@@ -772,7 +766,7 @@ impl FormatContext for IrFormatContext {
tab_width: 2,
print_width: self.line_width(),
line_ending: LineEnding::LineFeed,
- indent_string: " ".to_string(),
+ indent_style: IndentStyle::Space(2),
}
}
}

MODIFY
crates/rome_formatter/src/format_extensions.rs
crates/rome_formatter/src/format_extensions.rs
@@ -142,7 +142,7 @@ impl<T, Context> MemoizeFormat<Context> for T where T: Format<Context> {}
#[derive(Debug)]
pub struct Memoized<F, Context> {
inner: F,
- memory: RefCell<Option<FormatResult<Interned>>>,
+ memory: RefCell<Option<FormatResult<FormatElement>>>,
options: PhantomData<Context>,
}
@@ -218,7 +218,8 @@ where
.get_or_insert_with(|| f.intern(&self.inner));
match result.as_ref() {
- Ok(content) => Ok(content.deref()),
+ Ok(FormatElement::Interned(interned)) => Ok(interned.deref()),
+ Ok(other) => Ok(other),
Err(error) => Err(*error),
}
}
@@ -234,7 +235,7 @@ where
match result {
Ok(elements) => {
- f.write_element(FormatElement::Interned(elements.clone()))?;
+ f.write_element(elements.clone())?;
Ok(())
}

MODIFY
crates/rome_formatter/src/formatter.rs
crates/rome_formatter/src/formatter.rs
@@ -171,12 +171,19 @@ impl<'buf, Context> Formatter<'buf, Context> {
}
/// Formats `content` into an interned element without writing it to the formatter's buffer.
- pub fn intern(&mut self, content: &dyn Format<Context>) -> FormatResult<Interned> {
+ pub fn intern(&mut self, content: &dyn Format<Context>) -> FormatResult<FormatElement> {
let mut buffer = VecBuffer::new(self.state_mut());
crate::write!(&mut buffer, [content])?;
- Ok(buffer.into_element().intern())
+ let interned = match buffer.into_element() {
+ // No point in interning an empty list as that would only result in an unnecessary allocation.
+ FormatElement::List(list) if list.is_empty() => FormatElement::List(list),
+ interned @ FormatElement::Interned(_) => interned,
+ element => FormatElement::Interned(Interned::new(element)),
+ };
+
+ Ok(interned)
}
}

MODIFY
crates/rome_formatter/src/lib.rs
crates/rome_formatter/src/lib.rs
@@ -68,7 +68,7 @@ use std::num::ParseIntError;
use std::rc::Rc;
use std::str::FromStr;
-#[derive(Debug, Eq, PartialEq, Clone, Copy)]
+#[derive(Debug, Eq, PartialEq, Clone, Copy, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum IndentStyle {
/// Tab
@@ -79,6 +79,16 @@ pub enum IndentStyle {
impl IndentStyle {
pub const DEFAULT_SPACES: u8 = 2;
+
+ /// Returns `true` if this is an [IndentStyle::Tab].
+ pub const fn is_tab(&self) -> bool {
+ matches!(self, IndentStyle::Tab)
+ }
+
+ /// Returns `true` if this is an [IndentStyle::Space].
+ pub const fn is_space(&self) -> bool {
+ matches!(self, IndentStyle::Space(_))
+ }
}
impl Default for IndentStyle {

MODIFY
crates/rome_formatter/src/printer/mod.rs
crates/rome_formatter/src/printer/mod.rs
@@ -7,7 +7,7 @@ use crate::format_element::{
Align, ConditionalGroupContent, Group, LineMode, PrintMode, VerbatimKind,
};
use crate::intersperse::Intersperse;
-use crate::{FormatElement, GroupId, Printed, SourceMarker, TextRange};
+use crate::{FormatElement, GroupId, IndentStyle, Printed, SourceMarker, TextRange};
use rome_rowan::TextSize;
use std::iter::{once, Rev};
@@ -80,15 +80,25 @@ impl<'a> Printer<'a> {
}
FormatElement::Text(token) => {
if !self.state.pending_indent.is_empty() {
- self.print_str(
- self.options
- .indent_string
- .repeat(self.state.pending_indent.level() as usize)
- .as_str(),
- );
+ let (indent_char, repeat_count) = match self.options.indent_style() {
+ IndentStyle::Tab => ('\t', 1),
+ IndentStyle::Space(count) => (' ', count),
+ };
+
+ let indent = std::mem::take(&mut self.state.pending_indent);
+ let total_indent_char_count = indent.level() as usize * repeat_count as usize;
+
+ self.state
+ .buffer
+ .reserve(total_indent_char_count + indent.align() as usize);
+
+ for _ in 0..total_indent_char_count {
+ self.print_char(indent_char);
+ }
- self.print_str(&" ".repeat(self.state.pending_indent.align() as usize));
- self.state.pending_indent = Indention::default();
+ for _ in 0..indent.align() {
+ self.print_char(' ');
+ }
}
// Print pending spaces
@@ -170,7 +180,10 @@ impl<'a> Printer<'a> {
}
FormatElement::Indent(content) => {
- queue.extend_with_args(content.iter(), args.increment_indent_level());
+ queue.extend_with_args(
+ content.iter(),
+ args.increment_indent_level(self.options.indent_style()),
+ );
}
FormatElement::Dedent(content) => {
@@ -460,6 +473,13 @@ impl<'a> Printer<'a> {
fn print_str(&mut self, content: &str) {
for char in content.chars() {
+ self.print_char(char);
+
+ self.state.has_empty_line = false;
+ }
+ }
+
+ fn print_char(&mut self, char: char) {
if char == '\n' {
self.state
.buffer
@@ -479,9 +499,6 @@ impl<'a> Printer<'a> {
self.state.line_width += char_width;
}
-
- self.state.has_empty_line = false;
- }
}
}
@@ -555,8 +572,8 @@ impl PrintElementArgs {
}
}
- pub fn increment_indent_level(mut self) -> Self {
- self.indent = self.indent.increment_level();
+ pub fn increment_indent_level(mut self, indent_style: IndentStyle) -> Self {
+ self.indent = self.indent.increment_level(indent_style);
self
}
@@ -622,12 +639,22 @@ impl Indention {
/// Increments the level by one.
///
- /// It sets `align` to 0 if the current value is [Indent::Align] and increments the level by one.
- fn increment_level(self) -> Self {
+ /// The behaviour depends on the [`indent_style`][IndentStyle] if this is an [Indent::Align]:
+ /// * **Tabs**: `align` is converted into an indent. This results in `level` increasing by two: once for the align, once for the level increment
+ /// * **Spaces**: Increments the `level` by one and keeps the `align` unchanged.
+ /// Keeps any the current value is [Indent::Align] and increments the level by one.
+ fn increment_level(self, indent_style: IndentStyle) -> Self {
match self {
Indention::Level(count) => Indention::Level(count + 1),
// Increase the indent AND convert the align to an indent
- Indention::Align { level: indent, .. } => Indention::Level(indent + 2),
+ Indention::Align { level, .. } if indent_style.is_tab() => Indention::Level(level + 2),
+ Indention::Align {
+ level: indent,
+ align,
+ } => Indention::Align {
+ level: indent + 1,
+ align,
+ },
}
}
@@ -878,9 +905,10 @@ fn fits_element_on_line<'a, 'rest>(
}
}
- FormatElement::Indent(content) => {
- queue.extend(content.iter(), args.increment_indent_level())
- }
+ FormatElement::Indent(content) => queue.extend(
+ content.iter(),
+ args.increment_indent_level(options.indent_style()),
+ ),
FormatElement::Dedent(content) => queue.extend(content.iter(), args.decrement_indent()),
@@ -918,9 +946,9 @@ fn fits_element_on_line<'a, 'rest>(
),
FormatElement::Text(token) => {
- state.line_width += state.pending_indent.level() as usize * options.indent_string.len()
- + state.pending_indent.align() as usize;
- state.pending_indent = Indention::default();
+ let indent = std::mem::take(&mut state.pending_indent);
+ state.line_width +=
+ indent.level() as usize * options.indent_width() as usize + indent.align() as usize;
if state.pending_space {
state.line_width += 1;
@@ -1079,13 +1107,13 @@ impl<'a, 'rest> MeasureQueue<'a, 'rest> {
mod tests {
use crate::prelude::*;
use crate::printer::{LineEnding, Printer, PrinterOptions};
- use crate::{format_args, write, FormatState, LineWidth, Printed, VecBuffer};
+ use crate::{format_args, write, FormatState, IndentStyle, LineWidth, Printed, VecBuffer};
fn format(root: &dyn Format<()>) -> Printed {
format_with_options(
root,
PrinterOptions {
- indent_string: String::from(" "),
+ indent_style: IndentStyle::Space(2),
..PrinterOptions::default()
},
)
@@ -1226,7 +1254,7 @@ two lines`,
#[test]
fn it_use_the_indent_character_specified_in_the_options() {
let options = PrinterOptions {
- indent_string: String::from("\t"),
+ indent_style: IndentStyle::Tab,
tab_width: 4,
print_width: LineWidth::try_from(19).unwrap(),
..PrinterOptions::default()

MODIFY
crates/rome_formatter/src/printer/printer_options/mod.rs
crates/rome_formatter/src/printer/printer_options/mod.rs
@@ -12,12 +12,8 @@ pub struct PrinterOptions {
/// The type of line ending to apply to the printed input
pub line_ending: LineEnding,
- /// The never ending question whatever to use spaces or tabs, and if spaces, how many spaces
- /// to indent code.
- ///
- /// * Tab: Value is '\t'
- /// * Spaces: String containing the number of spaces per indention level, e.g. " " for using two spaces
- pub indent_string: String,
+ /// Whether the printer should use tabs or spaces to indent code and if spaces, by how many.
+ pub indent_style: IndentStyle,
}
impl PrinterOptions {
@@ -27,18 +23,21 @@ impl PrinterOptions {
}
pub fn with_indent(mut self, style: IndentStyle) -> Self {
- match style {
- IndentStyle::Tab => {
- self.indent_string = String::from("\t");
- self.tab_width = 2;
- }
- IndentStyle::Space(quantity) => {
- self.indent_string = " ".repeat(quantity as usize);
- self.tab_width = quantity;
+ self.indent_style = style;
+
+ self
}
+
+ pub(crate) fn indent_style(&self) -> IndentStyle {
+ self.indent_style
}
- self
+ /// Width of an indent in characters.
+ pub(super) const fn indent_width(&self) -> u8 {
+ match self.indent_style {
+ IndentStyle::Tab => self.tab_width,
+ IndentStyle::Space(count) => count,
+ }
}
}
@@ -71,7 +70,7 @@ impl Default for PrinterOptions {
PrinterOptions {
tab_width: 2,
print_width: LineWidth::default(),
- indent_string: String::from("\t"),
+ indent_style: Default::default(),
line_ending: LineEnding::LineFeed,
}
}

MODIFY
crates/rome_js_formatter/tests/specs/js/module/arrow/call.js.snap
crates/rome_js_formatter/tests/specs/js/module/arrow/call.js.snap
---
source: crates/rome_js_formatter/tests/spec_test.rs
-assertion_line: 256
expression: call.js
---
# Input
@@ -74,7 +73,8 @@ expect(() => asyncRequest({ url: "/test-endpoint-but-with-a-long-url" }));
// .toThrowError(/Required parameter/);
expect(
- () => asyncRequest({ url: "/test-endpoint-but-with-a-suuuuuuuuper-long-url" }),
+ () =>
+ asyncRequest({ url: "/test-endpoint-but-with-a-suuuuuuuuper-long-url" }),
);
// .toThrowError(/Required parameter/);
@@ -82,7 +82,8 @@ expect(() => asyncRequest({ type: "foo", url: "/test-endpoint" })).not
.toThrowError();
expect(
- () => asyncRequest({ type: "foo", url: "/test-endpoint-but-with-a-long-url" }),
+ () =>
+ asyncRequest({ type: "foo", url: "/test-endpoint-but-with-a-long-url" }),
).not.toThrowError();
const a = Observable.fromPromise(axiosInstance.post("/carts/mine")).map(

feat(rome_formatter): `align > indent` nesting with `IndentStyle::Space` (#3021)