nextest_runner/indenter.rs
1// Copyright (c) The nextest Contributors
2// SPDX-License-Identifier: MIT OR Apache-2.0
3
4//! Support for indenting multi-line displays.
5//!
6//! This module is adapted from [indenter](https://github.com/eyre-rs/indenter) and is used under the
7//! terms of the MIT or Apache-2.0 licenses.
8//!
9//! # Notes
10//!
11//! We previously used to use `indenter` to indent multi-line `fmt::Display`
12//! instances. However, at some point we needed to also indent
13//! `std::io::Write`s, not just `fmt::Write`. `indenter` 0.3.3 doesn't support
14//! `std::io::Write`, so we switched to `indent_write`.
15//!
16//! This file still has the `indenter` API. So we have both APIs floating around
17//! for a bit... oh well. Still in two minds about which one's better here.
18
19use crate::write_str::WriteStr;
20use std::io;
21
22/// The set of supported formats for indentation
23#[expect(missing_debug_implementations)]
24pub enum Format<'a> {
25 /// Insert uniform indentation before every line
26 ///
27 /// This format takes a static string as input and inserts it after every newline
28 Uniform {
29 /// The string to insert as indentation
30 indentation: &'static str,
31 },
32 /// Inserts a number before the first line
33 ///
34 /// This format hard codes the indentation level to match the indentation from
35 /// `core::backtrace::Backtrace`
36 Numbered {
37 /// The index to insert before the first line of output
38 ind: usize,
39 },
40 /// A custom indenter which is executed after every newline
41 ///
42 /// Custom indenters are passed the current line number and the buffer to be written to as args
43 Custom {
44 /// The custom indenter
45 inserter: &'a mut Inserter,
46 },
47}
48
49/// Helper struct for efficiently indenting multi line display implementations
50///
51/// # Explanation
52///
53/// This type will never allocate a string to handle inserting indentation. It instead leverages
54/// the `write_str` function that serves as the foundation of the `core::fmt::Write` trait. This
55/// lets it intercept each piece of output as its being written to the output buffer. It then
56/// splits on newlines giving slices into the original string. Finally we alternate writing these
57/// lines and the specified indentation to the output buffer.
58#[expect(missing_debug_implementations)]
59pub struct Indented<'a, D: ?Sized> {
60 inner: &'a mut D,
61 needs_indent: bool,
62 format: Format<'a>,
63}
64
65/// A callback for `Format::Custom` used to insert indenation after a new line
66///
67/// The first argument is the line number within the output, starting from 0
68pub type Inserter = dyn FnMut(usize, &mut dyn WriteStr) -> io::Result<()>;
69
70impl Format<'_> {
71 fn insert_indentation(&mut self, line: usize, f: &mut dyn WriteStr) -> io::Result<()> {
72 match self {
73 Format::Uniform { indentation } => write!(f, "{indentation}"),
74 Format::Numbered { ind } => {
75 if line == 0 {
76 write!(f, "{ind: >4}: ")
77 } else {
78 write!(f, " ")
79 }
80 }
81 Format::Custom { inserter } => inserter(line, f),
82 }
83 }
84}
85
86impl<'a, D: ?Sized> Indented<'a, D> {
87 /// Sets the format to `Format::Numbered` with the provided index
88 pub fn ind(self, ind: usize) -> Self {
89 self.with_format(Format::Numbered { ind })
90 }
91
92 /// Sets the format to `Format::Uniform` with the provided static string
93 pub fn with_str(self, indentation: &'static str) -> Self {
94 self.with_format(Format::Uniform { indentation })
95 }
96
97 /// Construct an indenter with a user defined format
98 pub fn with_format(mut self, format: Format<'a>) -> Self {
99 self.format = format;
100 self
101 }
102
103 /// Returns the inner writer.
104 pub fn into_inner(self) -> &'a mut D {
105 self.inner
106 }
107}
108
109impl<T> WriteStr for Indented<'_, T>
110where
111 T: WriteStr + ?Sized,
112{
113 fn write_str(&mut self, s: &str) -> io::Result<()> {
114 for (ind, line) in s.split('\n').enumerate() {
115 if ind > 0 {
116 self.inner.write_char('\n')?;
117 self.needs_indent = true;
118 }
119
120 if self.needs_indent {
121 // Don't render the line unless its actually got text on it
122 if line.is_empty() {
123 continue;
124 }
125
126 self.format.insert_indentation(ind, &mut self.inner)?;
127 self.needs_indent = false;
128 }
129
130 self.inner.write_fmt(format_args!("{line}"))?;
131 }
132
133 Ok(())
134 }
135
136 fn write_str_flush(&mut self) -> io::Result<()> {
137 // We don't need to do any flushing ourselves, because there's no intermediate state
138 // possible here.
139 self.inner.write_str_flush()
140 }
141}
142
143/// Helper function for creating a default indenter
144pub fn indented<D: ?Sized>(f: &mut D) -> Indented<'_, D> {
145 Indented {
146 inner: f,
147 needs_indent: true,
148 format: Format::Uniform {
149 indentation: " ",
150 },
151 }
152}