mirror of
https://github.com/astral-sh/ruff.git
synced 2025-09-27 04:19:18 +00:00
Add rome_formatter
fork as ruff_formatter
(#2872)
The Ruff autoformatter is going to be based on an intermediate representation (IR) formatted via [Wadler's algorithm](https://homepages.inf.ed.ac.uk/wadler/papers/prettier/prettier.pdf). This is architecturally similar to [Rome](https://github.com/rome/tools), Prettier, [Skip](https://github.com/skiplang/skip/blob/master/src/tools/printer/printer.sk), and others. This PR adds a fork of the `rome_formatter` crate from [Rome](https://github.com/rome/tools), renamed here to `ruff_formatter`, which provides generic definitions for a formatter IR as well as a generic IR printer. (We've also pulled in `rome_rowan`, `rome_text_size`, and `rome_text_edit`, though some of these will be removed in future PRs.) Why fork? `rome_formatter` contains code that's specific to Rome's AST representation (e.g., it relies on a fork of rust-analyzer's `rowan`), and we'll likely want to support different abstractions and formatting capabilities (there are already a few changes coming in future PRs). Once we've dropped `ruff_rowan` and trimmed down `ruff_formatter` to the code we currently need, it's also not a huge surface area to maintain and update.
This commit is contained in:
parent
ac028cd9f8
commit
3ef1c2e303
83 changed files with 27547 additions and 1 deletions
714
crates/ruff_formatter/src/format_element/document.rs
Normal file
714
crates/ruff_formatter/src/format_element/document.rs
Normal file
|
@ -0,0 +1,714 @@
|
|||
use super::tag::Tag;
|
||||
use crate::format_element::tag::DedentMode;
|
||||
use crate::prelude::tag::GroupMode;
|
||||
use crate::prelude::*;
|
||||
use crate::printer::LineEnding;
|
||||
use crate::{format, write};
|
||||
use crate::{
|
||||
BufferExtensions, Format, FormatContext, FormatElement, FormatOptions, FormatResult, Formatter,
|
||||
IndentStyle, LineWidth, PrinterOptions, TransformSourceMap,
|
||||
};
|
||||
use ruff_rowan::TextSize;
|
||||
use rustc_hash::FxHashMap;
|
||||
use std::collections::HashMap;
|
||||
use std::ops::Deref;
|
||||
|
||||
/// A formatted document.
|
||||
#[derive(Debug, Clone, Eq, PartialEq, Default)]
|
||||
pub struct Document {
|
||||
elements: Vec<FormatElement>,
|
||||
}
|
||||
|
||||
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 [FormatElement::ExpandParent]
|
||||
///
|
||||
/// [`BestFitting`] elements act as expand boundaries, meaning that the fact that a
|
||||
/// [`BestFitting`]'s content expands is not propagated past the [`BestFitting`] element.
|
||||
///
|
||||
/// [`BestFitting`]: FormatElement::BestFitting
|
||||
pub(crate) fn propagate_expand(&mut self) {
|
||||
#[derive(Debug)]
|
||||
enum Enclosing<'a> {
|
||||
Group(&'a tag::Group),
|
||||
BestFitting,
|
||||
}
|
||||
|
||||
fn expand_parent(enclosing: &[Enclosing]) {
|
||||
if let Some(Enclosing::Group(group)) = enclosing.last() {
|
||||
group.propagate_expand();
|
||||
}
|
||||
}
|
||||
|
||||
fn propagate_expands<'a>(
|
||||
elements: &'a [FormatElement],
|
||||
enclosing: &mut Vec<Enclosing<'a>>,
|
||||
checked_interned: &mut FxHashMap<&'a Interned, bool>,
|
||||
) -> bool {
|
||||
let mut expands = false;
|
||||
for element in elements {
|
||||
let element_expands = match element {
|
||||
FormatElement::Tag(Tag::StartGroup(group)) => {
|
||||
enclosing.push(Enclosing::Group(group));
|
||||
false
|
||||
}
|
||||
FormatElement::Tag(Tag::EndGroup) => match enclosing.pop() {
|
||||
Some(Enclosing::Group(group)) => !group.mode().is_flat(),
|
||||
_ => false,
|
||||
},
|
||||
FormatElement::Interned(interned) => match checked_interned.get(interned) {
|
||||
Some(interned_expands) => *interned_expands,
|
||||
None => {
|
||||
let interned_expands =
|
||||
propagate_expands(interned, enclosing, checked_interned);
|
||||
checked_interned.insert(interned, interned_expands);
|
||||
interned_expands
|
||||
}
|
||||
},
|
||||
FormatElement::BestFitting(best_fitting) => {
|
||||
enclosing.push(Enclosing::BestFitting);
|
||||
|
||||
for variant in best_fitting.variants() {
|
||||
propagate_expands(variant, enclosing, checked_interned);
|
||||
}
|
||||
|
||||
// Best fitting acts as a boundary
|
||||
expands = false;
|
||||
enclosing.pop();
|
||||
continue;
|
||||
}
|
||||
FormatElement::StaticText { text } => text.contains('\n'),
|
||||
FormatElement::DynamicText { text, .. } => text.contains('\n'),
|
||||
FormatElement::SyntaxTokenTextSlice { slice, .. } => slice.contains('\n'),
|
||||
FormatElement::ExpandParent
|
||||
| FormatElement::Line(LineMode::Hard | LineMode::Empty) => true,
|
||||
_ => false,
|
||||
};
|
||||
|
||||
if element_expands {
|
||||
expands = true;
|
||||
expand_parent(enclosing)
|
||||
}
|
||||
}
|
||||
|
||||
expands
|
||||
}
|
||||
|
||||
let mut enclosing: Vec<Enclosing> = Vec::new();
|
||||
let mut interned: FxHashMap<&Interned, bool> = FxHashMap::default();
|
||||
propagate_expands(self, &mut enclosing, &mut interned);
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Vec<FormatElement>> for Document {
|
||||
fn from(elements: Vec<FormatElement>) -> Self {
|
||||
Self { elements }
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for Document {
|
||||
type Target = [FormatElement];
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
self.elements.as_slice()
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for Document {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
let formatted = format!(IrFormatContext::default(), [self.elements.as_slice()])
|
||||
.expect("Formatting not to throw any FormatErrors");
|
||||
|
||||
f.write_str(
|
||||
formatted
|
||||
.print()
|
||||
.expect("Expected a valid document")
|
||||
.as_code(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Default, Debug)]
|
||||
struct IrFormatContext {
|
||||
/// The interned elements that have been printed to this point
|
||||
printed_interned_elements: HashMap<Interned, usize>,
|
||||
}
|
||||
|
||||
impl FormatContext for IrFormatContext {
|
||||
type Options = IrFormatOptions;
|
||||
|
||||
fn options(&self) -> &Self::Options {
|
||||
&IrFormatOptions
|
||||
}
|
||||
|
||||
fn source_map(&self) -> Option<&TransformSourceMap> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
struct IrFormatOptions;
|
||||
|
||||
impl FormatOptions for IrFormatOptions {
|
||||
fn indent_style(&self) -> IndentStyle {
|
||||
IndentStyle::Space(2)
|
||||
}
|
||||
|
||||
fn line_width(&self) -> LineWidth {
|
||||
LineWidth(80)
|
||||
}
|
||||
|
||||
fn as_print_options(&self) -> PrinterOptions {
|
||||
PrinterOptions {
|
||||
tab_width: 2,
|
||||
print_width: self.line_width().into(),
|
||||
line_ending: LineEnding::LineFeed,
|
||||
indent_style: IndentStyle::Space(2),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Format<IrFormatContext> for &[FormatElement] {
|
||||
fn fmt(&self, f: &mut Formatter<IrFormatContext>) -> FormatResult<()> {
|
||||
use Tag::*;
|
||||
|
||||
write!(f, [ContentArrayStart])?;
|
||||
|
||||
let mut tag_stack = Vec::new();
|
||||
let mut first_element = true;
|
||||
let mut in_text = false;
|
||||
|
||||
let mut iter = self.iter().peekable();
|
||||
|
||||
while let Some(element) = iter.next() {
|
||||
if !first_element && !in_text && !element.is_end_tag() {
|
||||
// Write a separator between every two elements
|
||||
write!(f, [text(","), soft_line_break_or_space()])?;
|
||||
}
|
||||
|
||||
first_element = false;
|
||||
|
||||
match element {
|
||||
element @ FormatElement::Space
|
||||
| element @ FormatElement::StaticText { .. }
|
||||
| element @ FormatElement::DynamicText { .. }
|
||||
| element @ FormatElement::SyntaxTokenTextSlice { .. } => {
|
||||
if !in_text {
|
||||
write!(f, [text("\"")])?;
|
||||
}
|
||||
|
||||
in_text = true;
|
||||
|
||||
match element {
|
||||
FormatElement::Space => {
|
||||
write!(f, [text(" ")])?;
|
||||
}
|
||||
element if element.is_text() => f.write_element(element.clone())?,
|
||||
_ => unreachable!(),
|
||||
}
|
||||
|
||||
let is_next_text = iter.peek().map_or(false, |e| e.is_text() || e.is_space());
|
||||
|
||||
if !is_next_text {
|
||||
write!(f, [text("\"")])?;
|
||||
in_text = false;
|
||||
}
|
||||
}
|
||||
|
||||
FormatElement::Line(mode) => match mode {
|
||||
LineMode::SoftOrSpace => {
|
||||
write!(f, [text("soft_line_break_or_space")])?;
|
||||
}
|
||||
LineMode::Soft => {
|
||||
write!(f, [text("soft_line_break")])?;
|
||||
}
|
||||
LineMode::Hard => {
|
||||
write!(f, [text("hard_line_break")])?;
|
||||
}
|
||||
LineMode::Empty => {
|
||||
write!(f, [text("empty_line")])?;
|
||||
}
|
||||
},
|
||||
FormatElement::ExpandParent => {
|
||||
write!(f, [text("expand_parent")])?;
|
||||
}
|
||||
|
||||
FormatElement::LineSuffixBoundary => {
|
||||
write!(f, [text("line_suffix_boundary")])?;
|
||||
}
|
||||
|
||||
FormatElement::BestFitting(best_fitting) => {
|
||||
write!(f, [text("best_fitting([")])?;
|
||||
f.write_elements([
|
||||
FormatElement::Tag(StartIndent),
|
||||
FormatElement::Line(LineMode::Hard),
|
||||
])?;
|
||||
|
||||
for variant in best_fitting.variants() {
|
||||
write!(f, [variant.deref(), hard_line_break()])?;
|
||||
}
|
||||
|
||||
f.write_elements([
|
||||
FormatElement::Tag(EndIndent),
|
||||
FormatElement::Line(LineMode::Hard),
|
||||
])?;
|
||||
|
||||
write!(f, [text("])")])?;
|
||||
}
|
||||
|
||||
FormatElement::Interned(interned) => {
|
||||
let interned_elements = &mut f.context_mut().printed_interned_elements;
|
||||
|
||||
match interned_elements.get(interned).copied() {
|
||||
None => {
|
||||
let index = interned_elements.len();
|
||||
interned_elements.insert(interned.clone(), index);
|
||||
|
||||
write!(
|
||||
f,
|
||||
[
|
||||
dynamic_text(
|
||||
&std::format!("<interned {index}>"),
|
||||
TextSize::default()
|
||||
),
|
||||
space(),
|
||||
&interned.deref(),
|
||||
]
|
||||
)?;
|
||||
}
|
||||
Some(reference) => {
|
||||
write!(
|
||||
f,
|
||||
[dynamic_text(
|
||||
&std::format!("<ref interned *{reference}>"),
|
||||
TextSize::default()
|
||||
)]
|
||||
)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
FormatElement::Tag(tag) => {
|
||||
if tag.is_start() {
|
||||
first_element = true;
|
||||
tag_stack.push(tag.kind());
|
||||
}
|
||||
// Handle documents with mismatching start/end or superfluous end tags
|
||||
else {
|
||||
match tag_stack.pop() {
|
||||
None => {
|
||||
// Only write the end tag without any indent to ensure the output document is valid.
|
||||
write!(
|
||||
f,
|
||||
[
|
||||
text("<END_TAG_WITHOUT_START<"),
|
||||
dynamic_text(
|
||||
&std::format!("{:?}", tag.kind()),
|
||||
TextSize::default()
|
||||
),
|
||||
text(">>"),
|
||||
]
|
||||
)?;
|
||||
first_element = false;
|
||||
continue;
|
||||
}
|
||||
Some(start_kind) if start_kind != tag.kind() => {
|
||||
write!(
|
||||
f,
|
||||
[
|
||||
ContentArrayEnd,
|
||||
text(")"),
|
||||
soft_line_break_or_space(),
|
||||
text("ERROR<START_END_TAG_MISMATCH<start: "),
|
||||
dynamic_text(
|
||||
&std::format!("{start_kind:?}"),
|
||||
TextSize::default()
|
||||
),
|
||||
text(", end: "),
|
||||
dynamic_text(
|
||||
&std::format!("{:?}", tag.kind()),
|
||||
TextSize::default()
|
||||
),
|
||||
text(">>")
|
||||
]
|
||||
)?;
|
||||
first_element = false;
|
||||
continue;
|
||||
}
|
||||
_ => {
|
||||
// all ok
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
match tag {
|
||||
StartIndent => {
|
||||
write!(f, [text("indent(")])?;
|
||||
}
|
||||
|
||||
StartDedent(mode) => {
|
||||
let label = match mode {
|
||||
DedentMode::Level => "dedent",
|
||||
DedentMode::Root => "dedentRoot",
|
||||
};
|
||||
|
||||
write!(f, [text(label), text("(")])?;
|
||||
}
|
||||
|
||||
StartAlign(tag::Align(count)) => {
|
||||
write!(
|
||||
f,
|
||||
[
|
||||
text("align("),
|
||||
dynamic_text(&count.to_string(), TextSize::default()),
|
||||
text(","),
|
||||
space(),
|
||||
]
|
||||
)?;
|
||||
}
|
||||
|
||||
StartLineSuffix => {
|
||||
write!(f, [text("line_suffix(")])?;
|
||||
}
|
||||
|
||||
StartVerbatim(_) => {
|
||||
write!(f, [text("verbatim(")])?;
|
||||
}
|
||||
|
||||
StartGroup(group) => {
|
||||
write!(f, [text("group(")])?;
|
||||
|
||||
if let Some(group_id) = group.id() {
|
||||
write!(
|
||||
f,
|
||||
[
|
||||
dynamic_text(
|
||||
&std::format!("\"{group_id:?}\""),
|
||||
TextSize::default()
|
||||
),
|
||||
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,
|
||||
[
|
||||
text("indent_if_group_breaks("),
|
||||
dynamic_text(&std::format!("\"{id:?}\""), TextSize::default()),
|
||||
text(","),
|
||||
space(),
|
||||
]
|
||||
)?;
|
||||
}
|
||||
|
||||
StartConditionalContent(condition) => {
|
||||
match condition.mode {
|
||||
PrintMode::Flat => {
|
||||
write!(f, [text("if_group_fits_on_line(")])?;
|
||||
}
|
||||
PrintMode::Expanded => {
|
||||
write!(f, [text("if_group_breaks(")])?;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(group_id) = condition.group_id {
|
||||
write!(
|
||||
f,
|
||||
[
|
||||
dynamic_text(
|
||||
&std::format!("\"{group_id:?}\""),
|
||||
TextSize::default()
|
||||
),
|
||||
text(","),
|
||||
space(),
|
||||
]
|
||||
)?;
|
||||
}
|
||||
}
|
||||
|
||||
StartLabelled(label_id) => {
|
||||
write!(
|
||||
f,
|
||||
[
|
||||
text("label("),
|
||||
dynamic_text(
|
||||
&std::format!("\"{label_id:?}\""),
|
||||
TextSize::default()
|
||||
),
|
||||
text(","),
|
||||
space(),
|
||||
]
|
||||
)?;
|
||||
}
|
||||
|
||||
StartFill => {
|
||||
write!(f, [text("fill(")])?;
|
||||
}
|
||||
|
||||
StartEntry => {
|
||||
// handled after the match for all start tags
|
||||
}
|
||||
EndEntry => write!(f, [ContentArrayEnd])?,
|
||||
|
||||
EndFill
|
||||
| EndLabelled
|
||||
| EndConditionalContent
|
||||
| EndIndentIfGroupBreaks
|
||||
| EndAlign
|
||||
| EndIndent
|
||||
| EndGroup
|
||||
| EndLineSuffix
|
||||
| EndDedent
|
||||
| EndVerbatim => {
|
||||
write!(f, [ContentArrayEnd, text(")")])?;
|
||||
}
|
||||
};
|
||||
|
||||
if tag.is_start() {
|
||||
write!(f, [ContentArrayStart])?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
while let Some(top) = tag_stack.pop() {
|
||||
write!(
|
||||
f,
|
||||
[
|
||||
ContentArrayEnd,
|
||||
text(")"),
|
||||
soft_line_break_or_space(),
|
||||
dynamic_text(
|
||||
&std::format!("<START_WITHOUT_END<{top:?}>>"),
|
||||
TextSize::default()
|
||||
),
|
||||
]
|
||||
)?;
|
||||
}
|
||||
|
||||
write!(f, [ContentArrayEnd])
|
||||
}
|
||||
}
|
||||
|
||||
struct ContentArrayStart;
|
||||
|
||||
impl Format<IrFormatContext> for ContentArrayStart {
|
||||
fn fmt(&self, f: &mut Formatter<IrFormatContext>) -> FormatResult<()> {
|
||||
use Tag::*;
|
||||
|
||||
write!(f, [text("[")])?;
|
||||
|
||||
f.write_elements([
|
||||
FormatElement::Tag(StartGroup(tag::Group::new())),
|
||||
FormatElement::Tag(StartIndent),
|
||||
FormatElement::Line(LineMode::Soft),
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
struct ContentArrayEnd;
|
||||
|
||||
impl Format<IrFormatContext> for ContentArrayEnd {
|
||||
fn fmt(&self, f: &mut Formatter<IrFormatContext>) -> FormatResult<()> {
|
||||
use Tag::*;
|
||||
f.write_elements([
|
||||
FormatElement::Tag(EndIndent),
|
||||
FormatElement::Line(LineMode::Soft),
|
||||
FormatElement::Tag(EndGroup),
|
||||
])?;
|
||||
|
||||
write!(f, [text("]")])
|
||||
}
|
||||
}
|
||||
|
||||
impl FormatElements for [FormatElement] {
|
||||
fn will_break(&self) -> bool {
|
||||
use Tag::*;
|
||||
let mut ignore_depth = 0usize;
|
||||
|
||||
for element in self {
|
||||
match element {
|
||||
// Line suffix
|
||||
// Ignore if any of its content breaks
|
||||
FormatElement::Tag(StartLineSuffix) => {
|
||||
ignore_depth += 1;
|
||||
}
|
||||
FormatElement::Tag(EndLineSuffix) => {
|
||||
ignore_depth -= 1;
|
||||
}
|
||||
FormatElement::Interned(interned) if ignore_depth == 0 => {
|
||||
if interned.will_break() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
element if ignore_depth == 0 && element.will_break() => {
|
||||
return true;
|
||||
}
|
||||
_ => continue,
|
||||
}
|
||||
}
|
||||
|
||||
debug_assert_eq!(ignore_depth, 0, "Unclosed start container");
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
fn has_label(&self, expected: LabelId) -> bool {
|
||||
self.first()
|
||||
.map_or(false, |element| element.has_label(expected))
|
||||
}
|
||||
|
||||
fn start_tag(&self, kind: TagKind) -> Option<&Tag> {
|
||||
// Assert that the document ends at a tag with the specified kind;
|
||||
let _ = self.end_tag(kind)?;
|
||||
|
||||
fn traverse_slice<'a>(
|
||||
slice: &'a [FormatElement],
|
||||
kind: TagKind,
|
||||
depth: &mut usize,
|
||||
) -> Option<&'a Tag> {
|
||||
for element in slice.iter().rev() {
|
||||
match element {
|
||||
FormatElement::Tag(tag) if tag.kind() == kind => {
|
||||
if tag.is_start() {
|
||||
if *depth == 0 {
|
||||
// Invalid document
|
||||
return None;
|
||||
} else if *depth == 1 {
|
||||
return Some(tag);
|
||||
} else {
|
||||
*depth -= 1;
|
||||
}
|
||||
} else {
|
||||
*depth += 1;
|
||||
}
|
||||
}
|
||||
FormatElement::Interned(interned) => {
|
||||
match traverse_slice(interned, kind, depth) {
|
||||
Some(start) => {
|
||||
return Some(start);
|
||||
}
|
||||
// Reached end or invalid document
|
||||
None if *depth == 0 => {
|
||||
return None;
|
||||
}
|
||||
_ => {
|
||||
// continue with other elements
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
let mut depth = 0usize;
|
||||
|
||||
traverse_slice(self, kind, &mut depth)
|
||||
}
|
||||
|
||||
fn end_tag(&self, kind: TagKind) -> Option<&Tag> {
|
||||
self.last().and_then(|element| element.end_tag(kind))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::prelude::*;
|
||||
use crate::SimpleFormatContext;
|
||||
use crate::{format, format_args, write};
|
||||
|
||||
#[test]
|
||||
fn display_elements() {
|
||||
let formatted = format!(
|
||||
SimpleFormatContext::default(),
|
||||
[format_with(|f| {
|
||||
write!(
|
||||
f,
|
||||
[group(&format_args![
|
||||
text("("),
|
||||
soft_block_indent(&format_args![
|
||||
text("Some longer content"),
|
||||
space(),
|
||||
text("That should ultimately break"),
|
||||
])
|
||||
])]
|
||||
)
|
||||
})]
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let document = formatted.into_document();
|
||||
|
||||
assert_eq!(
|
||||
&std::format!("{document}"),
|
||||
r#"[
|
||||
group([
|
||||
"(",
|
||||
indent([
|
||||
soft_line_break,
|
||||
"Some longer content That should ultimately break"
|
||||
]),
|
||||
soft_line_break
|
||||
])
|
||||
]"#
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn display_invalid_document() {
|
||||
use Tag::*;
|
||||
|
||||
let document = Document::from(vec![
|
||||
FormatElement::StaticText { text: "[" },
|
||||
FormatElement::Tag(StartGroup(tag::Group::new())),
|
||||
FormatElement::Tag(StartIndent),
|
||||
FormatElement::Line(LineMode::Soft),
|
||||
FormatElement::StaticText { text: "a" },
|
||||
// Close group instead of indent
|
||||
FormatElement::Tag(EndGroup),
|
||||
FormatElement::Line(LineMode::Soft),
|
||||
FormatElement::Tag(EndIndent),
|
||||
FormatElement::StaticText { text: "]" },
|
||||
// End tag without start
|
||||
FormatElement::Tag(EndIndent),
|
||||
// Start tag without an end
|
||||
FormatElement::Tag(StartIndent),
|
||||
]);
|
||||
|
||||
assert_eq!(
|
||||
&std::format!("{document}"),
|
||||
r#"[
|
||||
"[",
|
||||
group([
|
||||
indent([soft_line_break, "a"])
|
||||
ERROR<START_END_TAG_MISMATCH<start: Indent, end: Group>>,
|
||||
soft_line_break
|
||||
])
|
||||
ERROR<START_END_TAG_MISMATCH<start: Group, end: Indent>>,
|
||||
"]"<END_TAG_WITHOUT_START<Indent>>,
|
||||
indent([])
|
||||
<START_WITHOUT_END<Indent>>
|
||||
]"#
|
||||
);
|
||||
}
|
||||
}
|
287
crates/ruff_formatter/src/format_element/tag.rs
Normal file
287
crates/ruff_formatter/src/format_element/tag.rs
Normal file
|
@ -0,0 +1,287 @@
|
|||
use crate::format_element::PrintMode;
|
||||
use crate::{GroupId, TextSize};
|
||||
#[cfg(debug_assertions)]
|
||||
use std::any::type_name;
|
||||
use std::any::TypeId;
|
||||
use std::cell::Cell;
|
||||
use std::num::NonZeroU8;
|
||||
|
||||
/// A Tag marking the start and end of some content to which some special formatting should be applied.
|
||||
///
|
||||
/// Tags always come in pairs of a start and an end tag and the styling defined by this tag
|
||||
/// will be applied to all elements in between the start/end tags.
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub enum Tag {
|
||||
/// Indents the content one level deeper, see [crate::builders::indent] for documentation and examples.
|
||||
StartIndent,
|
||||
EndIndent,
|
||||
|
||||
/// Variant of [TagKind::Indent] that indents content by a number of spaces. For example, `Align(2)`
|
||||
/// indents any content following a line break by an additional two spaces.
|
||||
///
|
||||
/// Nesting (Aligns)[TagKind::Align] has the effect that all except the most inner align are handled as (Indent)[TagKind::Indent].
|
||||
StartAlign(Align),
|
||||
EndAlign,
|
||||
|
||||
/// Reduces the indention of the specified content either by one level or to the root, depending on the mode.
|
||||
/// Reverse operation of `Indent` and can be used to *undo* an `Align` for nested content.
|
||||
StartDedent(DedentMode),
|
||||
EndDedent,
|
||||
|
||||
/// Creates a logical group where its content is either consistently printed:
|
||||
/// * on a single line: Omitting `LineMode::Soft` line breaks and printing spaces for `LineMode::SoftOrSpace`
|
||||
/// * on multiple lines: Printing all line breaks
|
||||
///
|
||||
/// See [crate::builders::group] for documentation and examples.
|
||||
StartGroup(Group),
|
||||
EndGroup,
|
||||
|
||||
/// Allows to specify content that gets printed depending on whatever the enclosing group
|
||||
/// is printed on a single line or multiple lines. See [crate::builders::if_group_breaks] for examples.
|
||||
StartConditionalContent(Condition),
|
||||
EndConditionalContent,
|
||||
|
||||
/// Optimized version of [Tag::StartConditionalContent] for the case where some content
|
||||
/// should be indented if the specified group breaks.
|
||||
StartIndentIfGroupBreaks(GroupId),
|
||||
EndIndentIfGroupBreaks,
|
||||
|
||||
/// Concatenates multiple elements together with a given separator printed in either
|
||||
/// flat or expanded mode to fill the print width. Expect that the content is a list of alternating
|
||||
/// [element, separator] See [crate::Formatter::fill].
|
||||
StartFill,
|
||||
EndFill,
|
||||
|
||||
/// Entry inside of a [Tag::StartFill]
|
||||
StartEntry,
|
||||
EndEntry,
|
||||
|
||||
/// Delay the printing of its content until the next line break
|
||||
StartLineSuffix,
|
||||
EndLineSuffix,
|
||||
|
||||
/// A token that tracks tokens/nodes that are printed as verbatim.
|
||||
StartVerbatim(VerbatimKind),
|
||||
EndVerbatim,
|
||||
|
||||
/// Special semantic element marking the content with a label.
|
||||
/// This does not directly influence how the content will be printed.
|
||||
///
|
||||
/// See [crate::builders::labelled] for documentation.
|
||||
StartLabelled(LabelId),
|
||||
EndLabelled,
|
||||
}
|
||||
|
||||
impl Tag {
|
||||
/// Returns `true` if `self` is any start tag.
|
||||
pub const fn is_start(&self) -> bool {
|
||||
matches!(
|
||||
self,
|
||||
Tag::StartIndent
|
||||
| Tag::StartAlign(_)
|
||||
| Tag::StartDedent(_)
|
||||
| Tag::StartGroup { .. }
|
||||
| Tag::StartConditionalContent(_)
|
||||
| Tag::StartIndentIfGroupBreaks(_)
|
||||
| Tag::StartFill
|
||||
| Tag::StartEntry
|
||||
| Tag::StartLineSuffix
|
||||
| Tag::StartVerbatim(_)
|
||||
| Tag::StartLabelled(_)
|
||||
)
|
||||
}
|
||||
|
||||
/// Returns `true` if `self` is any end tag.
|
||||
pub const fn is_end(&self) -> bool {
|
||||
!self.is_start()
|
||||
}
|
||||
|
||||
pub const fn kind(&self) -> TagKind {
|
||||
use Tag::*;
|
||||
|
||||
match self {
|
||||
StartIndent | EndIndent => TagKind::Indent,
|
||||
StartAlign(_) | EndAlign => TagKind::Align,
|
||||
StartDedent(_) | EndDedent => TagKind::Dedent,
|
||||
StartGroup(_) | EndGroup => TagKind::Group,
|
||||
StartConditionalContent(_) | EndConditionalContent => TagKind::ConditionalContent,
|
||||
StartIndentIfGroupBreaks(_) | EndIndentIfGroupBreaks => TagKind::IndentIfGroupBreaks,
|
||||
StartFill | EndFill => TagKind::Fill,
|
||||
StartEntry | EndEntry => TagKind::Entry,
|
||||
StartLineSuffix | EndLineSuffix => TagKind::LineSuffix,
|
||||
StartVerbatim(_) | EndVerbatim => TagKind::Verbatim,
|
||||
StartLabelled(_) | EndLabelled => TagKind::Labelled,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The kind of a [Tag].
|
||||
///
|
||||
/// Each start end tag pair has its own [tag kind](TagKind).
|
||||
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub enum TagKind {
|
||||
Indent,
|
||||
Align,
|
||||
Dedent,
|
||||
Group,
|
||||
ConditionalContent,
|
||||
IndentIfGroupBreaks,
|
||||
Fill,
|
||||
Entry,
|
||||
LineSuffix,
|
||||
Verbatim,
|
||||
Labelled,
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Default, Clone, Eq, PartialEq)]
|
||||
pub enum GroupMode {
|
||||
/// Print group in flat mode.
|
||||
#[default]
|
||||
Flat,
|
||||
|
||||
/// The group should be printed in expanded mode
|
||||
Expand,
|
||||
|
||||
/// Expand mode has been propagated from an enclosing group to this group.
|
||||
Propagated,
|
||||
}
|
||||
|
||||
impl GroupMode {
|
||||
pub const fn is_flat(&self) -> bool {
|
||||
matches!(self, GroupMode::Flat)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Eq, PartialEq, Default)]
|
||||
pub struct Group {
|
||||
id: Option<GroupId>,
|
||||
mode: Cell<GroupMode>,
|
||||
}
|
||||
|
||||
impl Group {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
id: None,
|
||||
mode: Cell::new(GroupMode::Flat),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_id(mut self, id: Option<GroupId>) -> Self {
|
||||
self.id = id;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_mode(mut self, mode: GroupMode) -> Self {
|
||||
self.mode = Cell::new(mode);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn mode(&self) -> GroupMode {
|
||||
self.mode.get()
|
||||
}
|
||||
|
||||
pub fn propagate_expand(&self) {
|
||||
if self.mode.get() == GroupMode::Flat {
|
||||
self.mode.set(GroupMode::Propagated)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn id(&self) -> Option<GroupId> {
|
||||
self.id
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
|
||||
pub enum DedentMode {
|
||||
/// Reduces the indent by a level (if the current indent is > 0)
|
||||
Level,
|
||||
|
||||
/// Reduces the indent to the root
|
||||
Root,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Eq, PartialEq)]
|
||||
pub struct Condition {
|
||||
/// * Flat -> Omitted if the enclosing group is a multiline group, printed for groups fitting on a single line
|
||||
/// * Multiline -> Omitted if the enclosing group fits on a single line, printed if the group breaks over multiple lines.
|
||||
pub(crate) mode: PrintMode,
|
||||
|
||||
/// The id of the group for which it should check if it breaks or not. The group must appear in the document
|
||||
/// before the conditional group (but doesn't have to be in the ancestor chain).
|
||||
pub(crate) group_id: Option<GroupId>,
|
||||
}
|
||||
|
||||
impl Condition {
|
||||
pub fn new(mode: PrintMode) -> Self {
|
||||
Self {
|
||||
mode,
|
||||
group_id: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_group_id(mut self, id: Option<GroupId>) -> Self {
|
||||
self.group_id = id;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn mode(&self) -> PrintMode {
|
||||
self.mode
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct Align(pub(crate) NonZeroU8);
|
||||
|
||||
impl Align {
|
||||
pub fn count(&self) -> NonZeroU8 {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Eq, PartialEq, Copy, Clone)]
|
||||
pub struct LabelId {
|
||||
id: TypeId,
|
||||
#[cfg(debug_assertions)]
|
||||
label: &'static str,
|
||||
}
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
impl std::fmt::Debug for LabelId {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.write_str(self.label)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(debug_assertions))]
|
||||
impl std::fmt::Debug for LabelId {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
std::write!(f, "#{:?}", self.id)
|
||||
}
|
||||
}
|
||||
|
||||
impl LabelId {
|
||||
pub fn of<T: ?Sized + 'static>() -> Self {
|
||||
Self {
|
||||
id: TypeId::of::<T>(),
|
||||
#[cfg(debug_assertions)]
|
||||
label: type_name::<T>(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Eq, PartialEq, Debug)]
|
||||
pub enum VerbatimKind {
|
||||
Bogus,
|
||||
Suppressed,
|
||||
Verbatim {
|
||||
/// the length of the formatted node
|
||||
length: TextSize,
|
||||
},
|
||||
}
|
||||
|
||||
impl VerbatimKind {
|
||||
pub const fn is_bogus(&self) -> bool {
|
||||
matches!(self, VerbatimKind::Bogus)
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue