Skip to content
This repository has been archived by the owner on Aug 31, 2023. It is now read-only.

feat(rome_js_formatter): add format element label #2783

Merged
merged 13 commits into from
Jul 1, 2022
Merged
84 changes: 84 additions & 0 deletions crates/rome_formatter/src/builders.rs
Original file line number Diff line number Diff line change
Expand Up @@ -523,6 +523,90 @@ impl<Context> std::fmt::Debug for FormatComment<'_, Context> {
}
}

/// Marks some content with a label.
///
/// This does not directly influence how this content will be printed, but some
/// parts of the formatter may inspect the `FormatElement::Label` and chose to handle this element in a specific way
denbezrukov marked this conversation as resolved.
Show resolved Hide resolved
///
/// # Example
///
/// ```rust
/// use rome_formatter::prelude::*;
/// use rome_formatter::{format, write, LabelId};
///
/// enum SomeLabelId {}
///
/// #[derive(Default)]
/// struct Labelled;
///
/// impl Format<SimpleFormatContext> for Labelled {
/// fn fmt(&self, f: &mut Formatter<SimpleFormatContext>) -> FormatResult<()> {
/// let label_id = LabelId::of::<SomeLabelId>();
denbezrukov marked this conversation as resolved.
Show resolved Hide resolved
///
/// write!(f, [labelled(label_id, &token("labelled"))])?;
/// Ok(())
/// }
/// }
///
/// let content = format_with(|f| {
/// let mut labelled = Labelled::default().memoized();
/// let labelled_content = labelled.inspect(f)?;
/// let label_id = LabelId::of::<SomeLabelId>();
///
/// let is_labelled = match labelled_content {
/// FormatElement::Label(labelled) => labelled.label_id() == label_id,
/// _ => false,
/// };
Copy link
Contributor

@ematipico ematipico Jun 27, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not sure this is the correct usage. This example shows that the consumer is forced to extract manually the FormatElement which is a low-level API for our formatter.

I wonder instead if we should provide an API, something like f.label_assert_of/label_id.assert_of (it asserts if the type of the label), which returns a boolean for us.

The suggestion is vague because I still need to understand how we do want to use the label and it can be used inside a real world example.

Copy link
Contributor Author

@denbezrukov denbezrukov Jun 27, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've noticed this prettier API then I looked into isPoorlyBreakableMemberOrCallChain for variable assignment.

They use label to understand that CallExpression has been printed as a member-chain.
I've just realized that it seems they traverse tree to extract label. Because they call isPoorlyBreakableMemberOrCallChain with properties array.

But current this PR API allows look only last FormatElement.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We would need to come up with a different solution here, I suppose. I am not sure if printCallExpression actually prints in their main buffer or not (it seems not, which works for their logic). Regardless, they are able to extract the IR for they right-hand side of the assignment like expression and then decide the layout.

Our formatter works left-to-right, which means that once we write the right-hand side, it's there, unless we write it in a temporary buffer (which can be expensive, so we should avoid it).

I would suggest another solution for the assignment case. They create the label only when they actually create a member chain. They don't create the member chain in a specific case: https://github.com/prettier/prettier/blob/9dd761a6e491ffff3856eea47fb10b4573b351a6/src/language-js/print/member-chain.js#L342-L347

That condition is this one: https://github.com/rome/tools/blob/main/crates/rome_js_formatter/src/utils/member_chain/groups.rs#L42-L48

If we are able to use that logic inside the assignment like formatting, we might be able to not use the label.

Otherwise, the only solution that I can see is the write the right-hand side inside a temporary buffer, than inspect that buffer and check the label. But as said before, this has a big impact on memory usage.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Make sense👍
I guess that we can try it (:

What do you think about another case?
Now we have should_not_indent_if_parent_indents in binary_like_expression module. This function is aware about place where it will be printed. It uses should_break_after_operator from assignment_like module because when binary_like_expression is right part and layout is BreakAfterOperator assignment_like adds indent and to avoid double indent we have to check the same logic in binary_like_expression . May be we can invert this dependency and use label for binary_like_expression that it already has indent and check this label in assignment_like module.

  • fn should_not_indent_if_parent_indents(current_node: &JsAnyBinaryLikeLeftExpression) -> bool {
    let parent = current_node.syntax().parent();
    let parent_kind = parent.as_ref().map(|node| node.kind());
    let great_parent = parent.and_then(|parent| parent.parent());
    let great_parent_kind = great_parent.map(|node| node.kind());
    match (parent_kind, great_parent_kind) {
    (Some(JsSyntaxKind::JS_PROPERTY_OBJECT_MEMBER), _)
    | (Some(JsSyntaxKind::JS_INITIALIZER_CLAUSE), Some(JsSyntaxKind::JS_VARIABLE_DECLARATOR)) => {
    current_node
    .as_expression()
    .and_then(|expression| should_break_after_operator(expression).ok())
    .unwrap_or(false)
    }
    (
    Some(JsSyntaxKind::JS_RETURN_STATEMENT | JsSyntaxKind::JS_ARROW_FUNCTION_EXPRESSION),
    _,
    ) => true,
    _ => false,
    }
    }

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not sure if that would work. Mainly because at the end of the format phase, where that function is used, the logic might add parenthesis to the binary expression, and the indentation has to stay inside the parenthesis. That's why checking the AST is better in this case.

Copy link
Contributor Author

@denbezrukov denbezrukov Jun 29, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Got it! Thank you!
So to emulate prettier label functionality we can add new extension inspect_label, InspectLabelBuffer and method for FormatElement has_label which traverses IR tree and search expected label?

EDIT:
It seems that second case also doesn't write in the main buffer.

pub fn inspect(&mut self, f: &mut Formatter<Context>) -> FormatResult<&FormatElement> {
let result = self
.memory
.get_mut()
.get_or_insert_with(|| f.intern(&self.inner));
match result.as_ref() {
Ok(content) => Ok(content.deref()),
Err(error) => Err(*error),
}
}

/// Formats `content` into an interned element without writing it to the formatter's buffer.
pub fn intern(&mut self, content: &dyn Format<Context>) -> FormatResult<Interned> {
let mut buffer = VecBuffer::new(self.state_mut());
crate::write!(&mut buffer, [content])?;
Ok(buffer.into_element().intern())
}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

has_label which traverses IR tree and search expected label?

Is this what Prettier does? Does it traverse ALL the IR in order to find a label? I thought it just checks the first element

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

has_label which traverses IR tree and search expected label?

Is this what Prettier does? Does it traverse ALL the IR in order to find a label? I thought it just checks the first element

Sorry, you're right.
I double checked and prettier checks the first element.

Copy link
Contributor Author

@denbezrukov denbezrukov Jul 5, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would suggest another solution for the assignment case. They create the label only when they actually create a member chain. They don't create the member chain in a specific case: https://github.com/prettier/prettier/blob/9dd761a6e491ffff3856eea47fb10b4573b351a6/src/language-js/print/member-chain.js#L342-L347

That condition is this one: https://github.com/rome/tools/blob/main/crates/rome_js_formatter/src/utils/member_chain/groups.rs#L42-L48

Could you please help me?🙏🏽 I can't find this case in Rome code:

Because it seems this conditional:

is prettier:

UPDATE:
Do I understand correctly that prettier uses only one array for all groups and Rome uses two structs (HeadGroup and Groups)? I was wondering about cutoff value. It can be 2 and 3 depends on should_merge.
It seems that it always should be 1. Because Rome uses two structs and then should_merge is true it mutates Groups vec.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The shouldMerge value is used to essentially decide the layout of the formatting, and this affects the head of the group. So I went for a different approach because prettier's one was not working for us.

Yes, in theory cutoff should not be needed anymore, now that we actually mutate the groups vector

///
/// if is_labelled {
/// write!(f, [token("This is "), &labelled])
/// } else {
/// write!(f, [token("This is not "), &labelled])
/// }
/// });
///
/// let formatted = format!(SimpleFormatContext::default(), [content]).unwrap();
/// assert_eq!("This is labelled", formatted.print().as_code())
/// ```
#[inline]
pub fn labelled<Content, Context>(label_id: LabelId, content: &Content) -> FormatLabelled<Context>
denbezrukov marked this conversation as resolved.
Show resolved Hide resolved
where
Content: Format<Context>,
{
FormatLabelled {
label_id,
content: Argument::new(content),
}
}

#[derive(Copy, Clone)]
pub struct FormatLabelled<'a, Context> {
label_id: LabelId,
content: Argument<'a, Context>,
}

impl<Context> Format<Context> for FormatLabelled<'_, Context> {
fn fmt(&self, f: &mut Formatter<Context>) -> FormatResult<()> {
let mut buffer = VecBuffer::new(f.state_mut());

buffer.write_fmt(Arguments::from(&self.content))?;
let content = buffer.into_vec();

let label = Label::new(self.label_id, content);

f.write_element(FormatElement::Label(label))
}
}

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

/// Inserts a single space. Allows to separate different tokens.
///
/// # Examples
Expand Down
58 changes: 58 additions & 0 deletions crates/rome_formatter/src/format_element.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@ use crate::{GroupId, TextSize};
#[cfg(target_pointer_width = "64")]
use rome_rowan::static_assert;
use rome_rowan::SyntaxTokenText;
#[cfg(debug_assertions)]
use std::any::type_name;
use std::any::TypeId;
use std::borrow::Cow;
use std::fmt::{self, Debug, Formatter};
use std::ops::Deref;
Expand Down Expand Up @@ -70,6 +73,11 @@ pub enum FormatElement {
/// An interned format element. Useful when the same content must be emitted multiple times to avoid
/// deep cloning the IR when using the `best_fitting!` macro or `if_group_fits_on_line` and `if_group_breaks`.
Interned(Interned),

/// Special semantic element marking the content with a label.
/// This does not directly influence how the content will be printed.
/// See [crate::labelled] for documentation.
Label(Label),
}

#[derive(Clone, Copy, Eq, PartialEq, Debug)]
Expand Down Expand Up @@ -150,6 +158,10 @@ impl Debug for FormatElement {
}
FormatElement::ExpandParent => write!(fmt, "ExpandParent"),
FormatElement::Interned(inner) => inner.fmt(fmt),
FormatElement::Label(label) => {
write!(fmt, "Label")?;
label.fmt(fmt)
}
}
}
}
Expand Down Expand Up @@ -352,6 +364,51 @@ impl Deref for Interned {
}
}

#[derive(Eq, PartialEq, Copy, Clone, Debug)]
pub struct LabelId {
id: TypeId,
#[cfg(debug_assertions)]
label: &'static str,
}

impl LabelId {
pub fn of<T: ?Sized + 'static>() -> Self {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's unstable api to make this function const.

Self {
id: TypeId::of::<T>(),
#[cfg(debug_assertions)]
label: type_name::<T>(),
}
}
}

#[derive(Clone, PartialEq, Eq)]
pub struct Label {
pub(crate) content: Box<[FormatElement]>,
label_id: LabelId,
}

impl Label {
pub fn new(label_id: LabelId, content: Vec<FormatElement>) -> Self {
Self {
content: content.into_boxed_slice(),
label_id,
}
}

pub fn label_id(&self) -> LabelId {
self.label_id
}
}

impl Debug for Label {
fn fmt(&self, fmt: &mut Formatter) -> fmt::Result {
fmt.debug_struct("")
.field("label_id", &self.label_id)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks:

label_id: LabelId {
        id: TypeId {
            t: 4451653586406524753,
        },
        label: "rome_formatter::arguments::tests::test_nesting::SomeChain",
    },

.field("content", &self.content)
.finish()
}
}

#[derive(Debug, Clone, Eq, PartialEq)]
pub struct ConditionalGroupContent {
pub(crate) content: Content,
Expand Down Expand Up @@ -513,6 +570,7 @@ impl FormatElement {
| FormatElement::Comment(content)
| FormatElement::Fill(Fill { content, .. })
| FormatElement::Verbatim(Verbatim { content, .. })
| FormatElement::Label(Label { content, .. })
| FormatElement::Indent(content) => content.iter().any(FormatElement::will_break),
FormatElement::List(list) => list.content.iter().any(FormatElement::will_break),
FormatElement::Token(token) => token.contains('\n'),
Expand Down
6 changes: 4 additions & 2 deletions crates/rome_formatter/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,12 +47,14 @@ pub use arguments::{Argument, Arguments};
pub use buffer::{Buffer, BufferExtensions, BufferSnapshot, Inspect, PreambleBuffer, VecBuffer};
pub use builders::{
block_indent, comment, empty_line, get_lines_before, group_elements, hard_line_break,
if_group_breaks, if_group_fits_on_line, indent, line_suffix, soft_block_indent,
if_group_breaks, if_group_fits_on_line, indent, labelled, line_suffix, soft_block_indent,
soft_line_break, soft_line_break_or_space, soft_line_indent_or_space, space_token, token,
BestFitting,
};
pub use comments::{CommentContext, CommentKind, SourceComment};
pub use format_element::{normalize_newlines, FormatElement, Token, Verbatim, LINE_TERMINATORS};
pub use format_element::{
normalize_newlines, FormatElement, LabelId, Token, Verbatim, LINE_TERMINATORS,
};
pub use group_id::GroupId;
use indexmap::IndexSet;
use rome_rowan::{
Expand Down
7 changes: 7 additions & 0 deletions crates/rome_formatter/src/printer/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -285,6 +285,12 @@ impl<'a> Printer<'a> {
}
}
FormatElement::Interned(content) => queue.enqueue(PrintElementCall::new(content, args)),
FormatElement::Label(label) => queue.extend(
label
.content
.iter()
.map(|element| PrintElementCall::new(element, args)),
),
}
}

Expand Down Expand Up @@ -819,6 +825,7 @@ fn fits_element_on_line<'a, 'rest>(
}
}
FormatElement::Interned(content) => queue.enqueue(PrintElementCall::new(content, args)),
FormatElement::Label(label) => queue.extend(label.content.iter(), args),
}

Fits::Maybe
Expand Down