mirror of
https://github.com/oxc-project/oxc.git
synced 2026-11-04 10:29:58 +00:00
feat(formatter_core): add prefix_align builder and Tag::(Start|End)Prefix (#26520)
Mainly for blockquote for Markdown. ```md > If this line is long and to be broken, how printer know new line needs `> ` prefix? ``` This will also help JSDoc formatting (`* ` prefix) if we update it via IR, currently just splitting lines.
This commit is contained in:
1 parent
d35d89f78c
commit
586f27a166
9 files changed
+516
-104
No files matched your search
@@ -14,7 +14,7 @@ use serde_json::Value;
|
||||
use oxc_allocator::{Allocator, ArenaStringBuilder, ArenaVec};
|
||||
use oxc_formatter_core::{
|
||||
Align, Condition, DedentMode, FormatElement, Group, GroupId, GroupMode, IndentWidth, LineMode,
|
||||
PrintMode, Tag, TextWidth, UniqueGroupIdBuilder, format_element::BestFittingElement,
|
||||
Prefix, PrintMode, Tag, TextWidth, UniqueGroupIdBuilder, format_element::BestFittingElement,
|
||||
};
|
||||
|
||||
/// Marker string used to represent `-Infinity` in JSON.
|
||||
@@ -325,8 +325,19 @@ fn convert_align<'a>(
|
||||
out.push(FormatElement::Tag(Tag::EndDedent(DedentMode::Root)));
|
||||
Ok(())
|
||||
}
|
||||
Value::String(s) => {
|
||||
// String alignment (e.g., " " for markdown list continuation indent).
|
||||
// A visible string align is a prefix on every line
|
||||
// (markdown's blockquote `"> "` is the only one Prettier's own printers emit):
|
||||
// intern here if a plugin brings another.
|
||||
Value::String(s) if s == "> " => {
|
||||
out.push(FormatElement::Tag(Tag::StartPrefix(Prefix::new(&"> "))));
|
||||
if let Some(contents) = obj.get("contents") {
|
||||
convert_doc(contents, out, ctx)?;
|
||||
}
|
||||
out.push(FormatElement::Tag(Tag::EndPrefix));
|
||||
Ok(())
|
||||
}
|
||||
Value::String(s) if s.trim().is_empty() => {
|
||||
// Whitespace alignment (e.g., " " for markdown list continuation indent).
|
||||
// Prettier uses the string length as the number of spaces to align by.
|
||||
if s.is_empty() {
|
||||
// Empty string → no alignment, just render contents
|
||||
@@ -580,6 +591,15 @@ mod tests {
|
||||
.into_code()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn blockquote_string_align_keeps_its_prefix() {
|
||||
let doc = json!([
|
||||
"> ",
|
||||
{ "type": "align", "n": "> ", "contents": ["a", { "type": "line", "hard": true }, "b"] }
|
||||
]);
|
||||
assert_eq!(print_doc(&doc, 80), "> a\n> b");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn conditional_group_selects_the_first_fitting_state() {
|
||||
let group = json!({
|
||||
|
||||
@@ -97,9 +97,17 @@ struct StackEntry {
|
||||
enum StartTagInfo {
|
||||
Indent,
|
||||
Align(u8),
|
||||
/// Prettier's string `align`
|
||||
Prefix(&'static str),
|
||||
Dedent(DedentMode),
|
||||
Group { id: Option<GroupId>, should_break: bool },
|
||||
ConditionalContent { mode: PrintMode, group_id: Option<GroupId> },
|
||||
Group {
|
||||
id: Option<GroupId>,
|
||||
should_break: bool,
|
||||
},
|
||||
ConditionalContent {
|
||||
mode: PrintMode,
|
||||
group_id: Option<GroupId>,
|
||||
},
|
||||
IndentIfGroupBreaks(GroupId),
|
||||
Fill,
|
||||
Entry,
|
||||
@@ -551,6 +559,7 @@ fn extract_start_tag_info(tag: &Tag) -> StartTagInfo {
|
||||
match tag {
|
||||
Tag::StartIndent => StartTagInfo::Indent,
|
||||
Tag::StartAlign(align) => StartTagInfo::Align(align.count().get()),
|
||||
Tag::StartPrefix(prefix) => StartTagInfo::Prefix(prefix.text()),
|
||||
Tag::StartDedent(mode) => StartTagInfo::Dedent(*mode),
|
||||
Tag::StartGroup(group) => {
|
||||
StartTagInfo::Group { id: group.id(), should_break: !group.mode().is_flat() }
|
||||
@@ -588,6 +597,9 @@ fn build_doc(start_info: Option<&StartTagInfo>, children: Vec<Value>) -> Value {
|
||||
StartTagInfo::Align(count) => {
|
||||
json!({"type": "align", "n": *count, "contents": normalize_array(children)})
|
||||
}
|
||||
StartTagInfo::Prefix(prefix) => {
|
||||
json!({"type": "align", "n": *prefix, "contents": normalize_array(children)})
|
||||
}
|
||||
StartTagInfo::MarkAsRoot => {
|
||||
// Prettier's `markAsRoot()` = `align({type: "root"}, ...)`
|
||||
json!({"type": "align", "n": {"type": "root"}, "contents": normalize_array(children)})
|
||||
@@ -816,7 +828,7 @@ mod tests {
|
||||
|
||||
use serde_json::{Value, json};
|
||||
|
||||
use oxc_formatter_core::{DedentMode, FormatElement, LineMode, Tag};
|
||||
use oxc_formatter_core::{DedentMode, FormatElement, LineMode, Prefix, Tag};
|
||||
|
||||
use super::{format_elements_to_prettier_doc, is_hard_line};
|
||||
|
||||
@@ -925,6 +937,16 @@ mod tests {
|
||||
assert_eq!(count_hardlines(&doc), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn prefix_align_is_a_string_align() {
|
||||
let doc = to_doc(&[
|
||||
FormatElement::Tag(Tag::StartPrefix(Prefix::new(&"> "))),
|
||||
A,
|
||||
FormatElement::Tag(Tag::EndPrefix),
|
||||
]);
|
||||
assert_eq!(doc["doc"], json!({"type": "align", "n": "> ", "contents": "a"}));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn root_dedent_is_null_and_flagged() {
|
||||
let doc = to_doc(&[
|
||||
|
||||
@@ -16,12 +16,12 @@ Formatting is two stages:
|
||||
|
||||
Key IR pieces are all exported from the crate root.
|
||||
|
||||
Every scope `Tag` acts on the line breaks inside it, never on the content (the `indent` rustdoc has the examples and the rationale).
|
||||
|
||||
The semantics of each building block live in the `write/builders.rs` rustdocs.
|
||||
e.g. the mechanisms for verbatim multi-line content (`exact_line_breaks()` for blank runs exempt from newline collapsing, `literal_line_break()`, multiline `text()`, `text(..).without_expand_parent()`, and `mark_as_root` / `dedent_to_root`), with the non-obvious behaviors pinned by printer tests verified against Prettier's `printDocToString`.
|
||||
|
||||
Prettier doc primitives are ported on demand; still missing: the `trim`.
|
||||
|
||||
Composite primitives translate rather than port 1:1:
|
||||
Every Prettier doc primitive its own language printers emit has a counterpart here; composite ones translate rather than port 1:1:
|
||||
|
||||
- `hardlineWithoutBreakParent` is `hard_line_break().without_expand_parent()`
|
||||
- `conditionalGroup` is `best_fitting!`: same expansion boundary, same flat-first variant trial.
|
||||
@@ -29,6 +29,7 @@ Composite primitives translate rather than port 1:1:
|
||||
that manual wiring is `ifBreak({groupId})` there and `if_group_breaks(..).with_group_id(..)` here
|
||||
See `oxc_formatter_yaml`'s `mapping_item.rs` for the full pattern.
|
||||
Oxfmt's Doc→IR mechanical conversion maps `expandedStates` to the same `BestFitting` primitive.
|
||||
- `align` is `prefix_align()`
|
||||
|
||||
### The printer never trims
|
||||
|
||||
|
||||
@@ -91,9 +91,10 @@ where
|
||||
fn fmt(&self, f: &mut Formatter<'_, 'a, C>) {
|
||||
use Tag::{
|
||||
EndAlign, EndConditionalContent, EndDedent, EndEntry, EndFill, EndGroup, EndIndent,
|
||||
EndIndentIfGroupBreaks, EndLabelled, EndLineSuffix, EndMarkAsRoot, StartAlign,
|
||||
StartConditionalContent, StartDedent, StartEntry, StartFill, StartGroup, StartIndent,
|
||||
StartIndentIfGroupBreaks, StartLabelled, StartLineSuffix, StartMarkAsRoot,
|
||||
EndIndentIfGroupBreaks, EndLabelled, EndLineSuffix, EndMarkAsRoot, EndPrefix,
|
||||
StartAlign, StartConditionalContent, StartDedent, StartEntry, StartFill, StartGroup,
|
||||
StartIndent, StartIndentIfGroupBreaks, StartLabelled, StartLineSuffix, StartMarkAsRoot,
|
||||
StartPrefix,
|
||||
};
|
||||
|
||||
w!(f, [ContentArrayStart]);
|
||||
@@ -322,6 +323,18 @@ where
|
||||
);
|
||||
}
|
||||
|
||||
StartPrefix(prefix) => {
|
||||
w!(
|
||||
f,
|
||||
[
|
||||
token("prefix_align("),
|
||||
text(f.allocator().alloc_str(&format!("{:?}", prefix.0))),
|
||||
token(","),
|
||||
space(),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
StartLineSuffix => {
|
||||
w!(f, [token("line_suffix(")]);
|
||||
}
|
||||
@@ -423,6 +436,7 @@ where
|
||||
| EndConditionalContent
|
||||
| EndIndentIfGroupBreaks(_)
|
||||
| EndAlign
|
||||
| EndPrefix
|
||||
| EndIndent
|
||||
| EndGroup
|
||||
| EndLineSuffix
|
||||
|
||||
@@ -21,6 +21,11 @@ pub enum Tag {
|
||||
StartAlign(Align),
|
||||
EndAlign,
|
||||
|
||||
/// Prints a string after the indention on every line break inside the content
|
||||
/// (Prettier's `align("> ", doc)`), see [crate::builders::prefix_align].
|
||||
StartPrefix(Prefix),
|
||||
EndPrefix,
|
||||
|
||||
/// 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),
|
||||
@@ -79,6 +84,7 @@ impl Tag {
|
||||
self,
|
||||
Tag::StartIndent
|
||||
| Tag::StartAlign(_)
|
||||
| Tag::StartPrefix(_)
|
||||
| Tag::StartDedent(_)
|
||||
| Tag::StartGroup { .. }
|
||||
| Tag::StartConditionalContent(_)
|
||||
@@ -99,14 +105,16 @@ impl Tag {
|
||||
pub const fn kind(&self) -> TagKind {
|
||||
use Tag::{
|
||||
EndAlign, EndConditionalContent, EndDedent, EndEntry, EndFill, EndGroup, EndIndent,
|
||||
EndIndentIfGroupBreaks, EndLabelled, EndLineSuffix, EndMarkAsRoot, StartAlign,
|
||||
StartConditionalContent, StartDedent, StartEntry, StartFill, StartGroup, StartIndent,
|
||||
StartIndentIfGroupBreaks, StartLabelled, StartLineSuffix, StartMarkAsRoot,
|
||||
EndIndentIfGroupBreaks, EndLabelled, EndLineSuffix, EndMarkAsRoot, EndPrefix,
|
||||
StartAlign, StartConditionalContent, StartDedent, StartEntry, StartFill, StartGroup,
|
||||
StartIndent, StartIndentIfGroupBreaks, StartLabelled, StartLineSuffix, StartMarkAsRoot,
|
||||
StartPrefix,
|
||||
};
|
||||
|
||||
match self {
|
||||
StartIndent | EndIndent => TagKind::Indent,
|
||||
StartAlign(_) | EndAlign => TagKind::Align,
|
||||
StartPrefix(_) | EndPrefix => TagKind::Prefix,
|
||||
StartDedent(_) | EndDedent(_) => TagKind::Dedent,
|
||||
StartGroup(_) | EndGroup => TagKind::Group,
|
||||
StartConditionalContent(_) | EndConditionalContent => TagKind::ConditionalContent,
|
||||
@@ -127,6 +135,7 @@ impl Tag {
|
||||
pub enum TagKind {
|
||||
Indent,
|
||||
Align,
|
||||
Prefix,
|
||||
Dedent,
|
||||
Group,
|
||||
ConditionalContent,
|
||||
@@ -249,6 +258,23 @@ impl Align {
|
||||
}
|
||||
}
|
||||
|
||||
/// The string a [Tag::StartPrefix] prints after the indention on every new line.
|
||||
///
|
||||
/// Prefixes are syntax tokens (`"> "`, `" * "`), so `'static`;
|
||||
/// the double reference keeps the payload one pointer wide (see the size assertion in `format_element/mod.rs`).
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub struct Prefix(pub(crate) &'static &'static str);
|
||||
|
||||
impl Prefix {
|
||||
pub fn new(text: &'static &'static str) -> Self {
|
||||
Self(text)
|
||||
}
|
||||
|
||||
pub fn text(self) -> &'static str {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
/// Identifies a label applied via [crate::builders::labelled].
|
||||
///
|
||||
/// The label's debug name lives in a side table, not an inline field,
|
||||
|
||||
@@ -36,7 +36,7 @@ pub use format_element::document::Document;
|
||||
pub use format_element::formatted::Formatted;
|
||||
pub use format_element::group_id::{GroupId, UniqueGroupIdBuilder};
|
||||
pub use format_element::tag::{
|
||||
self, Align, Condition, DedentMode, Group, GroupMode, Label, LabelId, Tag, TagKind,
|
||||
self, Align, Condition, DedentMode, Group, GroupMode, Label, LabelId, Prefix, Tag, TagKind,
|
||||
};
|
||||
pub use format_element::{
|
||||
BestFittingElement, FormatElement, FormatElements, Interned, LINE_TERMINATORS, LineMode,
|
||||
|
||||
@@ -3,7 +3,7 @@ use std::{fmt::Debug, num::NonZeroU8};
|
||||
use crate::{IndentStyle, InvalidDocumentError, PrintError, PrintMode, PrintResult, TagKind};
|
||||
|
||||
use super::{
|
||||
Indention,
|
||||
Indention, PrefixNode,
|
||||
stack::{Stack, StackedStack},
|
||||
};
|
||||
|
||||
@@ -247,6 +247,16 @@ pub(super) trait IndentStack {
|
||||
let next_indent = self.indention().set_align(count);
|
||||
self.current_stack_mut().push(next_indent);
|
||||
}
|
||||
/// `Tag::StartPrefix`; `nodes` is the printer's prefix arena.
|
||||
fn prefix(
|
||||
&mut self,
|
||||
prefix: &'static str,
|
||||
nodes: &mut Vec<PrefixNode>,
|
||||
indent_style: IndentStyle,
|
||||
) {
|
||||
let next_indent = self.indention().set_prefix(prefix, nodes, indent_style);
|
||||
self.current_stack_mut().push(next_indent);
|
||||
}
|
||||
}
|
||||
|
||||
/// Indent stack used for storing indention history when printing the [`FormatElement`](crate::FormatElement)s
|
||||
|
||||
@@ -13,7 +13,7 @@ use unicode_width::UnicodeWidthChar;
|
||||
|
||||
use crate::{
|
||||
ActualStart, BestFittingElement, Condition, DedentMode, FormatElement, GroupId, IndentStyle,
|
||||
InvalidDocumentError, LineMode, PrintError, PrintMode, Tag, TagKind, TextWidth,
|
||||
IndentWidth, InvalidDocumentError, LineMode, PrintError, PrintMode, Tag, TagKind, TextWidth,
|
||||
};
|
||||
|
||||
use self::call_stack::{
|
||||
@@ -104,7 +104,8 @@ impl<'a> Printer<'a> {
|
||||
) -> PrintResult<Printed> {
|
||||
let mut stack = PrintCallStack::new(PrintElementArgs::new());
|
||||
let mut queue: PrintQueue<'a> = PrintQueue::new(document);
|
||||
let mut indent_stack = PrintIndentStack::new(Indention::Level(indent));
|
||||
let mut indent_stack =
|
||||
PrintIndentStack::new(Indention { level: indent, ..Indention::default() });
|
||||
|
||||
while let Some(element) = queue.pop() {
|
||||
self.print_element(&mut stack, &mut indent_stack, &mut queue, element)?;
|
||||
@@ -127,9 +128,10 @@ impl<'a> Printer<'a> {
|
||||
) -> PrintResult<()> {
|
||||
use Tag::{
|
||||
EndAlign, EndConditionalContent, EndDedent, EndEntry, EndFill, EndGroup, EndIndent,
|
||||
EndIndentIfGroupBreaks, EndLabelled, EndLineSuffix, EndMarkAsRoot, StartAlign,
|
||||
StartConditionalContent, StartDedent, StartEntry, StartFill, StartGroup, StartIndent,
|
||||
StartIndentIfGroupBreaks, StartLabelled, StartLineSuffix, StartMarkAsRoot,
|
||||
EndIndentIfGroupBreaks, EndLabelled, EndLineSuffix, EndMarkAsRoot, EndPrefix,
|
||||
StartAlign, StartConditionalContent, StartDedent, StartEntry, StartFill, StartGroup,
|
||||
StartIndent, StartIndentIfGroupBreaks, StartLabelled, StartLineSuffix, StartMarkAsRoot,
|
||||
StartPrefix,
|
||||
};
|
||||
|
||||
let args = stack.top();
|
||||
@@ -176,12 +178,12 @@ impl<'a> Printer<'a> {
|
||||
// A blank line is left behind from the start of a line always,
|
||||
// mid-line only when a break remains after the line-ending one.
|
||||
let leaves_blank = count > 1 || self.state.line_width == 0;
|
||||
let indention = indent_stack.indention();
|
||||
for _ in 0..count {
|
||||
self.print_char('\n');
|
||||
self.print_line_break(indention);
|
||||
}
|
||||
self.state.has_empty_line = leaves_blank;
|
||||
self.state.pending_space = false;
|
||||
self.state.pending_indent = indent_stack.indention();
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
@@ -194,8 +196,7 @@ impl<'a> Printer<'a> {
|
||||
// when a line break follows; this printer never trims, so it must not write indention that no content claims.
|
||||
// See the [literal_line_break] divergence note)
|
||||
self.flush_pending_indention_and_space();
|
||||
self.print_char('\n');
|
||||
self.state.pending_indent = indent_stack.root();
|
||||
self.print_line_break(indent_stack.root());
|
||||
self.state.has_empty_line = false;
|
||||
return Ok(());
|
||||
}
|
||||
@@ -205,19 +206,20 @@ impl<'a> Printer<'a> {
|
||||
// NOTE: no end-of-line trimming happens here (unlike Prettier's `printDocToString`):
|
||||
// the pending space/indent mechanisms never materialize at a line end,
|
||||
// and Text/Token content is the emitter's responsibility to keep trimmed.
|
||||
let indention = indent_stack.indention();
|
||||
if self.state.line_width > 0 {
|
||||
self.print_char('\n');
|
||||
self.print_line_break(indention);
|
||||
self.state.has_empty_line = false;
|
||||
}
|
||||
|
||||
// Print a second line break if this is an empty line
|
||||
if line_mode == &LineMode::Empty && !self.state.has_empty_line {
|
||||
self.print_char('\n');
|
||||
self.print_line_break(indention);
|
||||
self.state.has_empty_line = true;
|
||||
}
|
||||
|
||||
self.state.pending_space = false;
|
||||
self.state.pending_indent = indent_stack.indention();
|
||||
self.state.pending_indent = indention;
|
||||
}
|
||||
|
||||
FormatElement::ExpandParent => {
|
||||
@@ -296,6 +298,15 @@ impl<'a> Printer<'a> {
|
||||
stack.push(TagKind::Align, args);
|
||||
}
|
||||
|
||||
FormatElement::Tag(StartPrefix(prefix)) => {
|
||||
indent_stack.prefix(
|
||||
prefix.0,
|
||||
&mut self.state.prefix_nodes,
|
||||
self.options.indent_style(),
|
||||
);
|
||||
stack.push(TagKind::Prefix, args);
|
||||
}
|
||||
|
||||
FormatElement::Tag(StartMarkAsRoot) => {
|
||||
indent_stack.mark_root();
|
||||
stack.push(TagKind::MarkAsRoot, args);
|
||||
@@ -349,7 +360,7 @@ impl<'a> Printer<'a> {
|
||||
}
|
||||
stack.pop(tag.kind())?;
|
||||
}
|
||||
FormatElement::Tag(tag @ (EndIndent | EndAlign | EndLineSuffix)) => {
|
||||
FormatElement::Tag(tag @ (EndIndent | EndAlign | EndPrefix | EndLineSuffix)) => {
|
||||
stack.pop(tag.kind())?;
|
||||
indent_stack.pop();
|
||||
}
|
||||
@@ -736,25 +747,58 @@ impl<'a> Printer<'a> {
|
||||
invalid_end_tag(TagKind::Entry, stack.top_kind())
|
||||
}
|
||||
|
||||
/// Prints `indention` (indent levels + align spaces) and adds its width to the current line.
|
||||
/// Prints `indention` (prefix chain, indent levels, align spaces) and adds its width to the current line.
|
||||
#[inline]
|
||||
fn print_indention(&mut self, indention: Indention) {
|
||||
if indention.is_empty() {
|
||||
return;
|
||||
}
|
||||
self.state.line_width +=
|
||||
indention.width(&self.state.prefix_nodes, self.options.indent_width());
|
||||
if indention.prefix.is_some() {
|
||||
self.emit_prefix_chain(indention, false);
|
||||
}
|
||||
self.emit_level_and_align(indention);
|
||||
}
|
||||
|
||||
let level = indention.level() as usize;
|
||||
self.state.buffer.print_indent(level);
|
||||
self.state.line_width += level * self.options.indent_width().value() as usize;
|
||||
/// Writes `indention`'s prefix chain up to and including its last prefix
|
||||
/// (`trim_last`: that one without its trailing whitespace).
|
||||
/// Width is the caller's business.
|
||||
fn emit_prefix_chain(&mut self, indention: Indention, trim_last: bool) {
|
||||
let node =
|
||||
PrefixNode::get(&self.state.prefix_nodes, indention.prefix.expect("has a prefix"));
|
||||
if node.before.prefix.is_some() {
|
||||
self.emit_prefix_chain(node.before, false);
|
||||
}
|
||||
self.emit_level_and_align(node.before);
|
||||
self.state.buffer.print_str(if trim_last { node.prefix.trim_end() } else { node.prefix });
|
||||
}
|
||||
|
||||
let align_count = indention.align() as usize;
|
||||
for _ in 0..align_count {
|
||||
#[inline]
|
||||
fn emit_level_and_align(&mut self, indention: Indention) {
|
||||
self.state.buffer.print_indent(indention.level as usize);
|
||||
for _ in 0..indention.align {
|
||||
// SAFETY: `' '` is an valid ASCII character
|
||||
unsafe {
|
||||
self.state.buffer.print_byte_unchecked(b' ');
|
||||
}
|
||||
}
|
||||
self.state.line_width += align_count;
|
||||
}
|
||||
|
||||
/// Ends the current line; `next` becomes the pending indention of the new one.
|
||||
///
|
||||
/// Ending an EMPTY line inside `prefix_align` still prints its prefixes, the last one trimmed:
|
||||
/// Prettier writes the whole indention at every newline and trims trailing whitespace at the next,
|
||||
/// so a blank line in a blockquote is `>`.
|
||||
/// The levels / aligns after the last prefix are that whitespace.
|
||||
/// (A literal line materializes the pending indention itself first, so nothing is left for this.)
|
||||
#[inline]
|
||||
fn print_line_break(&mut self, next: Indention) {
|
||||
if self.state.line_width == 0 && self.state.pending_indent.prefix.is_some() {
|
||||
self.emit_prefix_chain(self.state.pending_indent, true);
|
||||
}
|
||||
self.print_char('\n');
|
||||
self.state.pending_indent = next;
|
||||
}
|
||||
|
||||
/// Materializes the pending indention and pending space before printing visible content.
|
||||
@@ -876,6 +920,8 @@ struct PrinterState<'a> {
|
||||
fits_stack_tem_indent: Vec<Indention>,
|
||||
fits_root_indent_stack: Vec<Indention>,
|
||||
fits_queue: Vec<&'a [FormatElement<'a>]>,
|
||||
/// Every `prefix_align` entered so far (see [Indention]).
|
||||
prefix_nodes: Vec<PrefixNode>,
|
||||
/// Sorted Tailwind CSS classes for lookup during printing
|
||||
sorted_tailwind_classes: &'a [String],
|
||||
}
|
||||
@@ -919,81 +965,112 @@ impl GroupModes {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Eq, PartialEq, Debug)]
|
||||
enum Indention {
|
||||
/// Indent the content by `count` levels by using the indention sequence specified by the printer options.
|
||||
Level(u16),
|
||||
/// The indention of a line: `level` indent levels, `align` spaces,
|
||||
/// and before them (when `prefix` is set) the chain of `prefix_align` strings in effect,
|
||||
/// each preceded by the indention it was added to.
|
||||
///
|
||||
/// `Copy`, so the stacks and the pending indent copy it freely;
|
||||
/// a prefix chain is one index into [PrinterState::prefix_nodes], created by the `StartPrefix` tag only.
|
||||
#[derive(Copy, Clone, Eq, PartialEq, Debug, Default)]
|
||||
struct Indention {
|
||||
/// Indent levels after the last prefix (all of them when there is none).
|
||||
level: u16,
|
||||
/// Align spaces after the last prefix.
|
||||
align: u8,
|
||||
/// How many `align` tags contributed to `align`
|
||||
/// (each becomes one tab in tab mode, see [Self::flush_align]).
|
||||
align_count: u8,
|
||||
/// The innermost `prefix_align` in effect.
|
||||
prefix: Option<PrefixId>,
|
||||
}
|
||||
|
||||
/// Indent the content by n-`level`s using the indention sequence specified by the printer options and `align` spaces.
|
||||
Align { level: u16, align: NonZeroU8, align_count: u16 },
|
||||
/// Index + 1 into [PrinterState::prefix_nodes].
|
||||
type PrefixId = std::num::NonZeroU32;
|
||||
|
||||
/// One `prefix_align` in effect.
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
struct PrefixNode {
|
||||
/// What prints before the prefix: the indention the prefix was added to (its own chain included).
|
||||
before: Indention,
|
||||
prefix: &'static str,
|
||||
}
|
||||
|
||||
impl PrefixNode {
|
||||
#[inline]
|
||||
fn get(nodes: &[PrefixNode], id: PrefixId) -> Self {
|
||||
nodes[id.get() as usize - 1]
|
||||
}
|
||||
}
|
||||
|
||||
impl Indention {
|
||||
#[inline]
|
||||
const fn is_empty(self) -> bool {
|
||||
matches!(self, Indention::Level(0))
|
||||
self.level == 0 && self.align == 0 && self.prefix.is_none()
|
||||
}
|
||||
|
||||
/// Creates a new indention level with a zero-indent.
|
||||
const fn new() -> Self {
|
||||
Indention::Level(0)
|
||||
}
|
||||
|
||||
/// Returns the indention level
|
||||
fn level(self) -> u16 {
|
||||
match self {
|
||||
Indention::Level(count) => count,
|
||||
Indention::Align { level: indent, .. } => indent,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the number of trailing align spaces or 0 if none
|
||||
fn align(self) -> u8 {
|
||||
match self {
|
||||
Indention::Level(_) => 0,
|
||||
Indention::Align { align, .. } => align.into(),
|
||||
/// In tab mode every `align` tag so far becomes one indent level
|
||||
/// (Prettier's `makeAlign`: aligns before an indent or a string align are flushed to tabs).
|
||||
fn flush_align(self, indent_style: IndentStyle) -> Self {
|
||||
if self.align > 0 && indent_style.is_tab() {
|
||||
Self {
|
||||
level: self.level + u16::from(self.align_count),
|
||||
align: 0,
|
||||
align_count: 0,
|
||||
..self
|
||||
}
|
||||
} else {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// Increments the level by one.
|
||||
///
|
||||
/// The behaviour depends on the [`indent_style`][IndentStyle] if this is an [`Indention::Align`]:
|
||||
/// * **Tabs**: `align` is converted into an indent. This results in `level` increasing by two: once for the align, once for the level increment
|
||||
/// * **Spaces**: Increments the `level` by one and keeps the `align` unchanged.
|
||||
fn increment_level(self, indent_style: IndentStyle) -> Self {
|
||||
match self {
|
||||
Indention::Level(count) => Indention::Level(count + 1),
|
||||
// Increase the indent AND convert the align to an indent
|
||||
Indention::Align { level, align_count, .. } if indent_style.is_tab() => {
|
||||
Indention::Level(level + align_count + 1)
|
||||
}
|
||||
Indention::Align { level: indent, align, align_count } => {
|
||||
Indention::Align { level: indent + 1, align, align_count }
|
||||
}
|
||||
}
|
||||
let flushed = self.flush_align(indent_style);
|
||||
Self { level: flushed.level + 1, ..flushed }
|
||||
}
|
||||
|
||||
/// Adds an `align` of `count` spaces to the current indention.
|
||||
///
|
||||
/// It increments the `level` value if the current value is [`Indention::Align`].
|
||||
fn set_align(self, count: NonZeroU8) -> Self {
|
||||
match self {
|
||||
Indention::Level(indent_count) => {
|
||||
Indention::Align { level: indent_count, align: count, align_count: 1 }
|
||||
}
|
||||
|
||||
// Convert the existing align to an indent
|
||||
Indention::Align { level: indent, align, align_count } => Indention::Align {
|
||||
level: indent,
|
||||
align: align.saturating_add(count.get()),
|
||||
align_count: align_count + 1,
|
||||
},
|
||||
Self {
|
||||
align: self.align.saturating_add(count.get()),
|
||||
align_count: self.align_count.saturating_add(1),
|
||||
..self
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Indention {
|
||||
fn default() -> Self {
|
||||
Indention::new()
|
||||
/// Appends `prefix`: the current indention becomes what prints before it (`nodes` records that),
|
||||
/// and the new indention starts empty after it.
|
||||
fn set_prefix(
|
||||
self,
|
||||
prefix: &'static str,
|
||||
nodes: &mut Vec<PrefixNode>,
|
||||
indent_style: IndentStyle,
|
||||
) -> Self {
|
||||
nodes.push(PrefixNode { before: self.flush_align(indent_style), prefix });
|
||||
let id = PrefixId::new(u32::try_from(nodes.len()).expect("prefix count fits u32"))
|
||||
.expect("nodes.len() > 0 after push");
|
||||
Self { prefix: Some(id), ..Self::default() }
|
||||
}
|
||||
|
||||
/// Columns this indention occupies.
|
||||
#[inline]
|
||||
fn width(self, nodes: &[PrefixNode], indent_width: IndentWidth) -> usize {
|
||||
let own = self.level as usize * indent_width.value() as usize + self.align as usize;
|
||||
match self.prefix {
|
||||
None => own,
|
||||
Some(id) => own + Self::chain_width(nodes, id, indent_width),
|
||||
}
|
||||
}
|
||||
|
||||
// PERF: Out of line so that `width` -> `fits_text` (every measured token),
|
||||
// stays small enough to be inlined into `fits`.
|
||||
#[inline(never)]
|
||||
fn chain_width(nodes: &[PrefixNode], id: PrefixId, indent_width: IndentWidth) -> usize {
|
||||
let node = PrefixNode::get(nodes, id);
|
||||
let own =
|
||||
node.before.level as usize * indent_width.value() as usize + node.before.align as usize;
|
||||
let before = node.before.prefix.map_or(0, |id| Self::chain_width(nodes, id, indent_width));
|
||||
before + own + TextWidth::from_text(node.prefix, indent_width).value() as usize
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1013,6 +1090,8 @@ struct FitsMeasurer<'a, 'print> {
|
||||
/// The separator mode above only applies to entries of the fill the printer is currently printing (depth 0),
|
||||
/// not to entries of nested fills the walk enters on its own.
|
||||
fill_depth: u32,
|
||||
/// Prefix nodes the measurement creates are dropped again in [Self::finish] (nothing printed refers to them).
|
||||
prefix_nodes_len: usize,
|
||||
}
|
||||
|
||||
impl<'a, 'print> FitsMeasurer<'a, 'print> {
|
||||
@@ -1066,6 +1145,7 @@ impl<'a, 'print> FitsMeasurer<'a, 'print> {
|
||||
};
|
||||
|
||||
let fill_separator_mode = printer.state.fill_separator_mode;
|
||||
let prefix_nodes_len = printer.state.prefix_nodes.len();
|
||||
|
||||
Self {
|
||||
state: fits_state,
|
||||
@@ -1075,6 +1155,7 @@ impl<'a, 'print> FitsMeasurer<'a, 'print> {
|
||||
must_be_flat: false,
|
||||
fill_separator_mode,
|
||||
fill_depth: 0,
|
||||
prefix_nodes_len,
|
||||
printer,
|
||||
}
|
||||
}
|
||||
@@ -1146,9 +1227,10 @@ impl<'a, 'print> FitsMeasurer<'a, 'print> {
|
||||
fn fits_element(&mut self, element: &'a FormatElement) -> PrintResult<Fits> {
|
||||
use Tag::{
|
||||
EndAlign, EndConditionalContent, EndDedent, EndEntry, EndFill, EndGroup, EndIndent,
|
||||
EndIndentIfGroupBreaks, EndLabelled, EndLineSuffix, EndMarkAsRoot, StartAlign,
|
||||
StartConditionalContent, StartDedent, StartEntry, StartFill, StartGroup, StartIndent,
|
||||
StartIndentIfGroupBreaks, StartLabelled, StartLineSuffix, StartMarkAsRoot,
|
||||
EndIndentIfGroupBreaks, EndLabelled, EndLineSuffix, EndMarkAsRoot, EndPrefix,
|
||||
StartAlign, StartConditionalContent, StartDedent, StartEntry, StartFill, StartGroup,
|
||||
StartIndent, StartIndentIfGroupBreaks, StartLabelled, StartLineSuffix, StartMarkAsRoot,
|
||||
StartPrefix,
|
||||
};
|
||||
|
||||
let args = self.stack.top();
|
||||
@@ -1287,6 +1369,15 @@ impl<'a, 'print> FitsMeasurer<'a, 'print> {
|
||||
self.stack.push(TagKind::Align, args);
|
||||
}
|
||||
|
||||
FormatElement::Tag(StartPrefix(prefix)) => {
|
||||
self.indent_stack.prefix(
|
||||
prefix.0,
|
||||
&mut self.printer.state.prefix_nodes,
|
||||
self.printer.options.indent_style(),
|
||||
);
|
||||
self.stack.push(TagKind::Prefix, args);
|
||||
}
|
||||
|
||||
FormatElement::Tag(StartGroup(group)) => {
|
||||
if self.must_be_flat && !group.mode().is_flat() {
|
||||
return Ok(Fits::No);
|
||||
@@ -1388,7 +1479,7 @@ impl<'a, 'print> FitsMeasurer<'a, 'print> {
|
||||
}
|
||||
self.stack.pop(tag.kind())?;
|
||||
}
|
||||
FormatElement::Tag(tag @ (EndIndent | EndAlign)) => {
|
||||
FormatElement::Tag(tag @ (EndIndent | EndAlign | EndPrefix)) => {
|
||||
self.stack.pop(tag.kind())?;
|
||||
self.indent_stack.pop();
|
||||
}
|
||||
@@ -1421,9 +1512,8 @@ impl<'a, 'print> FitsMeasurer<'a, 'print> {
|
||||
|
||||
fn fits_text(&mut self, text: Text) -> Fits {
|
||||
let indent = std::mem::take(&mut self.state.pending_indent);
|
||||
self.state.line_width += indent.level() as usize
|
||||
* self.options().indent_width().value() as usize
|
||||
+ indent.align() as usize;
|
||||
self.state.line_width +=
|
||||
indent.width(&self.printer.state.prefix_nodes, self.options().indent_width());
|
||||
|
||||
if self.state.pending_space {
|
||||
self.state.line_width += 1;
|
||||
@@ -1459,6 +1549,7 @@ impl<'a, 'print> FitsMeasurer<'a, 'print> {
|
||||
}
|
||||
|
||||
fn finish(self) {
|
||||
self.printer.state.prefix_nodes.truncate(self.prefix_nodes_len);
|
||||
let mut queue = self.queue.finish();
|
||||
queue.clear();
|
||||
self.printer.state.fits_queue = queue;
|
||||
@@ -1564,7 +1655,7 @@ mod tests {
|
||||
builders::{
|
||||
align, block_indent, dedent_to_root, empty_line, exact_line_breaks, group,
|
||||
hard_line_break, if_group_breaks, if_group_fits_on_line, indent, line_suffix,
|
||||
literal_line_break, mark_as_root, soft_block_indent, soft_line_break,
|
||||
literal_line_break, mark_as_root, prefix_align, soft_block_indent, soft_line_break,
|
||||
soft_line_break_or_space, space, text, token,
|
||||
},
|
||||
format_args,
|
||||
@@ -1640,6 +1731,169 @@ mod tests {
|
||||
assert_eq!("a\n\n\n\nb", formatted.as_code());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn prefix_align_prints_the_prefix_on_every_new_line() {
|
||||
let allocator = Allocator::default();
|
||||
let formatted = format_simple(
|
||||
&allocator,
|
||||
&format_args!(
|
||||
token("> "),
|
||||
prefix_align(&"> ", &format_args!(token("a"), hard_line_break(), token("b"))),
|
||||
hard_line_break(),
|
||||
token("c")
|
||||
),
|
||||
);
|
||||
// No prefix leaks onto the line after the tag
|
||||
assert_eq!("> a\n> b\nc", formatted.as_code());
|
||||
|
||||
// A break ending the scope opens a prefixed line (a line's indention is its break's, see `indent`)
|
||||
let formatted = format_simple(
|
||||
&allocator,
|
||||
&format_args!(
|
||||
token("> "),
|
||||
prefix_align(&"> ", &format_args!(token("a"), hard_line_break())),
|
||||
token("b")
|
||||
),
|
||||
);
|
||||
assert_eq!("> a\n> b", formatted.as_code());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn prefix_align_keeps_its_order_with_align_and_indent() {
|
||||
let allocator = Allocator::default();
|
||||
let inner = test_format_with(|f| write!(f, [token("a"), hard_line_break(), token("b")]));
|
||||
let formatted = format_simple(
|
||||
&allocator,
|
||||
&format_args!(
|
||||
align(2, &prefix_align(&"> ", &inner)),
|
||||
hard_line_break(),
|
||||
prefix_align(&"> ", &align(2, &inner)),
|
||||
hard_line_break(),
|
||||
prefix_align(&"> ", &indent(&prefix_align(&"> ", &inner)))
|
||||
),
|
||||
);
|
||||
assert_eq!("a\n > b\na\n> b\na\n> > b", formatted.as_code());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn prefix_align_nests_and_survives_root_and_suffix_mechanics() {
|
||||
let allocator = Allocator::default();
|
||||
let body = test_format_with(|f| {
|
||||
write!(
|
||||
f,
|
||||
[
|
||||
token("a"),
|
||||
line_suffix(&token(" # c")),
|
||||
hard_line_break(),
|
||||
// The marked root keeps the prefixes: a literal line stays inside the quote
|
||||
mark_as_root(&format_args!(
|
||||
token("b"),
|
||||
literal_line_break(),
|
||||
dedent_to_root(&token("d")),
|
||||
hard_line_break(),
|
||||
token("e")
|
||||
))
|
||||
]
|
||||
);
|
||||
});
|
||||
let formatted = format_simple(
|
||||
&allocator,
|
||||
&format_args!(
|
||||
token("> > "),
|
||||
prefix_align(&"> ", &prefix_align(&"> ", &body)),
|
||||
hard_line_break(),
|
||||
token("f")
|
||||
),
|
||||
);
|
||||
assert_eq!("> > a # c\n> > b\n> > d\n> > e\nf", formatted.as_code());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn prefix_align_blank_lines_keep_a_trimmed_prefix() {
|
||||
let allocator = Allocator::default();
|
||||
let formatted = format_simple(
|
||||
&allocator,
|
||||
&format_args!(
|
||||
token("> "),
|
||||
prefix_align(
|
||||
&"> ",
|
||||
&format_args!(
|
||||
token("a"),
|
||||
empty_line(),
|
||||
token("b"),
|
||||
exact_line_breaks(3),
|
||||
token("c"),
|
||||
hard_line_break(),
|
||||
mark_as_root(&literal_line_break()),
|
||||
token("d")
|
||||
)
|
||||
),
|
||||
empty_line(),
|
||||
token("e")
|
||||
),
|
||||
);
|
||||
// Levels / aligns after the last prefix are trailing whitespace, trimmed like Prettier does;
|
||||
// a literal line materializes `> ` as is.
|
||||
assert_eq!("> a\n>\n> b\n>\n>\n> c\n> \n> d\n\ne", formatted.as_code());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn prefix_align_narrows_the_line_for_groups() {
|
||||
let allocator = Allocator::default();
|
||||
let content = test_format_with(|f| {
|
||||
write!(
|
||||
f,
|
||||
[
|
||||
group(&format_args!(
|
||||
token("aaaa"),
|
||||
soft_line_break_or_space(),
|
||||
token("bbbb"),
|
||||
soft_line_break_or_space(),
|
||||
token("cccc")
|
||||
)),
|
||||
hard_line_break(),
|
||||
token("x")
|
||||
]
|
||||
);
|
||||
});
|
||||
let options = PrinterOptions {
|
||||
print_width: PrintWidth::new(16),
|
||||
indent_style: IndentStyle::Space,
|
||||
indent_width: 2.try_into().unwrap(),
|
||||
line_ending: LineEnding::Lf,
|
||||
};
|
||||
// 14 columns fit alone, not behind a 4-column prefix
|
||||
let plain = format_simple_with_options(&allocator, &content, options.clone());
|
||||
assert_eq!("aaaa bbbb cccc\nx", plain.as_code());
|
||||
let prefixed = format_simple_with_options(
|
||||
&allocator,
|
||||
&format_args!(token("> > "), prefix_align(&"> > ", &content)),
|
||||
options,
|
||||
);
|
||||
assert_eq!("> > aaaa\n> > bbbb\n> > cccc\n> > x", prefixed.as_code());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn prefix_align_in_tab_mode_flushes_the_align_before_it_only() {
|
||||
let allocator = Allocator::default();
|
||||
let inner = test_format_with(|f| write!(f, [token("a"), hard_line_break(), token("b")]));
|
||||
let formatted = format_simple_with_options(
|
||||
&allocator,
|
||||
&format_args!(
|
||||
align(2, &prefix_align(&"> ", &align(2, &inner))),
|
||||
hard_line_break(),
|
||||
indent(&inner)
|
||||
),
|
||||
PrinterOptions {
|
||||
indent_style: IndentStyle::Tab,
|
||||
line_ending: LineEnding::Lf,
|
||||
..PrinterOptions::default()
|
||||
},
|
||||
);
|
||||
// Prettier's `makeAlign`: aligns before a string align become tabs, the trailing one stays spaces
|
||||
assert_eq!("a\n\t> b\na\n\tb", formatted.as_code());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn exact_line_breaks_clear_the_pending_space() {
|
||||
let allocator = Allocator::default();
|
||||
|
||||
@@ -19,9 +19,9 @@ use crate::{
|
||||
|
||||
use Tag::{
|
||||
EndAlign, EndConditionalContent, EndDedent, EndEntry, EndFill, EndGroup, EndIndent,
|
||||
EndIndentIfGroupBreaks, EndLabelled, EndLineSuffix, StartAlign, StartConditionalContent,
|
||||
StartDedent, StartEntry, StartFill, StartGroup, StartIndent, StartIndentIfGroupBreaks,
|
||||
StartLabelled, StartLineSuffix,
|
||||
EndIndentIfGroupBreaks, EndLabelled, EndLineSuffix, EndPrefix, StartAlign,
|
||||
StartConditionalContent, StartDedent, StartEntry, StartFill, StartGroup, StartIndent,
|
||||
StartIndentIfGroupBreaks, StartLabelled, StartLineSuffix, StartPrefix,
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -471,6 +471,13 @@ impl<C> std::fmt::Debug for FormatLabelled<'_, '_, C> {
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Adds a level of indentation to the given content.
|
||||
///
|
||||
/// Indention is a property of the LINE BREAK, not of the content:
|
||||
/// every line break taken inside `content` starts its line one level deeper, whatever is printed on that line.
|
||||
/// So `[indent([a, hard_line_break()]), b]` indents `b`, and `[hard_line_break(), indent([b])]` does not indent `b`'s first line.
|
||||
/// Place the break in the scope whose indention the next line should take (the same holds for `align` and `prefix_align`).
|
||||
/// This is what lets `group(indent(line))` followed by an opaque `rhs` indent only the first line of `rhs`;
|
||||
/// a content-scoped rule (Biome re-syncs the pending indention at `EndAlign`) cannot express that, so it is not adopted.
|
||||
#[inline]
|
||||
pub fn indent<'a, 'ast, C, Content>(content: &'a Content) -> Indent<'a, 'ast, C>
|
||||
where
|
||||
@@ -590,6 +597,8 @@ impl<C> std::fmt::Debug for Dedent<'_, '_, C> {
|
||||
|
||||
/// Aligns its content by indenting the content by `count` spaces.
|
||||
///
|
||||
/// Like [indent], this applies to the line breaks taken inside `content`, not to the content itself.
|
||||
///
|
||||
/// A `count` of `0` writes the content as-is:
|
||||
/// no align frame is pushed, so it neither measures nor turns into a tab when the indent style is tabs.
|
||||
/// `count` is often computed (a column difference, the indent width, which may be `0`),
|
||||
@@ -628,6 +637,62 @@ impl<C> std::fmt::Debug for Align<'_, '_, C> {
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// PrefixAlign
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Prints `prefix` after the indention on every line break inside `content`.
|
||||
///
|
||||
/// Prettier's `align("> ", doc)`: a markdown blockquote, a JSDoc `* `.
|
||||
/// `prefix_align(&"> ", &body)`: see [tag::Prefix] for the double reference.
|
||||
///
|
||||
/// Like [indent], this applies to the line breaks taken inside `content`:
|
||||
/// a break as the last element puts the prefix before whatever follows the tag,
|
||||
/// and a blank line meant to END the prefixed block has to sit outside the tag
|
||||
/// (`[prefix_align(&"> ", ..), empty_line(), b]`, not inside it).
|
||||
/// Inside the prefix, `indent` / `align` add to the right of it,
|
||||
/// so `prefix_align(&"> ", indent(..))` prints `> x`
|
||||
/// while `align(2, prefix_align(&"> ", ..))` prints ` > x`.
|
||||
/// A blank line inside prints the prefixes up to the last one with its trailing whitespace trimmed (`>`),
|
||||
/// as Prettier's end-of-line trimming leaves them.
|
||||
#[inline]
|
||||
pub fn prefix_align<'a, 'ast, C, Content>(
|
||||
prefix: &'static &'static str,
|
||||
content: &'a Content,
|
||||
) -> PrefixAlign<'a, 'ast, C>
|
||||
where
|
||||
Content: Format<'ast, C>,
|
||||
{
|
||||
debug_assert!(
|
||||
!prefix.trim().is_empty() && !prefix.contains(['\n', '\t']),
|
||||
"a prefix is a visible token on its line (spaces alone are `align`)"
|
||||
);
|
||||
PrefixAlign { prefix, content: Argument::new(content) }
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone)]
|
||||
pub struct PrefixAlign<'a, 'ast, C> {
|
||||
prefix: &'static &'static str,
|
||||
content: Argument<'a, 'ast, C>,
|
||||
}
|
||||
|
||||
impl<'ast, C> Format<'ast, C> for PrefixAlign<'_, 'ast, C> {
|
||||
fn fmt(&self, f: &mut Formatter<'_, 'ast, C>) {
|
||||
f.write_element(FormatElement::Tag(StartPrefix(tag::Prefix(self.prefix))));
|
||||
Arguments::from(&self.content).fmt(f);
|
||||
f.write_element(FormatElement::Tag(EndPrefix));
|
||||
}
|
||||
}
|
||||
|
||||
impl<C> std::fmt::Debug for PrefixAlign<'_, '_, C> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("PrefixAlign")
|
||||
.field("prefix", &self.prefix)
|
||||
.field("content", &"{{content}}")
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// BlockIndent
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
Reference in new issue
Block a user