feat(formatter_core): add no-expand-parent for multiline text (#24050)

Add new IR primitives required for implementing YAML and also fix like https://github.com/prettier/prettier/blob/79f6cdfd9873a91be9b25c9c6a41d26dcd9a6656/changelog_unreleased/css/18605.md
This commit is contained in:
leaysgur committed 2026-07-02 08:14:03 +00:00
1 parent 62af71751d
commit 213a96b21d
3 files changed
+67 -16

No files matched your search

+21 -8
View File
@@ -199,7 +199,7 @@ impl std::fmt::Debug for Token {
/// Creates a text from a dynamic string and a range of the input source
pub fn text(text: &str) -> Text<'_> {
debug_assert_no_cr_line_break(text);
Text { text, width: None }
Text { text, width: None, expand_parent: true }
}
/// Creates a text from a dynamic string that contains no whitespace characters
@@ -208,13 +208,27 @@ pub fn text_without_whitespace(text: &str) -> Text<'_> {
text.as_bytes().iter().all(|&b| !b.is_ascii_whitespace()),
"The content '{text}' contains whitespace characters but text must not contain any whitespace characters."
);
Text { text, width: Some(TextWidth::from_non_whitespace_str(text)) }
Text { text, width: Some(TextWidth::from_non_whitespace_str(text)), expand_parent: true }
}
#[derive(Eq, PartialEq)]
pub struct Text<'a> {
#[expect(clippy::struct_field_names)] // Keep the name the same as it is in the original source
text: &'a str,
width: Option<TextWidth>,
expand_parent: bool,
}
impl Text<'_> {
/// Prints embedded newlines literally but does NOT force enclosing groups to expand
/// (Prettier's `replaceEndOfLine(..., literallineWithoutBreakParent)`).
///
/// Fits measurement still only counts the first line, and each newline resets the line width.
#[must_use]
pub fn without_expand_parent(mut self) -> Self {
self.expand_parent = false;
self
}
}
impl<'a, C> Format<'a, C> for Text<'a>
@@ -222,12 +236,11 @@ where
C: FormatContext,
{
fn fmt(&self, f: &mut Formatter<'_, 'a, C>) {
f.write_element(FormatElement::Text {
text: self.text,
width: self
.width
.unwrap_or_else(|| TextWidth::from_text(self.text, f.options().indent_width())),
});
let width = self
.width
.unwrap_or_else(|| TextWidth::from_text(self.text, f.options().indent_width()));
let width = if self.expand_parent { width } else { width.without_expand_parent() };
f.write_element(FormatElement::Text { text: self.text, width });
}
}
@@ -54,7 +54,8 @@ impl<'a> Document<'a> {
impl Document<'_> {
/// Sets [`expand`](tag::Group::expand) to [`GroupMode::Propagated`] if the group contains any of:
/// * a group with [`expand`](tag::Group::expand) set to [GroupMode::Propagated] or [GroupMode::Expand].
/// * a non-soft [line break](FormatElement::Line) with mode [LineMode::Hard], [LineMode::Empty], or [LineMode::Literal].
/// * a non-soft [line break](FormatElement::Line) with mode [LineMode::Hard] or [LineMode::Empty].
/// * a multiline [FormatElement::Text] whose `TextWidth` is not marked `without_expand_parent`.
/// * a [FormatElement::ExpandParent]
///
/// [`BestFitting`] elements act as expand boundaries, meaning that the fact that a
@@ -146,7 +147,7 @@ impl Document<'_> {
false
}
// `FormatElement::Token` cannot contain line breaks
FormatElement::Text { text: _, width } => width.is_multiline(),
FormatElement::Text { text: _, width } => width.propagates_expand(),
FormatElement::ExpandParent
| FormatElement::Line(LineMode::Hard | LineMode::Empty) => true,
_ => false,
@@ -276,7 +276,7 @@ impl FormatElements for FormatElement<'_> {
FormatElement::ExpandParent => true,
FormatElement::Tag(Tag::StartGroup(group)) => !group.mode().is_flat(),
FormatElement::Line(line_mode) => line_mode.will_break(),
FormatElement::Text { text: _, width } => width.is_multiline(),
FormatElement::Text { text: _, width } => width.propagates_expand(),
FormatElement::Interned(interned) => interned.will_break(),
// Traverse into the most flat version because the content is guaranteed to expand when even
// the most flat version contains some content that forces a break.
@@ -423,13 +423,14 @@ pub trait FormatElements {
///
/// ## Representation
///
/// Uses a single `u32` to efficiently store both the width value and a multiline flag:
/// Uses a single `u32` to efficiently store the width value and two flags:
///
/// - Bit 31: Multiline flag (1 = multiline, 0 = single line)
/// - Bits 0-30: Width value (stored directly)
/// - Bit 30: No-expand flag (1 = multiline text that does not expand enclosing groups)
/// - Bits 0-29: Width value (stored directly)
///
/// This encoding allows `TextWidth` to fit in 4 bytes while supporting both a width value
/// and a boolean flag. The maximum representable width is 2^31 - 1 (0x7FFFFFFF).
/// and boolean flags. The maximum representable width is 2^30 - 1 (0x3FFFFFFF).
///
/// The maximum value is sufficient in practice as texts that long would exceed any
/// reasonable line width configuration (typically < 500 columns).
@@ -446,8 +447,13 @@ impl TextWidth {
/// Bit mask for the multiline flag (highest bit)
const MULTILINE_MASK: u32 = 1 << 31;
/// Bit mask for extracting the width value (all bits except the highest)
const WIDTH_MASK: u32 = Self::MULTILINE_MASK - 1;
/// Bit mask for the "multiline but does not expand enclosing groups" flag.
/// Equivalent to Prettier's `literallineWithoutBreakParent`: the newlines still
/// print literally, but `Document::propagate_expand` / `will_break` ignore them.
const NO_EXPAND_MASK: u32 = 1 << 30;
/// Bit mask for extracting the width value (all bits except the flags)
const WIDTH_MASK: u32 = Self::NO_EXPAND_MASK - 1;
/// Encodes width and multiline flag into a single u32.
const fn encode(width: u32, multiline: bool) -> u32 {
@@ -525,6 +531,19 @@ impl TextWidth {
pub const fn is_multiline(self) -> bool {
(self.0 & Self::MULTILINE_MASK) != 0
}
/// Marks a multiline width so its newlines do NOT expand enclosing groups
/// (Prettier's `literallineWithoutBreakParent`). No-op for single-line widths.
#[must_use]
pub const fn without_expand_parent(self) -> Self {
if self.is_multiline() { Self(self.0 | Self::NO_EXPAND_MASK) } else { self }
}
/// Returns true if the text forces enclosing groups to expand:
/// it contains newlines and is not marked [`Self::without_expand_parent`].
pub const fn propagates_expand(self) -> bool {
(self.0 & (Self::MULTILINE_MASK | Self::NO_EXPAND_MASK)) == Self::MULTILINE_MASK
}
}
#[cfg(test)]
@@ -580,6 +599,24 @@ mod tests {
debug_assert!(multi.is_multiline());
}
#[test]
fn without_expand_parent_disables_propagation_keeping_width() {
let multi = TextWidth::multiline(3);
debug_assert!(multi.propagates_expand());
let no_expand = multi.without_expand_parent();
debug_assert!(no_expand.is_multiline());
debug_assert!(!no_expand.propagates_expand());
debug_assert_eq!(no_expand.value(), 3);
}
#[test]
fn without_expand_parent_is_noop_for_single_line() {
let single = TextWidth::single(3);
debug_assert!(!single.propagates_expand());
debug_assert_eq!(single.without_expand_parent(), single);
}
#[test]
fn from_text_handles_unicode() {
// Emoji width