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 11, 2023
1 parent 15c7b6b commit 6d11701
Show file tree
Hide file tree
Showing 5 changed files with 581 additions and 56 deletions.
216 changes: 215 additions & 1 deletion crates/ruff_formatter/src/builders.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1410,14 +1410,143 @@ impl<Context> Format<Context> for Group<'_, Context> {

impl<Context> std::fmt::Debug for Group<'_, Context> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("GroupElements")
f.debug_struct("Group")
.field("group_id", &self.group_id)
.field("should_expand", &self.should_expand)
.field("content", &"{{content}}")
.finish()
}
}

/// 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(),
/// conditional_group(&format_args![
/// text("'aaaaaaa'"),
/// soft_line_break_or_space(),
/// text("+"),
/// space(),
/// fits_expanded(&conditional_group(&format_args![
/// text("["),
/// soft_block_indent(&format_args![
/// text("'Good morning!',"),
/// soft_line_break_or_space(),
/// text("'How are you?'"),
/// ]),
/// text("]"),
/// ], tag::Condition::if_group_fits_on_line(parentheses_id))),
/// soft_line_break_or_space(),
/// text("+"),
/// space(),
/// conditional_group(&format_args![
/// text("'bbbb'"),
/// soft_line_break_or_space(),
/// text("and"),
/// space(),
/// text("'c'")
/// ], tag::Condition::if_group_fits_on_line(parentheses_id))
/// ], 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(())
/// # }
/// ```
#[inline]
pub fn conditional_group<Content, Context>(
content: &Content,
condition: Condition,
) -> ConditionalGroup<Context>
where
Content: Format<Context>,
{
ConditionalGroup {
content: Argument::new(content),
condition,
}
}

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

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

impl<Context> std::fmt::Debug for ConditionalGroup<'_, Context> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ConditionalGroup")
.field("condition", &self.condition)
.field("content", &"{{content}}")
.finish()
}
}

/// IR element that forces the parent group to print in expanded mode.
///
/// Has no effect if used outside of a group or element that introduce implicit groups (fill element).
Expand Down Expand Up @@ -1841,6 +1970,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<Content, Context>(content: &Content) -> FitsExpanded<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
101 changes: 98 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),
ConditionalGroup(&'a tag::ConditionalGroup),
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::ConditionalGroup(group)) => group.propagate_expand(),
Some(Enclosing::FitsExpanded(fits_expanded)) => fits_expanded.propagate_expand(),
_ => {}
}
}

Expand All @@ -58,6 +63,14 @@ impl Document {
Some(Enclosing::Group(group)) => !group.mode().is_flat(),
_ => false,
},
FormatElement::Tag(Tag::StartConditionalGroup(group)) => {
enclosing.push(Enclosing::ConditionalGroup(group));
false
}
FormatElement::Tag(Tag::EndConditionalGroup) => match enclosing.pop() {
Some(Enclosing::ConditionalGroup(group)) => !group.mode().is_flat(),
_ => false,
},
FormatElement::Interned(interned) => match checked_interned.get(interned) {
Some(interned_expands) => *interned_expands,
None => {
Expand All @@ -79,6 +92,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 @@ -441,6 +464,29 @@ impl Format<IrFormatContext<'_>> for &[FormatElement] {
}
}

StartConditionalGroup(group) => {
write!(
f,
[
text("conditional_group(condition:"),
space(),
group.condition(),
text(","),
space()
]
)?;

match group.mode() {
GroupMode::Flat => {}
GroupMode::Expand => {
write!(f, [text("expand: true,"), space()])?;
}
GroupMode::Propagated => {
write!(f, [text("expand: propagated,"), space()])?;
}
}
}

StartIndentIfGroupBreaks(id) => {
write!(
f,
Expand Down Expand Up @@ -491,6 +537,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 @@ -503,8 +571,10 @@ impl Format<IrFormatContext<'_>> for &[FormatElement] {
| EndAlign
| EndIndent
| EndGroup
| EndConditionalGroup
| EndLineSuffix
| EndDedent
| EndFitsExpanded
| EndVerbatim => {
write!(f, [ContentArrayEnd, text(")")])?;
}
Expand Down Expand Up @@ -658,6 +728,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 6d11701

Please sign in to comment.