mirror of
https://github.com/oxc-project/oxc.git
synced 2026-11-04 10:29:58 +00:00
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:
1 parent
62af71751d
commit
213a96b21d
3 files changed
+67
-16
No files matched your search
@@ -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
|
||||
|
||||
Reference in new issue
Block a user