use std::collections::{HashMap, HashSet};
use differential_schema as schema;
use crate::lang::LanguageRegistry;
use crate::model::DiffView;
pub fn apply(doc: &mut schema::PlanDocument, view: &DiffView, langs: &LanguageRegistry) {
let Some(groups) = doc.groups.take() else {
return;
};
let hunks_of_class: HashMap<&str, &schema::ClassEntry> =
doc.classes.iter().map(|c| (c.id.as_str(), c)).collect();
let hunk_index: HashMap<&str, usize> = doc
.hunks
.iter()
.enumerate()
.map(|(i, h)| (h.id.as_str(), i))
.collect();
let mut defs: Vec<HashSet<Vec<u8>>> = Vec::with_capacity(groups.len());
let mut refs: Vec<HashSet<Vec<u8>>> = Vec::with_capacity(groups.len());
for g in &groups {
let mut d = HashSet::new();
let mut r = HashSet::new();
if g.effort != schema::Effort::Noise {
for cid in &g.class_ids {
for hid in &hunks_of_class[cid.as_str()].hunk_ids {
let h = &view.hunks[hunk_index[hid.as_str()]];
let lang = langs.detect(&view.file_of(h).path);
for line in &h.added {
d.extend(lang.symbol_definitions(line));
r.extend(lang.symbol_references(line));
}
}
}
}
defs.push(d);
refs.push(r);
}
let mut definer: HashMap<&[u8], Option<usize>> = HashMap::new();
for (gi, d) in defs.iter().enumerate() {
for sym in d {
definer
.entry(sym.as_slice())
.and_modify(|e| *e = None)
.or_insert(Some(gi));
}
}
let mut deps: Vec<HashSet<usize>> = vec![HashSet::new(); groups.len()];
for (gi, r) in refs.iter().enumerate() {
for sym in r {
if let Some(&Some(def_gi)) = definer.get(sym.as_slice())
&& def_gi != gi
{
deps[gi].insert(def_gi);
}
}
}
let mut close_len = groups
.iter()
.position(|g| g.effort != schema::Effort::Close)
.unwrap_or(groups.len());
if close_len == groups.len() && doc.audit.classes_missing.unwrap_or(0) > 0 {
close_len -= 1;
}
let hunk_count = |gi: usize| -> usize {
groups[gi]
.class_ids
.iter()
.map(|c| hunks_of_class[c.as_str()].hunk_ids.len())
.sum()
};
let order = toposort_prefix(close_len, &deps, &hunk_count);
let mut new_order: Vec<usize> = order;
new_order.extend(close_len..groups.len());
let depended_on: HashSet<usize> = deps.iter().flatten().copied().collect();
let role_of = |gi: usize| -> Option<schema::Role> {
let g = &groups[gi];
match g.effort {
schema::Effort::Noise => g.role, schema::Effort::Skim => Some(schema::Role::Mechanical),
schema::Effort::Close => {
if depended_on.contains(&gi) {
Some(schema::Role::Foundation)
} else if !deps[gi].is_empty() {
Some(schema::Role::Consumer)
} else {
None
}
}
}
};
let mut reordered: Vec<schema::Group> = Vec::with_capacity(groups.len());
for (rank, &gi) in new_order.iter().enumerate() {
let mut g = groups[gi].clone();
g.rank = rank as u32;
g.role = role_of(gi);
let mut edges: Vec<String> = deps[gi].iter().map(|&d| groups[d].id.clone()).collect();
edges.sort_unstable();
g.depends_on = edges;
reordered.push(g);
}
if let Some(plan) = doc.reading_plan.take() {
let mut by_group: HashMap<&str, Vec<schema::ReadingStep>> = HashMap::new();
for step in &plan {
by_group
.entry(step.group.as_str())
.or_default()
.push(step.clone());
}
doc.reading_plan = Some(
reordered
.iter()
.flat_map(|g| by_group.remove(g.id.as_str()).unwrap_or_default())
.collect(),
);
}
doc.groups = Some(reordered);
doc.generator.stages.push("order".to_string());
}
fn toposort_prefix(
len: usize,
deps: &[HashSet<usize>],
hunk_count: &dyn Fn(usize) -> usize,
) -> Vec<usize> {
let mut remaining: Vec<usize> = (0..len).collect();
let mut emitted: HashSet<usize> = HashSet::new();
let mut out = Vec::with_capacity(len);
while !remaining.is_empty() {
let ready: Vec<usize> = remaining
.iter()
.copied()
.filter(|&gi| deps[gi].iter().all(|d| *d >= len || emitted.contains(d)))
.collect();
let pool = if ready.is_empty() { &remaining } else { &ready };
let chosen = pool
.iter()
.copied()
.max_by_key(|&gi| (hunk_count(gi), usize::MAX - gi))
.expect("pool is non-empty");
remaining.retain(|&gi| gi != chosen);
emitted.insert(chosen);
out.push(chosen);
}
out
}
#[cfg(test)]
mod tests {
use super::*;
fn set(v: &[usize]) -> HashSet<usize> {
v.iter().copied().collect()
}
#[test]
fn foundation_precedes_consumer() {
let deps = vec![set(&[]), set(&[0])];
let counts = [5usize, 5];
let order = toposort_prefix(2, &deps, &|i| counts[i]);
assert_eq!(order, vec![0, 1]);
let deps = vec![set(&[1]), set(&[])];
let order = toposort_prefix(2, &deps, &|i| counts[i]);
assert_eq!(order, vec![1, 0]);
}
#[test]
fn ties_break_by_descending_hunk_count_then_position() {
let deps = vec![set(&[]), set(&[]), set(&[])];
let counts = [3usize, 9, 9];
let order = toposort_prefix(3, &deps, &|i| counts[i]);
assert_eq!(order, vec![1, 2, 0]);
}
#[test]
fn cycle_falls_back_deterministically() {
let deps = vec![set(&[1]), set(&[0]), set(&[])];
let counts = [2usize, 8, 1];
let order = toposort_prefix(3, &deps, &|i| counts[i]);
assert_eq!(order[0], 2);
assert_eq!(order[1], 1);
assert_eq!(order[2], 0);
}
#[test]
fn edges_outside_the_prefix_do_not_block() {
let deps = vec![set(&[3]), set(&[]), set(&[]), set(&[])];
let counts = [4usize, 2, 1, 9];
let order = toposort_prefix(3, &deps, &|i| counts[i]);
assert_eq!(order, vec![0, 1, 2]);
}
}