1use std::sync::Arc;
4
5use rich::measure::Measurement;
6use rich::{Console, ConsoleOptions, Renderable, Segment, Text};
7#[cfg(feature = "syntax")]
8use rich::{Style, StyleType, Syntax};
9
10use super::Layout;
11use super::engine::split_lines_inclusive;
12#[cfg(feature = "syntax")]
13use super::view::text_lines;
14use super::view::{DiffView, LineLinks, Side};
15use crate::hyperlink::Hyperlinker;
16
17pub(crate) fn language_for_path(path: &str) -> String {
20 let name = path.rsplit(['/', '\\']).next().unwrap_or(path);
21 match name.rsplit_once('.') {
22 Some((stem, ext)) if !stem.is_empty() => ext.to_string(),
23 _ => name.to_string(),
24 }
25}
26
27#[cfg(feature = "syntax")]
29fn foreground_only(style: &Style) -> Style {
30 let definition = style.definition();
31 let kept = match definition.split_once(" on ") {
32 Some((before, _)) => before.to_string(),
33 None if definition.starts_with("on ") => String::new(),
34 None => definition,
35 };
36 Style::parse(&kept).unwrap_or_default()
37}
38
39fn plain_lines(code: &str) -> Vec<Text> {
41 split_lines_inclusive(code)
42 .into_iter()
43 .map(|l| Text::new(super::engine::strip_eol(l)))
44 .collect()
45}
46
47#[cfg(feature = "syntax")]
52pub(crate) fn highlight_lines(
53 code: &str,
54 language: Option<&str>,
55 console: Option<&Console>,
56) -> Vec<Text> {
57 let count = split_lines_inclusive(code).len();
58 let Some(language) = language else {
59 return plain_lines(code);
60 };
61 let syntax = Syntax::new(code, language);
62 let highlighted = match console {
63 Some(console) => syntax.highlight_for(console),
64 None => syntax.highlight(),
65 };
66 let mut plain = Text::new(highlighted.plain());
67 for span in highlighted.spans() {
68 if let StyleType::Style(style) = &span.style {
69 let fg = foreground_only(style);
70 if !fg.is_null() {
71 plain.stylize(fg, span.start, span.end);
72 }
73 }
74 }
75 let mut lines = text_lines(&plain);
76 lines.resize_with(count, || Text::new(""));
77 lines
78}
79
80#[cfg(not(feature = "syntax"))]
82pub(crate) fn highlight_lines(
83 code: &str,
84 _language: Option<&str>,
85 _console: Option<&Console>,
86) -> Vec<Text> {
87 plain_lines(code)
88}
89
90#[derive(Clone, Debug)]
105#[must_use = "builder methods return a new value; it does nothing until it is rendered or used"]
106pub struct SourceDiff {
107 old: String,
108 new: String,
109 language: Option<String>,
110 old_path: Option<String>,
111 new_path: Option<String>,
112 layout: Layout,
113 line_numbers: bool,
114 wrap: bool,
115 context: usize,
116 titles: bool,
117 linker: Option<Hyperlinker>,
118}
119
120impl SourceDiff {
121 pub fn new(old: impl Into<String>, new: impl Into<String>) -> Self {
123 SourceDiff {
124 old: old.into(),
125 new: new.into(),
126 language: None,
127 old_path: None,
128 new_path: None,
129 layout: Layout::Unified,
130 line_numbers: true,
131 wrap: true,
132 context: 3,
133 titles: true,
134 linker: None,
135 }
136 }
137 pub fn language(mut self, language: impl Into<String>) -> Self {
139 self.language = Some(language.into());
140 self
141 }
142 pub fn path(mut self, path: impl Into<String>) -> Self {
145 let path = path.into();
146 self.old_path = Some(path.clone());
147 self.new_path = Some(path);
148 self
149 }
150 pub fn paths(mut self, old: impl Into<String>, new: impl Into<String>) -> Self {
152 self.old_path = Some(old.into());
153 self.new_path = Some(new.into());
154 self
155 }
156 pub fn layout(mut self, layout: Layout) -> Self {
158 self.layout = layout;
159 self
160 }
161 pub fn line_numbers(mut self, show: bool) -> Self {
163 self.line_numbers = show;
164 self
165 }
166 pub fn wrap(mut self, wrap: bool) -> Self {
168 self.wrap = wrap;
169 self
170 }
171 pub fn context(mut self, lines: usize) -> Self {
173 self.context = lines;
174 self
175 }
176 pub fn titles(mut self, show: bool) -> Self {
178 self.titles = show;
179 self
180 }
181 pub fn link_template(mut self, template: impl Into<String>) -> Self {
185 self.linker = Some(Hyperlinker::new().editor(template));
186 self
187 }
188 pub fn hyperlinker(mut self, linker: Hyperlinker) -> Self {
191 self.linker = Some(linker);
192 self
193 }
194
195 fn resolved_language(&self) -> Option<String> {
196 self.language.clone().or_else(|| {
197 self.new_path
198 .as_deref()
199 .or(self.old_path.as_deref())
200 .map(language_for_path)
201 })
202 }
203
204 pub fn view(&self) -> DiffView {
206 self.view_with(None)
207 }
208
209 pub fn view_for(&self, console: &Console) -> DiffView {
212 self.view_with(Some(console))
213 }
214
215 fn view_with(&self, console: Option<&Console>) -> DiffView {
216 let language = self.resolved_language();
217 let old = highlight_lines(&self.old, language.as_deref(), console);
218 let new = highlight_lines(&self.new, language.as_deref(), console);
219 let links: Option<LineLinks> = match (&self.linker, &self.old_path, &self.new_path) {
220 (Some(linker), Some(old_path), Some(new_path)) => {
221 let (linker, old_path, new_path) =
222 (linker.clone(), old_path.clone(), new_path.clone());
223 Some(Arc::new(move |side: Side, line: usize| {
224 let path = if side == Side::Old {
225 &old_path
226 } else {
227 &new_path
228 };
229 linker.file_url(path, Some(line), None)
230 }))
231 }
232 _ => None,
233 };
234 let mut view = DiffView::from_parts(
235 old,
236 new,
237 &split_lines_inclusive(&self.old),
238 &split_lines_inclusive(&self.new),
239 false,
240 )
241 .layout(self.layout)
242 .line_numbers(self.line_numbers)
243 .wrap(self.wrap)
244 .context(self.context)
245 .links(links);
246 if self.titles
247 && let (Some(old), Some(new)) = (&self.old_path, &self.new_path)
248 {
249 view = view.titles(old.clone(), new.clone());
250 }
251 view
252 }
253}
254
255impl Renderable for SourceDiff {
256 fn rich_render(&self, console: &Console, options: &ConsoleOptions) -> Vec<Segment> {
257 self.view_for(console).rich_render(console, options)
258 }
259 fn measure(&self, console: &Console, options: &ConsoleOptions) -> Measurement {
260 self.view_for(console).measure(console, options)
261 }
262}