Skip to content

Commit

Permalink
Add FitsExpanded and conditional group IR
Browse files Browse the repository at this point in the history
  • Loading branch information
MichaReiser committed Jul 8, 2023
1 parent b22e6c3 commit c025995
Show file tree
Hide file tree
Showing 5 changed files with 434 additions and 27 deletions.
187 changes: 186 additions & 1 deletion crates/ruff_formatter/src/builders.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1363,13 +1363,15 @@ pub fn group<Context>(content: &impl Format<Context>) -> Group<Context> {
content: Argument::new(content),
group_id: None,
should_expand: false,
condition: None,
}
}

#[derive(Copy, Clone)]
pub struct Group<'a, Context> {
content: Argument<'a, Context>,
group_id: Option<GroupId>,
condition: Option<Condition>,
should_expand: bool,
}

Expand All @@ -1389,6 +1391,101 @@ impl<Context> Group<'_, Context> {
self.should_expand = should_expand;
self
}

/// Sets the `condition` for the group. The element will behave as a regular group if `condition` is met,
/// and as *ungrouped* content if the condition is not met.
///
/// ## Examples
///
/// Only expand before operators if the parentheses are necessary.
///
/// ```
/// # use ruff_formatter::prelude::*;
/// # use ruff_formatter::{format, format_args, LineWidth, SimpleFormatOptions};
///
/// # fn main() -> FormatResult<()> {
/// use ruff_formatter::Formatted;
/// let content = format_with(|f| { ///
/// let parentheses_id = f.group_id("parentheses");
/// group(&format_args![
/// if_group_breaks(&text("(")),
/// indent_if_group_breaks(&format_args![
/// soft_line_break(),
/// group(&format_args![
/// text("'aaaaaaa'"),
/// soft_line_break_or_space(),
/// text("+"),
/// space(),
/// fits_expanded(&group(&format_args![
/// text("["),
/// soft_block_indent(&format_args![
/// text("'Good morning!',"),
/// soft_line_break_or_space(),
/// text("'How are you?'"),
/// ]),
/// text("]"),
/// ])).with_condition(Some(tag::Condition::if_group_fits_on_line(parentheses_id))),
/// soft_line_break_or_space(),
/// text("+"),
/// space(),
/// group(&format_args![
/// text("'bbbb'"),
/// soft_line_break_or_space(),
/// text("and"),
/// space(),
/// text("'c'")
/// ]).with_condition(Some(tag::Condition::if_group_breaks(parentheses_id)))
/// ]).with_condition(Some(tag::Condition::if_breaks())),
/// ], parentheses_id),
/// soft_line_break(),
/// if_group_breaks(&text(")"))
/// ])
/// .with_group_id(Some(parentheses_id))
/// .fmt(f)
/// });
///
/// let formatted = format!(SimpleFormatContext::default(), [content])?;
/// let document = formatted.into_document();
///
/// // All content fits
/// let all_fits = Formatted::new(document.clone(), SimpleFormatContext::new(SimpleFormatOptions {
/// line_width: LineWidth::try_from(65).unwrap(),
/// ..SimpleFormatOptions::default()
/// }));
///
/// assert_eq!(
/// "'aaaaaaa' + ['Good morning!', 'How are you?'] + 'bbbb' and 'c'",
/// all_fits.print()?.as_code()
/// );
///
/// // The parentheses group fits, because it can expand the list,
/// let list_expanded = Formatted::new(document.clone(), SimpleFormatContext::new(SimpleFormatOptions {
/// line_width: LineWidth::try_from(21).unwrap(),
/// ..SimpleFormatOptions::default()
/// }));
///
/// assert_eq!(
/// "'aaaaaaa' + [\n\t'Good morning!',\n\t'How are you?'\n] + 'bbbb' and 'c'",
/// list_expanded.print()?.as_code()
/// );
///
/// // It is necessary to split all groups to fit the content
/// let all_expanded = Formatted::new(document, SimpleFormatContext::new(SimpleFormatOptions {
/// line_width: LineWidth::try_from(11).unwrap(),
/// ..SimpleFormatOptions::default()
/// }));
///
/// assert_eq!(
/// "(\n\t'aaaaaaa'\n\t+ [\n\t\t'Good morning!',\n\t\t'How are you?'\n\t]\n\t+ 'bbbb'\n\tand 'c'\n)",
/// all_expanded.print()?.as_code()
/// );
/// # Ok(())
/// # }
/// ```
pub fn with_condition(mut self, condition: Option<Condition>) -> Self {
self.condition = condition;
self
}
}

impl<Context> Format<Context> for Group<'_, Context> {
Expand All @@ -1399,7 +1496,10 @@ impl<Context> Format<Context> for Group<'_, Context> {
};

f.write_element(FormatElement::Tag(StartGroup(
tag::Group::new().with_id(self.group_id).with_mode(mode),
tag::Group::new()
.with_id(self.group_id)
.with_mode(mode)
.with_condition(self.condition),
)))?;

Arguments::from(&self.content).fmt(f)?;
Expand Down Expand Up @@ -1841,6 +1941,91 @@ impl<Context> std::fmt::Debug for IndentIfGroupBreaks<'_, Context> {
}
}

/// Changes the definition of *fits* for `content`. Instead of measuring it in *flat*, measure it with
/// all line breaks expanded and test if no line exceeds the line width. The [`FitsExpanded`] acts
/// as a expands boundary similar to best fitting, meaning that a [hard_line_break] will not cause the parent group to expand.
///
/// Useful in conjunction with a group with a condition.
///
/// ## Examples
/// The outer group with the binary expression remains *flat* regardless of the array expression
/// that spans multiple lines.
///
/// ```
/// # use ruff_formatter::{format, format_args, LineWidth, SimpleFormatOptions, write};
/// # use ruff_formatter::prelude::*;
///
/// # fn main() -> FormatResult<()> {
/// let content = format_with(|f| {
/// let group_id = f.group_id("header");
///
/// write!(f, [
/// group(&format_args![
/// text("a"),
/// soft_line_break_or_space(),
/// text("+"),
/// space(),
/// fits_expanded(&group(&format_args![
/// text("["),
/// soft_block_indent(&format_args![
/// text("a,"), space(), text("# comment"), expand_parent(), soft_line_break_or_space(),
/// text("b")
/// ]),
/// text("]")
/// ]))
/// ]),
/// ])
/// });
///
/// let formatted = format!(SimpleFormatContext::new(SimpleFormatOptions {
/// line_width: LineWidth::try_from(16).unwrap(),
/// ..SimpleFormatOptions::default()
/// }),
/// [content]
/// )?;
///
/// assert_eq!(
/// "a + [\n\ta, # comment\n\tb\n]",
/// formatted.print()?.as_code()
/// );
/// # Ok(())
/// # }
/// ```
pub fn fits_expanded<'a, Content, Context>(content: &'a Content) -> FitsExpanded<'a, Context>
where
Content: Format<Context>,
{
FitsExpanded {
content: Argument::new(content),
condition: None,
}
}

#[derive(Clone)]
pub struct FitsExpanded<'a, Context> {
content: Argument<'a, Context>,
condition: Option<Condition>,
}

impl<Context> FitsExpanded<'_, Context> {
/// Sets a `condition` to when the content should fit in expanded mode. The content uses the regular fits
/// definition if the `condition` is not met.
pub fn with_condition(mut self, condition: Option<Condition>) -> Self {
self.condition = condition;
self
}
}

impl<Context> Format<Context> for FitsExpanded<'_, Context> {
fn fmt(&self, f: &mut Formatter<Context>) -> FormatResult<()> {
f.write_element(FormatElement::Tag(StartFitsExpanded(
tag::FitsExpanded::new().with_condition(self.condition),
)))?;
f.write_fmt(Arguments::from(&self.content))?;
f.write_element(FormatElement::Tag(EndFitsExpanded))
}
}

/// Utility for formatting some content with an inline lambda function.
#[derive(Copy, Clone)]
pub struct FormatWith<Context, T> {
Expand Down
2 changes: 1 addition & 1 deletion crates/ruff_formatter/src/format_element.rs
Original file line number Diff line number Diff line change
Expand Up @@ -446,7 +446,7 @@ mod sizes {
assert_eq_size!(crate::SourceCodeSlice, [u8; 8]);

#[cfg(not(debug_assertions))]
assert_eq_size!(crate::format_element::Tag, [u8; 16]);
assert_eq_size!(crate::format_element::Tag, [u8; 24]);

#[cfg(not(debug_assertions))]
assert_eq_size!(crate::FormatElement, [u8; 24]);
Expand Down
76 changes: 73 additions & 3 deletions crates/ruff_formatter/src/format_element/document.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use super::tag::Tag;
use crate::format_element::tag::DedentMode;
use crate::format_element::tag::{Condition, DedentMode};
use crate::prelude::tag::GroupMode;
use crate::prelude::*;
use crate::printer::LineEnding;
Expand Down Expand Up @@ -33,12 +33,17 @@ impl Document {
#[derive(Debug)]
enum Enclosing<'a> {
Group(&'a tag::Group),
FitsExpanded(&'a tag::FitsExpanded),
BestFitting,
}

fn expand_parent(enclosing: &[Enclosing]) {
if let Some(Enclosing::Group(group)) = enclosing.last() {
group.propagate_expand();
match enclosing.last() {
Some(Enclosing::Group(group)) => {
group.propagate_expand();
}
Some(Enclosing::FitsExpanded(fits_expanded)) => fits_expanded.propagate_expand(),
_ => {}
}
}

Expand Down Expand Up @@ -79,6 +84,16 @@ impl Document {
enclosing.pop();
continue;
}
FormatElement::Tag(Tag::StartFitsExpanded(fits_expanded)) => {
enclosing.push(Enclosing::FitsExpanded(fits_expanded));
false
}
FormatElement::Tag(Tag::EndFitsExpanded) => {
enclosing.pop();
// Fits expanded acts as a boundary
expands = false;
continue;
}
FormatElement::StaticText { text } => text.contains('\n'),
FormatElement::DynamicText { text, .. } => text.contains('\n'),
FormatElement::SourceCodeSlice {
Expand Down Expand Up @@ -439,6 +454,13 @@ impl Format<IrFormatContext<'_>> for &[FormatElement] {
write!(f, [text("expand: propagated,"), space()])?;
}
}

if let Some(condition) = group.condition() {
write!(
f,
[text("condition:"), space(), condition, text(","), space()]
)?;
}
}

StartIndentIfGroupBreaks(id) => {
Expand Down Expand Up @@ -491,6 +513,28 @@ impl Format<IrFormatContext<'_>> for &[FormatElement] {
write!(f, [text("fill(")])?;
}

StartFitsExpanded(tag::FitsExpanded {
condition,
propagate_expand,
}) => {
write!(f, [text("fits_expanded(propagate_expand:"), space()])?;

if propagate_expand.get() {
write!(f, [text("true")])?;
} else {
write!(f, [text("false")])?;
}

write!(f, [text(","), space()])?;

if let Some(condition) = condition {
write!(
f,
[text("condition:"), space(), condition, text(","), space()]
)?;
}
}

StartEntry => {
// handled after the match for all start tags
}
Expand All @@ -505,6 +549,7 @@ impl Format<IrFormatContext<'_>> for &[FormatElement] {
| EndGroup
| EndLineSuffix
| EndDedent
| EndFitsExpanded
| EndVerbatim => {
write!(f, [ContentArrayEnd, text(")")])?;
}
Expand Down Expand Up @@ -658,6 +703,31 @@ impl FormatElements for [FormatElement] {
}
}

impl Format<IrFormatContext<'_>> for Condition {
fn fmt(&self, f: &mut Formatter<IrFormatContext>) -> FormatResult<()> {
match (self.mode, self.group_id) {
(PrintMode::Flat, None) => write!(f, [text("if_fits_on_line")]),
(PrintMode::Flat, Some(id)) => write!(
f,
[
text("if_group_fits_on_line("),
dynamic_text(&std::format!("\"{id:?}\""), None),
text(")")
]
),
(PrintMode::Expanded, None) => write!(f, [text("if_breaks")]),
(PrintMode::Expanded, Some(id)) => write!(
f,
[
text("if_group_breaks("),
dynamic_text(&std::format!("\"{id:?}\""), None),
text(")")
]
),
}
}
}

#[cfg(test)]
mod tests {
use crate::prelude::*;
Expand Down
Loading

0 comments on commit c025995

Please sign in to comment.