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}