feat: follow forward working-checkout changes in diffs
This commit is contained in:
+209
-3
@@ -122,6 +122,10 @@ pub enum Operation {
|
||||
mode: ComparisonMode,
|
||||
buffers: Box<[(PathBuf, String)]>,
|
||||
},
|
||||
RefreshWorking {
|
||||
comparison_id: u64,
|
||||
buffers: Box<[(PathBuf, String)]>,
|
||||
},
|
||||
LoadFile {
|
||||
comparison_id: u64,
|
||||
file_index: usize,
|
||||
@@ -131,6 +135,7 @@ pub enum Operation {
|
||||
pub enum Outcome {
|
||||
Catalog(Catalog),
|
||||
Comparison(Comparison),
|
||||
WorkingRefresh(Option<Comparison>),
|
||||
File {
|
||||
comparison_id: u64,
|
||||
file_index: usize,
|
||||
@@ -190,6 +195,9 @@ pub enum DisplayRow {
|
||||
enum RequestKind {
|
||||
Catalog,
|
||||
Comparison,
|
||||
WorkingRefresh {
|
||||
comparison_id: u64,
|
||||
},
|
||||
File {
|
||||
comparison_id: u64,
|
||||
file_index: usize,
|
||||
@@ -297,6 +305,9 @@ impl DiffState {
|
||||
}
|
||||
|
||||
pub(crate) fn take_request(&mut self) -> Option<Operation> {
|
||||
if !self.request_ready() {
|
||||
return None;
|
||||
}
|
||||
self.pending.take()
|
||||
}
|
||||
|
||||
@@ -304,7 +315,49 @@ impl DiffState {
|
||||
self.pending.is_some()
|
||||
}
|
||||
|
||||
pub(crate) fn request_ready(&self) -> bool {
|
||||
self.pending.as_ref().is_some_and(|operation| {
|
||||
!self.refresh_pending() || matches!(operation, Operation::RefreshWorking { .. })
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn refresh_pending(&self) -> bool {
|
||||
matches!(self.request_kind, Some(RequestKind::WorkingRefresh { .. }))
|
||||
}
|
||||
|
||||
pub(crate) fn queue_working_refresh(&mut self) {
|
||||
if self.phase != Phase::View
|
||||
|| self.loading
|
||||
|| self.request_kind.is_some()
|
||||
|| self.pending.is_some()
|
||||
|| self.comparison.is_none()
|
||||
|| !self
|
||||
.after
|
||||
.as_ref()
|
||||
.is_some_and(|after| after.target == RevisionTarget::Working)
|
||||
{
|
||||
return;
|
||||
}
|
||||
if let Some(comparison_id) = self.comparison_id {
|
||||
self.request_kind = Some(RequestKind::WorkingRefresh { comparison_id });
|
||||
self.pending = Some(Operation::RefreshWorking {
|
||||
comparison_id,
|
||||
buffers: Box::new([]),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn queue(&mut self, operation: Operation) {
|
||||
if self.refresh_pending()
|
||||
&& !matches!(self.pending, Some(Operation::RefreshWorking { .. }))
|
||||
&& matches!(operation, Operation::LoadFile { .. })
|
||||
{
|
||||
// Finish the refresh before issuing a load against its frozen comparison ID.
|
||||
self.pending = Some(operation);
|
||||
self.loading = true;
|
||||
self.error = None;
|
||||
return;
|
||||
}
|
||||
self.request_kind = Some(match operation {
|
||||
Operation::Compare { .. } => RequestKind::Comparison,
|
||||
Operation::LoadFile {
|
||||
@@ -321,9 +374,66 @@ impl DiffState {
|
||||
self.error = None;
|
||||
}
|
||||
|
||||
pub(crate) fn apply_result(&mut self, id: u64, result: Result<Outcome, String>) {
|
||||
pub(crate) fn apply_result(&mut self, id: u64, result: Result<Outcome, String>) -> bool {
|
||||
if self.request_kind.is_none() {
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
if let Some(RequestKind::WorkingRefresh { comparison_id }) = self.request_kind {
|
||||
if self.comparison_id != Some(comparison_id) {
|
||||
return false;
|
||||
}
|
||||
self.request_kind = None;
|
||||
let deferred = self.pending.take();
|
||||
match result {
|
||||
Ok(Outcome::WorkingRefresh(None)) => {
|
||||
if let Some(operation) = deferred {
|
||||
self.queue(operation);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
Ok(Outcome::WorkingRefresh(Some(comparison))) => {
|
||||
let selected = self
|
||||
.comparison
|
||||
.as_ref()
|
||||
.and_then(|comparison| comparison.files.get(self.file))
|
||||
.map(|file| &file.path);
|
||||
self.file = selected
|
||||
.and_then(|path| {
|
||||
comparison.files.iter().position(|file| &file.path == path)
|
||||
})
|
||||
.unwrap_or(0);
|
||||
if let Some(files) = &mut self.files {
|
||||
files.replace_paths(
|
||||
comparison
|
||||
.files
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(index, file)| (index, file.path.clone())),
|
||||
);
|
||||
}
|
||||
self.comparison = Some(comparison);
|
||||
self.comparison_id = Some(id);
|
||||
self.snapshot_roots = [None, None];
|
||||
self.reviewed_files.clear();
|
||||
self.live_files.clear();
|
||||
self.live_buffer = None;
|
||||
self.live_revision = None;
|
||||
self.follow_caret = false;
|
||||
self.prepare_selected_file();
|
||||
self.normalize_focus();
|
||||
}
|
||||
Err(error) => {
|
||||
if let Some(operation) = deferred {
|
||||
self.queue(operation);
|
||||
}
|
||||
self.error = Some(error);
|
||||
}
|
||||
Ok(_) => self.error = Some("Unexpected repository response".into()),
|
||||
}
|
||||
return true;
|
||||
}
|
||||
if matches!(result, Ok(Outcome::WorkingRefresh(_))) {
|
||||
return false;
|
||||
}
|
||||
if let Ok(Outcome::File {
|
||||
comparison_id,
|
||||
@@ -337,7 +447,7 @@ impl DiffState {
|
||||
file_index: *file_index,
|
||||
}))
|
||||
{
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
let expected = self.request_kind.take();
|
||||
self.loading = false;
|
||||
@@ -436,6 +546,7 @@ impl DiffState {
|
||||
Ok(_) => self.error = Some("Unexpected repository response".into()),
|
||||
Err(error) => self.error = Some(error),
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
fn refilter(&mut self) {
|
||||
@@ -817,6 +928,15 @@ impl DiffState {
|
||||
self.focus = match (self.focus, self.pane_layout()) {
|
||||
(Focus::Files, _) if self.files_visible => Focus::Files,
|
||||
(Focus::Before | Focus::After, PaneLayout::Split) => self.focus,
|
||||
(Focus::Before | Focus::After, PaneLayout::Empty)
|
||||
if self
|
||||
.comparison
|
||||
.as_ref()
|
||||
.is_some_and(|comparison| !comparison.files.is_empty())
|
||||
&& !self.selected_file_loaded() =>
|
||||
{
|
||||
self.focus
|
||||
}
|
||||
_ => self.first_text_focus(),
|
||||
};
|
||||
if let Some(files) = &mut self.files {
|
||||
@@ -956,6 +1076,16 @@ impl DiffState {
|
||||
}
|
||||
|
||||
fn prepare_selected_file(&mut self) {
|
||||
if self.refresh_pending() {
|
||||
self.align_file();
|
||||
if !self.selected_file_loaded() {
|
||||
self.load_file(self.file);
|
||||
} else if matches!(self.pending, Some(Operation::LoadFile { .. })) {
|
||||
self.pending = None;
|
||||
self.loading = false;
|
||||
}
|
||||
return;
|
||||
}
|
||||
self.pending = None;
|
||||
self.request_kind = None;
|
||||
self.loading = false;
|
||||
@@ -3148,6 +3278,82 @@ mod tests {
|
||||
state
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn working_refresh_preserves_noops_errors_and_view_state_but_invalidates_old_identity() {
|
||||
let mut state = live_state(vec!["old".into()], vec!["new".into()], false);
|
||||
state.loading = false;
|
||||
state.pending = None;
|
||||
state.request_kind = None;
|
||||
state.comparison_id = Some(7);
|
||||
state.word_wrap = true;
|
||||
state.view_mode = ViewMode::Unified;
|
||||
state.comparison_mode = ComparisonMode::Snapshots;
|
||||
state.split_axis = crate::panes::SplitAxis::Horizontal;
|
||||
state.files_visible = false;
|
||||
state.reviewed_files.insert(0);
|
||||
state.error = Some("previous notice".into());
|
||||
let files = state.files.as_mut().unwrap();
|
||||
files.set_view(FileView::Tree);
|
||||
files.start_search();
|
||||
files.query = "file".into();
|
||||
files.refilter();
|
||||
let original = format!("{state:?}");
|
||||
state.queue_working_refresh();
|
||||
assert!(matches!(
|
||||
state.take_request(),
|
||||
Some(Operation::RefreshWorking {
|
||||
comparison_id: 7,
|
||||
..
|
||||
})
|
||||
));
|
||||
assert!(!state.apply_result(8, Ok(Outcome::WorkingRefresh(None))));
|
||||
assert_eq!(format!("{state:?}"), original);
|
||||
|
||||
state.queue_working_refresh();
|
||||
state.take_request();
|
||||
assert!(state.apply_result(9, Err("refresh failed".into())));
|
||||
assert_eq!(state.comparison_id, Some(7));
|
||||
assert_eq!(state.comparison.as_ref().unwrap().files[0].after, ["new"]);
|
||||
assert_eq!(state.reviewed_files, BTreeSet::from([0]));
|
||||
assert_eq!(state.error.as_deref(), Some("refresh failed"));
|
||||
|
||||
state.queue_working_refresh();
|
||||
state.take_request();
|
||||
let mut replacement = live_state(vec!["old".into()], vec!["ahead".into()], false)
|
||||
.comparison
|
||||
.take()
|
||||
.unwrap();
|
||||
replacement.files.insert(0, comparison().files.remove(0));
|
||||
assert!(state.apply_result(10, Ok(Outcome::WorkingRefresh(Some(replacement)))));
|
||||
assert_eq!(state.comparison_id, Some(10));
|
||||
assert_eq!(state.file, 1);
|
||||
assert_eq!(state.focus, Focus::After);
|
||||
assert!(state.word_wrap && !state.files_visible);
|
||||
assert_eq!(state.split_axis, crate::panes::SplitAxis::Horizontal);
|
||||
assert_eq!(state.view_mode, ViewMode::Unified);
|
||||
assert_eq!(state.comparison_mode, ComparisonMode::Snapshots);
|
||||
assert!(state.reviewed_files.is_empty());
|
||||
let files = state.files.as_ref().unwrap();
|
||||
assert_eq!(files.query, "file");
|
||||
assert_eq!(files.view, FileView::Tree);
|
||||
assert!(files.searching);
|
||||
assert_eq!(
|
||||
files.entries().nth(files.selected).unwrap().path,
|
||||
Path::new("file")
|
||||
);
|
||||
let refreshed = format!("{state:?}");
|
||||
assert!(!state.apply_result(7, Ok(Outcome::WorkingRefresh(None))));
|
||||
assert_eq!(format!("{state:?}"), refreshed);
|
||||
state.after.as_mut().unwrap().target = RevisionTarget::Commit("historical".into());
|
||||
state.queue_working_refresh();
|
||||
assert!(!state.request_queued());
|
||||
state.after.as_mut().unwrap().target = RevisionTarget::Working;
|
||||
state.load_file(0);
|
||||
let user_request = format!("{state:?}");
|
||||
state.queue_working_refresh();
|
||||
assert_eq!(format!("{state:?}"), user_request);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reviewed_files_use_highlighted_catalog_indices_not_sorted_or_filtered_rows() {
|
||||
let mut state = DiffState::new();
|
||||
|
||||
Reference in New Issue
Block a user