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::engine::split_lines_inclusive;
11#[cfg(feature = "syntax")]
12use super::view::text_lines;
13use super::view::{DiffView, LineLinks, Side};
14use super::Layout;
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)]
105pub struct SourceDiff {
106 old: String,
107 new: String,
108 language: Option<String>,
109 old_path: Option<String>,
110 new_path: Option<String>,
111 layout: Layout,
112 line_numbers: bool,
113 wrap: bool,
114 context: usize,
115 titles: bool,
116 linker: Option<Hyperlinker>,
117}
118
119impl SourceDiff {
120 pub fn new(old: impl Into<String>, new: impl Into<String>) -> Self {
122 SourceDiff {
123 old: old.into(),
124 new: new.into(),
125 language: None,
126 old_path: None,
127 new_path: None,
128 layout: Layout::Unified,
129 line_numbers: true,
130 wrap: true,
131 context: 3,
132 titles: true,
133 linker: None,
134 }
135 }
136 pub fn language(mut self, language: impl Into<String>) -> Self {
138 self.language = Some(language.into());
139 self
140 }
141 pub fn path(mut self, path: impl Into<String>) -> Self {
144 let path = path.into();
145 self.old_path = Some(path.clone());
146 self.new_path = Some(path);
147 self
148 }
149 pub fn paths(mut self, old: impl Into<String>, new: impl Into<String>) -> Self {
151 self.old_path = Some(old.into());
152 self.new_path = Some(new.into());
153 self
154 }
155 pub fn layout(mut self, layout: Layout) -> Self {
157 self.layout = layout;
158 self
159 }
160 pub fn line_numbers(mut self, show: bool) -> Self {
162 self.line_numbers = show;
163 self
164 }
165 pub fn wrap(mut self, wrap: bool) -> Self {
167 self.wrap = wrap;
168 self
169 }
170 pub fn context(mut self, lines: usize) -> Self {
172 self.context = lines;
173 self
174 }
175 pub fn titles(mut self, show: bool) -> Self {
177 self.titles = show;
178 self
179 }
180 pub fn link_template(mut self, template: impl Into<String>) -> Self {
184 self.linker = Some(Hyperlinker::new().editor(template));
185 self
186 }
187 pub fn hyperlinker(mut self, linker: Hyperlinker) -> Self {
190 self.linker = Some(linker);
191 self
192 }
193
194 fn resolved_language(&self) -> Option<String> {
195 self.language.clone().or_else(|| {
196 self.new_path
197 .as_deref()
198 .or(self.old_path.as_deref())
199 .map(language_for_path)
200 })
201 }
202
203 pub fn view(&self) -> DiffView {
205 self.view_with(None)
206 }
207
208 pub fn view_for(&self, console: &Console) -> DiffView {
211 self.view_with(Some(console))
212 }
213
214 fn view_with(&self, console: Option<&Console>) -> DiffView {
215 let language = self.resolved_language();
216 let old = highlight_lines(&self.old, language.as_deref(), console);
217 let new = highlight_lines(&self.new, language.as_deref(), console);
218 let links: Option<LineLinks> = match (&self.linker, &self.old_path, &self.new_path) {
219 (Some(linker), Some(old_path), Some(new_path)) => {
220 let (linker, old_path, new_path) =
221 (linker.clone(), old_path.clone(), new_path.clone());
222 Some(Arc::new(move |side: Side, line: usize| {
223 let path = if side == Side::Old {
224 &old_path
225 } else {
226 &new_path
227 };
228 linker.file_url(path, Some(line), None)
229 }))
230 }
231 _ => None,
232 };
233 let mut view = DiffView::from_parts(
234 old,
235 new,
236 &split_lines_inclusive(&self.old),
237 &split_lines_inclusive(&self.new),
238 false,
239 )
240 .layout(self.layout)
241 .line_numbers(self.line_numbers)
242 .wrap(self.wrap)
243 .context(self.context)
244 .links(links);
245 if self.titles {
246 if let (Some(old), Some(new)) = (&self.old_path, &self.new_path) {
247 view = view.titles(old.clone(), new.clone());
248 }
249 }
250 view
251 }
252}
253
254impl Renderable for SourceDiff {
255 fn rich_render(&self, console: &Console, options: &ConsoleOptions) -> Vec<Segment> {
256 self.view_for(console).rich_render(console, options)
257 }
258 fn measure(&self, console: &Console, options: &ConsoleOptions) -> Measurement {
259 self.view_for(console).measure(console, options)
260 }
261}