feat(rome_formatter): `Align` IR element
MODIFY
crates/rome_formatter/src/builders.rs
crates/rome_formatter/src/builders.rs
@@ -8,6 +8,7 @@ use rome_rowan::{Language, SyntaxNode, SyntaxToken, SyntaxTokenText, TextLen};
use std::borrow::Cow;
use std::cell::Cell;
use std::marker::PhantomData;
+use std::num::NonZeroU8;
use std::ops::Deref;
/// A line break that only gets printed if the enclosing `Group` doesn't fit on a single line.
@@ -664,6 +665,93 @@ impl<Context> std::fmt::Debug for Indent<'_, Context> {
}
}
+/// Aligns its content by indenting the content by `count` spaces.
+///
+/// [align] is a variant of `[indent]` that indents its content by a specified number of spaces rather than
+/// using the configured indent character (tab or a specified number of spaces).
+///
+/// You should use [align] when you want to indent a content by a specific number of spaces.
+/// Using [indent] is preferred in all other situations as it respects the users preferred indent character.
+///
+/// # Examples
+/// ```
+/// use std::num::NonZeroU8;
+/// use rome_formatter::{format, format_args};
+/// use rome_formatter::prelude::*;
+///
+/// let block = format!(SimpleFormatContext::default(), [
+/// 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\t\tconsole.log('test');\n };",
+/// block.print().as_code()
+/// );
+/// ```
+///
+/// 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.
+///
+pub fn align<Content, Context>(count: u8, content: &Content) -> Align<Context>
+where
+ Content: Format<Context>,
+{
+ Align {
+ count: NonZeroU8::new(count).expect("Alignment count must be a non-zero number."),
+ content: Argument::new(content),
+ }
+}
+
+#[derive(Copy, Clone)]
+pub struct Align<'a, Context> {
+ count: NonZeroU8,
+ content: Argument<'a, Context>,
+}
+
+impl<Context> Format<Context> for Align<'_, Context> {
+ fn fmt(&self, f: &mut Formatter<Context>) -> FormatResult<()> {
+ let mut buffer = VecBuffer::new(f.state_mut());
+
+ buffer.write_fmt(Arguments::from(&self.content))?;
+
+ if buffer.is_empty() {
+ return Ok(());
+ }
+
+ let content = buffer.into_vec();
+ f.write_element(FormatElement::Align(format_element::Align {
+ content: content.into_boxed_slice(),
+ count: self.count,
+ }))
+ }
+}
+
+impl<Context> std::fmt::Debug for Align<'_, Context> {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ f.debug_struct("Align")
+ .field("count", &self.count)
+ .field("content", &"{{content}}")
+ .finish()
+ }
+}
+
/// Inserts a hard line break before and after the content and increases the indention level for the content by one.
///
/// Block indents indent a block of code, such as in a function body, and therefore insert a line

MODIFY
crates/rome_formatter/src/format_element.rs
crates/rome_formatter/src/format_element.rs
@@ -14,6 +14,7 @@ use std::any::type_name;
use std::any::TypeId;
use std::borrow::Cow;
use std::hash::{Hash, Hasher};
+use std::num::NonZeroU8;
use std::ops::Deref;
use std::rc::Rc;
@@ -33,6 +34,12 @@ pub enum FormatElement {
/// Indents the content one level deeper, see [crate::indent] for documentation and examples.
Indent(Content),
+ /// Variant of [FormatElement::Indent] that indents content by a number of spaces. For example, `Align(2)`
+ /// indents any content following a line break by an additional two spaces.
+ ///
+ /// Nesting (Aligns)[FormatElement::Align] has the effect that all except the most inner align are handled as (Indent)[FormatElement::Indent].
+ Align(Align),
+
/// Creates a logical group where its content is either consistently printed:
/// * on a single line: Omitting `LineMode::Soft` line breaks and printing spaces for `LineMode::SoftOrSpace`
/// * on multiple lines: Printing all line breaks
@@ -150,6 +157,11 @@ impl std::fmt::Debug for FormatElement {
FormatElement::Space => write!(fmt, "Space"),
FormatElement::Line(content) => fmt.debug_tuple("Line").field(content).finish(),
FormatElement::Indent(content) => fmt.debug_tuple("Indent").field(content).finish(),
+ FormatElement::Align(Align { count, content }) => fmt
+ .debug_struct("Align")
+ .field("count", count)
+ .field("content", content)
+ .finish(),
FormatElement::Group(content) => {
write!(fmt, "Group")?;
content.fmt(fmt)
@@ -246,6 +258,22 @@ impl Fill {
}
}
+#[derive(Clone, Debug, Eq, PartialEq)]
+pub struct Align {
+ pub(super) content: Content,
+ pub(super) count: NonZeroU8,
+}
+
+impl Align {
+ pub fn count(&self) -> NonZeroU8 {
+ self.count
+ }
+
+ pub fn content(&self) -> &[FormatElement] {
+ &self.content
+ }
+}
+
/// Group is a special token that controls how the child tokens are printed.
///
/// The printer first tries to print all tokens in the group onto a single line (ignoring soft line wraps)
@@ -622,7 +650,10 @@ impl FormatElement {
| FormatElement::Fill(Fill { content, .. })
| FormatElement::Verbatim(Verbatim { content, .. })
| FormatElement::Label(Label { content, .. })
- | FormatElement::Indent(content) => content.iter().any(FormatElement::will_break),
+ | FormatElement::Indent(content)
+ | FormatElement::Align(Align { content, .. }) => {
+ content.iter().any(FormatElement::will_break)
+ }
FormatElement::List(list) => list.content.iter().any(FormatElement::will_break),
FormatElement::Text(token) => token.contains('\n'),
FormatElement::LineSuffix(_) => false,
@@ -770,6 +801,19 @@ impl Format<IrFormatContext> for FormatElement {
FormatElement::Indent(content) => {
write!(f, [text("indent("), content.as_ref(), text(")")])
}
+ FormatElement::Align(Align { content, count }) => {
+ write!(
+ f,
+ [
+ text("align("),
+ dynamic_text(&count.to_string(), TextSize::default()),
+ text(","),
+ space(),
+ content.as_ref(),
+ text(")")
+ ]
+ )
+ }
FormatElement::List(list) => {
write!(f, [list.as_ref()])
}

MODIFY
crates/rome_formatter/src/printer/mod.rs
crates/rome_formatter/src/printer/mod.rs
mod printer_options;
pub use printer_options::*;
+use std::cmp::Ordering;
-use crate::format_element::{ConditionalGroupContent, Group, LineMode, PrintMode, VerbatimKind};
+use crate::format_element::{
+ Align, ConditionalGroupContent, Group, LineMode, PrintMode, VerbatimKind,
+};
use crate::intersperse::Intersperse;
use crate::{FormatElement, GroupId, Printed, SourceMarker, TextRange};
use rome_rowan::TextSize;
use std::iter::{once, Rev};
+use std::num::NonZeroU8;
/// Prints the format elements into a string
#[derive(Debug, Default)]
@@ -37,7 +41,7 @@ impl<'a> Printer<'a> {
queue.enqueue(PrintElementCall::new(
element,
- PrintElementArgs::new(indent),
+ PrintElementArgs::new(Indention::Level(indent)),
));
while let Some(print_element_call) = queue.dequeue() {
@@ -75,15 +79,16 @@ impl<'a> Printer<'a> {
}
}
FormatElement::Text(token) => {
- // Print pending indention
- if self.state.pending_indent > 0 {
+ if !self.state.pending_indent.is_empty() {
self.print_str(
self.options
.indent_string
- .repeat(self.state.pending_indent as usize)
+ .repeat(self.state.pending_indent.level() as usize)
.as_str(),
);
- self.state.pending_indent = 0;
+
+ self.print_str(&" ".repeat(self.state.pending_indent.align() as usize));
+ self.state.pending_indent = Indention::default();
}
// Print pending spaces
@@ -165,7 +170,11 @@ impl<'a> Printer<'a> {
}
FormatElement::Indent(content) => {
- queue.extend_with_args(content.iter(), args.with_incremented_indent());
+ queue.extend_with_args(content.iter(), args.indented());
+ }
+
+ FormatElement::Align(Align { content, count }) => {
+ queue.extend_with_args(content.iter(), args.aligned(*count))
}
FormatElement::ConditionalGroupContent(ConditionalGroupContent {
@@ -480,7 +489,7 @@ struct PrinterState<'a> {
buffer: String,
source_markers: Vec<SourceMarker>,
source_position: TextSize,
- pending_indent: u16,
+ pending_indent: Indention,
pending_space: bool,
measured_group_fits: bool,
generated_line: usize,
@@ -530,20 +539,25 @@ impl GroupModes {
/// data structures. Such structures should be stored on the [PrinterState] instead.
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
struct PrintElementArgs {
- indent: u16,
+ indent: Indention,
mode: PrintMode,
}
impl PrintElementArgs {
- pub fn new(indent: u16) -> Self {
+ pub fn new(indent: Indention) -> Self {
Self {
indent,
..Self::default()
}
}
- pub fn with_incremented_indent(mut self) -> Self {
- self.indent += 1;
+ pub fn indented(mut self) -> Self {
+ self.indent = self.indent.increment_level();
+ self
+ }
+
+ pub fn aligned(mut self, count: NonZeroU8) -> Self {
+ self.indent = self.indent.add_align(count);
self
}
@@ -556,12 +570,123 @@ impl PrintElementArgs {
impl Default for PrintElementArgs {
fn default() -> Self {
Self {
- indent: 0,
+ indent: Indention::Level(0),
mode: PrintMode::Expanded,
}
}
}
+#[derive(Copy, Clone, Eq, PartialEq, Debug)]
+enum Indention {
+ /// Indent the content by `count` levels by using the indention sequence specified by the printer options.
+ Level(u16),
+
+ /// Indent the content by n-`level`s using the indention sequence specified by the printer options and `align` spaces.
+ Align { level: u16, align: NonZeroU8 },
+}
+
+impl Indention {
+ const fn is_empty(&self) -> bool {
+ matches!(self, Indention::Level(0))
+ }
+
+ /// Creates a new indention level with a zero-indent.
+ const fn new() -> Self {
+ Indention::Level(0)
+ }
+
+ /// Returns the indention level
+ fn level(&self) -> u16 {
+ match self {
+ Indention::Level(count) => *count,
+ Indention::Align { level: indent, .. } => *indent,
+ }
+ }
+
+ /// Returns the number of trailing align spaces or 0 if none
+ fn align(&self) -> u8 {
+ match self {
+ Indention::Level(_) => 0,
+ Indention::Align { align, .. } => (*align).into(),
+ }
+ }
+
+ /// 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 {
+ 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),
+ }
+ }
+
+ /// Adds an `align` of `count` spaces to the current indention.
+ ///
+ /// It increments the `level` value if the current value is [Indent::IndentAlign].
+ fn add_align(self, count: NonZeroU8) -> Self {
+ match self {
+ Indention::Level(indent_count) => Indention::Align {
+ level: indent_count,
+ align: count,
+ },
+
+ // Convert the existing align to an indent
+ Indention::Align { level: indent, .. } => Indention::Align {
+ level: indent + 1,
+ align: count,
+ },
+ }
+ }
+}
+
+/// Compares two [Indent]s by first comparing the indent level and, if equal, comparing the `align`.
+/// This implementation assumes that `width(align) < width(1-level)`.
+impl PartialOrd for Indention {
+ fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
+ Some(match (self, other) {
+ (Indention::Level(this_indent), Indention::Level(other_indent)) => {
+ this_indent.cmp(other_indent)
+ }
+ (
+ Indention::Align {
+ level: this_indent,
+ align: this_align,
+ },
+ Indention::Align {
+ level: other_indent,
+ align: other_align,
+ },
+ ) => {
+ let indent_ordering = this_indent.cmp(other_indent);
+
+ if indent_ordering == Ordering::Equal {
+ this_align.cmp(other_align)
+ } else {
+ indent_ordering
+ }
+ }
+ (Indention::Level(this_indent), Indention::Align { level: indent, .. }) => {
+ let indent_ordering = this_indent.cmp(indent);
+
+ if indent_ordering == Ordering::Equal {
+ Ordering::Greater
+ } else {
+ indent_ordering
+ }
+ }
+ _ => return other.partial_cmp(self).map(|ordering| ordering.reverse()),
+ })
+ }
+}
+
+impl Default for Indention {
+ fn default() -> Self {
+ Indention::new()
+ }
+}
+
/// The Printer uses a stack that emulates recursion. E.g. recursively processing the elements:
/// `indent(&concat(string, string))` would result in the following call stack:
///
@@ -732,8 +857,10 @@ fn fits_element_on_line<'a, 'rest>(
}
}
- FormatElement::Indent(content) => {
- queue.extend(content.iter(), args.with_incremented_indent())
+ FormatElement::Indent(content) => queue.extend(content.iter(), args.indented()),
+
+ FormatElement::Align(Align { content, count }) => {
+ queue.extend(content.iter(), args.aligned(*count))
}
FormatElement::Group(group) => {
@@ -766,8 +893,9 @@ fn fits_element_on_line<'a, 'rest>(
),
FormatElement::Text(token) => {
- state.line_width += state.pending_indent as usize * options.indent_string.len();
- state.pending_indent = 0;
+ state.line_width += state.pending_indent.level() as usize * options.indent_string.len()
+ + state.pending_indent.align() as usize;
+ state.pending_indent = Indention::default();
if state.pending_space {
state.line_width += 1;
@@ -849,7 +977,7 @@ impl From<bool> for Fits {
/// State used when measuring if a group fits on a single line
#[derive(Debug)]
struct MeasureState<'group> {
- pending_indent: u16,
+ pending_indent: Indention,
pending_space: bool,
has_line_suffix: bool,
line_width: usize,

feat(rome_formatter): `Align` IR element (#2995)