use crate::model::{DiffFile, DiffLine};
use crate::syntax::SyntaxHighlighter;
pub fn decorate_diff_files(files: &mut [DiffFile], highlighter: &SyntaxHighlighter) {
for file in files {
if file.is_binary || file.is_too_large || file.hunks.is_empty() {
continue;
}
let file_path = match file.new_path.as_ref().or(file.old_path.as_ref()) {
Some(p) => p.clone(),
None => continue,
};
for hunk in &mut file.hunks {
decorate_hunk_lines(&mut hunk.lines, &file_path, highlighter);
}
}
}
fn decorate_hunk_lines(
lines: &mut [DiffLine],
file_path: &std::path::Path,
highlighter: &SyntaxHighlighter,
) {
let line_contents: Vec<String> = lines.iter().map(|l| l.content.clone()).collect();
let line_origins: Vec<_> = lines.iter().map(|l| l.origin).collect();
let highlight_sequences =
SyntaxHighlighter::split_diff_lines_for_highlighting(&line_contents, &line_origins);
let old_highlighted_lines =
highlighter.highlight_file_lines(file_path, &highlight_sequences.old_lines);
let new_highlighted_lines =
highlighter.highlight_file_lines(file_path, &highlight_sequences.new_lines);
for (idx, line) in lines.iter_mut().enumerate() {
line.highlighted_spans = highlighter.highlighted_line_for_diff_with_background(
old_highlighted_lines.as_deref(),
new_highlighted_lines.as_deref(),
highlight_sequences.old_line_indices[idx],
highlight_sequences.new_line_indices[idx],
line.origin,
);
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::model::{DiffHunk, FileStatus, LineOrigin};
use std::path::PathBuf;
fn rust_file_with_one_hunk() -> DiffFile {
DiffFile {
old_path: None,
new_path: Some(PathBuf::from("main.rs")),
status: FileStatus::Added,
hunks: vec![DiffHunk {
header: "@@ -0,0 +1,3 @@".to_string(),
lines: vec![
DiffLine {
origin: LineOrigin::Addition,
content: "fn main() {".to_string(),
old_lineno: None,
new_lineno: Some(1),
highlighted_spans: None,
},
DiffLine {
origin: LineOrigin::Addition,
content: " let x = 42;".to_string(),
old_lineno: None,
new_lineno: Some(2),
highlighted_spans: None,
},
DiffLine {
origin: LineOrigin::Addition,
content: "}".to_string(),
old_lineno: None,
new_lineno: Some(3),
highlighted_spans: None,
},
],
old_start: 0,
old_count: 0,
new_start: 1,
new_count: 3,
}],
is_binary: false,
is_too_large: false,
is_commit_message: false,
}
}
#[test]
fn pre_decoration_lines_have_no_highlighted_spans() {
let file = rust_file_with_one_hunk();
for line in &file.hunks[0].lines {
assert!(line.highlighted_spans.is_none());
}
}
#[test]
fn decorate_populates_spans_for_recognized_file_types() {
let mut files = vec![rust_file_with_one_hunk()];
let highlighter = SyntaxHighlighter::default();
decorate_diff_files(&mut files, &highlighter);
for (idx, line) in files[0].hunks[0].lines.iter().enumerate() {
assert!(
line.highlighted_spans.is_some(),
"line {idx} should have spans after decoration"
);
let spans = line.highlighted_spans.as_ref().unwrap();
assert!(!spans.is_empty(), "line {idx} should have non-empty spans");
}
}
#[test]
fn decorate_skips_binary_files() {
let mut files = vec![DiffFile {
old_path: None,
new_path: Some(PathBuf::from("image.png")),
status: FileStatus::Added,
hunks: Vec::new(),
is_binary: true,
is_too_large: false,
is_commit_message: false,
}];
let highlighter = SyntaxHighlighter::default();
decorate_diff_files(&mut files, &highlighter);
assert!(files[0].hunks.is_empty());
}
#[test]
fn decorate_leaves_spans_none_for_unresolvable_syntax() {
let mut files = vec![DiffFile {
old_path: None,
new_path: None,
status: FileStatus::Modified,
hunks: vec![DiffHunk {
header: "@@ -1 +1 @@".to_string(),
lines: vec![DiffLine {
origin: LineOrigin::Context,
content: "no path".to_string(),
old_lineno: Some(1),
new_lineno: Some(1),
highlighted_spans: None,
}],
old_start: 1,
old_count: 1,
new_start: 1,
new_count: 1,
}],
is_binary: false,
is_too_large: false,
is_commit_message: false,
}];
let highlighter = SyntaxHighlighter::default();
decorate_diff_files(&mut files, &highlighter);
assert!(files[0].hunks[0].lines[0].highlighted_spans.is_none());
}
}