mamediff/
widget_diff_tree.rs

1use std::cmp::Ordering;
2
3use orfail::OrFail;
4use tuinix::{TerminalSize, TerminalStyle};
5
6use crate::{
7    canvas::{Canvas, Token},
8    diff::{ChunkDiff, ContentDiff, Diff, FileDiff, LineDiff},
9    git,
10};
11
12#[derive(Debug, Clone)]
13pub struct DiffTreeWidget {
14    unstaged_diff: PhasedDiff,
15    staged_diff: PhasedDiff,
16    root_node: DiffTreeNode,
17    cursor: Cursor,
18}
19
20impl DiffTreeWidget {
21    pub fn new(terminal_size: TerminalSize) -> orfail::Result<Self> {
22        let mut this = Self {
23            unstaged_diff: PhasedDiff {
24                phase: DiffPhase::Unstaged,
25                diff: Diff::default(),
26            },
27            staged_diff: PhasedDiff {
28                phase: DiffPhase::Staged,
29                diff: Diff::default(),
30            },
31            root_node: DiffTreeNode::new_root_node(),
32            cursor: Cursor::root(),
33        };
34        this.reload().or_fail()?;
35        this.expand_if_possible(terminal_size).or_fail()?;
36        Ok(this)
37    }
38
39    pub fn render(&self, canvas: &mut Canvas) {
40        for (node, diff) in self.children_and_diffs() {
41            if !node.render_if_need(canvas, &self.cursor, diff) {
42                break;
43            }
44        }
45    }
46
47    pub fn can_cursor_up(&self) -> bool {
48        self.root_node.cursor_up(&self.cursor).is_some()
49    }
50
51    pub fn can_cursor_down(&self) -> bool {
52        self.root_node.cursor_down(&self.cursor).is_some()
53    }
54
55    pub fn can_cursor_right(&self) -> bool {
56        self.root_node.cursor_right(&self.cursor).is_some()
57    }
58
59    pub fn can_cursor_left(&self) -> bool {
60        self.cursor.parent().is_some()
61    }
62
63    pub fn can_toggle(&self) -> bool {
64        self.root_node
65            .get_node(&self.cursor)
66            .ok()
67            .is_some_and(|n| !n.children.is_empty())
68    }
69
70    pub fn can_stage_or_discard(&self) -> bool {
71        self.root_node.children[0]
72            .can_alter(&self.cursor, &self.unstaged_diff)
73            .ok()
74            .is_some_and(|b| b)
75    }
76
77    pub fn can_unstage(&self) -> bool {
78        self.root_node.children[1]
79            .can_alter(&self.cursor, &self.staged_diff)
80            .ok()
81            .is_some_and(|b| b)
82    }
83
84    pub fn cursor_up(&mut self) -> orfail::Result<bool> {
85        if let Some(new_cursor) = self.root_node.cursor_up(&self.cursor) {
86            self.cursor = new_cursor;
87            self.expand_parent().or_fail()?;
88            Ok(true)
89        } else {
90            Ok(false)
91        }
92    }
93
94    pub fn cursor_down(&mut self) -> orfail::Result<bool> {
95        if let Some(new_cursor) = self.root_node.cursor_down(&self.cursor) {
96            self.cursor = new_cursor;
97            self.expand_parent().or_fail()?;
98            Ok(true)
99        } else {
100            Ok(false)
101        }
102    }
103
104    pub fn cursor_right(&mut self) -> orfail::Result<bool> {
105        if let Some(new_cursor) = self.root_node.cursor_right(&self.cursor) {
106            self.cursor = new_cursor;
107            self.expand_parent().or_fail()?;
108            Ok(true)
109        } else {
110            Ok(false)
111        }
112    }
113
114    pub fn cursor_left(&mut self) -> bool {
115        if let Some(parent) = self.cursor.parent() {
116            self.cursor = parent;
117            true
118        } else {
119            false
120        }
121    }
122
123    pub fn cursor_row(&self) -> usize {
124        let root_node_offset = 1;
125        self.root_node.cursor_row(&self.cursor) - root_node_offset
126    }
127
128    pub fn toggle(&mut self) -> orfail::Result<()> {
129        self.root_node.toggle(&self.cursor).or_fail()
130    }
131
132    pub fn stage(&mut self) -> orfail::Result<bool> {
133        if !self.can_stage_or_discard() {
134            return Ok(false);
135        }
136        self.root_node.children[0]
137            .stage(&self.cursor, &self.unstaged_diff.diff)
138            .or_fail()?;
139        self.reload().or_fail()?;
140        Ok(true)
141    }
142
143    pub fn discard(&mut self) -> orfail::Result<bool> {
144        if !self.can_stage_or_discard() {
145            return Ok(false);
146        }
147        self.root_node.children[0]
148            .discard(&self.cursor, &self.unstaged_diff.diff)
149            .or_fail()?;
150        self.reload().or_fail()?;
151        Ok(true)
152    }
153
154    pub fn unstage(&mut self) -> orfail::Result<bool> {
155        if !self.can_unstage() {
156            return Ok(false);
157        }
158        self.root_node.children[1]
159            .unstage(&self.cursor, &self.staged_diff.diff)
160            .or_fail()?;
161        self.reload().or_fail()?;
162        Ok(true)
163    }
164
165    fn expand_if_possible(&mut self, terminal_size: TerminalSize) -> orfail::Result<()> {
166        if !self.cursor_right().or_fail()? {
167            return Ok(());
168        }
169
170        loop {
171            self.root_node.toggle(&self.cursor).or_fail()?;
172            if self.rows() > terminal_size.rows {
173                self.root_node.toggle(&self.cursor).or_fail()?;
174                break;
175            }
176            if !self.cursor_down().or_fail()? {
177                break;
178            }
179        }
180
181        self.cursor = Cursor::root();
182        Ok(())
183    }
184
185    fn expand_parent(&mut self) -> orfail::Result<()> {
186        if let Some(parent) = self.cursor.parent() {
187            self.root_node.get_node_mut(&parent).or_fail()?.expanded = true;
188        }
189        Ok(())
190    }
191
192    fn rows(&self) -> usize {
193        let root_node_offset = 1;
194        self.root_node.rows() - root_node_offset
195    }
196
197    pub fn reload(&mut self) -> orfail::Result<()> {
198        let old = self.clone();
199        let (unstaged_diff, staged_diff) = git::unstaged_and_staged_diffs().or_fail()?;
200
201        self.unstaged_diff.diff = unstaged_diff;
202        self.staged_diff.diff = staged_diff;
203        for (node, diff) in self.children_and_diffs_mut() {
204            node.children.clear();
205            for (i, file) in diff.diff.files.iter().enumerate() {
206                let path = node.path.join(i);
207                let child = DiffTreeNode::new_file_diff_node(path, file);
208                node.children.push(child);
209            }
210
211            node.restore_expanded_state(
212                &diff.diff,
213                &old.children_and_diffs()
214                    .map(|x| (x.0, &x.1.diff))
215                    .collect::<Vec<_>>(),
216            );
217        }
218
219        while !self.root_node.is_valid_cursor(&self.cursor) {
220            if let Some(sibling_cursor) = self.cursor.prev_sibling() {
221                self.cursor = sibling_cursor;
222            } else if let Some(parent_cursor) = self.cursor.parent() {
223                self.cursor = parent_cursor;
224            } else {
225                self.cursor = Cursor::root();
226                break;
227            }
228        }
229
230        self.expand_parent().or_fail()?;
231
232        Ok(())
233    }
234
235    fn children_and_diffs(&self) -> impl '_ + Iterator<Item = (&DiffTreeNode, &PhasedDiff)> {
236        self.root_node
237            .children
238            .iter()
239            .zip([&self.unstaged_diff, &self.staged_diff])
240    }
241
242    fn children_and_diffs_mut(
243        &mut self,
244    ) -> impl '_ + Iterator<Item = (&mut DiffTreeNode, &mut PhasedDiff)> {
245        self.root_node
246            .children
247            .iter_mut()
248            .zip([&mut self.unstaged_diff, &mut self.staged_diff])
249    }
250}
251
252#[derive(Debug, Clone)]
253struct DiffTreeNode {
254    path: NodePath,
255    expanded: bool,
256    children: Vec<Self>,
257}
258
259impl DiffTreeNode {
260    fn new_root_node() -> Self {
261        let root_path = NodePath::root();
262        Self {
263            path: root_path.clone(),
264            expanded: true,
265            children: vec![
266                Self::new_diff_node(root_path.join(0)),
267                Self::new_diff_node(root_path.join(1)),
268            ],
269        }
270    }
271
272    fn new_diff_node(path: NodePath) -> Self {
273        Self {
274            path,
275            expanded: true,
276            children: Vec::new(),
277        }
278    }
279
280    fn new_file_diff_node(path: NodePath, diff: &FileDiff) -> Self {
281        let children = diff
282            .chunks()
283            .iter()
284            .enumerate()
285            .map(|(i, c)| DiffTreeNode::new_chunk_diff_node(path.join(i), c))
286            .collect();
287        Self {
288            path,
289            expanded: false,
290            children,
291        }
292    }
293
294    fn new_chunk_diff_node(path: NodePath, diff: &ChunkDiff) -> Self {
295        let children = (0..diff.lines.len())
296            .map(|i| DiffTreeNode::new_line_diff_node(path.join(i)))
297            .collect();
298        Self {
299            path,
300            expanded: true,
301            children,
302        }
303    }
304
305    fn new_line_diff_node(path: NodePath) -> Self {
306        Self {
307            path,
308            expanded: false,
309            children: Vec::new(),
310        }
311    }
312
313    fn restore_expanded_state(&mut self, diff: &Diff, old: &[(&Self, &Diff)]) {
314        if old.is_empty() {
315            return;
316        }
317
318        self.expanded = old.iter().any(|x| x.0.expanded);
319
320        for (c, d) in self.children.iter_mut().zip(diff.files.iter()) {
321            let expanded = old
322                .iter()
323                .flat_map(|x| x.0.children.iter().zip(x.1.files.iter()))
324                .filter(|x| x.1.path() == d.path())
325                .any(|x| x.0.expanded);
326            c.expanded = expanded;
327        }
328    }
329
330    fn render<T>(&self, canvas: &mut Canvas, cursor: &Cursor, content: &T)
331    where
332        T: DiffTreeNodeContent,
333    {
334        cursor.render(canvas, &self.path);
335        for token in content.head_line_tokens() {
336            canvas.draw(token);
337        }
338        if !self.expanded && !self.children.is_empty() {
339            canvas.draw(Token::new("…"));
340        }
341        canvas.newline();
342
343        if self.expanded {
344            for child in self.children.iter().zip(content.children().iter()) {
345                if !child.0.render_if_need(canvas, cursor, child.1) {
346                    break;
347                }
348            }
349        }
350    }
351
352    fn render_if_need<T>(&self, canvas: &mut Canvas, cursor: &Cursor, content: &T) -> bool
353    where
354        T: DiffTreeNodeContent,
355    {
356        if canvas.is_frame_exceeded() {
357            return false;
358        }
359
360        let mut canvas_cursor = canvas.cursor();
361        let drawn_rows = self.rows();
362        if canvas
363            .frame_row_range()
364            .start
365            .checked_sub(canvas_cursor.row)
366            .is_some_and(|n| n >= drawn_rows)
367        {
368            canvas_cursor.row += drawn_rows;
369            canvas.set_cursor(canvas_cursor);
370        } else {
371            self.render(canvas, cursor, content);
372        }
373        true
374    }
375
376    fn rows(&self) -> usize {
377        if self.expanded {
378            1 + self.children.iter().map(|c| c.rows()).sum::<usize>()
379        } else {
380            1
381        }
382    }
383
384    fn cursor_row(&self, cursor: &Cursor) -> usize {
385        match cursor.path.0[..self.path.len()].cmp(&self.path.0) {
386            Ordering::Less => 0,
387            Ordering::Equal if cursor.path.len() == self.path.len() => 0,
388            Ordering::Equal => {
389                1 + self
390                    .children
391                    .iter()
392                    .map(|c| c.cursor_row(cursor))
393                    .sum::<usize>()
394            }
395            Ordering::Greater => self.rows(),
396        }
397    }
398
399    fn check_cursor(&self, cursor: &Cursor) -> orfail::Result<()> {
400        cursor.path.starts_with(&self.path).or_fail_with(|()| {
401            format!(
402                "invalid cursor: path={:?}, cursor={:?}",
403                self.path, cursor.path
404            )
405        })?;
406        Ok(())
407    }
408
409    fn can_alter<T>(&self, cursor: &Cursor, content: &T) -> orfail::Result<bool>
410    where
411        T: DiffTreeNodeContent,
412    {
413        self.check_cursor(cursor).or_fail()?;
414
415        if let Some(i) = cursor.path.get(self.path.len()) {
416            let child_node = self.children.get(i).or_fail()?;
417            let child_content = content.children().get(i).or_fail()?;
418            child_node.can_alter(cursor, child_content).or_fail()
419        } else {
420            Ok(content.can_alter())
421        }
422    }
423
424    fn is_valid_cursor(&self, cursor: &Cursor) -> bool {
425        self.get_node(cursor).is_ok()
426    }
427
428    fn toggle(&mut self, cursor: &Cursor) -> orfail::Result<()> {
429        let node = self.get_node_mut(cursor).or_fail()?;
430        node.expanded = !node.expanded;
431        Ok(())
432    }
433
434    fn get_node(&self, cursor: &Cursor) -> orfail::Result<&Self> {
435        if let Some((_, child)) = self.get_maybe_child(cursor).or_fail()? {
436            child.get_node(cursor).or_fail()
437        } else {
438            Ok(self)
439        }
440    }
441
442    fn get_node_mut(&mut self, cursor: &Cursor) -> orfail::Result<&mut Self> {
443        cursor.path.starts_with(&self.path).or_fail()?;
444
445        if let Some(i) = cursor.path.get(self.path.len()) {
446            let child = self.children.get_mut(i).or_fail()?;
447            child.get_node_mut(cursor).or_fail()
448        } else {
449            Ok(self)
450        }
451    }
452
453    fn get_maybe_child(&self, cursor: &Cursor) -> orfail::Result<Option<(usize, &Self)>> {
454        cursor.path.starts_with(&self.path).or_fail()?;
455
456        if let Some(i) = cursor.path.get(self.path.len()) {
457            let child = self.children.get(i).or_fail()?;
458            Ok(Some((i, child)))
459        } else {
460            Ok(None)
461        }
462    }
463
464    fn stage(&self, cursor: &Cursor, diff: &Diff) -> orfail::Result<()> {
465        let diff = self.get_diff(cursor, diff, false).or_fail()?;
466        git::stage(&diff).or_fail()?;
467        Ok(())
468    }
469
470    fn discard(&self, cursor: &Cursor, diff: &Diff) -> orfail::Result<()> {
471        let diff = self.get_diff(cursor, diff, true).or_fail()?;
472        git::discard(&diff).or_fail()?;
473        Ok(())
474    }
475
476    fn unstage(&self, cursor: &Cursor, diff: &Diff) -> orfail::Result<()> {
477        let diff = self.get_diff(cursor, diff, true).or_fail()?;
478        git::unstage(&diff).or_fail()?;
479        Ok(())
480    }
481
482    fn get_diff(&self, cursor: &Cursor, diff: &Diff, reverse: bool) -> orfail::Result<Diff> {
483        let Some((i, node)) = self.get_maybe_child(cursor).or_fail()? else {
484            return Ok(diff.clone());
485        };
486        let file = diff.files.get(i).or_fail()?;
487        let path = file.path();
488
489        let Some((i, node)) = node.get_maybe_child(cursor).or_fail()? else {
490            return Ok(file.to_diff());
491        };
492        let chunk = file.chunks().get(i).or_fail()?;
493
494        let Some((i, _node)) = node.get_maybe_child(cursor).or_fail()? else {
495            return Ok(chunk.to_diff(path));
496        };
497
498        Ok(chunk.get_line_chunk(i, reverse).or_fail()?.to_diff(path))
499    }
500
501    fn cursor_right(&self, cursor: &Cursor) -> Option<Cursor> {
502        let mut cursor = cursor.clone();
503
504        while cursor.path.len() >= self.path.len() {
505            let child_cursor = cursor.first_child();
506            if self.is_valid_cursor(&child_cursor) {
507                return Some(child_cursor);
508            }
509
510            let sibling_cursor = cursor.next_sibling();
511            if self.is_valid_cursor(&sibling_cursor) {
512                cursor = sibling_cursor;
513            } else {
514                break;
515            }
516        }
517
518        None
519    }
520
521    fn cursor_down(&self, cursor: &Cursor) -> Option<Cursor> {
522        let sibling_cursor = cursor.next_sibling();
523        if self.is_valid_cursor(&sibling_cursor) {
524            return Some(sibling_cursor);
525        }
526
527        let mut base_cursor = cursor.clone();
528        loop {
529            base_cursor = base_cursor.parent()?;
530
531            let mut next_cursor = base_cursor.next_sibling();
532            while next_cursor.path.len() < cursor.path.len() {
533                next_cursor = next_cursor.first_child();
534            }
535
536            if self.is_valid_cursor(&next_cursor) {
537                return Some(next_cursor);
538            }
539        }
540    }
541
542    fn cursor_up(&self, cursor: &Cursor) -> Option<Cursor> {
543        if let Some(sibling_cursor) = cursor.prev_sibling() {
544            return Some(sibling_cursor);
545        }
546
547        let mut base_cursor = cursor.clone();
548        loop {
549            base_cursor = base_cursor.parent()?;
550
551            let Some(mut next_cursor) = base_cursor.prev_sibling() else {
552                continue;
553            };
554            while next_cursor.path.len() < cursor.path.len() {
555                let index = self
556                    .get_node(&next_cursor)
557                    .ok()
558                    .map(|n| n.children.len().saturating_sub(1))
559                    .unwrap_or_default();
560                next_cursor = next_cursor.join(index);
561            }
562            if self.is_valid_cursor(&next_cursor) {
563                return Some(next_cursor);
564            }
565        }
566    }
567}
568
569pub trait DiffTreeNodeContent {
570    type Child: DiffTreeNodeContent;
571
572    fn head_line_tokens(&self) -> impl Iterator<Item = Token>;
573    fn can_alter(&self) -> bool;
574    fn children(&self) -> &[Self::Child];
575}
576
577impl DiffTreeNodeContent for PhasedDiff {
578    type Child = FileDiff;
579
580    fn head_line_tokens(&self) -> impl Iterator<Item = Token> {
581        std::iter::once(Token::with_style(
582            format!("{:?} changes ({} files)", self.phase, self.diff.files.len()),
583            TerminalStyle::new().bold(),
584        ))
585    }
586
587    fn can_alter(&self) -> bool {
588        !self.diff.files.is_empty()
589    }
590
591    fn children(&self) -> &[Self::Child] {
592        &self.diff.files
593    }
594}
595
596impl DiffTreeNodeContent for FileDiff {
597    type Child = ChunkDiff;
598
599    fn head_line_tokens(&self) -> impl Iterator<Item = Token> {
600        let path = Token::with_style(
601            self.path().display().to_string(),
602            TerminalStyle::new().underline(),
603        );
604        let tokens = match self {
605            FileDiff::Update {
606                old_mode, new_mode, ..
607            } => {
608                let mode = if let Some(old_mode) = old_mode {
609                    format!(", {old_mode} -> {new_mode} mode")
610                } else {
611                    "".to_string()
612                };
613                vec![
614                    Token::new("modified "),
615                    path,
616                    Token::new(format!(
617                        " ({} chunks, -{} +{} lines{})",
618                        self.children().len(),
619                        self.removed_lines(),
620                        self.added_lines(),
621                        mode
622                    )),
623                ]
624            }
625            FileDiff::New { content, .. } => {
626                vec![
627                    Token::new("added "),
628                    path,
629                    if matches!(content, ContentDiff::Binary) {
630                        Token::new(" (binary)")
631                    } else {
632                        Token::new(format!(" (+{} lines)", self.added_lines()))
633                    },
634                ]
635            }
636            FileDiff::Rename {
637                old_path, content, ..
638            } => {
639                let old_path = Token::with_style(
640                    old_path.display().to_string(),
641                    TerminalStyle::new().underline(),
642                );
643
644                let summary = if content.is_some() {
645                    Token::new(format!(
646                        " ({} chunks, -{} +{} lines)",
647                        self.children().len(),
648                        self.removed_lines(),
649                        self.added_lines(),
650                    ))
651                } else {
652                    Token::new("")
653                };
654
655                vec![
656                    Token::new("renamed "),
657                    old_path,
658                    Token::new(" -> "),
659                    path,
660                    summary,
661                ]
662            }
663            FileDiff::Delete { content, .. } => {
664                vec![
665                    Token::new("deleted "),
666                    path,
667                    if matches!(content, ContentDiff::Binary) {
668                        Token::new(" (binary)")
669                    } else {
670                        Token::new(format!(" (-{} lines)", self.removed_lines()))
671                    },
672                ]
673            }
674            FileDiff::Chmod {
675                old_mode, new_mode, ..
676            } => {
677                vec![
678                    Token::new("mode changed "),
679                    path,
680                    Token::new(format!(" {} -> {}", old_mode, new_mode)),
681                ]
682            }
683        };
684        tokens.into_iter()
685    }
686
687    fn can_alter(&self) -> bool {
688        true
689    }
690
691    fn children(&self) -> &[Self::Child] {
692        self.chunks()
693    }
694}
695
696impl DiffTreeNodeContent for ChunkDiff {
697    type Child = LineDiff;
698
699    fn head_line_tokens(&self) -> impl Iterator<Item = Token> {
700        std::iter::once(Token::new(self.head_line()))
701    }
702
703    fn can_alter(&self) -> bool {
704        true
705    }
706
707    fn children(&self) -> &[Self::Child] {
708        &self.lines
709    }
710}
711
712impl DiffTreeNodeContent for LineDiff {
713    type Child = Self;
714
715    fn head_line_tokens(&self) -> impl Iterator<Item = Token> {
716        let style = TerminalStyle::new();
717        let style = match self {
718            LineDiff::Old(_) => style.dim(),
719            LineDiff::New(_) => style.bold(),
720            LineDiff::Both(_) => style,
721            LineDiff::NoNewlineAtEndOfFile => style,
722        };
723        std::iter::once(Token::with_style(self.to_string(), style))
724    }
725
726    fn can_alter(&self) -> bool {
727        !matches!(self, Self::Both(_))
728    }
729
730    fn children(&self) -> &[Self::Child] {
731        &[]
732    }
733}
734
735#[derive(Debug, Clone, PartialEq, Eq)]
736struct NodePath(Vec<usize>);
737
738impl NodePath {
739    fn root() -> Self {
740        Self(vec![0])
741    }
742
743    fn join(&self, index: usize) -> Self {
744        let mut child = self.clone();
745        child.0.push(index);
746        child
747    }
748
749    fn starts_with(&self, other: &Self) -> bool {
750        self.0.starts_with(&other.0)
751    }
752
753    fn len(&self) -> usize {
754        self.0.len()
755    }
756
757    fn get(&self, i: usize) -> Option<usize> {
758        self.0.get(i).copied()
759    }
760}
761
762#[derive(Debug, Clone, PartialEq, Eq)]
763struct Cursor {
764    path: NodePath,
765}
766
767impl Cursor {
768    fn root() -> Self {
769        Self {
770            path: NodePath::root().join(0),
771        }
772    }
773
774    fn join(&self, index: usize) -> Self {
775        Self {
776            path: self.path.join(index),
777        }
778    }
779
780    fn parent(&self) -> Option<Self> {
781        (self.path.len() > 2).then(|| {
782            let mut path = self.path.clone();
783            path.0.pop();
784            Self { path }
785        })
786    }
787
788    fn first_child(&self) -> Self {
789        let path = self.path.join(0);
790        Self { path }
791    }
792
793    fn next_sibling(&self) -> Self {
794        let mut path = self.path.clone();
795        *path.0.last_mut().expect("infallible") += 1;
796        Self { path }
797    }
798
799    fn prev_sibling(&self) -> Option<Self> {
800        let mut path = self.path.clone();
801        if path.0.last().copied() == Some(0) {
802            return None;
803        }
804        *path.0.last_mut().expect("infallible") -= 1;
805        Some(Self { path })
806    }
807
808    fn render(&self, canvas: &mut Canvas, path: &NodePath) {
809        let mut text = String::with_capacity(path.len() * 2);
810        let selected = *path == self.path;
811
812        if selected {
813            text.push('-');
814        } else {
815            text.push(' ');
816        }
817
818        for i in 2..path.len() {
819            if i == self.path.len() && path.starts_with(&self.path) {
820                text.push_str(" :")
821            } else if selected {
822                text.push_str("--")
823            } else {
824                text.push_str("  ")
825            }
826        }
827
828        if selected {
829            text.push_str(">| ");
830        } else if path.len() == self.path.len() {
831            text.push_str(" | ");
832        } else {
833            text.push_str("   ");
834        }
835
836        canvas.draw(Token::new(text));
837    }
838}
839
840#[derive(Debug, Clone, Copy, PartialEq, Eq)]
841enum DiffPhase {
842    Unstaged,
843    Staged,
844}
845
846#[derive(Debug, Clone)]
847struct PhasedDiff {
848    phase: DiffPhase,
849    diff: Diff,
850}