// Copyright 2020 The Jujutsu Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // https://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. use std::hash::Hash; use std::io::Write; use std::{cmp, io}; use itertools::Itertools; use jj_lib::settings::UserSettings; use renderdag::{Ancestor, GraphRowRenderer, Renderer}; #[derive(Debug, Clone, PartialEq, Eq)] // An edge to another node in the graph pub enum Edge { Present { target: T, direct: bool }, Missing, } impl Edge { pub fn missing() -> Self { Edge::Missing } pub fn direct(id: T) -> Self { Edge::Present { target: id, direct: true, } } pub fn indirect(id: T) -> Self { Edge::Present { target: id, direct: false, } } } pub trait GraphLog { fn add_node( &mut self, id: &K, edges: &[Edge], node_symbol: &str, text: &str, ) -> io::Result<()>; fn default_node_symbol(&self) -> &str; fn width(&self, id: &K, edges: &[Edge]) -> usize; } pub struct SaplingGraphLog<'writer, R> { renderer: R, writer: &'writer mut dyn Write, default_node_symbol: String, } impl From<&Edge> for Ancestor { fn from(e: &Edge) -> Self { match e { Edge::Present { target, direct: true, } => Ancestor::Parent(target.clone()), Edge::Present { target, .. } => Ancestor::Ancestor(target.clone()), Edge::Missing => Ancestor::Anonymous, } } } impl<'writer, K, R> GraphLog for SaplingGraphLog<'writer, R> where K: Clone + Eq + Hash, R: Renderer, { fn add_node( &mut self, id: &K, edges: &[Edge], node_symbol: &str, text: &str, ) -> io::Result<()> { let row = self.renderer.next_row( id.clone(), edges.iter().map_into().collect(), node_symbol.into(), text.into(), ); write!(self.writer, "{row}") } fn default_node_symbol(&self) -> &str { &self.default_node_symbol } fn width(&self, id: &K, edges: &[Edge]) -> usize { let parents = edges.iter().map_into().collect(); let w: u64 = self.renderer.width(Some(id), Some(&parents)); w.try_into().unwrap() } } impl<'writer, R> SaplingGraphLog<'writer, R> { pub fn create( renderer: R, formatter: &'writer mut dyn Write, default_node_symbol: &str, ) -> Box + 'writer> where K: Clone + Eq + Hash + 'writer, R: Renderer + 'writer, { Box::new(SaplingGraphLog { renderer, writer: formatter, default_node_symbol: default_node_symbol.to_owned(), }) } } pub fn get_graphlog<'a, K: Clone + Eq + Hash + 'a>( settings: &UserSettings, formatter: &'a mut dyn Write, ) -> Box + 'a> { let builder = GraphRowRenderer::new().output().with_min_row_height(0); match settings.graph_style().as_str() { "curved" => SaplingGraphLog::create(builder.build_box_drawing(), formatter, "◉"), "square" => SaplingGraphLog::create( builder.build_box_drawing().with_square_glyphs(), formatter, "◉", ), "ascii" => SaplingGraphLog::create(builder.build_ascii(), formatter, "o"), "ascii-large" => SaplingGraphLog::create(builder.build_ascii_large(), formatter, "o"), _ => Box::new(AsciiGraphDrawer::new(formatter)), } } pub struct AsciiGraphDrawer<'writer, K> { writer: &'writer mut dyn Write, edges: Vec>, pending_text: Vec, } impl<'writer, K> AsciiGraphDrawer<'writer, K> where K: Clone + Eq + Hash, { pub fn new(writer: &'writer mut dyn Write) -> Self { Self { writer, edges: Default::default(), pending_text: Default::default(), } } } impl<'writer, K: Clone + Eq + Hash> GraphLog for AsciiGraphDrawer<'writer, K> { fn add_node( &mut self, id: &K, edges: &[Edge], node_symbol: &str, text: &str, ) -> io::Result<()> { assert!(self.pending_text.is_empty()); for line in text.split(|x| x == '\n') { self.pending_text.push(line.into()); } if self.pending_text.last() == Some(&"".into()) { self.pending_text.pop().unwrap(); } self.pending_text.reverse(); // Check if an existing edge should be terminated by the new node. If there // is, draw the new node in the same column. Otherwise, insert it at the right. let edge_index = if let Some(edge_index) = self.index_by_target(id) { // This edge terminates in the node we're adding // If we're inserting a merge somewhere that's not the very right, the edges // right of it will move further right, so we need to prepare by inserting rows // of '\'. if edges.len() > 2 && edge_index < self.edges.len() - 1 { for i in 2..edges.len() { for edge in self.edges.iter().take(edge_index + 1) { AsciiGraphDrawer::straight_edge(&mut self.writer, edge)?; } for _ in 0..i - 2 { write!(self.writer, " ")?; } for _ in edge_index + 1..self.edges.len() { write!(self.writer, " \\")?; } writeln!(self.writer)?; } } self.edges.remove(edge_index); edge_index } else { self.edges.len() }; // Draw the edges to the left of the new node for edge in self.edges.iter().take(edge_index) { AsciiGraphDrawer::straight_edge(&mut self.writer, edge)?; } // Draw the new node write!(self.writer, "{node_symbol}")?; // If it's a merge of many nodes, draw a vertical line to the right for _ in 3..edges.len() { write!(self.writer, "--")?; } if edges.len() > 2 { write!(self.writer, "-.")?; } write!(self.writer, " ")?; // Draw the edges to the right of the new node for edge in self.edges.iter().skip(edge_index) { AsciiGraphDrawer::straight_edge(&mut self.writer, edge)?; } if edges.len() > 1 { write!(self.writer, " ")?; } self.maybe_write_pending_text()?; // Update the data model. for (i, edge) in edges.iter().enumerate() { self.edges.insert(edge_index + i, edge.clone()); } // If it's a merge commit, insert a row of '\'. if edges.len() >= 2 { for edge in self.edges.iter().take(edge_index) { AsciiGraphDrawer::straight_edge(&mut self.writer, edge)?; } AsciiGraphDrawer::straight_edge_no_space(&mut self.writer, &self.edges[edge_index])?; for _ in edge_index + 1..self.edges.len() { write!(self.writer, "\\ ")?; } write!(self.writer, " ")?; self.maybe_write_pending_text()?; } let pad_to_index = self.edges.len() + usize::from(edges.is_empty()); // Close any edges to missing nodes. for (i, edge) in edges.iter().enumerate().rev() { if *edge == Edge::Missing { self.close_missing_edge(edge_index + i, pad_to_index)?; } } // If this was the last node in a chain, add a "/" for any edges to the right of // it. if edges.is_empty() && edge_index < self.edges.len() { self.close_edge(edge_index, pad_to_index)?; } // Merge new edges that share the same target. let mut source_index = 1; while source_index < self.edges.len() { if let Edge::Present { target, .. } = &self.edges[source_index] { if let Some(target_index) = self.index_by_target(target) { // There already is an edge leading to the same target node. Mark that we // want to merge the higher index into the lower index. if source_index > target_index { self.merge_edges(source_index, target_index, pad_to_index)?; // Don't increment source_index. continue; } } } source_index += 1; } // Emit any remaining lines of text. while !self.pending_text.is_empty() { for edge in &self.edges { AsciiGraphDrawer::straight_edge(&mut self.writer, edge)?; } for _ in self.edges.len()..pad_to_index { write!(self.writer, " ")?; } self.maybe_write_pending_text()?; } Ok(()) } fn default_node_symbol(&self) -> &str { "o" } fn width(&self, id: &K, edges: &[Edge]) -> usize { let orig = self.edges.len() - usize::from(self.index_by_target(id).is_some()); let added = cmp::max(edges.len(), 1); 2 * (orig + added) } } impl<'writer, K: Clone + Eq + Hash> AsciiGraphDrawer<'writer, K> { fn index_by_target(&self, id: &K) -> Option { for (i, edge) in self.edges.iter().enumerate() { match edge { Edge::Present { target, .. } if target == id => return Some(i), _ => {} } } None } /// Not an instance method so the caller doesn't need mutable access to the /// whole struct. fn straight_edge(writer: &mut dyn Write, edge: &Edge) -> io::Result<()> { AsciiGraphDrawer::straight_edge_no_space(writer, edge)?; write!(writer, " ") } /// Not an instance method so the caller doesn't need mutable access to the /// whole struct. fn straight_edge_no_space(writer: &mut dyn Write, edge: &Edge) -> io::Result<()> { match edge { Edge::Present { direct: true, .. } => { write!(writer, "|")?; } Edge::Present { direct: false, .. } => { write!(writer, ":")?; } Edge::Missing => { write!(writer, "|")?; } } Ok(()) } fn merge_edges(&mut self, source: usize, target: usize, pad_to_index: usize) -> io::Result<()> { assert!(target < source); self.edges.remove(source); for i in 0..target { AsciiGraphDrawer::straight_edge(&mut self.writer, &self.edges[i])?; } if source == target + 1 { // If we're merging exactly one step to the left, draw a '/' to join the lines. AsciiGraphDrawer::straight_edge_no_space(&mut self.writer, &self.edges[target])?; for _ in source..self.edges.len() + 1 { write!(self.writer, "/ ")?; } write!(self.writer, " ")?; for _ in self.edges.len() + 1..pad_to_index { write!(self.writer, " ")?; } } else { // If we're merging more than one step to the left, we need two rows: // | |_|_|/ // |/| | | AsciiGraphDrawer::straight_edge(&mut self.writer, &self.edges[target])?; for i in target + 1..source - 1 { AsciiGraphDrawer::straight_edge_no_space(&mut self.writer, &self.edges[i])?; write!(self.writer, "_")?; } AsciiGraphDrawer::straight_edge_no_space(&mut self.writer, &self.edges[source - 1])?; for _ in source..self.edges.len() + 1 { write!(self.writer, "/ ")?; } write!(self.writer, " ")?; for _ in self.edges.len() + 1..pad_to_index { write!(self.writer, " ")?; } self.maybe_write_pending_text()?; for i in 0..target { AsciiGraphDrawer::straight_edge(&mut self.writer, &self.edges[i])?; } AsciiGraphDrawer::straight_edge_no_space(&mut self.writer, &self.edges[target])?; write!(self.writer, "/")?; for i in target + 1..self.edges.len() { AsciiGraphDrawer::straight_edge(&mut self.writer, &self.edges[i])?; } for _ in self.edges.len()..pad_to_index { write!(self.writer, " ")?; } } self.maybe_write_pending_text()?; Ok(()) } fn close_missing_edge(&mut self, source: usize, pad_to_index: usize) -> io::Result<()> { self.edges.remove(source); for i in 0..source { AsciiGraphDrawer::straight_edge(&mut self.writer, &self.edges[i])?; } write!(self.writer, "~")?; for _ in source..self.edges.len() { write!(self.writer, "/ ")?; } write!(self.writer, " ")?; for _ in self.edges.len() + 1..pad_to_index { write!(self.writer, " ")?; } self.maybe_write_pending_text() } fn close_edge(&mut self, source: usize, pad_to_index: usize) -> io::Result<()> { for i in 0..source { AsciiGraphDrawer::straight_edge(&mut self.writer, &self.edges[i])?; } write!(self.writer, " ")?; for _ in source..self.edges.len() { write!(self.writer, "/ ")?; } write!(self.writer, " ")?; for _ in self.edges.len() + 1..pad_to_index { write!(self.writer, " ")?; } self.maybe_write_pending_text() } fn maybe_write_pending_text(&mut self) -> io::Result<()> { if let Some(text) = self.pending_text.pop() { write!(self.writer, "{text}")?; } writeln!(self.writer) } } #[cfg(test)] mod tests { use super::*; /// Adapter to insert width calculation for each `.add_node()` struct AsciiGraphDrawerWithWidths<'writer, K> { inner: AsciiGraphDrawer<'writer, K>, } impl<'writer, K: Clone + Eq + Hash> AsciiGraphDrawerWithWidths<'writer, K> { fn new(writer: &'writer mut dyn Write) -> Self { AsciiGraphDrawerWithWidths { inner: AsciiGraphDrawer::new(writer), } } fn add_node( &mut self, id: &K, edges: &[Edge], node_symbol: &str, text: &str, ) -> io::Result<()> { let width = self.inner.width(id, edges); let text = format!("{text} [width={width}]"); self.inner.add_node(id, edges, node_symbol, &text) } } #[test] fn single_node() -> io::Result<()> { let mut buffer = vec![]; let mut graph = AsciiGraphDrawerWithWidths::new(&mut buffer); graph.add_node(&1, &[], "@", "node 1")?; insta::assert_snapshot!(String::from_utf8_lossy(&buffer), @r###" @ node 1 [width=2] "###); Ok(()) } #[test] fn long_description() -> io::Result<()> { let mut buffer = vec![]; let mut graph = AsciiGraphDrawer::new(&mut buffer); graph.add_node(&2, &[Edge::direct(1)], "@", "many\nlines\nof\ntext\n")?; graph.add_node(&1, &[], "o", "single line")?; insta::assert_snapshot!(String::from_utf8_lossy(&buffer), @r###" @ many | lines | of | text o single line "###); Ok(()) } #[test] fn long_description_blank_lines() -> io::Result<()> { let mut buffer = vec![]; let mut graph = AsciiGraphDrawer::new(&mut buffer); graph.add_node( &2, &[Edge::direct(1)], "@", "\n\nmany\n\nlines\n\nof\n\ntext\n\n\n", )?; graph.add_node(&1, &[], "o", "single line")?; // A final newline is ignored but all other newlines are respected. insta::assert_snapshot!(String::from_utf8_lossy(&buffer), @r###" @ | | many | | lines | | of | | text | | o single line "###); Ok(()) } #[test] fn chain() -> io::Result<()> { let mut buffer = vec![]; let mut graph = AsciiGraphDrawerWithWidths::new(&mut buffer); graph.add_node(&3, &[Edge::direct(2)], "@", "node 3")?; graph.add_node(&2, &[Edge::direct(1)], "o", "node 2")?; graph.add_node(&1, &[], "o", "node 1")?; insta::assert_snapshot!(String::from_utf8_lossy(&buffer), @r###" @ node 3 [width=2] o node 2 [width=2] o node 1 [width=2] "###); Ok(()) } #[test] fn interleaved_chains() -> io::Result<()> { let mut buffer = vec![]; let mut graph = AsciiGraphDrawerWithWidths::new(&mut buffer); graph.add_node(&7, &[Edge::direct(5)], "o", "node 7")?; graph.add_node(&6, &[Edge::direct(4)], "o", "node 6")?; graph.add_node(&5, &[Edge::direct(3)], "o", "node 5")?; graph.add_node(&4, &[Edge::direct(2)], "o", "node 4")?; graph.add_node(&3, &[Edge::direct(1)], "@", "node 3")?; graph.add_node(&2, &[], "o", "node 2")?; graph.add_node(&1, &[], "o", "node 1")?; insta::assert_snapshot!(String::from_utf8_lossy(&buffer), @r###" o node 7 [width=2] | o node 6 [width=4] o | node 5 [width=4] | o node 4 [width=4] @ | node 3 [width=4] | o node 2 [width=4] o node 1 [width=2] "###); Ok(()) } #[test] fn independent_nodes() -> io::Result<()> { let mut buffer = vec![]; let mut graph = AsciiGraphDrawerWithWidths::new(&mut buffer); graph.add_node(&3, &[Edge::missing()], "o", "node 3")?; graph.add_node(&2, &[Edge::missing()], "o", "node 2")?; graph.add_node(&1, &[Edge::missing()], "@", "node 1")?; insta::assert_snapshot!(String::from_utf8_lossy(&buffer), @r###" o node 3 [width=2] ~ o node 2 [width=2] ~ @ node 1 [width=2] ~ "###); Ok(()) } #[test] fn left_chain_ends() -> io::Result<()> { let mut buffer = vec![]; let mut graph = AsciiGraphDrawerWithWidths::new(&mut buffer); graph.add_node(&4, &[Edge::direct(2)], "o", "node 4")?; graph.add_node(&3, &[Edge::direct(1)], "o", "node 3")?; graph.add_node(&2, &[Edge::missing()], "o", "node 2")?; graph.add_node(&1, &[], "o", "node 1")?; insta::assert_snapshot!(String::from_utf8_lossy(&buffer), @r###" o node 4 [width=2] | o node 3 [width=4] o | node 2 [width=4] ~/ o node 1 [width=2] "###); Ok(()) } #[test] fn left_chain_ends_with_no_missing_edge() -> io::Result<()> { let mut buffer = vec![]; let mut graph = AsciiGraphDrawerWithWidths::new(&mut buffer); graph.add_node(&4, &[Edge::direct(2)], "o", "node 4")?; graph.add_node(&3, &[Edge::direct(1)], "o", "node 3")?; graph.add_node(&2, &[], "o", "node 2\nmore\ntext")?; graph.add_node(&1, &[], "o", "node 1")?; insta::assert_snapshot!(String::from_utf8_lossy(&buffer), @r###" o node 4 [width=2] | o node 3 [width=4] o | node 2 / more | text [width=4] o node 1 [width=2] "###); Ok(()) } #[test] fn right_chain_ends() -> io::Result<()> { let mut buffer = vec![]; let mut graph = AsciiGraphDrawerWithWidths::new(&mut buffer); graph.add_node(&4, &[Edge::direct(1)], "o", "node 4")?; graph.add_node(&3, &[Edge::direct(2)], "o", "node 3")?; graph.add_node(&2, &[Edge::missing()], "o", "node 2")?; graph.add_node(&1, &[], "o", "node 1\nmore\ntext")?; insta::assert_snapshot!(String::from_utf8_lossy(&buffer), @r###" o node 4 [width=2] | o node 3 [width=4] | o node 2 [width=4] | ~ o node 1 more text [width=2] "###); Ok(()) } #[test] fn right_chain_ends_long_description() -> io::Result<()> { let mut buffer = vec![]; let mut graph = AsciiGraphDrawer::new(&mut buffer); graph.add_node(&3, &[Edge::direct(1)], "o", "node 3")?; graph.add_node( &2, &[Edge::missing()], "o", "node 2\nwith\nlong\ndescription", )?; graph.add_node(&1, &[], "o", "node 1")?; insta::assert_snapshot!(String::from_utf8_lossy(&buffer), @r###" o node 3 | o node 2 | ~ with | long | description o node 1 "###); Ok(()) } #[test] fn fork_multiple() -> io::Result<()> { let mut buffer = vec![]; let mut graph = AsciiGraphDrawerWithWidths::new(&mut buffer); graph.add_node(&4, &[Edge::direct(1)], "@", "node 4")?; graph.add_node(&3, &[Edge::direct(1)], "o", "node 3")?; graph.add_node(&2, &[Edge::direct(1)], "o", "node 2")?; graph.add_node(&1, &[], "o", "node 1")?; insta::assert_snapshot!(String::from_utf8_lossy(&buffer), @r###" @ node 4 [width=2] | o node 3 [width=4] |/ | o node 2 [width=4] |/ o node 1 [width=2] "###); Ok(()) } #[test] fn fork_multiple_chains() -> io::Result<()> { let mut buffer = vec![]; let mut graph = AsciiGraphDrawerWithWidths::new(&mut buffer); graph.add_node(&10, &[Edge::direct(7)], "o", "node 10")?; graph.add_node(&9, &[Edge::direct(6)], "o", "node 9")?; graph.add_node(&8, &[Edge::direct(5)], "o", "node 8")?; graph.add_node(&7, &[Edge::direct(4)], "o", "node 7")?; graph.add_node(&6, &[Edge::direct(3)], "o", "node 6")?; graph.add_node(&5, &[Edge::direct(2)], "o", "node 5")?; graph.add_node(&4, &[Edge::direct(1)], "o", "node 4")?; graph.add_node(&3, &[Edge::direct(1)], "o", "node 3")?; graph.add_node(&2, &[Edge::direct(1)], "o", "node 2")?; graph.add_node(&1, &[], "o", "node 1")?; insta::assert_snapshot!(String::from_utf8_lossy(&buffer), @r###" o node 10 [width=2] | o node 9 [width=4] | | o node 8 [width=6] o | | node 7 [width=6] | o | node 6 [width=6] | | o node 5 [width=6] o | | node 4 [width=6] | o | node 3 [width=6] |/ / | o node 2 [width=4] |/ o node 1 [width=2] "###); Ok(()) } #[test] fn cross_over() -> io::Result<()> { let mut buffer = vec![]; let mut graph = AsciiGraphDrawerWithWidths::new(&mut buffer); graph.add_node(&5, &[Edge::direct(1)], "o", "node 5")?; graph.add_node(&4, &[Edge::direct(2)], "o", "node 4")?; graph.add_node(&3, &[Edge::direct(1)], "o", "node 3")?; graph.add_node(&2, &[Edge::direct(1)], "o", "node 2")?; graph.add_node(&1, &[], "o", "node 1")?; insta::assert_snapshot!(String::from_utf8_lossy(&buffer), @r###" o node 5 [width=2] | o node 4 [width=4] | | o node 3 [width=6] | |/ |/| | o node 2 [width=4] |/ o node 1 [width=2] "###); Ok(()) } #[test] fn cross_over_multiple() -> io::Result<()> { let mut buffer = vec![]; let mut graph = AsciiGraphDrawerWithWidths::new(&mut buffer); graph.add_node(&7, &[Edge::direct(1)], "o", "node 7")?; graph.add_node(&6, &[Edge::direct(3)], "o", "node 6")?; graph.add_node(&5, &[Edge::direct(2)], "o", "node 5")?; graph.add_node(&4, &[Edge::direct(1)], "o", "node 4")?; graph.add_node(&3, &[Edge::direct(1)], "o", "node 3")?; graph.add_node(&2, &[Edge::direct(1)], "o", "node 2")?; graph.add_node(&1, &[], "o", "node 1")?; insta::assert_snapshot!(String::from_utf8_lossy(&buffer), @r###" o node 7 [width=2] | o node 6 [width=4] | | o node 5 [width=6] | | | o node 4 [width=8] | |_|/ |/| | | o | node 3 [width=6] |/ / | o node 2 [width=4] |/ o node 1 [width=2] "###); Ok(()) } #[test] fn cross_over_new_on_left() -> io::Result<()> { let mut buffer = vec![]; let mut graph = AsciiGraphDrawerWithWidths::new(&mut buffer); graph.add_node(&6, &[Edge::direct(3)], "o", "node 6")?; graph.add_node(&5, &[Edge::direct(2)], "o", "node 5")?; graph.add_node(&4, &[Edge::direct(1)], "o", "node 4")?; graph.add_node(&3, &[Edge::direct(1)], "o", "node 3")?; graph.add_node(&2, &[Edge::direct(1)], "o", "node 2")?; graph.add_node(&1, &[], "o", "node 1")?; insta::assert_snapshot!(String::from_utf8_lossy(&buffer), @r###" o node 6 [width=2] | o node 5 [width=4] | | o node 4 [width=6] o | | node 3 [width=6] | |/ |/| | o node 2 [width=4] |/ o node 1 [width=2] "###); Ok(()) } #[test] fn merge_multiple() -> io::Result<()> { let mut buffer = vec![]; let mut graph = AsciiGraphDrawerWithWidths::new(&mut buffer); graph.add_node( &5, &[ Edge::direct(1), Edge::direct(2), Edge::direct(3), Edge::direct(4), ], "@", "node 5\nmore\ntext", )?; graph.add_node(&4, &[Edge::missing()], "o", "node 4")?; graph.add_node(&3, &[Edge::missing()], "o", "node 3")?; graph.add_node(&2, &[Edge::missing()], "o", "node 2")?; graph.add_node(&1, &[Edge::missing()], "o", "node 1")?; insta::assert_snapshot!(String::from_utf8_lossy(&buffer), @r###" @---. node 5 |\ \ \ more | | | | text [width=8] | | | o node 4 [width=8] | | | ~ | | o node 3 [width=6] | | ~ | o node 2 [width=4] | ~ o node 1 [width=2] ~ "###); Ok(()) } #[test] fn fork_merge_in_central_edge() -> io::Result<()> { let mut buffer = vec![]; let mut graph = AsciiGraphDrawerWithWidths::new(&mut buffer); graph.add_node(&8, &[Edge::direct(1)], "o", "node 8")?; graph.add_node(&7, &[Edge::direct(5)], "o", "node 7")?; graph.add_node( &6, &[Edge::direct(2)], "o", "node 6\nwith\nsome\nmore\nlines", )?; graph.add_node(&5, &[Edge::direct(4), Edge::direct(3)], "o", "node 5")?; graph.add_node(&4, &[Edge::direct(1)], "o", "node 4")?; graph.add_node(&3, &[Edge::direct(1)], "o", "node 3")?; graph.add_node(&2, &[Edge::direct(1)], "o", "node 2")?; graph.add_node(&1, &[], "o", "node 1")?; insta::assert_snapshot!(String::from_utf8_lossy(&buffer), @r###" o node 8 [width=2] | o node 7 [width=4] | | o node 6 | | | with | | | some | | | more | | | lines [width=6] | o | node 5 [width=8] | |\ \ | o | | node 4 [width=8] |/ / / | o | node 3 [width=6] |/ / | o node 2 [width=4] |/ o node 1 [width=2] "###); Ok(()) } #[test] fn fork_merge_multiple() -> io::Result<()> { let mut buffer = vec![]; let mut graph = AsciiGraphDrawerWithWidths::new(&mut buffer); graph.add_node(&6, &[Edge::direct(5)], "o", "node 6")?; graph.add_node( &5, &[Edge::direct(2), Edge::direct(3), Edge::direct(4)], "o", "node 5", )?; graph.add_node(&4, &[Edge::direct(1)], "o", "node 4")?; graph.add_node(&3, &[Edge::direct(1)], "o", "node 3")?; graph.add_node(&2, &[Edge::direct(1)], "o", "node 2")?; graph.add_node(&1, &[], "o", "node 1")?; insta::assert_snapshot!(String::from_utf8_lossy(&buffer), @r###" o node 6 [width=2] o-. node 5 [width=6] |\ \ | | o node 4 [width=6] | o | node 3 [width=6] | |/ o | node 2 [width=4] |/ o node 1 [width=2] "###); Ok(()) } #[test] fn fork_merge_multiple_in_central_edge() -> io::Result<()> { let mut buffer = vec![]; let mut graph = AsciiGraphDrawerWithWidths::new(&mut buffer); graph.add_node(&10, &[Edge::direct(1)], "o", "node 10")?; graph.add_node(&9, &[Edge::direct(7)], "o", "node 9")?; graph.add_node(&8, &[Edge::direct(2)], "o", "node 8")?; graph.add_node( &7, &[ Edge::direct(6), Edge::direct(5), Edge::direct(4), Edge::direct(3), ], "o", "node 7", )?; graph.add_node(&6, &[Edge::direct(1)], "o", "node 6")?; graph.add_node(&5, &[Edge::direct(1)], "o", "node 5")?; graph.add_node(&4, &[Edge::direct(1)], "o", "node 4")?; graph.add_node(&3, &[Edge::direct(1)], "o", "node 3")?; graph.add_node(&2, &[Edge::direct(1)], "o", "node 2")?; graph.add_node(&1, &[], "o", "node 1")?; insta::assert_snapshot!(String::from_utf8_lossy(&buffer), @r###" o node 10 [width=2] | o node 9 [width=4] | | o node 8 [width=6] | | \ | | \ | o---. | node 7 [width=12] | |\ \ \ \ | o | | | | node 6 [width=12] |/ / / / / | o | | | node 5 [width=10] |/ / / / | o | | node 4 [width=8] |/ / / | o | node 3 [width=6] |/ / | o node 2 [width=4] |/ o node 1 [width=2] "###); Ok(()) } #[test] fn merge_multiple_missing_edges() -> io::Result<()> { let mut buffer = vec![]; let mut graph = AsciiGraphDrawerWithWidths::new(&mut buffer); graph.add_node( &1, &[ Edge::missing(), Edge::missing(), Edge::missing(), Edge::missing(), ], "@", "node 1\nwith\nmany\nlines\nof\ntext", )?; insta::assert_snapshot!(String::from_utf8_lossy(&buffer), @r###" @---. node 1 |\ \ \ with | | | ~ many | | ~ lines | ~ of ~ text [width=8] "###); Ok(()) } #[test] fn merge_missing_edges_and_fork() -> io::Result<()> { let mut buffer = vec![]; let mut graph = AsciiGraphDrawerWithWidths::new(&mut buffer); graph.add_node(&3, &[Edge::direct(1)], "o", "node 3")?; graph.add_node( &2, &[ Edge::missing(), Edge::indirect(1), Edge::missing(), Edge::indirect(1), ], "o", "node 2\nwith\nmany\nlines\nof\ntext", )?; graph.add_node(&1, &[], "o", "node 1")?; insta::assert_snapshot!(String::from_utf8_lossy(&buffer), @r###" o node 3 [width=2] | o---. node 2 | |\ \ \ with | | : ~/ many | ~/ / lines |/ / of |/ text [width=10] o node 1 [width=2] "###); Ok(()) } }