1use similar::{ChangeTag, TextDiff};
8use syntect::easy::HighlightLines;
9use syntect::highlighting::Style as SynStyle;
10use syntect::parsing::SyntaxSet;
11use syntect::util::as_24_bit_terminal_escaped;
12
13use super::renderer::{Renderer, Writer};
14
15impl Renderer {
16 pub fn render_diff(&self, w: &dyn Writer, old: &str, new: &str) {
20 let diff = TextDiff::from_lines(old, new);
21 for change in diff.iter_all_changes() {
22 let (sign, style) = match change.tag() {
23 ChangeTag::Insert => ("+", &self.theme.diff_add),
24 ChangeTag::Delete => ("-", &self.theme.diff_remove),
25 ChangeTag::Equal => (" ", &self.theme.diff_context),
26 };
27 let body = format!("{sign}{change}");
28 let body = body.trim_end_matches('\n');
29 let styled = style.apply_to(body).to_string();
30 w.write_line(&styled);
31 }
32 }
33
34 pub fn render_syntax_highlight(
38 &self,
39 w: &dyn Writer,
40 code: &str,
41 lang: &str,
42 syntax_set: &SyntaxSet,
43 theme_set: &syntect::highlighting::ThemeSet,
44 ) {
45 let syntax = syntax_set
46 .find_syntax_by_token(lang)
47 .or_else(|| syntax_set.find_syntax_by_extension(lang))
48 .unwrap_or_else(|| syntax_set.find_syntax_plain_text());
49 let Some(theme) = theme_set
50 .themes
51 .get("base16-ocean.dark")
52 .or_else(|| theme_set.themes.values().next())
53 else {
54 for line in code.lines() {
56 w.write_line(line);
57 }
58 return;
59 };
60 let mut h = HighlightLines::new(syntax, theme);
61 for line in code.lines() {
62 let ranges: Vec<(SynStyle, &str)> =
63 h.highlight_line(line, syntax_set).unwrap_or_default();
64 let escaped = as_24_bit_terminal_escaped(&ranges, false);
65 w.write_line(&escaped);
66 }
67 }
68}
69
70impl super::Printer {
71 pub fn diff(&self, old: &str, new: &str) {
73 self.renderer
74 .render_diff(self.sink_stderr.as_ref(), old, new);
75 }
76
77 pub fn syntax_highlight(&self, code: &str, lang: &str) {
79 self.renderer.render_syntax_highlight(
80 self.sink_stderr.as_ref(),
81 code,
82 lang,
83 &self.syntax_set,
84 &self.theme_set,
85 );
86 }
87
88 pub fn data_line(&self, text: &str) {
91 self.sink_stdout.write_line(text);
92 }
93}
94
95#[cfg(test)]
96mod tests {
97 use std::sync::{Arc, Mutex};
98
99 use super::super::renderer::StringSink;
100 use super::super::{Theme, Verbosity};
101 use super::*;
102 use crate::output::strip_ansi;
103
104 #[test]
105 fn diff_marks_changed_lines() {
106 let buf = Arc::new(Mutex::new(String::new()));
107 let sink = StringSink(buf.clone());
108 let r = Renderer::new(Theme::default(), Verbosity::Normal);
109 r.render_diff(&sink, "a\nb\nc\n", "a\nB\nc\n");
110 let out = strip_ansi(&buf.lock().unwrap());
111 assert!(out.contains("-b"), "got: {out:?}");
112 assert!(out.contains("+B"), "got: {out:?}");
113 }
114
115 #[test]
116 fn syntax_highlight_renders_lines() {
117 let buf = Arc::new(Mutex::new(String::new()));
118 let sink = StringSink(buf.clone());
119 let r = Renderer::new(Theme::default(), Verbosity::Normal);
120 let ss = SyntaxSet::load_defaults_newlines();
121 let ts = syntect::highlighting::ThemeSet::load_defaults();
122 r.render_syntax_highlight(&sink, "let x = 1;\nlet y = 2;\n", "rs", &ss, &ts);
123 let out = buf.lock().unwrap();
124 let stripped = strip_ansi(&out);
125 assert!(
126 stripped.contains("let x"),
127 "stripped output missing 'let x': {stripped:?}"
128 );
129 assert!(
130 stripped.contains("let y"),
131 "stripped output missing 'let y': {stripped:?}"
132 );
133 }
134
135 #[test]
136 fn data_line_writes_to_stdout_raw() {
137 use super::super::Verbosity;
138 use super::super::printer::Printer;
139
140 let stdout_buf = Arc::new(Mutex::new(String::new()));
141 let stderr_buf = Arc::new(Mutex::new(String::new()));
142 let mut p = Printer::new(Verbosity::Normal);
143 p.sink_stdout = Arc::new(StringSink(stdout_buf.clone()));
145 p.sink_stderr = Arc::new(StringSink(stderr_buf.clone()));
146
147 p.data_line("raw payload");
148 p.flush();
149
150 let stdout = stdout_buf.lock().unwrap();
151 let stderr = stderr_buf.lock().unwrap();
152 assert!(stdout.contains("raw payload"), "stdout got: {stdout:?}");
154 assert!(
156 !stderr.contains("raw payload"),
157 "leaked to stderr: {stderr:?}"
158 );
159 }
160}