libdiffsitter/render/
mod.rs

1//! Utilities and modules related to rendering diff outputs.
2//!
3//! We have a modular system for displaying diff data to the terminal. Using this system makes it
4//! much easier to extend with new formats that people may request.
5//!
6//! This library defines a fairly minimal interface for renderers: a single trait called
7//! `Renderer`. From there implementers are free to do whatever they want with the diff data.
8//!
9//! This module also defines utilities that may be useful for `Renderer` implementations.
10
11mod json;
12mod unified;
13
14use self::json::Json;
15use crate::diff::RichHunks;
16use anyhow::anyhow;
17use console::{Color, Style, Term};
18use enum_dispatch::enum_dispatch;
19use serde::{Deserialize, Serialize};
20use std::io::Write;
21use strum::{self, Display, EnumIter, EnumString, IntoEnumIterator};
22use unified::Unified;
23
24/// The parameters required to display a diff for a particular document
25#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
26pub struct DocumentDiffData<'a> {
27    /// The filename of the document
28    pub filename: &'a str,
29    /// The full text of the document
30    pub text: &'a str,
31}
32
33/// The parameters a [Renderer] instance receives to render a diff.
34#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
35pub struct DisplayData<'a> {
36    /// The hunks constituting the diff.
37    pub hunks: RichHunks<'a>,
38    /// The parameters that correspond to the old document
39    pub old: DocumentDiffData<'a>,
40    /// The parameters that correspond to the new document
41    pub new: DocumentDiffData<'a>,
42}
43
44#[enum_dispatch]
45#[derive(Clone, Eq, PartialEq, Debug, Serialize, Deserialize, Display, EnumIter, EnumString)]
46#[strum(serialize_all = "snake_case")]
47#[serde(tag = "type", rename_all = "snake_case")]
48pub enum Renderers {
49    Unified,
50    Json,
51}
52
53impl Default for Renderers {
54    fn default() -> Self {
55        Renderers::Unified(Unified::default())
56    }
57}
58
59/// An interface that renders given diff data.
60#[enum_dispatch(Renderers)]
61pub trait Renderer {
62    /// Render a diff.
63    ///
64    /// We use anyhow for errors so errors are free form for implementors, as they are not
65    /// recoverable.
66    ///
67    /// `writer` can be any generic writer - it's not guaranteed that we're writing to a particular sink (could be a
68    /// pager, stdout, etc). `data` is the data that the renderer needs to display, this has information about the
69    /// document being written out. `term_info` is an optional reference to a term object that can be used by the
70    /// renderer to access information about the terminal if the current process is a TTY output.
71    fn render(
72        &self,
73        writer: &mut dyn Write,
74        data: &DisplayData,
75        term_info: Option<&Term>,
76    ) -> anyhow::Result<()>;
77}
78
79/// A copy of the [Color](console::Color) enum so we can serialize using serde, and get around the
80/// orphan rule.
81#[derive(Debug, PartialEq, Eq, Copy, Clone, Serialize, Deserialize)]
82#[serde(remote = "Color", rename_all = "snake_case")]
83#[derive(Default)]
84enum ColorDef {
85    Color256(u8),
86    #[default]
87    Black,
88    Red,
89    Green,
90    Yellow,
91    Blue,
92    Magenta,
93    Cyan,
94    White,
95}
96
97impl From<ColorDef> for Color {
98    fn from(c: ColorDef) -> Self {
99        match c {
100            ColorDef::Black => Color::Black,
101            ColorDef::White => Color::White,
102            ColorDef::Red => Color::Red,
103            ColorDef::Green => Color::Green,
104            ColorDef::Yellow => Color::Yellow,
105            ColorDef::Blue => Color::Blue,
106            ColorDef::Magenta => Color::Magenta,
107            ColorDef::Cyan => Color::Cyan,
108            ColorDef::Color256(c) => Color::Color256(c),
109        }
110    }
111}
112
113/// Workaround so we can use the `ColorDef` remote serialization mechanism with optional types
114mod opt_color_def {
115    use super::{Color, ColorDef};
116    use serde::{Deserialize, Deserializer, Serialize, Serializer};
117
118    #[allow(clippy::trivially_copy_pass_by_ref)]
119    pub fn serialize<S>(value: &Option<Color>, serializer: S) -> Result<S::Ok, S::Error>
120    where
121        S: Serializer,
122    {
123        #[derive(Serialize)]
124        struct Helper<'a>(#[serde(with = "ColorDef")] &'a Color);
125
126        value.as_ref().map(Helper).serialize(serializer)
127    }
128
129    pub fn deserialize<'de, D>(deserializer: D) -> Result<Option<Color>, D::Error>
130    where
131        D: Deserializer<'de>,
132    {
133        #[derive(Deserialize)]
134        struct Helper(#[serde(with = "ColorDef")] Color);
135
136        let helper = Option::deserialize(deserializer)?;
137        Ok(helper.map(|Helper(external)| external))
138    }
139}
140
141/// A helper function for the serde serializer
142///
143/// Due to the shenanigans we're using to serialize the optional color, we need to supply this
144/// method so serde can infer a default value for an option when its key is missing.
145fn default_option<T>() -> Option<T> {
146    None
147}
148
149/// The style that applies to regular text in a diff
150#[derive(Clone, Debug, PartialEq, Eq)]
151struct RegularStyle(Style);
152
153/// The style that applies to emphasized text in a diff
154#[derive(Clone, Debug, PartialEq, Eq)]
155struct EmphasizedStyle(Style);
156
157/// The formatting directives to use with emphasized text in the line of a diff
158///
159/// `Bold` is used as the default emphasis strategy between two lines.
160#[derive(Debug, PartialEq, EnumString, Serialize, Deserialize, Eq)]
161#[strum(serialize_all = "snake_case")]
162#[derive(Default)]
163pub enum Emphasis {
164    /// Don't emphasize anything
165    ///
166    /// This field exists because the absence of a value implies that the user wants to use the
167    /// default emphasis strategy.
168    None,
169    /// Bold the differences between the two lines for emphasis
170    #[default]
171    Bold,
172    /// Underline the differences between two lines for emphasis
173    Underline,
174    /// Use a colored highlight for emphasis
175    Highlight(HighlightColors),
176}
177
178/// The colors to use when highlighting additions and deletions
179#[derive(Debug, Eq, PartialEq, Serialize, Deserialize)]
180pub struct HighlightColors {
181    /// The background color to use with an addition
182    #[serde(with = "ColorDef")]
183    pub addition: Color,
184    /// The background color to use with a deletion
185    #[serde(with = "ColorDef")]
186    pub deletion: Color,
187}
188
189impl Default for HighlightColors {
190    fn default() -> Self {
191        HighlightColors {
192            addition: Color::Color256(0),
193            deletion: Color::Color256(0),
194        }
195    }
196}
197
198/// Configurations and templates for different configuration aliases
199///
200/// The user can define settings for each renderer as well as custom tags for different renderer
201/// configurations.
202#[derive(Serialize, Deserialize, Debug, Eq, PartialEq, Clone)]
203#[serde(rename_all = "snake_case", default)]
204pub struct RenderConfig {
205    /// The default diff renderer to use.
206    ///
207    /// This is used if no renderer is specified at the command line.
208    default: String,
209
210    unified: unified::Unified,
211    json: json::Json,
212}
213
214impl Default for RenderConfig {
215    fn default() -> Self {
216        let default_renderer = Renderers::default();
217        RenderConfig {
218            default: default_renderer.to_string(),
219            unified: Unified::default(),
220            json: Json::default(),
221        }
222    }
223}
224
225impl RenderConfig {
226    /// Get the renderer specified by the given tag.
227    ///
228    /// If the tag is not specified this will fall back to the default renderer. This is a
229    /// relatively expensive operation so it should be used once and the result should be saved.
230    pub fn get_renderer(self, tag: Option<String>) -> anyhow::Result<Renderers> {
231        if let Some(t) = tag {
232            let cand_enum = Renderers::iter().find(|e| e.to_string() == t);
233            match cand_enum {
234                None => Err(anyhow!("'{}' is not a valid renderer", &t)),
235                Some(renderer) => Ok(renderer),
236            }
237        } else {
238            Ok(Renderers::default())
239        }
240    }
241}
242
243#[cfg(test)]
244mod tests {
245    use super::*;
246    use test_case::test_case;
247
248    #[test_case("unified")]
249    #[test_case("json")]
250    fn test_get_renderer_custom_tag(tag: &str) {
251        let cfg = RenderConfig::default();
252        let res = cfg.get_renderer(Some(tag.into()));
253        assert!(res.is_ok());
254    }
255
256    #[test]
257    fn test_render_config_default_tag() {
258        let cfg = RenderConfig::default();
259        let res = cfg.get_renderer(None);
260        assert_eq!(res.unwrap(), Renderers::default());
261    }
262}