feat: show single-pane diffs for added deleted and renamed files
This commit is contained in:
+202
-20
@@ -120,6 +120,14 @@ pub enum Focus {
|
||||
Before,
|
||||
After,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub(crate) enum PaneLayout {
|
||||
Split,
|
||||
Before,
|
||||
After,
|
||||
Empty,
|
||||
}
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub enum RowKind {
|
||||
Equal,
|
||||
@@ -299,8 +307,12 @@ impl DiffState {
|
||||
self.comparison = Some(comparison);
|
||||
self.comparison_id = Some(id);
|
||||
self.phase = Phase::View;
|
||||
self.focus = Focus::Files;
|
||||
self.file = 0;
|
||||
self.focus = if self.files_visible {
|
||||
Focus::Files
|
||||
} else {
|
||||
self.first_text_focus()
|
||||
};
|
||||
self.align_file();
|
||||
self.error = None;
|
||||
}
|
||||
@@ -396,7 +408,7 @@ impl DiffState {
|
||||
{
|
||||
files.focused = false;
|
||||
self.files_visible = files.presentation == FilePresentation::Docked;
|
||||
self.focus = Focus::Before;
|
||||
self.focus = self.first_text_focus();
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -529,7 +541,7 @@ impl DiffState {
|
||||
if let Some(path) = files.accept()
|
||||
&& self.select_file(&path)
|
||||
{
|
||||
self.focus = Focus::Before;
|
||||
self.focus = self.first_text_focus();
|
||||
let files = self.files.as_mut().expect("comparison picker");
|
||||
files.focused = false;
|
||||
self.files_visible = files.presentation == FilePresentation::Docked;
|
||||
@@ -543,22 +555,8 @@ impl DiffState {
|
||||
}
|
||||
}
|
||||
match input {
|
||||
Input::Char('\t') => {
|
||||
self.focus = match self.focus {
|
||||
Focus::Files => Focus::Before,
|
||||
Focus::Before => Focus::After,
|
||||
Focus::After if self.files_visible => Focus::Files,
|
||||
Focus::After => Focus::Before,
|
||||
}
|
||||
}
|
||||
Input::BackTab => {
|
||||
self.focus = match self.focus {
|
||||
Focus::Files => Focus::After,
|
||||
Focus::After => Focus::Before,
|
||||
Focus::Before if self.files_visible => Focus::Files,
|
||||
Focus::Before => Focus::After,
|
||||
}
|
||||
}
|
||||
Input::Char('\t') => self.cycle_focus(true),
|
||||
Input::BackTab => self.cycle_focus(false),
|
||||
Input::Up | Input::Ctrl('k') => self.navigate(false),
|
||||
Input::Down | Input::Ctrl('j') => self.navigate(true),
|
||||
Input::Left | Input::Ctrl('h') => match self.focus {
|
||||
@@ -591,8 +589,76 @@ impl DiffState {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn pane_layout(&self) -> PaneLayout {
|
||||
let Some(file) = self
|
||||
.comparison
|
||||
.as_ref()
|
||||
.and_then(|comparison| comparison.files.get(self.file))
|
||||
else {
|
||||
return PaneLayout::Empty;
|
||||
};
|
||||
match file.status {
|
||||
FileStatus::Added => PaneLayout::After,
|
||||
FileStatus::Removed => PaneLayout::Before,
|
||||
FileStatus::Renamed
|
||||
if file.before == file.after && file.before_eof == file.after_eof =>
|
||||
{
|
||||
PaneLayout::Empty
|
||||
}
|
||||
FileStatus::Modified | FileStatus::Renamed => PaneLayout::Split,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn first_text_focus(&self) -> Focus {
|
||||
if self.pane_layout() == PaneLayout::After {
|
||||
Focus::After
|
||||
} else {
|
||||
Focus::Before
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn normalize_focus(&mut self) {
|
||||
self.focus = match (self.focus, self.pane_layout()) {
|
||||
(Focus::Files, _) if self.files_visible => Focus::Files,
|
||||
(Focus::Before | Focus::After, PaneLayout::Split) => self.focus,
|
||||
_ => self.first_text_focus(),
|
||||
};
|
||||
if let Some(files) = &mut self.files {
|
||||
files.focused = self.files_visible && self.focus == Focus::Files;
|
||||
}
|
||||
}
|
||||
|
||||
fn cycle_focus(&mut self, forward: bool) {
|
||||
let first = self.first_text_focus();
|
||||
let last = if self.pane_layout() == PaneLayout::Split {
|
||||
Focus::After
|
||||
} else {
|
||||
first
|
||||
};
|
||||
self.focus = match self.focus {
|
||||
Focus::Files => {
|
||||
if forward {
|
||||
first
|
||||
} else {
|
||||
last
|
||||
}
|
||||
}
|
||||
focus if forward && focus != last => last,
|
||||
focus if !forward && focus != first => first,
|
||||
_ if self.files_visible => Focus::Files,
|
||||
_ => {
|
||||
if forward {
|
||||
first
|
||||
} else {
|
||||
last
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
pub fn after_editable(&self) -> bool {
|
||||
self.phase == Phase::View
|
||||
&& matches!(self.pane_layout(), PaneLayout::Split | PaneLayout::After)
|
||||
&& self
|
||||
.after
|
||||
.as_ref()
|
||||
@@ -616,6 +682,7 @@ impl DiffState {
|
||||
return false;
|
||||
};
|
||||
self.file = index;
|
||||
self.normalize_focus();
|
||||
self.live_buffer = None;
|
||||
self.live_revision = None;
|
||||
self.follow_caret = false;
|
||||
@@ -783,6 +850,7 @@ impl DiffState {
|
||||
}
|
||||
|
||||
fn rebuild_file(&mut self, reset: bool) {
|
||||
self.normalize_focus();
|
||||
self.rows.clear();
|
||||
self.display_rows.clear();
|
||||
self.excerpt_ranges.clear();
|
||||
@@ -1491,6 +1559,120 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn status_layout_cycles_only_present_panes_and_normalizes_selection() {
|
||||
let mut state = live_state(vec!["old".into()], vec!["new".into()], true);
|
||||
state.comparison = Some(comparison());
|
||||
state.files = Some(FilePicker::from_paths(
|
||||
PathBuf::new(),
|
||||
[
|
||||
(0, "renamed.txt".into()),
|
||||
(1, "added.txt".into()),
|
||||
(2, "removed.bin".into()),
|
||||
],
|
||||
));
|
||||
for (path, layout, first, last) in [
|
||||
(
|
||||
"renamed.txt",
|
||||
PaneLayout::Split,
|
||||
Focus::Before,
|
||||
Focus::After,
|
||||
),
|
||||
("added.txt", PaneLayout::After, Focus::After, Focus::After),
|
||||
(
|
||||
"removed.bin",
|
||||
PaneLayout::Before,
|
||||
Focus::Before,
|
||||
Focus::Before,
|
||||
),
|
||||
] {
|
||||
state.files_visible = true;
|
||||
state.focus = Focus::Files;
|
||||
assert!(state.select_file(Path::new(path)));
|
||||
assert_eq!(state.pane_layout(), layout);
|
||||
state.handle(Input::Char('\t'));
|
||||
assert_eq!(state.focus, first);
|
||||
state.handle(Input::BackTab);
|
||||
assert_eq!(state.focus, Focus::Files);
|
||||
state.handle(Input::BackTab);
|
||||
assert_eq!(state.focus, last);
|
||||
state.handle(Input::Char('\t'));
|
||||
assert_eq!(state.focus, Focus::Files);
|
||||
state.files_visible = false;
|
||||
state.normalize_focus();
|
||||
assert_eq!(state.focus, first);
|
||||
state.handle(Input::BackTab);
|
||||
assert_eq!(state.focus, last);
|
||||
state.handle(Input::Char('\t'));
|
||||
assert_eq!(state.focus, first);
|
||||
}
|
||||
state.focus = Focus::Before;
|
||||
assert!(state.select_file(Path::new("added.txt")));
|
||||
assert_eq!(state.focus, Focus::After);
|
||||
assert!(state.select_file(Path::new("removed.bin")));
|
||||
assert_eq!(state.focus, Focus::Before);
|
||||
assert!(state.select_file(Path::new("renamed.txt")));
|
||||
assert_eq!(state.focus, Focus::Before);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rename_layout_and_editability_include_eof_changes() {
|
||||
let mut state = live_state(vec!["same".into()], vec!["same".into()], true);
|
||||
state.comparison.as_mut().unwrap().files[0].status = FileStatus::Renamed;
|
||||
assert_eq!(state.pane_layout(), PaneLayout::Empty);
|
||||
assert!(!state.after_editable());
|
||||
state.normalize_focus();
|
||||
assert_eq!(state.focus, Focus::Before);
|
||||
state.files_visible = false;
|
||||
for input in [Input::Char('\t'), Input::BackTab] {
|
||||
state.handle(input);
|
||||
assert_eq!(state.focus, Focus::Before);
|
||||
}
|
||||
state.comparison.as_mut().unwrap().files[0].after_eof = false;
|
||||
assert_eq!(state.pane_layout(), PaneLayout::Split);
|
||||
assert!(state.after_editable());
|
||||
state.comparison.as_mut().unwrap().files[0].after[0] = "changed".into();
|
||||
assert!(state.after_editable());
|
||||
state.comparison.as_mut().unwrap().files[0].status = FileStatus::Removed;
|
||||
assert!(!state.after_editable());
|
||||
state.comparison.as_mut().unwrap().files[0].status = FileStatus::Added;
|
||||
assert!(state.after_editable());
|
||||
let file = &mut state.comparison.as_mut().unwrap().files[0];
|
||||
file.before.clear();
|
||||
file.after.clear();
|
||||
file.notice = Some("empty file".into());
|
||||
state.normalize_focus();
|
||||
assert_eq!(state.pane_layout(), PaneLayout::After);
|
||||
assert_eq!(state.focus, Focus::After);
|
||||
assert!(state.after_editable());
|
||||
assert_eq!(
|
||||
state.comparison.as_ref().unwrap().files[0]
|
||||
.notice
|
||||
.as_deref(),
|
||||
Some("empty file")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn added_file_picker_cancel_close_and_mouse_focus_use_after() {
|
||||
let mut editor = editor();
|
||||
open(&mut editor);
|
||||
let id = choose_pair(&mut editor);
|
||||
let mut changes = comparison();
|
||||
changes.files = vec![changes.files.remove(1)];
|
||||
editor.apply_diff_result(id, Ok(Outcome::Comparison(changes)));
|
||||
editor.handle(Input::Escape);
|
||||
assert_eq!(editor.diff().unwrap().focus, Focus::After);
|
||||
text(&mut editor, " e");
|
||||
editor.handle(Input::Ctrl('w'));
|
||||
assert_eq!(editor.diff().unwrap().focus, Focus::After);
|
||||
assert!(!editor.diff().unwrap().files_visible);
|
||||
editor.scroll_diff(Focus::Before, true);
|
||||
assert_eq!(editor.diff().unwrap().focus, Focus::After);
|
||||
editor.handle(Input::BackTab);
|
||||
assert_eq!(editor.diff().unwrap().focus, Focus::After);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn selector_filters_literal_unicode_tokens_and_pins_working_in_each_category() {
|
||||
let mut editor = editor();
|
||||
@@ -1623,7 +1805,7 @@ mod tests {
|
||||
text(&mut editor, "x/y");
|
||||
editor.handle(Input::Enter);
|
||||
assert_eq!(editor.diff().unwrap().file, 1);
|
||||
assert_eq!(editor.diff().unwrap().focus, Focus::Before);
|
||||
assert_eq!(editor.diff().unwrap().focus, Focus::After);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user