use diffler_core::diffalgo::DiffAlgorithm;
use diffler_core::highlight::Highlighter;
use diffler_core::model::{BlobIds, FileDiff, HashCache, Hunk};
use diffler_core::pairing;
use super::App;
use super::diff::{FileHighlights, FileScope};
#[derive(Debug)]
pub struct EnrichJob {
pub path: String,
pub hash: String,
pub old_text: Option<String>,
pub new_text: Option<String>,
pub hunks: Vec<Hunk>,
pub semantic: bool,
pub stamp: EnrichStamp,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct EnrichStamp {
pub algorithm: DiffAlgorithm,
pub highlighter: u64,
}
#[derive(Debug)]
pub struct EnrichOutcome {
pub path: String,
pub hash: String,
pub hunks: Vec<Hunk>,
pub highlights: FileHighlights,
pub scope: FileScope,
pub stamp: EnrichStamp,
}
pub(super) fn queue_if_stale(
inflight: &mut std::collections::HashSet<String>,
pending: &mut Vec<EnrichJob>,
file: &FileDiff,
semantic: bool,
stamp: EnrichStamp,
ready: bool,
) {
if ready {
return;
}
let hash = file.sides_hash();
if !inflight.insert(hash.clone()) {
return;
}
pending.push(EnrichJob {
path: file.path.clone(),
hash,
old_text: file.old_text.clone(),
new_text: file.new_text.clone(),
hunks: file.hunks.clone(),
semantic,
stamp,
});
}
pub fn run_enrich(highlighter: &Highlighter, job: EnrichJob) -> EnrichOutcome {
let mut file = FileDiff {
path: job.path,
old_path: None,
status: diffler_core::model::FileStatus::Modified,
binary: false,
old_text: job.old_text,
new_text: job.new_text,
hunks: job.hunks,
hashes: HashCache::default(),
blobs: BlobIds::default(),
};
for line in file.hunks.iter_mut().flat_map(|hunk| &mut hunk.lines) {
line.emphasis.clear();
line.reformat_only = false;
}
let structural = job.stamp.algorithm == DiffAlgorithm::Structural;
if !(job.semantic && highlighter.syntactic_emphasis(&mut file, structural)) {
pairing::enrich_file(&mut file);
}
let highlight = |text: &Option<String>| {
text.as_deref()
.map(|content| highlighter.highlight(&file.path, content))
.unwrap_or_default()
};
let highlights = FileHighlights {
hash: job.hash.clone(),
old: highlight(&file.old_text),
new: highlight(&file.new_text),
};
let scope = FileScope {
hash: job.hash.clone(),
index: file
.new_text
.as_deref()
.map(|content| highlighter.scope_index(&file.path, content))
.unwrap_or_default(),
};
EnrichOutcome {
path: file.path,
hash: job.hash,
hunks: file.hunks,
highlights,
scope,
stamp: job.stamp,
}
}
impl App {
pub(crate) fn queue_enrich_selected(&mut self) {
let Some(diff) = self.diff.as_ref() else {
return;
};
let selected = diff.selected;
let file_count = diff
.commit_model
.as_ref()
.unwrap_or_else(|| self.review.model())
.files
.len();
let mut targets = vec![selected];
targets.extend(selected.checked_sub(1));
if selected + 1 < file_count {
targets.push(selected + 1);
}
targets.extend(self.referenced_files());
for index in targets {
self.queue_enrich_file(index);
}
self.queue_enrich_context_files();
}
fn referenced_files(&self) -> Vec<usize> {
let Some(diff) = self.diff.as_ref().filter(|diff| diff.refs_visible()) else {
return Vec::new();
};
let Some(lens) = diff.lens.as_ref() else {
return Vec::new();
};
let model = diff.model(&self.review);
lens.view
.refs
.iter()
.filter(|entry| entry.group_len.is_some())
.filter_map(|entry| model.files.iter().position(|file| file.path == entry.path))
.collect()
}
fn queue_enrich_context_files(&mut self) {
let stamp = self.enrich_stamp();
let Some(diff) = self.diff.as_ref() else {
return;
};
for file in &diff.context_files {
let ready = diff
.highlights
.get(&file.path)
.is_some_and(|cached| cached.hash == file.sides_hash());
queue_if_stale(
&mut self.enrich_inflight,
&mut self.pending_enrich,
file,
false,
stamp,
ready,
);
}
}
fn queue_enrich_file(&mut self, index: usize) {
let semantic = self.config.ui.semantic_diff;
let stamp = self.enrich_stamp();
let Some(diff) = self.diff.as_ref() else {
return;
};
let model = diff
.commit_model
.as_ref()
.unwrap_or_else(|| self.review.model());
let Some(file) = model.files.get(index) else {
return;
};
if file.binary || file.hunks.is_empty() {
return;
}
let ready = diff
.highlights
.get(&file.path)
.is_some_and(|cached| cached.hash == file.sides_hash())
&& diff.is_enriched(&file.path);
queue_if_stale(
&mut self.enrich_inflight,
&mut self.pending_enrich,
file,
semantic,
stamp,
ready,
);
}
pub(crate) fn on_enriched(&mut self, outcome: EnrichOutcome) {
if outcome.stamp.highlighter == self.highlighter_generation {
self.enrich_inflight.remove(&outcome.hash);
}
if outcome.stamp != self.enrich_stamp() {
return;
}
self.install_status_enrichment(&outcome);
let algorithm = self.config.diff.algorithm;
let indent_heuristic = self.config.diff.indent_heuristic;
let Some(diff) = self.diff.as_mut() else {
return;
};
let context = diff.context.get(&outcome.path).copied();
let positions = (context.is_some() && diff.rows_show(&outcome.path))
.then(|| diff.capture_positions(&self.review));
let same = |file: &FileDiff| file.path == outcome.path && file.sides_hash() == outcome.hash;
let file = match diff.commit_model.as_mut() {
Some(model) => model.files.iter_mut().find(|f| same(f)),
None => self.review.model_mut().files.iter_mut().find(|f| same(f)),
}
.or_else(|| diff.context_files.iter_mut().find(|f| same(f)));
let Some(file) = file else {
return;
};
file.hunks = outcome.hunks;
let reshaped = context.is_some_and(|context| {
super::expand::apply_context(file, context, algorithm, indent_heuristic)
});
let hunks = file.hunks.clone();
diff.highlights
.insert(outcome.path.clone(), outcome.highlights);
diff.scopes.insert(outcome.path.clone(), outcome.scope);
diff.mark_enriched(&outcome.path);
if let Some(cached) = diff
.merged_model
.as_mut()
.and_then(|merged| merged.files.iter_mut().find(|f| same(f)))
{
cached.hunks = hunks;
}
match positions.filter(|_| reshaped) {
Some(positions) => diff.rebuild_in_place(&self.review, positions),
None if reshaped || diff.rows_show(&outcome.path) => diff.mark_rows_dirty(),
None => {}
}
}
}
impl App {
pub fn enrich_now(&mut self) {
let jobs = std::mem::take(&mut self.pending_enrich);
for job in jobs {
let outcome = run_enrich(&self.highlighter, job);
self.on_enriched(outcome);
}
}
}
#[cfg(test)]
mod tests {
use diffler_core::model::{DiffLine, HunkId, LineKind};
use super::*;
#[test]
fn enriching_again_drops_the_marks_an_earlier_grammar_set() {
let mut line = DiffLine::new(LineKind::Added, None, Some(1), " a: 1".to_owned());
line.emphasis.push(0..2);
line.reformat_only = true;
let job = EnrichJob {
path: "a.yml".to_owned(),
hash: "h".to_owned(),
old_text: None,
new_text: Some(" a: 1\n".to_owned()),
hunks: vec![Hunk {
id: HunkId("h".to_owned()),
old_start: 0,
old_lines: 0,
new_start: 1,
new_lines: 1,
context: String::new(),
lines: vec![line],
}],
semantic: true,
stamp: EnrichStamp {
algorithm: DiffAlgorithm::Structural,
highlighter: 0,
},
};
let outcome = run_enrich(&Highlighter::default(), job);
let line = &outcome.hunks[0].lines[0];
assert!(!line.reformat_only);
assert!(line.emphasis.is_empty());
}
}