//! Lane layout for the history graph (hub spec ยง6.7): each change takes a //! lane, its first dependency continues that lane, other dependencies //! take free lanes, so edges flow downward from a change to what it //! depends on. Ported from the VS Code extension's webview. use std::collections::HashMap; /// A row of the graph: the change's lane and the edges to the rows below. #[derive(Clone, Debug, PartialEq, Eq)] pub struct Row { pub lane: usize, /// `(from_lane, to_lane)` for each dependency drawn from this row. pub edges: Vec<(usize, usize)>, /// Lanes that pass through this row without touching it. pub through: Vec, } /// `changes` newest first as `(hash, deps)`. Dependencies outside the list /// end their lane at the bottom. pub fn layout(changes: &[(String, Vec)]) -> Vec { let mut active: Vec> = Vec::new(); let mut rows = Vec::with_capacity(changes.len()); let known: std::collections::HashSet<&str> = changes.iter().map(|(h, _)| h.as_str()).collect(); let mut lanes: HashMap = HashMap::new(); for (hash, deps) in changes { let lane = match active.iter().position(|a| a.as_deref() == Some(hash)) { Some(l) => l, None => match active.iter().position(|a| a.is_none()) { Some(l) => { active[l] = Some(hash.clone()); l } None => { active.push(Some(hash.clone())); active.len() - 1 } }, }; lanes.insert(hash.clone(), lane); let mut edges = Vec::new(); let deps: Vec<&String> = deps.iter().filter(|d| known.contains(d.as_str())).collect(); active[lane] = None; for (i, d) in deps.iter().enumerate() { let target = match active.iter().position(|a| a.as_deref() == Some(d.as_str())) { Some(l) => l, None => { let l = if i == 0 && active[lane].is_none() { lane } else { match active.iter().position(|a| a.is_none()) { Some(l) => l, None => { active.push(None); active.len() - 1 } } }; active[l] = Some((*d).clone()); l } }; edges.push((lane, target)); } let through: Vec = active .iter() .enumerate() .filter(|(l, a)| a.is_some() && *l != lane && !edges.iter().any(|e| e.1 == *l)) .map(|(l, _)| l) .collect(); rows.push(Row { lane, edges, through }); // Trim trailing free lanes so the graph stays narrow. while active.last().is_some_and(|a| a.is_none()) { active.pop(); } } rows } pub fn width(rows: &[Row]) -> usize { rows.iter() .map(|r| r.lane.max(r.edges.iter().map(|e| e.1).max().unwrap_or(0)).max(r.through.iter().copied().max().unwrap_or(0))) .max() .map(|m| m + 1) .unwrap_or(1) } #[cfg(test)] mod tests { use super::*; fn c(h: &str, deps: &[&str]) -> (String, Vec) { (h.into(), deps.iter().map(|d| d.to_string()).collect()) } #[test] fn linear_history_is_one_lane() { let rows = layout(&[c("c", &["b"]), c("b", &["a"]), c("a", &[])]); assert!(rows.iter().all(|r| r.lane == 0), "{rows:?}"); assert_eq!(rows[0].edges, vec![(0, 0)]); assert_eq!(width(&rows), 1); } #[test] fn independent_changes_take_lanes_and_rejoin() { // d depends on b and c, which both depend on a: a diamond. let rows = layout(&[c("d", &["b", "c"]), c("b", &["a"]), c("c", &["a"]), c("a", &[])]); assert_eq!(rows[0].lane, 0); assert_eq!(rows[0].edges, vec![(0, 0), (0, 1)]); assert_eq!(rows[1].lane, 0); assert_eq!(rows[2].lane, 1); assert_eq!(rows[2].edges, vec![(1, 0)], "c joins a's lane"); assert_eq!(rows[3].lane, 0); assert_eq!(width(&rows), 2); // Two heads with no common change: two lanes throughout. let rows = layout(&[c("x", &[]), c("y", &[])]); assert_eq!((rows[0].lane, rows[1].lane), (0, 0), "a lane is reused once its change is drawn"); } }