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 /// Skip indenting the initial line.
104 ///
105 /// This is useful when you've already written some content on the current line
106 /// and want to start indenting only from the next line onwards.
107 pub fn skip_initial(mut self) -> Self {
108 self.needs_indent = false;
109 self
110 }
111
112 /// Returns the inner writer.
113 pub fn into_inner(self) -> &'a mut D {
114 self.inner
115 }
116}
117
118impl<T> WriteStr for Indented<'_, T>
119where
120 T: WriteStr + ?Sized,
121{
122 fn write_str(&mut self, s: &str) -> io::Result<()> {
123 for (ind, line) in s.split('\n').enumerate() {
124 if ind > 0 {
125 self.inner.write_char('\n')?;
126 self.needs_indent = true;
127 }
128
129 if self.needs_indent {
130 // Don't render the line unless its actually got text on it
131 if line.is_empty() {
132 continue;
133 }
134
135 self.format.insert_indentation(ind, &mut self.inner)?;
136 self.needs_indent = false;
137 }
138
139 self.inner.write_fmt(format_args!("{line}"))?;
140 }
141
142 Ok(())
143 }
144
145 fn write_str_flush(&mut self) -> io::Result<()> {
146 // We don't need to do any flushing ourselves, because there's no intermediate state
147 // possible here.
148 self.inner.write_str_flush()
149 }
150}
151
152/// Helper function for creating a default indenter
153pub fn indented<D: ?Sized>(f: &mut D) -> Indented<'_, D> {
154 Indented {
155 inner: f,
156 needs_indent: true,
157 format: Format::Uniform {
158 indentation: " ",
159 },
160 }
161}