Skip to content

Commit 658d950

Browse files
geraschenkometa-codesync[bot]
authored andcommitted
renderdag: emit link_line for same-column named parents (#1293)
Summary: Fixes #1292 The actual fix is the 3-line change to `eden/scm/lib/renderdag/src/render.rs`. Everything else is adding a new test fixture (`BASIC_DISCONNECTED`) and test (`basic_disconnected_min_row_height_1`) which demonstrates the issue. Before / After (box_drawing, min_row_height=1) for new `BASIC_DISCONNECTED` fixture (`dag: "A B-C"`, heads `["A", "C"]`): ``` Before: o C o B o A After: o C │ o B o A ``` Pull Request resolved: #1293 Reviewed By: giorgidze Differential Revision: D106722405 Pulled By: quark-zju fbshipit-source-id: 143c532a07a79d7bfda00abad1d6a294f1075c94
1 parent 110afc8 commit 658d950

6 files changed

Lines changed: 224 additions & 5 deletions

File tree

eden/scm/lib/renderdag/src/ascii.rs

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,10 @@ where
7676
out.push('\n');
7777
}
7878

79+
if line.blank_line_before {
80+
out.push('\n');
81+
}
82+
7983
// Render the nodeline
8084
let mut node_line = String::new();
8185
for entry in line.node_line.iter() {
@@ -240,6 +244,56 @@ mod tests {
240244
);
241245
}
242246

247+
#[test]
248+
fn basic_disconnected() {
249+
assert_eq!(
250+
render(&test_fixtures::BASIC_DISCONNECTED),
251+
r#"
252+
o D
253+
|
254+
o C
255+
256+
o B
257+
258+
o A"#
259+
);
260+
}
261+
262+
#[test]
263+
fn basic_disconnected_min_row_height_1() {
264+
let mut renderer = GraphRowRenderer::new()
265+
.output()
266+
.with_min_row_height(1)
267+
.build_ascii();
268+
assert_eq!(
269+
render_string(&test_fixtures::BASIC_DISCONNECTED, &mut renderer),
270+
r#"
271+
o D
272+
o C
273+
274+
o B
275+
276+
o A"#
277+
);
278+
}
279+
280+
#[test]
281+
fn basic_disconnected_min_row_height_1_staggered() {
282+
let mut renderer = GraphRowRenderer::new()
283+
.output()
284+
.with_min_row_height(1)
285+
.with_stagger_consecutive_disconnected_nodes(true)
286+
.build_ascii();
287+
assert_eq!(
288+
render_string(&test_fixtures::BASIC_DISCONNECTED, &mut renderer),
289+
r#"
290+
o D
291+
o C
292+
o B
293+
o A"#
294+
);
295+
}
296+
243297
#[test]
244298
fn branches_and_merges() {
245299
assert_eq!(

eden/scm/lib/renderdag/src/ascii_large.rs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,10 @@ where
7878
out.push('\n');
7979
}
8080

81+
if line.blank_line_before {
82+
out.push('\n');
83+
}
84+
8185
// Render the nodeline
8286
let mut node_line = String::new();
8387
for (i, entry) in line.node_line.iter().enumerate() {
@@ -296,6 +300,24 @@ mod tests {
296300
);
297301
}
298302

303+
#[test]
304+
fn basic_disconnected() {
305+
assert_eq!(
306+
render(&test_fixtures::BASIC_DISCONNECTED),
307+
r#"
308+
o D
309+
|
310+
|
311+
o C
312+
313+
314+
o B
315+
316+
317+
o A"#
318+
);
319+
}
320+
299321
#[test]
300322
fn branches_and_merges() {
301323
assert_eq!(

eden/scm/lib/renderdag/src/box_drawing.rs

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,10 @@ where
142142
out.push('\n');
143143
}
144144

145+
if line.blank_line_before {
146+
out.push('\n');
147+
}
148+
145149
// Render the nodeline
146150
let mut node_line = String::new();
147151
for entry in line.node_line.iter() {
@@ -319,6 +323,56 @@ mod tests {
319323
);
320324
}
321325

326+
#[test]
327+
fn basic_disconnected() {
328+
assert_eq!(
329+
render(&test_fixtures::BASIC_DISCONNECTED),
330+
r#"
331+
o D
332+
333+
o C
334+
335+
o B
336+
337+
o A"#
338+
);
339+
}
340+
341+
#[test]
342+
fn basic_disconnected_min_row_height_1() {
343+
let mut renderer = GraphRowRenderer::new()
344+
.output()
345+
.with_min_row_height(1)
346+
.build_box_drawing();
347+
assert_eq!(
348+
render_string(&test_fixtures::BASIC_DISCONNECTED, &mut renderer),
349+
r#"
350+
o D
351+
o C
352+
353+
o B
354+
355+
o A"#
356+
);
357+
}
358+
359+
#[test]
360+
fn basic_disconnected_min_row_height_1_staggered() {
361+
let mut renderer = GraphRowRenderer::new()
362+
.output()
363+
.with_min_row_height(1)
364+
.with_stagger_consecutive_disconnected_nodes(true)
365+
.build_box_drawing();
366+
assert_eq!(
367+
render_string(&test_fixtures::BASIC_DISCONNECTED, &mut renderer),
368+
r#"
369+
o D
370+
o C
371+
o B
372+
o A"#
373+
);
374+
}
375+
322376
#[test]
323377
fn branches_and_merges() {
324378
assert_eq!(

eden/scm/lib/renderdag/src/output.rs

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ use super::box_drawing::BoxDrawingRenderer;
1313
use super::render::GraphRow;
1414
use super::render::Renderer;
1515

16+
pub(crate) const DEFAULT_MIN_ROW_HEIGHT: usize = 2;
17+
1618
pub(crate) struct OutputRendererOptions {
1719
pub(crate) min_row_height: usize,
1820
}
@@ -33,13 +35,21 @@ where
3335
pub fn new(inner: R) -> Self {
3436
OutputRendererBuilder {
3537
inner,
36-
options: OutputRendererOptions { min_row_height: 2 },
38+
options: OutputRendererOptions {
39+
min_row_height: DEFAULT_MIN_ROW_HEIGHT,
40+
},
3741
_phantom: PhantomData,
3842
}
3943
}
4044

4145
pub fn with_min_row_height(mut self, min_row_height: usize) -> Self {
4246
self.options.min_row_height = min_row_height;
47+
self.inner.set_min_row_height(min_row_height);
48+
self
49+
}
50+
51+
pub fn with_stagger_consecutive_disconnected_nodes(mut self, stagger: bool) -> Self {
52+
self.inner.set_stagger_disconnected_nodes(stagger);
4353
self
4454
}
4555

eden/scm/lib/renderdag/src/render.rs

Lines changed: 71 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ use serde::Serialize;
1414

1515
use super::column::Column;
1616
use super::column::ColumnsExt;
17+
use super::output::DEFAULT_MIN_ROW_HEIGHT;
1718
use super::output::OutputRendererBuilder;
1819

1920
pub trait Renderer<N> {
@@ -22,6 +23,13 @@ pub trait Renderer<N> {
2223
// Returns the width of the graph line, possibly including another node.
2324
fn width(&self, new_node: Option<&N>, new_parents: Option<&Vec<Ancestor<N>>>) -> u64;
2425

26+
// Set the minimum rendered row height.
27+
fn set_min_row_height(&mut self, _min_row_height: usize) {}
28+
29+
// Set whether disconnected consecutive nodes should be staggered into
30+
// different columns instead of separated by a blank line.
31+
fn set_stagger_disconnected_nodes(&mut self, _stagger: bool) {}
32+
2533
// Reserve a column for the given node.
2634
fn reserve(&mut self, node: N);
2735

@@ -40,6 +48,15 @@ pub trait Renderer<N> {
4048
/// Converts a sequence of DAG node descriptions into rendered graph rows.
4149
pub struct GraphRowRenderer<N> {
4250
columns: Vec<Column<N>>,
51+
// The remaining fields only have an effect when min_row_height is 1. With
52+
// taller rows, the padding row already distinguishes connected same-column
53+
// nodes from disconnected same-column nodes. But when there is no padding
54+
// row, we must track what column the previous node is in so that either
55+
// vertical or horizontal space can be added to indicate that the next node
56+
// is not connected to the previous node.
57+
min_row_height: usize,
58+
stagger_disconnected_nodes: bool,
59+
previous_node_column: Option<usize>,
4360
}
4461

4562
/// Ancestor type indication for an ancestor or parent node.
@@ -298,6 +315,9 @@ pub struct GraphRow<N> {
298315

299316
/// The pad columns for this row.
300317
pub pad_lines: Vec<PadLine>,
318+
319+
/// True if a blank line should be rendered before this row.
320+
pub blank_line_before: bool,
301321
}
302322

303323
impl<N> GraphRowRenderer<N>
@@ -308,6 +328,9 @@ where
308328
pub fn new() -> Self {
309329
GraphRowRenderer {
310330
columns: Vec::new(),
331+
min_row_height: DEFAULT_MIN_ROW_HEIGHT,
332+
stagger_disconnected_nodes: false,
333+
previous_node_column: None,
311334
}
312335
}
313336

@@ -335,6 +358,20 @@ where
335358
// space for the node, then adding the new node would create
336359
// a new column.
337360
if self.columns.find(node).is_none() {
361+
if self.min_row_height == 1 && self.stagger_disconnected_nodes {
362+
if let Some(previous_node_column) = self.previous_node_column {
363+
if self.columns.get(previous_node_column) == Some(&Column::Empty) {
364+
// Dense stagger mode cannot use the previous node's column for an
365+
// unallocated node, so do not count that empty column as available.
366+
empty_columns = empty_columns.saturating_sub(1);
367+
} else if previous_node_column == self.columns.len() {
368+
// The previous node's column was trimmed from the end of the column
369+
// list. To keep the new node out of that column, rendering it requires
370+
// a blank placeholder column plus a new column for the node.
371+
width += 1;
372+
}
373+
}
374+
}
338375
if empty_columns == 0 {
339376
width += 1;
340377
} else {
@@ -360,6 +397,14 @@ where
360397
width as u64
361398
}
362399

400+
fn set_min_row_height(&mut self, min_row_height: usize) {
401+
self.min_row_height = min_row_height;
402+
}
403+
404+
fn set_stagger_disconnected_nodes(&mut self, stagger: bool) {
405+
self.stagger_disconnected_nodes = stagger;
406+
}
407+
363408
fn reserve(&mut self, node: N) {
364409
if self.columns.find(&node).is_none() {
365410
if let Some(index) = self.columns.first_empty() {
@@ -378,10 +423,30 @@ where
378423
message: String,
379424
) -> GraphRow<N> {
380425
// Find a column for this node.
381-
let column = self.columns.find(&node).unwrap_or_else(|| {
382-
self.columns
383-
.first_empty()
384-
.unwrap_or_else(|| self.columns.new_empty())
426+
let existing_column = self.columns.find(&node);
427+
let mut blank_line_before = false;
428+
let column = existing_column.unwrap_or_else(|| {
429+
let column = if self.min_row_height == 1 && self.stagger_disconnected_nodes {
430+
if let Some(index) = self.columns.iter().enumerate().find_map(|(index, column)| {
431+
(*column == Column::Empty && Some(index) != self.previous_node_column)
432+
.then_some(index)
433+
}) {
434+
index
435+
} else {
436+
if self.previous_node_column == Some(self.columns.len()) {
437+
self.columns.push(Column::Empty);
438+
}
439+
self.columns.new_empty()
440+
}
441+
} else {
442+
self.columns
443+
.first_empty()
444+
.unwrap_or_else(|| self.columns.new_empty())
445+
};
446+
blank_line_before = self.min_row_height == 1
447+
&& !self.stagger_disconnected_nodes
448+
&& Some(column) == self.previous_node_column;
449+
column
385450
});
386451
self.columns[column] = Column::Empty;
387452

@@ -527,6 +592,7 @@ where
527592

528593
// Now that we have assigned all the columns, reset their state.
529594
self.columns.reset();
595+
self.previous_node_column = Some(column);
530596

531597
// Filter out the link line or term line if they are not needed.
532598
let link_line = Some(link_line).filter(|_| need_link_line);
@@ -541,6 +607,7 @@ where
541607
link_line,
542608
term_line,
543609
pad_lines,
610+
blank_line_before,
544611
}
545612
}
546613
}

eden/scm/lib/renderdag/src/test_fixtures.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,18 @@ pub(crate) const BASIC: TestFixture = TestFixture {
2323
missing: &[],
2424
};
2525

26+
// C-D is a connected pair; A and B are isolated nodes that end up in the same
27+
// column after D's column is freed. Used to verify that the renderer visually
28+
// distinguishes connected from unconnected adjacent nodes.
29+
pub(crate) const BASIC_DISCONNECTED: TestFixture = TestFixture {
30+
dag: "A B C-D",
31+
messages: &[],
32+
heads: &["A", "B", "D"],
33+
reserve: &[],
34+
ancestors: &[],
35+
missing: &[],
36+
};
37+
2638
pub(crate) const BRANCHES_AND_MERGES: TestFixture = TestFixture {
2739
dag: r#"
2840
T /---------------N--O---\ T

0 commit comments

Comments
 (0)