OXYGENHUB

Coverage / crates/oxy-core/src/lanes.rs 94.5%

188 of 199 lines run, 36 of 49 functions · run #393 on identical-sides · run missed

1
//! Lane layout for drawing history as a graph: each change takes a lane,
2
//! its first dependency continues that lane, other dependencies take free
3
//! lanes, so edges flow from a change down to what it depends on. Shared
4
//! by `oxy log` and hosts, so every view draws the same picture.
5
6
use crate::hash::ChangeHash;
7
use std::collections::{HashMap, HashSet};
8
9
/// A row of the graph: the change's lane and the edges drawn from it.
10
#[derive(Clone, Debug, PartialEq, Eq)]
11
pub struct Row {
12
    pub lane: usize,
13
    /// `(from_lane, to_lane)` for each dependency drawn from this row to
14
    /// the rows below.
15
    pub edges: Vec<(usize, usize)>,
16
    /// Lanes that pass through this row without touching it.
17
    pub through: Vec<usize>,
18
}
19
20
/// `changes` newest first as `(hash, deps)`. A dependency outside the
21
/// list ends its lane at the bottom.
222×
pub fn layout(changes: &[(ChangeHash, Vec<ChangeHash>)]) -> Vec<Row> {
232×
    let mut active: Vec<Option<ChangeHash>> = Vec::new();
242×
    let mut rows = Vec::with_capacity(changes.len());
252×
    let known: HashSet<&ChangeHash> = changes.iter().map(|(h, _)| h).collect();
262×
    let mut lanes: HashMap<ChangeHash, usize> = HashMap::new();
277×
    for (hash, deps) in changes {
287×
        let lane = match active.iter().position(|a| a.as_ref() == Some(hash)) {
295×
            Some(l) => l,
302×
            None => match active.iter().position(|a| a.is_none()) {
310×
                Some(l) => {
320×
                    active[l] = Some(*hash);
330×
                    l
34
                }
35
                None => {
362×
                    active.push(Some(*hash));
372×
                    active.len() - 1
38
                }
39
            },
40
        };
417×
        lanes.insert(*hash, lane);
427×
        let mut edges = Vec::new();
437×
        let deps: Vec<&ChangeHash> = deps.iter().filter(|d| known.contains(d)).collect();
447×
        active[lane] = None;
457×
        for (i, d) in deps.iter().enumerate() {
467×
            let target = match active.iter().position(|a| a.as_ref() == Some(*d)) {
471×
                Some(l) => l,
48
                None => {
495×
                    let l = if i == 0 && active[lane].is_none() {
504×
                        lane
51
                    } else {
521×
                        match active.iter().position(|a| a.is_none()) {
530×
                            Some(l) => l,
54
                            None => {
551×
                                active.push(None);
561×
                                active.len() - 1
57
                            }
58
                        }
59
                    };
605×
                    active[l] = Some(**d);
615×
                    l
62
                }
63
            };
646×
            edges.push((lane, target));
65
        }
667×
        let through: Vec<usize> = active
677×
            .iter()
687×
            .enumerate()
6910×
            .filter(|(l, a)| a.is_some() && *l != lane && !edges.iter().any(|e| e.1 == *l))
707×
            .map(|(l, _)| l)
717×
            .collect();
727×
        rows.push(Row {
737×
            lane,
747×
            edges,
757×
            through,
767×
        });
7710×
        while active.last().is_some_and(|a| a.is_none()) {
783×
            active.pop();
793×
        }
80
    }
812×
    rows
822×
}
83
84
/// The number of lanes the rows use.
852×
pub fn width(rows: &[Row]) -> usize {
862×
    rows.iter()
877×
        .map(|r| {
887×
            r.lane
897×
                .max(r.edges.iter().map(|e| e.1).max().unwrap_or(0))
907×
                .max(r.through.iter().copied().max().unwrap_or(0))
917×
        })
922×
        .max()
932×
        .map(|m| m + 1)
942×
        .unwrap_or(1)
952×
}
96
97
/// One cell of a graph row's lower line, the line under the change.
98
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
99
pub enum Cell {
100
    /// Nothing in this column.
101
    Blank,
102
    /// A lane carries on down.
103
    Line,
104
    /// The change's lane ends at a dependency not drawn (too far down, or
105
    /// not in the list): `~`.
106
    Elided,
107
    /// The left end of a join: this lane carries on, and the lane to its
108
    /// right turns into it (`├`).
109
    JoinLeft,
110
    /// A join passes a column with no lane (`─`).
111
    JoinPass,
112
    /// A join crosses a lane that carries on (`┼`).
113
    JoinCross,
114
    /// The right end of a join: this lane turns left and ends (`╯`).
115
    JoinRight,
116
}
117
118
/// A row of the drawn graph: the change's column, which other columns
119
/// carry a lane past it, and the line under it.
120
#[derive(Clone, Debug, PartialEq, Eq)]
121
pub struct GraphRow {
122
    pub column: usize,
123
    /// Columns with a lane on the change's own line, besides its own.
124
    pub passing: Vec<usize>,
125
    /// The line under the change, one cell per column.
126
    pub below: Vec<Cell>,
127
}
128
129
/// Lay out `changes` (newest first, as `(hash, deps)`) for a terminal
130
/// graph. A change's dependencies form a wide graph, since a change builds
131
/// on whatever last touched the lines it edits, so only one edge is drawn
132
/// per change: to its newest dependency, the change it most directly
133
/// builds on. An edge longer than `max_edge` rows, or to a change not in
134
/// the list, is elided (`~`), as are the other dependencies, so the
135
/// picture keeps to a few lanes. Two lanes heading for the same change
136
/// join into the left one.
1374×
pub fn graph(changes: &[(ChangeHash, Vec<ChangeHash>)], max_edge: usize) -> Vec<GraphRow> {
1384×
    let index: HashMap<&ChangeHash, usize> = changes
1394×
        .iter()
1404×
        .enumerate()
14150×
        .map(|(i, (h, _))| (h, i))
1424×
        .collect();
1434×
    let mut active: Vec<Option<ChangeHash>> = Vec::new();
1444×
    let mut rows = Vec::with_capacity(changes.len());
14550×
    for (i, (hash, deps)) in changes.iter().enumerate() {
14650×
        let column = match active.iter().position(|a| a.as_ref() == Some(hash)) {
1475×
            Some(c) => c,
14845×
            None => match active.iter().position(Option::is_none) {
1490×
                Some(c) => {
1500×
                    active[c] = Some(*hash);
1510×
                    c
152
                }
153
                None => {
15445×
                    active.push(Some(*hash));
15545×
                    active.len() - 1
156
                }
157
            },
158
        };
15950×
        let passing: Vec<usize> = active
16050×
            .iter()
16150×
            .enumerate()
16271×
            .filter(|(c, a)| *c != column && a.is_some())
16350×
            .map(|(c, _)| c)
16450×
            .collect();
165
        // The newest dependency below this row (the list is newest first).
16650×
        let nearest = deps
16750×
            .iter()
16850×
            .filter_map(|d| index.get(d).copied())
16950×
            .filter(|&j| j > i)
17050×
            .min();
17150×
        let parent = nearest.filter(|&j| j - i <= max_edge).map(|j| changes[j].0);
17250×
        active[column] = None;
17350×
        let mut join: Option<(usize, usize)> = None;
17450×
        if let Some(p) = parent {
1757×
            match active.iter().position(|a| a.as_ref() == Some(&p)) {
176
                // Another lane already heads there: join the two, leftwards.
1772×
                Some(other) => {
1782×
                    let (left, right) = (other.min(column), other.max(column));
1792×
                    active[right] = None;
1802×
                    active[left] = Some(p);
1812×
                    join = Some((left, right));
1822×
                }
1835×
                None => active[column] = Some(p),
184
            }
18543×
        }
18650×
        let width = active.len().max(column + 1);
18750×
        let mut below = vec![Cell::Blank; width];
18871×
        for (c, a) in active.iter().enumerate() {
18971×
            if a.is_some() {
19026×
                below[c] = Cell::Line;
19145×
            }
192
        }
19350×
        if let Some((left, right)) = join {
1942×
            below[left] = Cell::JoinLeft;
1952×
            for cell in below.iter_mut().take(right).skip(left + 1) {
1960×
                *cell = if *cell == Cell::Line {
1970×
                    Cell::JoinCross
198
                } else {
1990×
                    Cell::JoinPass
200
                };
201
            }
2022×
            below[right] = Cell::JoinRight;
20348×
        } else if parent.is_none() && !deps.is_empty() {
2041×
            below[column] = Cell::Elided;
20547×
        }
20669×
        while below.len() > 1 && below.last() == Some(&Cell::Blank) {
20719×
            below.pop();
20819×
        }
20950×
        rows.push(GraphRow {
21050×
            column,
21150×
            passing,
21250×
            below,
21350×
        });
21495×
        while active.last().is_some_and(Option::is_none) {
21545×
            active.pop();
21645×
        }
217
    }
2184×
    rows
2194×
}
220
221
#[cfg(test)]
222
mod tests {
223
    use super::*;
224
    use crate::hash::ObjectRef;
225
22643×
    fn h(n: u8) -> ChangeHash {
22743×
        ObjectRef([n; 32])
22843×
    }
229
230
    #[test]
2311×
    fn linear_history_is_one_lane() {
2321×
        let rows = layout(&[(h(3), vec![h(2)]), (h(2), vec![h(1)]), (h(1), vec![])]);
2333×
        assert!(rows.iter().all(|r| r.lane == 0), "{rows:?}");
2341×
        assert_eq!(rows[0].edges, vec![(0, 0)]);
2351×
        assert_eq!(width(&rows), 1);
2361×
    }
237
238
    #[test]
2391×
    fn a_diamond_takes_two_lanes_and_rejoins() {
2401×
        let rows = layout(&[
2411×
            (h(4), vec![h(2), h(3)]),
2421×
            (h(2), vec![h(1)]),
2431×
            (h(3), vec![h(1)]),
2441×
            (h(1), vec![]),
2451×
        ]);
2461×
        assert_eq!(rows[0].edges, vec![(0, 0), (0, 1)]);
2471×
        assert_eq!(rows[2].lane, 1);
2481×
        assert_eq!(
2491×
            rows[2].edges,
2501×
            vec![(1, 0)],
251
            "the second branch joins the first lane"
252
        );
2531×
        assert_eq!(width(&rows), 2);
2541×
    }
255
256
    #[test]
2571×
    fn the_graph_draws_the_newest_dependency_and_joins_lanes_left() {
258
        // 4 and 3 both build on 2; 2 on 1; 3 also depends on 1 (not drawn).
2591×
        let rows = graph(
2601×
            &[
2611×
                (h(4), vec![h(2)]),
2621×
                (h(3), vec![h(1), h(2)]),
2631×
                (h(2), vec![h(1)]),
2641×
                (h(1), vec![]),
2651×
            ],
266
            12,
267
        );
2681×
        assert_eq!(rows[0].column, 0);
2691×
        assert_eq!(rows[0].below, vec![Cell::Line]);
2701×
        assert_eq!(rows[1].column, 1, "a second head takes a new lane");
2711×
        assert_eq!(rows[1].passing, vec![0]);
2721×
        assert_eq!(rows[1].below, vec![Cell::JoinLeft, Cell::JoinRight]);
2731×
        assert_eq!(rows[2].column, 0);
2741×
        assert_eq!(rows[3].below, vec![Cell::Blank], "a root ends its lane");
2751×
    }
276
277
    #[test]
2781×
    fn a_long_edge_is_elided() {
2791×
        let mut changes = vec![(h(100), vec![h(1)])];
28019×
        for n in (2..=20).rev() {
28119×
            changes.push((h(n), vec![]));
28219×
        }
2831×
        changes.push((h(1), vec![]));
2841×
        let rows = graph(&changes, 5);
2851×
        assert_eq!(rows[0].below, vec![Cell::Elided]);
2861×
        assert!(
28721×
            rows.iter().all(|r| r.below.len() == 1),
288
            "one lane all the way"
289
        );
2901×
        let rows = graph(&changes, 100);
2911×
        assert_eq!(rows[0].below, vec![Cell::Line], "drawn when short enough");
2921×
    }
293
}