use std::sync::Arc;
use rich::measure::Measurement;
use rich::{Console, ConsoleOptions, Renderable, Segment, Text};
#[cfg(feature = "syntax")]
use rich::{Style, StyleType, Syntax};
use super::engine::split_lines_inclusive;
#[cfg(feature = "syntax")]
use super::view::text_lines;
use super::view::{DiffView, LineLinks, Side};
use super::Layout;
use crate::hyperlink::Hyperlinker;
pub(crate) fn language_for_path(path: &str) -> String {
let name = path.rsplit(['/', '\\']).next().unwrap_or(path);
match name.rsplit_once('.') {
Some((stem, ext)) if !stem.is_empty() => ext.to_string(),
_ => name.to_string(),
}
}
#[cfg(feature = "syntax")]
fn foreground_only(style: &Style) -> Style {
let definition = style.definition();
let kept = match definition.split_once(" on ") {
Some((before, _)) => before.to_string(),
None if definition.starts_with("on ") => String::new(),
None => definition,
};
Style::parse(&kept).unwrap_or_default()
}
fn plain_lines(code: &str) -> Vec<Text> {
split_lines_inclusive(code)
.into_iter()
.map(|l| Text::new(super::engine::strip_eol(l)))
.collect()
}
#[cfg(feature = "syntax")]
pub(crate) fn highlight_lines(
code: &str,
language: Option<&str>,
console: Option<&Console>,
) -> Vec<Text> {
let count = split_lines_inclusive(code).len();
let Some(language) = language else {
return plain_lines(code);
};
let syntax = Syntax::new(code, language);
let highlighted = match console {
Some(console) => syntax.highlight_for(console),
None => syntax.highlight(),
};
let mut plain = Text::new(highlighted.plain());
for span in highlighted.spans() {
if let StyleType::Style(style) = &span.style {
let fg = foreground_only(style);
if !fg.is_null() {
plain.stylize(fg, span.start, span.end);
}
}
}
let mut lines = text_lines(&plain);
lines.resize_with(count, || Text::new(""));
lines
}
#[cfg(not(feature = "syntax"))]
pub(crate) fn highlight_lines(
code: &str,
_language: Option<&str>,
_console: Option<&Console>,
) -> Vec<Text> {
plain_lines(code)
}
#[derive(Clone, Debug)]
pub struct SourceDiff {
old: String,
new: String,
language: Option<String>,
old_path: Option<String>,
new_path: Option<String>,
layout: Layout,
line_numbers: bool,
wrap: bool,
context: usize,
titles: bool,
linker: Option<Hyperlinker>,
}
impl SourceDiff {
pub fn new(old: impl Into<String>, new: impl Into<String>) -> Self {
SourceDiff {
old: old.into(),
new: new.into(),
language: None,
old_path: None,
new_path: None,
layout: Layout::Unified,
line_numbers: true,
wrap: true,
context: 3,
titles: true,
linker: None,
}
}
pub fn language(mut self, language: impl Into<String>) -> Self {
self.language = Some(language.into());
self
}
pub fn path(mut self, path: impl Into<String>) -> Self {
let path = path.into();
self.old_path = Some(path.clone());
self.new_path = Some(path);
self
}
pub fn paths(mut self, old: impl Into<String>, new: impl Into<String>) -> Self {
self.old_path = Some(old.into());
self.new_path = Some(new.into());
self
}
pub fn layout(mut self, layout: Layout) -> Self {
self.layout = layout;
self
}
pub fn line_numbers(mut self, show: bool) -> Self {
self.line_numbers = show;
self
}
pub fn wrap(mut self, wrap: bool) -> Self {
self.wrap = wrap;
self
}
pub fn context(mut self, lines: usize) -> Self {
self.context = lines;
self
}
pub fn titles(mut self, show: bool) -> Self {
self.titles = show;
self
}
pub fn link_template(mut self, template: impl Into<String>) -> Self {
self.linker = Some(Hyperlinker::new().editor(template));
self
}
pub fn hyperlinker(mut self, linker: Hyperlinker) -> Self {
self.linker = Some(linker);
self
}
fn resolved_language(&self) -> Option<String> {
self.language.clone().or_else(|| {
self.new_path
.as_deref()
.or(self.old_path.as_deref())
.map(language_for_path)
})
}
pub fn view(&self) -> DiffView {
self.view_with(None)
}
pub fn view_for(&self, console: &Console) -> DiffView {
self.view_with(Some(console))
}
fn view_with(&self, console: Option<&Console>) -> DiffView {
let language = self.resolved_language();
let old = highlight_lines(&self.old, language.as_deref(), console);
let new = highlight_lines(&self.new, language.as_deref(), console);
let links: Option<LineLinks> = match (&self.linker, &self.old_path, &self.new_path) {
(Some(linker), Some(old_path), Some(new_path)) => {
let (linker, old_path, new_path) =
(linker.clone(), old_path.clone(), new_path.clone());
Some(Arc::new(move |side: Side, line: usize| {
let path = if side == Side::Old {
&old_path
} else {
&new_path
};
linker.file_url(path, Some(line), None)
}))
}
_ => None,
};
let mut view = DiffView::from_parts(
old,
new,
&split_lines_inclusive(&self.old),
&split_lines_inclusive(&self.new),
false,
)
.layout(self.layout)
.line_numbers(self.line_numbers)
.wrap(self.wrap)
.context(self.context)
.links(links);
if self.titles {
if let (Some(old), Some(new)) = (&self.old_path, &self.new_path) {
view = view.titles(old.clone(), new.clone());
}
}
view
}
}
impl Renderable for SourceDiff {
fn rich_render(&self, console: &Console, options: &ConsoleOptions) -> Vec<Segment> {
self.view_for(console).rich_render(console, options)
}
fn measure(&self, console: &Console, options: &ConsoleOptions) -> Measurement {
self.view_for(console).measure(console, options)
}
}