OXYGENHUB
◆ v0.5.0 oxygen / crates / oxy-core / src / lanes.rs
10.1 KiB · Rust · last changed oxy log draws a graph: lanes, bookmarks inline, unique ID prefixes (closes #50) 1 month ago source blame history raw
119bbf50 Andy McCall ↶ 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.
22
pub fn layout(changes: &[(ChangeHash, Vec<ChangeHash>)]) -> Vec<Row> {
23
    let mut active: Vec<Option<ChangeHash>> = Vec::new();
24
    let mut rows = Vec::with_capacity(changes.len());
25
    let known: HashSet<&ChangeHash> = changes.iter().map(|(h, _)| h).collect();
26
    let mut lanes: HashMap<ChangeHash, usize> = HashMap::new();
27
    for (hash, deps) in changes {
28
        let lane = match active.iter().position(|a| a.as_ref() == Some(hash)) {
29
            Some(l) => l,
30
            None => match active.iter().position(|a| a.is_none()) {
31
                Some(l) => {
32
                    active[l] = Some(*hash);
33
                    l
34
                }
35
                None => {
36
                    active.push(Some(*hash));
37
                    active.len() - 1
38
                }
39
            },
40
        };
41
        lanes.insert(*hash, lane);
42
        let mut edges = Vec::new();
43
        let deps: Vec<&ChangeHash> = deps.iter().filter(|d| known.contains(d)).collect();
44
        active[lane] = None;
45
        for (i, d) in deps.iter().enumerate() {
46
            let target = match active.iter().position(|a| a.as_ref() == Some(*d)) {
47
                Some(l) => l,
48
                None => {
49
                    let l = if i == 0 && active[lane].is_none() {
50
                        lane
51
                    } else {
52
                        match active.iter().position(|a| a.is_none()) {
53
                            Some(l) => l,
54
                            None => {
55
                                active.push(None);
56
                                active.len() - 1
57
                            }
58
                        }
59
                    };
60
                    active[l] = Some(**d);
61
                    l
62
                }
63
            };
64
            edges.push((lane, target));
65
        }
66
        let through: Vec<usize> = active
67
            .iter()
68
            .enumerate()
69
            .filter(|(l, a)| a.is_some() && *l != lane && !edges.iter().any(|e| e.1 == *l))
70
            .map(|(l, _)| l)
71
            .collect();
9d333fc9 Andy McCall ↶ 72
        rows.push(Row {
73
            lane,
74
            edges,
75
            through,
76
        });
119bbf50 Andy McCall ↶ 77
        while active.last().is_some_and(|a| a.is_none()) {
78
            active.pop();
79
        }
80
    }
81
    rows
82
}
83
84
/// The number of lanes the rows use.
85
pub fn width(rows: &[Row]) -> usize {
86
    rows.iter()
9d333fc9 Andy McCall ↶ 87
        .map(|r| {
88
            r.lane
89
                .max(r.edges.iter().map(|e| e.1).max().unwrap_or(0))
90
                .max(r.through.iter().copied().max().unwrap_or(0))
91
        })
119bbf50 Andy McCall ↶ 92
        .max()
93
        .map(|m| m + 1)
94
        .unwrap_or(1)
95
}
96
2bf22305 Andy McCall ↶ 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.
137
pub fn graph(changes: &[(ChangeHash, Vec<ChangeHash>)], max_edge: usize) -> Vec<GraphRow> {
138
    let index: HashMap<&ChangeHash, usize> = changes
139
        .iter()
140
        .enumerate()
141
        .map(|(i, (h, _))| (h, i))
142
        .collect();
143
    let mut active: Vec<Option<ChangeHash>> = Vec::new();
144
    let mut rows = Vec::with_capacity(changes.len());
145
    for (i, (hash, deps)) in changes.iter().enumerate() {
146
        let column = match active.iter().position(|a| a.as_ref() == Some(hash)) {
147
            Some(c) => c,
148
            None => match active.iter().position(Option::is_none) {
149
                Some(c) => {
150
                    active[c] = Some(*hash);
151
                    c
152
                }
153
                None => {
154
                    active.push(Some(*hash));
155
                    active.len() - 1
156
                }
157
            },
158
        };
159
        let passing: Vec<usize> = active
160
            .iter()
161
            .enumerate()
162
            .filter(|(c, a)| *c != column && a.is_some())
163
            .map(|(c, _)| c)
164
            .collect();
165
        // The newest dependency below this row (the list is newest first).
166
        let nearest = deps
167
            .iter()
168
            .filter_map(|d| index.get(d).copied())
169
            .filter(|&j| j > i)
170
            .min();
171
        let parent = nearest.filter(|&j| j - i <= max_edge).map(|j| changes[j].0);
172
        active[column] = None;
173
        let mut join: Option<(usize, usize)> = None;
174
        if let Some(p) = parent {
175
            match active.iter().position(|a| a.as_ref() == Some(&p)) {
176
                // Another lane already heads there: join the two, leftwards.
177
                Some(other) => {
178
                    let (left, right) = (other.min(column), other.max(column));
179
                    active[right] = None;
180
                    active[left] = Some(p);
181
                    join = Some((left, right));
182
                }
183
                None => active[column] = Some(p),
184
            }
185
        }
186
        let width = active.len().max(column + 1);
187
        let mut below = vec![Cell::Blank; width];
188
        for (c, a) in active.iter().enumerate() {
189
            if a.is_some() {
190
                below[c] = Cell::Line;
191
            }
192
        }
193
        if let Some((left, right)) = join {
194
            below[left] = Cell::JoinLeft;
195
            for cell in below.iter_mut().take(right).skip(left + 1) {
196
                *cell = if *cell == Cell::Line {
197
                    Cell::JoinCross
198
                } else {
199
                    Cell::JoinPass
200
                };
201
            }
202
            below[right] = Cell::JoinRight;
203
        } else if parent.is_none() && !deps.is_empty() {
204
            below[column] = Cell::Elided;
205
        }
206
        while below.len() > 1 && below.last() == Some(&Cell::Blank) {
207
            below.pop();
208
        }
209
        rows.push(GraphRow {
210
            column,
211
            passing,
212
            below,
213
        });
214
        while active.last().is_some_and(Option::is_none) {
215
            active.pop();
216
        }
217
    }
218
    rows
219
}
220
119bbf50 Andy McCall ↶ 221
#[cfg(test)]
222
mod tests {
223
    use super::*;
224
    use crate::hash::ObjectRef;
225
226
    fn h(n: u8) -> ChangeHash {
227
        ObjectRef([n; 32])
228
    }
229
230
    #[test]
231
    fn linear_history_is_one_lane() {
232
        let rows = layout(&[(h(3), vec![h(2)]), (h(2), vec![h(1)]), (h(1), vec![])]);
233
        assert!(rows.iter().all(|r| r.lane == 0), "{rows:?}");
234
        assert_eq!(rows[0].edges, vec![(0, 0)]);
235
        assert_eq!(width(&rows), 1);
236
    }
237
238
    #[test]
239
    fn a_diamond_takes_two_lanes_and_rejoins() {
9d333fc9 Andy McCall ↶ 240
        let rows = layout(&[
241
            (h(4), vec![h(2), h(3)]),
242
            (h(2), vec![h(1)]),
243
            (h(3), vec![h(1)]),
244
            (h(1), vec![]),
245
        ]);
119bbf50 Andy McCall ↶ 246
        assert_eq!(rows[0].edges, vec![(0, 0), (0, 1)]);
247
        assert_eq!(rows[2].lane, 1);
9d333fc9 Andy McCall ↶ 248
        assert_eq!(
249
            rows[2].edges,
250
            vec![(1, 0)],
251
            "the second branch joins the first lane"
252
        );
119bbf50 Andy McCall ↶ 253
        assert_eq!(width(&rows), 2);
2bf22305 Andy McCall ↶ 254
    }
255
256
    #[test]
257
    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).
259
        let rows = graph(
260
            &[
261
                (h(4), vec![h(2)]),
262
                (h(3), vec![h(1), h(2)]),
263
                (h(2), vec![h(1)]),
264
                (h(1), vec![]),
265
            ],
266
            12,
267
        );
268
        assert_eq!(rows[0].column, 0);
269
        assert_eq!(rows[0].below, vec![Cell::Line]);
270
        assert_eq!(rows[1].column, 1, "a second head takes a new lane");
271
        assert_eq!(rows[1].passing, vec![0]);
272
        assert_eq!(rows[1].below, vec![Cell::JoinLeft, Cell::JoinRight]);
273
        assert_eq!(rows[2].column, 0);
274
        assert_eq!(rows[3].below, vec![Cell::Blank], "a root ends its lane");
275
    }
276
277
    #[test]
278
    fn a_long_edge_is_elided() {
279
        let mut changes = vec![(h(100), vec![h(1)])];
280
        for n in (2..=20).rev() {
281
            changes.push((h(n), vec![]));
282
        }
283
        changes.push((h(1), vec![]));
284
        let rows = graph(&changes, 5);
285
        assert_eq!(rows[0].below, vec![Cell::Elided]);
286
        assert!(
287
            rows.iter().all(|r| r.below.len() == 1),
288
            "one lane all the way"
289
        );
290
        let rows = graph(&changes, 100);
291
        assert_eq!(rows[0].below, vec![Cell::Line], "drawn when short enough");
119bbf50 Andy McCall ↶ 292
    }
293
}