feat: track checkout identity for pinned diff editability
This commit is contained in:
+157
-44
@@ -1,7 +1,7 @@
|
||||
use crate::editor::{BackspaceKind, Input};
|
||||
use crate::files::{FilePicker, FilePresentation, FileView};
|
||||
use std::{
|
||||
collections::BTreeSet,
|
||||
collections::{BTreeMap, BTreeSet},
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
use unicode_segmentation::UnicodeSegmentation;
|
||||
@@ -108,7 +108,7 @@ pub struct Comparison {
|
||||
pub after: String,
|
||||
pub files: Vec<ChangedFile>,
|
||||
pub root: PathBuf,
|
||||
/// Regular working-After candidates; loaded binary details are excluded.
|
||||
/// Eligible regular-file After paths; loaded binary details are excluded.
|
||||
pub editable_paths: BTreeSet<PathBuf>,
|
||||
}
|
||||
#[derive(Debug)]
|
||||
@@ -122,8 +122,9 @@ pub enum Operation {
|
||||
mode: ComparisonMode,
|
||||
buffers: Box<[(PathBuf, String)]>,
|
||||
},
|
||||
RefreshWorking {
|
||||
RefreshCheckout {
|
||||
comparison_id: u64,
|
||||
follow_working: bool,
|
||||
buffers: Box<[(PathBuf, String)]>,
|
||||
},
|
||||
LoadFile {
|
||||
@@ -135,7 +136,7 @@ pub enum Operation {
|
||||
pub enum Outcome {
|
||||
Catalog(Catalog),
|
||||
Comparison(Comparison),
|
||||
WorkingRefresh(Option<Comparison>),
|
||||
CheckoutRefresh(Option<Comparison>),
|
||||
File {
|
||||
comparison_id: u64,
|
||||
file_index: usize,
|
||||
@@ -195,7 +196,7 @@ pub enum DisplayRow {
|
||||
enum RequestKind {
|
||||
Catalog,
|
||||
Comparison,
|
||||
WorkingRefresh {
|
||||
CheckoutRefresh {
|
||||
comparison_id: u64,
|
||||
},
|
||||
File {
|
||||
@@ -238,6 +239,7 @@ pub struct DiffState {
|
||||
pub live_revision: Option<u64>,
|
||||
pub follow_caret: bool,
|
||||
live_files: BTreeSet<usize>,
|
||||
frozen_after: BTreeMap<usize, (Vec<String>, bool)>,
|
||||
context_lines: usize,
|
||||
expand_lines: usize,
|
||||
pending: Option<Operation>,
|
||||
@@ -285,6 +287,7 @@ impl DiffState {
|
||||
live_revision: None,
|
||||
follow_caret: false,
|
||||
live_files: BTreeSet::new(),
|
||||
frozen_after: BTreeMap::new(),
|
||||
context_lines: 10,
|
||||
expand_lines: 15,
|
||||
pending: Some(Operation::Discover),
|
||||
@@ -317,31 +320,32 @@ impl DiffState {
|
||||
|
||||
pub(crate) fn request_ready(&self) -> bool {
|
||||
self.pending.as_ref().is_some_and(|operation| {
|
||||
!self.refresh_pending() || matches!(operation, Operation::RefreshWorking { .. })
|
||||
!self.refresh_pending() || matches!(operation, Operation::RefreshCheckout { .. })
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn refresh_pending(&self) -> bool {
|
||||
matches!(self.request_kind, Some(RequestKind::WorkingRefresh { .. }))
|
||||
matches!(self.request_kind, Some(RequestKind::CheckoutRefresh { .. }))
|
||||
}
|
||||
|
||||
pub(crate) fn queue_working_refresh(&mut self) {
|
||||
pub(crate) fn queue_checkout_refresh(&mut self, follow_working: bool) {
|
||||
if self.phase != Phase::View
|
||||
|| self.loading
|
||||
|| self.request_kind.is_some()
|
||||
|| self.pending.is_some()
|
||||
|| self.comparison.is_none()
|
||||
|| !self
|
||||
|| self
|
||||
.after
|
||||
.as_ref()
|
||||
.is_some_and(|after| after.target == RevisionTarget::Working)
|
||||
.is_none_or(|after| after.target == RevisionTarget::Working && !follow_working)
|
||||
{
|
||||
return;
|
||||
}
|
||||
if let Some(comparison_id) = self.comparison_id {
|
||||
self.request_kind = Some(RequestKind::WorkingRefresh { comparison_id });
|
||||
self.pending = Some(Operation::RefreshWorking {
|
||||
self.request_kind = Some(RequestKind::CheckoutRefresh { comparison_id });
|
||||
self.pending = Some(Operation::RefreshCheckout {
|
||||
comparison_id,
|
||||
follow_working,
|
||||
buffers: Box::new([]),
|
||||
});
|
||||
}
|
||||
@@ -349,7 +353,7 @@ impl DiffState {
|
||||
|
||||
fn queue(&mut self, operation: Operation) {
|
||||
if self.refresh_pending()
|
||||
&& !matches!(self.pending, Some(Operation::RefreshWorking { .. }))
|
||||
&& !matches!(self.pending, Some(Operation::RefreshCheckout { .. }))
|
||||
&& matches!(operation, Operation::LoadFile { .. })
|
||||
{
|
||||
// Finish the refresh before issuing a load against its frozen comparison ID.
|
||||
@@ -378,20 +382,28 @@ impl DiffState {
|
||||
if self.request_kind.is_none() {
|
||||
return false;
|
||||
}
|
||||
if let Some(RequestKind::WorkingRefresh { comparison_id }) = self.request_kind {
|
||||
if let Some(RequestKind::CheckoutRefresh { 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)) => {
|
||||
Ok(Outcome::CheckoutRefresh(None)) => {
|
||||
if let Some(operation) = deferred {
|
||||
self.queue(operation);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
Ok(Outcome::WorkingRefresh(Some(comparison))) => {
|
||||
Ok(Outcome::CheckoutRefresh(Some(mut comparison))) => {
|
||||
let pinned = self
|
||||
.after
|
||||
.as_ref()
|
||||
.is_some_and(|after| after.target != RevisionTarget::Working);
|
||||
let view = (self.top, self.left_before, self.left_after);
|
||||
if !pinned {
|
||||
self.reviewed_files.clear();
|
||||
}
|
||||
let selected = self
|
||||
.comparison
|
||||
.as_ref()
|
||||
@@ -402,6 +414,19 @@ impl DiffState {
|
||||
comparison.files.iter().position(|file| &file.path == path)
|
||||
})
|
||||
.unwrap_or(0);
|
||||
if pinned {
|
||||
self.restore_frozen_after();
|
||||
if let Some(old) = self.comparison.take() {
|
||||
// Checkout-only refreshes retain the frozen catalog order.
|
||||
for (file, replacement) in
|
||||
old.files.into_iter().zip(&mut comparison.files)
|
||||
{
|
||||
if file.loaded {
|
||||
*replacement = file;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some(files) = &mut self.files {
|
||||
files.replace_paths(
|
||||
comparison
|
||||
@@ -414,25 +439,34 @@ impl DiffState {
|
||||
self.comparison = Some(comparison);
|
||||
self.comparison_id = Some(id);
|
||||
self.snapshot_roots = [None, None];
|
||||
self.reviewed_files.clear();
|
||||
self.frozen_after.clear();
|
||||
self.live_files.clear();
|
||||
self.live_buffer = None;
|
||||
self.live_revision = None;
|
||||
self.follow_caret = false;
|
||||
self.prepare_selected_file();
|
||||
if pinned {
|
||||
self.top = view.0.min(self.display_rows.len().saturating_sub(1));
|
||||
self.left_before = view.1.min(self.max_before);
|
||||
self.left_after = view.2.min(self.max_after);
|
||||
}
|
||||
self.normalize_focus();
|
||||
}
|
||||
Err(error) => {
|
||||
self.revoke_pinned_after();
|
||||
if let Some(operation) = deferred {
|
||||
self.queue(operation);
|
||||
}
|
||||
self.error = Some(error);
|
||||
}
|
||||
Ok(_) => self.error = Some("Unexpected repository response".into()),
|
||||
Ok(_) => {
|
||||
self.revoke_pinned_after();
|
||||
self.error = Some("Unexpected repository response".into());
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
if matches!(result, Ok(Outcome::WorkingRefresh(_))) {
|
||||
if matches!(result, Ok(Outcome::CheckoutRefresh(_))) {
|
||||
return false;
|
||||
}
|
||||
if let Ok(Outcome::File {
|
||||
@@ -483,6 +517,7 @@ impl DiffState {
|
||||
}
|
||||
Ok(Outcome::Comparison(comparison)) if expected == Some(RequestKind::Comparison) => {
|
||||
self.live_files.clear();
|
||||
self.frozen_after.clear();
|
||||
self.reviewed_files.clear();
|
||||
self.live_buffer = None;
|
||||
self.live_revision = None;
|
||||
@@ -972,18 +1007,48 @@ impl DiffState {
|
||||
};
|
||||
}
|
||||
|
||||
pub fn after_editable(&self) -> bool {
|
||||
matches!(self.pane_layout(), PaneLayout::Split | PaneLayout::After)
|
||||
&& self.working_after_available()
|
||||
fn revoke_pinned_after(&mut self) {
|
||||
if self
|
||||
.after
|
||||
.as_ref()
|
||||
.is_none_or(|after| after.target == RevisionTarget::Working)
|
||||
{
|
||||
return;
|
||||
}
|
||||
if let Some(comparison) = &mut self.comparison {
|
||||
comparison.editable_paths.clear();
|
||||
}
|
||||
self.restore_frozen_after();
|
||||
self.live_files.clear();
|
||||
self.live_buffer = None;
|
||||
self.live_revision = None;
|
||||
self.follow_caret = false;
|
||||
self.rebuild_file(false);
|
||||
self.refresh_files();
|
||||
}
|
||||
|
||||
pub(crate) fn working_after_available(&self) -> bool {
|
||||
fn restore_frozen_after(&mut self) {
|
||||
if let Some(comparison) = &mut self.comparison {
|
||||
for (index, (after, eof)) in std::mem::take(&mut self.frozen_after) {
|
||||
if let Some(file) = comparison.files.get_mut(index) {
|
||||
file.after = after;
|
||||
file.after_eof = eof;
|
||||
file.hunks =
|
||||
line_hunks(&file.before, file.before_eof, &file.after, file.after_eof);
|
||||
update_eof_notice(file);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn after_editable(&self) -> bool {
|
||||
matches!(self.pane_layout(), PaneLayout::Split | PaneLayout::After)
|
||||
&& self.live_after_available()
|
||||
}
|
||||
|
||||
pub(crate) fn live_after_available(&self) -> bool {
|
||||
self.phase == Phase::View
|
||||
&& self.selected_file_loaded()
|
||||
&& self
|
||||
.after
|
||||
.as_ref()
|
||||
.is_some_and(|revision| revision.target == RevisionTarget::Working)
|
||||
&& self.comparison.as_ref().is_some_and(|comparison| {
|
||||
comparison.files.get(self.file).is_some_and(|file| {
|
||||
comparison.editable_paths.contains(&file.path)
|
||||
@@ -1302,13 +1367,22 @@ impl DiffState {
|
||||
}
|
||||
|
||||
pub fn refresh_after(&mut self, lines: &[String], revision: u64) {
|
||||
if !self.working_after_available() {
|
||||
if !self.live_after_available() {
|
||||
return;
|
||||
}
|
||||
if self.live_revision == Some(revision) {
|
||||
return;
|
||||
}
|
||||
let file = &self.comparison.as_ref().unwrap().files[self.file];
|
||||
if self
|
||||
.after
|
||||
.as_ref()
|
||||
.is_some_and(|after| after.target != RevisionTarget::Working)
|
||||
{
|
||||
self.frozen_after
|
||||
.entry(self.file)
|
||||
.or_insert_with(|| (file.after.clone(), file.after_eof));
|
||||
}
|
||||
let was_live = self.live_files.contains(&self.file);
|
||||
let changed_lines = changed_line_range(&file.after, lines);
|
||||
if self.reviewed_files.contains(&self.file) {
|
||||
@@ -3297,19 +3371,19 @@ mod tests {
|
||||
files.start_search();
|
||||
files.query = "file".into();
|
||||
files.refilter();
|
||||
let original = format!("{state:?}");
|
||||
state.queue_working_refresh();
|
||||
state.queue_checkout_refresh(true);
|
||||
assert!(matches!(
|
||||
state.take_request(),
|
||||
Some(Operation::RefreshWorking {
|
||||
Some(Operation::RefreshCheckout {
|
||||
comparison_id: 7,
|
||||
..
|
||||
})
|
||||
));
|
||||
assert!(!state.apply_result(8, Ok(Outcome::WorkingRefresh(None))));
|
||||
assert_eq!(format!("{state:?}"), original);
|
||||
assert!(!state.apply_result(8, Ok(Outcome::CheckoutRefresh(None))));
|
||||
assert_eq!(state.comparison_id, Some(7));
|
||||
assert_eq!(state.reviewed_files, BTreeSet::from([0]));
|
||||
|
||||
state.queue_working_refresh();
|
||||
state.queue_checkout_refresh(true);
|
||||
state.take_request();
|
||||
assert!(state.apply_result(9, Err("refresh failed".into())));
|
||||
assert_eq!(state.comparison_id, Some(7));
|
||||
@@ -3317,14 +3391,14 @@ mod tests {
|
||||
assert_eq!(state.reviewed_files, BTreeSet::from([0]));
|
||||
assert_eq!(state.error.as_deref(), Some("refresh failed"));
|
||||
|
||||
state.queue_working_refresh();
|
||||
state.queue_checkout_refresh(true);
|
||||
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!(state.apply_result(10, Ok(Outcome::CheckoutRefresh(Some(replacement)))));
|
||||
assert_eq!(state.comparison_id, Some(10));
|
||||
assert_eq!(state.file, 1);
|
||||
assert_eq!(state.focus, Focus::After);
|
||||
@@ -3341,17 +3415,55 @@ mod tests {
|
||||
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);
|
||||
assert!(!state.apply_result(7, Ok(Outcome::CheckoutRefresh(None))));
|
||||
assert_eq!(state.comparison_id, Some(10));
|
||||
assert_eq!(state.file, 1);
|
||||
state.after.as_mut().unwrap().target = RevisionTarget::Commit("historical".into());
|
||||
state.queue_working_refresh();
|
||||
assert!(!state.request_queued());
|
||||
state.queue_checkout_refresh(false);
|
||||
assert!(state.request_queued());
|
||||
assert!(matches!(
|
||||
state.take_request(),
|
||||
Some(Operation::RefreshCheckout {
|
||||
follow_working: false,
|
||||
..
|
||||
})
|
||||
));
|
||||
assert!(!state.apply_result(11, Ok(Outcome::CheckoutRefresh(None))));
|
||||
state.reviewed_files.insert(1);
|
||||
state.queue_checkout_refresh(false);
|
||||
state.take_request();
|
||||
let mut demoted = live_state(vec!["old".into()], vec!["ahead".into()], false)
|
||||
.comparison
|
||||
.take()
|
||||
.unwrap();
|
||||
demoted.files.insert(0, comparison().files.remove(0));
|
||||
demoted.editable_paths.clear();
|
||||
assert!(state.apply_result(12, Ok(Outcome::CheckoutRefresh(Some(demoted)))));
|
||||
assert_eq!(state.reviewed_files, BTreeSet::from([1]));
|
||||
assert!(!state.live_after_available());
|
||||
state
|
||||
.comparison
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.editable_paths
|
||||
.insert("file".into());
|
||||
state.refresh_after(&["dirty".into()], 1);
|
||||
assert_eq!(state.comparison.as_ref().unwrap().files[1].after, ["dirty"]);
|
||||
state.queue_checkout_refresh(false);
|
||||
state.take_request();
|
||||
assert!(state.apply_result(13, Err("checkout unavailable".into())));
|
||||
assert!(!state.live_after_available());
|
||||
assert_eq!(state.comparison_id, Some(12));
|
||||
assert_eq!(state.comparison.as_ref().unwrap().files[1].after, ["ahead"]);
|
||||
state.after.as_mut().unwrap().target = RevisionTarget::Working;
|
||||
state.queue_checkout_refresh(false);
|
||||
assert!(!state.request_queued());
|
||||
state.load_file(0);
|
||||
let user_request = format!("{state:?}");
|
||||
state.queue_working_refresh();
|
||||
assert_eq!(format!("{state:?}"), user_request);
|
||||
state.queue_checkout_refresh(true);
|
||||
assert!(matches!(
|
||||
state.pending,
|
||||
Some(Operation::LoadFile { file_index: 0, .. })
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -3807,6 +3919,7 @@ mod tests {
|
||||
assert_eq!(empty.rows[0].kind, RowKind::Equal);
|
||||
assert_eq!(empty.after_display_row(0), Some(0));
|
||||
empty.after.as_mut().unwrap().target = RevisionTarget::Commit("historic".into());
|
||||
empty.comparison.as_mut().unwrap().editable_paths.clear();
|
||||
empty.refresh_after(&["must not apply".into()], 2);
|
||||
assert_eq!(empty.comparison.as_ref().unwrap().files[0].after, [""]);
|
||||
empty.handle(Input::Escape);
|
||||
|
||||
+156
-30
@@ -2588,7 +2588,7 @@ impl Editor {
|
||||
self.diff_display_options = (view_mode, split_axis);
|
||||
}
|
||||
|
||||
pub(crate) fn queue_working_refreshes(&mut self) {
|
||||
pub(crate) fn queue_checkout_refreshes(&mut self, follow_working: bool) {
|
||||
for index in 0..self.tab_order.len() {
|
||||
let tab = self.tab_order[index];
|
||||
let owner = if tab == self.tab_id {
|
||||
@@ -2599,7 +2599,7 @@ impl Editor {
|
||||
if let Some(owner) = owner {
|
||||
self.with_pane(owner, |editor| {
|
||||
if let Some(diff) = &mut editor.diff {
|
||||
diff.queue_working_refresh();
|
||||
diff.queue_checkout_refresh(follow_working);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -2638,7 +2638,19 @@ impl Editor {
|
||||
{
|
||||
Some(buffers)
|
||||
}
|
||||
Operation::RefreshWorking { buffers, .. } => Some(buffers),
|
||||
Operation::RefreshCheckout {
|
||||
follow_working: true,
|
||||
buffers,
|
||||
..
|
||||
} if self
|
||||
.diff
|
||||
.as_ref()
|
||||
.or(self.parked_diff.as_ref())
|
||||
.and_then(|diff| diff.after.as_ref())
|
||||
.is_some_and(|after| after.target == RevisionTarget::Working) =>
|
||||
{
|
||||
Some(buffers)
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
if let Some(buffers) = buffers {
|
||||
@@ -2694,6 +2706,25 @@ impl Editor {
|
||||
return self.with_pane(owner, |editor| editor.apply_diff_result(id, result));
|
||||
}
|
||||
self.outstanding_diff_request = None;
|
||||
if self.diff.as_ref().is_some_and(DiffState::refresh_pending)
|
||||
&& !matches!(&result, Ok(Outcome::CheckoutRefresh(None)))
|
||||
{
|
||||
self.finish_completion();
|
||||
self.snippet = None;
|
||||
self.clear_hover();
|
||||
if self.active.view.mode == Mode::Insert {
|
||||
self.finish_insert();
|
||||
self.leave_insert();
|
||||
}
|
||||
}
|
||||
let preserved_view = (self.diff.as_ref().is_some_and(|diff| {
|
||||
diff.refresh_pending()
|
||||
&& diff
|
||||
.after
|
||||
.as_ref()
|
||||
.is_some_and(|after| after.target != RevisionTarget::Working)
|
||||
}) && !matches!(&result, Ok(Outcome::CheckoutRefresh(None))))
|
||||
.then(|| self.active.view.inherit());
|
||||
let Some(diff) = &mut self.diff else {
|
||||
return false;
|
||||
};
|
||||
@@ -2701,15 +2732,10 @@ impl Editor {
|
||||
return false;
|
||||
}
|
||||
let refresh = diff.refresh_pending();
|
||||
let previous = diff.comparison_id;
|
||||
if !diff.apply_result(id, result) {
|
||||
return false;
|
||||
}
|
||||
let replaced = previous != diff.comparison_id;
|
||||
if refresh {
|
||||
if !replaced {
|
||||
return true;
|
||||
}
|
||||
self.pending_lsp_navigation = None;
|
||||
self.diff_bound = None;
|
||||
} else if let Some(files) = &mut diff.files {
|
||||
@@ -2719,7 +2745,34 @@ impl Editor {
|
||||
self.diff.as_ref().and_then(|diff| diff.comparison_id) != Some(role.0)
|
||||
});
|
||||
self.bind_diff_buffer();
|
||||
let pinned_refresh = preserved_view.is_some();
|
||||
if self.active.diff_snapshot.is_some()
|
||||
&& let Some(mut view) = preserved_view
|
||||
{
|
||||
view.clamp(&self.active.lines);
|
||||
self.active.view = view;
|
||||
}
|
||||
let reviewed = self.diff.as_mut().and_then(|diff| {
|
||||
(refresh
|
||||
&& diff
|
||||
.after
|
||||
.as_ref()
|
||||
.is_some_and(|after| after.target != RevisionTarget::Working))
|
||||
.then(|| std::mem::take(&mut diff.reviewed_files))
|
||||
});
|
||||
self.refresh_live_diff();
|
||||
if let Some(reviewed) = reviewed {
|
||||
self.diff
|
||||
.as_mut()
|
||||
.expect("comparison remains open")
|
||||
.reviewed_files = reviewed;
|
||||
}
|
||||
if pinned_refresh {
|
||||
self.diff
|
||||
.as_mut()
|
||||
.expect("comparison remains open")
|
||||
.follow_caret = false;
|
||||
}
|
||||
if refresh || obsolete_snapshot {
|
||||
self.prune_diff_snapshots();
|
||||
}
|
||||
@@ -2976,8 +3029,15 @@ impl Editor {
|
||||
diff.focus
|
||||
};
|
||||
let key = (comparison_id, diff.file, focus);
|
||||
let writable = focus == Focus::After && diff.after_editable();
|
||||
if self.diff_bound.is_some_and(|(id, file, side, buffer)| {
|
||||
(id, file, side) == key && buffer == self.active.id
|
||||
(id, file, side) == key
|
||||
&& buffer == self.active.id
|
||||
&& if writable {
|
||||
self.active.diff_snapshot.is_none() && diff.live_buffer == Some(self.active.id)
|
||||
} else {
|
||||
self.active.diff_snapshot == Some(key)
|
||||
}
|
||||
}) {
|
||||
return;
|
||||
}
|
||||
@@ -2996,7 +3056,6 @@ impl Editor {
|
||||
diff.corresponding_row(from, focus, target.active.row)
|
||||
.map(|row| (row, column))
|
||||
});
|
||||
let writable = focus == Focus::After && diff.after_editable();
|
||||
// Binding a comparison side is not a user departure from its palette.
|
||||
let palette = self.palette.take();
|
||||
if writable {
|
||||
@@ -3139,7 +3198,7 @@ impl Editor {
|
||||
}
|
||||
});
|
||||
if let Some(buffer) = buffer
|
||||
&& diff.working_after_available()
|
||||
&& diff.live_after_available()
|
||||
&& diff.live_revision != Some(buffer.text_revision)
|
||||
{
|
||||
diff.refresh_after(&buffer.lines, buffer.text_revision);
|
||||
@@ -3833,10 +3892,6 @@ impl Editor {
|
||||
return Err("Definition target is binary or non-UTF-8".into());
|
||||
}
|
||||
let writable = focus == Focus::After
|
||||
&& diff
|
||||
.after
|
||||
.as_ref()
|
||||
.is_some_and(|revision| revision.target == RevisionTarget::Working)
|
||||
&& comparison.editable_paths.contains(&file.path)
|
||||
&& file.status != crate::diff::FileStatus::Removed
|
||||
&& file
|
||||
@@ -3872,13 +3927,7 @@ impl Editor {
|
||||
let file = &comparison.files[file_index];
|
||||
let live = if writable {
|
||||
Some(target)
|
||||
} else if focus == Focus::Before
|
||||
&& diff
|
||||
.after
|
||||
.as_ref()
|
||||
.is_some_and(|revision| revision.target == RevisionTarget::Working)
|
||||
&& comparison.editable_paths.contains(&file.path)
|
||||
{
|
||||
} else if focus == Focus::Before && comparison.editable_paths.contains(&file.path) {
|
||||
let identity = path_identity(&comparison.root.join(&file.path)).ok();
|
||||
std::iter::once(&self.active)
|
||||
.chain(self.parked.values())
|
||||
@@ -12780,14 +12829,14 @@ mod tests {
|
||||
let visible_tab = editor.tab_id();
|
||||
let visible_pane = editor.pane_id();
|
||||
let cursor = editor.cursor();
|
||||
editor.queue_working_refreshes();
|
||||
editor.queue_checkout_refreshes(true);
|
||||
let (tab, request, operation) = editor.take_next_diff_request().unwrap();
|
||||
assert_eq!(tab, hidden_tab);
|
||||
assert!(
|
||||
matches!(operation, Operation::RefreshWorking { comparison_id, .. }
|
||||
matches!(operation, Operation::RefreshCheckout { comparison_id, .. }
|
||||
if comparison_id == old_comparison)
|
||||
);
|
||||
assert!(!editor.apply_diff_result(request, Ok(Outcome::WorkingRefresh(None))));
|
||||
assert!(!editor.apply_diff_result(request, Ok(Outcome::CheckoutRefresh(None))));
|
||||
assert_eq!(
|
||||
editor.tab_diff(hidden_tab).unwrap().comparison_id,
|
||||
Some(old_comparison)
|
||||
@@ -12797,12 +12846,12 @@ mod tests {
|
||||
assert_eq!(editor.buffer_id(), old_snapshot);
|
||||
assert_eq!(editor.cursor(), cursor);
|
||||
|
||||
editor.queue_working_refreshes();
|
||||
editor.queue_checkout_refreshes(true);
|
||||
let (_, request, _) = editor.take_next_diff_request().unwrap();
|
||||
let replacement = editor.with_pane(owner, |editor| {
|
||||
editor.diff.as_mut().unwrap().comparison.take().unwrap()
|
||||
});
|
||||
assert!(editor.apply_diff_result(request, Ok(Outcome::WorkingRefresh(Some(replacement)))));
|
||||
assert!(editor.apply_diff_result(request, Ok(Outcome::CheckoutRefresh(Some(replacement)))));
|
||||
assert_eq!(
|
||||
editor.tab_diff(hidden_tab).unwrap().comparison_id,
|
||||
Some(request)
|
||||
@@ -12816,7 +12865,7 @@ mod tests {
|
||||
.apply(vec![Effect::Insert("rejected".into())])
|
||||
.is_err()
|
||||
);
|
||||
assert!(!editor.apply_diff_result(request, Ok(Outcome::WorkingRefresh(None))));
|
||||
assert!(!editor.apply_diff_result(request, Ok(Outcome::CheckoutRefresh(None))));
|
||||
|
||||
editor.with_pane(owner, |editor| {
|
||||
assert_ne!(editor.buffer_id(), old_snapshot);
|
||||
@@ -12846,7 +12895,7 @@ mod tests {
|
||||
let mut target = lazy_lsp_target(&mut editor);
|
||||
let old_comparison = editor.diff().unwrap().comparison_id.unwrap();
|
||||
let virtual_root = editor.diff().unwrap().snapshot_roots[0].clone().unwrap();
|
||||
editor.queue_working_refreshes();
|
||||
editor.queue_checkout_refreshes(true);
|
||||
let (_, refresh, _) = editor.take_next_diff_request().unwrap();
|
||||
let loaded_before = target.before.clone();
|
||||
let loaded_after = target.after.clone();
|
||||
@@ -12878,7 +12927,7 @@ mod tests {
|
||||
let changed = request_kind != "noop_navigation";
|
||||
let outcome = if changed { Some(replacement) } else { None };
|
||||
assert_eq!(
|
||||
editor.apply_diff_result(refresh, Ok(Outcome::WorkingRefresh(outcome))),
|
||||
editor.apply_diff_result(refresh, Ok(Outcome::CheckoutRefresh(outcome))),
|
||||
changed
|
||||
);
|
||||
if changed {
|
||||
@@ -13832,6 +13881,83 @@ mod tests {
|
||||
assert!(editor.dirty());
|
||||
editor.handle(Input::Char('u'));
|
||||
assert_eq!(editor.lines(), ["beta"]);
|
||||
for refresh_error in [false, true] {
|
||||
let path = directory.path("file.txt");
|
||||
fs::write(&path, "original").unwrap();
|
||||
let mut editor = Editor::open(Some(path.clone()), vec![]).unwrap();
|
||||
historical_diff(
|
||||
&mut editor,
|
||||
&directory.0,
|
||||
vec!["before".into()],
|
||||
vec!["original".into()],
|
||||
);
|
||||
let endpoints = (
|
||||
editor.diff().unwrap().before.clone(),
|
||||
editor.diff().unwrap().after.clone(),
|
||||
);
|
||||
editor.queue_checkout_refreshes(false);
|
||||
let (_, request, _) = editor.take_next_diff_request().unwrap();
|
||||
let mut promoted = editor.diff.as_mut().unwrap().comparison.take().unwrap();
|
||||
promoted.editable_paths.insert("file.txt".into());
|
||||
editor.apply_diff_result(request, Ok(Outcome::CheckoutRefresh(Some(promoted))));
|
||||
editor.diff.as_mut().unwrap().focus = Focus::After;
|
||||
editor.bind_diff_buffer();
|
||||
assert!(editor.diff_editing());
|
||||
let live = editor.buffer_id();
|
||||
type_text(&mut editor, "ix");
|
||||
assert_eq!(editor.mode(), Mode::Insert);
|
||||
let comparison_id = editor.diff().unwrap().comparison_id;
|
||||
editor.queue_checkout_refreshes(false);
|
||||
let (_, request, _) = editor.take_next_diff_request().unwrap();
|
||||
if refresh_error {
|
||||
assert!(editor.apply_diff_result(request, Err("checkout unavailable".into())));
|
||||
assert_eq!(editor.diff().unwrap().comparison_id, comparison_id);
|
||||
} else {
|
||||
let mut demoted = editor.diff.as_mut().unwrap().comparison.take().unwrap();
|
||||
demoted.editable_paths.clear();
|
||||
demoted.files[0].after = vec!["original".into()];
|
||||
demoted.files[0].after_eof = false;
|
||||
demoted.files[0].hunks = crate::diff::line_hunks(
|
||||
&demoted.files[0].before,
|
||||
false,
|
||||
&demoted.files[0].after,
|
||||
false,
|
||||
);
|
||||
assert!(
|
||||
editor.apply_diff_result(request, Ok(Outcome::CheckoutRefresh(Some(demoted))),)
|
||||
);
|
||||
}
|
||||
assert!(!editor.diff_editable());
|
||||
assert_eq!(editor.lines(), ["original"]);
|
||||
assert_eq!(editor.buffer(live).unwrap().lines, ["xoriginal"]);
|
||||
assert!(editor.buffer(live).unwrap().dirty());
|
||||
assert_eq!(editor.buffer(live).unwrap().view.mode, Mode::Normal);
|
||||
assert!(
|
||||
editor
|
||||
.apply(vec![Effect::Insert("blocked".into())])
|
||||
.is_err()
|
||||
);
|
||||
editor.execute("w");
|
||||
assert_eq!(fs::read_to_string(&path).unwrap(), "original");
|
||||
editor.queue_checkout_refreshes(false);
|
||||
let (_, request, _) = editor.take_next_diff_request().unwrap();
|
||||
let mut promoted = editor.diff.as_mut().unwrap().comparison.take().unwrap();
|
||||
promoted.editable_paths.insert("file.txt".into());
|
||||
editor.apply_diff_result(request, Ok(Outcome::CheckoutRefresh(Some(promoted))));
|
||||
assert!(editor.diff_editing());
|
||||
assert!(editor.diff().unwrap().error.is_none());
|
||||
assert_eq!(editor.buffer_id(), live);
|
||||
assert_eq!(editor.lines(), ["xoriginal"]);
|
||||
assert_eq!(
|
||||
(
|
||||
editor.diff().unwrap().before.clone(),
|
||||
editor.diff().unwrap().after.clone(),
|
||||
),
|
||||
endpoints,
|
||||
);
|
||||
editor.handle(Input::Char('u'));
|
||||
assert_eq!(editor.lines(), ["original"]);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
+131
-25
@@ -311,6 +311,9 @@ struct PreparedComparison {
|
||||
after: String,
|
||||
working_after: bool,
|
||||
working_anchor: Option<String>,
|
||||
pinned_commit: Option<String>,
|
||||
pinned_checkout: Option<String>,
|
||||
editable_after: bool,
|
||||
}
|
||||
|
||||
pub struct Repository {
|
||||
@@ -933,7 +936,7 @@ impl Repository {
|
||||
"log",
|
||||
"--no-graph",
|
||||
"-r",
|
||||
&format!("commit_id({})", serde_json::to_string(id).unwrap()),
|
||||
&format!("present(commit_id({}))", serde_json::to_string(id).unwrap()),
|
||||
"-T",
|
||||
"conflict",
|
||||
],
|
||||
@@ -1335,6 +1338,19 @@ impl Repository {
|
||||
}
|
||||
Ok(base.into())
|
||||
}
|
||||
fn pinned_checkout(&self, id: &str) -> Result<Option<String>, String> {
|
||||
if self.kind == RepoKind::Git {
|
||||
return Ok(Some(id.into()));
|
||||
}
|
||||
// Private remote history need not exist in the active jj operation.
|
||||
let revset = format!("present(commit_id({}))", serde_json::to_string(id).unwrap());
|
||||
let identity = text(self.jj(
|
||||
&self.root,
|
||||
&["log", "--no-graph", "-r", &revset, "-T", "change_id"],
|
||||
)?)?;
|
||||
let identity = identity.trim();
|
||||
Ok((!identity.is_empty()).then(|| identity.into()))
|
||||
}
|
||||
fn working_anchor(&self) -> Result<Option<String>, String> {
|
||||
if self.kind == RepoKind::Jujutsu {
|
||||
return Ok(Some(
|
||||
@@ -1417,11 +1433,20 @@ impl Repository {
|
||||
|(before, after, label)| (before, after, label.as_str()),
|
||||
);
|
||||
let working_after = *after_target == RevisionTarget::Working;
|
||||
let working_anchor = if working_after {
|
||||
self.working_anchor()?
|
||||
} else {
|
||||
let checkout = self.working_anchor()?;
|
||||
let pinned = if working_after {
|
||||
None
|
||||
} else {
|
||||
let (store, id) = self.resolve(after_target)?;
|
||||
Some((store, id))
|
||||
};
|
||||
let pinned_checkout = pinned
|
||||
.as_ref()
|
||||
.map(|(_, id)| self.pinned_checkout(id))
|
||||
.transpose()?
|
||||
.flatten();
|
||||
let editable_after =
|
||||
working_after || (pinned_checkout.is_some() && pinned_checkout == checkout);
|
||||
let left = self.snapshot(before_target)?;
|
||||
let same = before_target == after_target && (!working_after || buffers.is_empty());
|
||||
let right = if same {
|
||||
@@ -1429,7 +1454,8 @@ impl Repository {
|
||||
} else if working_after {
|
||||
self.working_snapshot(buffers)?
|
||||
} else {
|
||||
self.snapshot(after_target)?
|
||||
let (store, id) = pinned.as_ref().unwrap();
|
||||
self.commit_snapshot(store, id)?
|
||||
};
|
||||
let mut prepared = PreparedComparison {
|
||||
id: comparison_id,
|
||||
@@ -1439,31 +1465,91 @@ impl Repository {
|
||||
before: before_label.into(),
|
||||
after: after.label.clone(),
|
||||
working_after,
|
||||
working_anchor,
|
||||
working_anchor: working_after.then(|| checkout.clone()).flatten(),
|
||||
pinned_commit: pinned.map(|(_, id)| id),
|
||||
pinned_checkout,
|
||||
editable_after,
|
||||
};
|
||||
let comparison = self.build_comparison(&mut prepared, same)?;
|
||||
if working_after && self.working_anchor()? != prepared.working_anchor {
|
||||
return Err("Working checkout moved during comparison".into());
|
||||
if self.working_anchor()? != checkout {
|
||||
return Err("Checkout moved during comparison".into());
|
||||
}
|
||||
self.comparison = Some(prepared);
|
||||
Ok(comparison)
|
||||
}
|
||||
|
||||
/// Advance only a selected Working After, keeping its original Before frozen.
|
||||
pub fn refresh_working_after(
|
||||
/// Follow Working forward when enabled, or update a pinned After's eligibility.
|
||||
/// Pinned sources and path catalogs never change when checkout eligibility does.
|
||||
pub fn refresh_checkout(
|
||||
&mut self,
|
||||
comparison_id: u64,
|
||||
new_id: u64,
|
||||
follow_working: bool,
|
||||
buffers: &[(PathBuf, String)],
|
||||
) -> Result<Option<Comparison>, String> {
|
||||
let result = self.refresh_checkout_inner(comparison_id, new_id, follow_working, buffers);
|
||||
if result.is_err()
|
||||
&& let Some(prepared) = self.comparison.as_mut()
|
||||
&& prepared.id == comparison_id
|
||||
&& !prepared.working_after
|
||||
{
|
||||
// The frontend revokes on error; a recovered checkout must emit a
|
||||
// fresh promotion even if its identity did not change meanwhile.
|
||||
prepared.editable_after = false;
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
fn refresh_checkout_inner(
|
||||
&mut self,
|
||||
comparison_id: u64,
|
||||
new_id: u64,
|
||||
follow_working: bool,
|
||||
buffers: &[(PathBuf, String)],
|
||||
) -> Result<Option<Comparison>, String> {
|
||||
let Some(prepared) = self
|
||||
.comparison
|
||||
.as_ref()
|
||||
.filter(|prepared| prepared.id == comparison_id && prepared.working_after)
|
||||
.filter(|prepared| prepared.id == comparison_id)
|
||||
else {
|
||||
return Ok(None);
|
||||
};
|
||||
self.check_cancelled()?;
|
||||
if prepared.working_after && !follow_working {
|
||||
return Ok(None);
|
||||
}
|
||||
if prepared.pinned_checkout.is_none()
|
||||
&& let Some(id) = prepared.pinned_commit.as_deref()
|
||||
{
|
||||
// A privately loaded endpoint may later be imported into active jj.
|
||||
// Retry only its frozen commit ID, never the mutable remote alias.
|
||||
let identity = self.pinned_checkout(id)?;
|
||||
self.comparison.as_mut().unwrap().pinned_checkout = identity;
|
||||
}
|
||||
let prepared = self.comparison.as_ref().unwrap();
|
||||
let anchor = self.working_anchor()?;
|
||||
if !prepared.working_after {
|
||||
let editable = prepared.pinned_checkout.is_some() && prepared.pinned_checkout == anchor;
|
||||
if editable == prepared.editable_after {
|
||||
return Ok(None);
|
||||
}
|
||||
// No live contents enter the frozen comparison, including dirty
|
||||
// frontend buffer overlays.
|
||||
let mut prepared = self.comparison.take().unwrap();
|
||||
prepared.editable_after = editable;
|
||||
let comparison = self.comparison_catalog(&prepared);
|
||||
let result = self.check_cancelled().and_then(|()| {
|
||||
if self.working_anchor()? != anchor {
|
||||
return Err("Checkout moved during comparison".into());
|
||||
}
|
||||
Ok(comparison)
|
||||
});
|
||||
if result.is_ok() {
|
||||
prepared.id = new_id;
|
||||
}
|
||||
self.comparison = Some(prepared);
|
||||
return result.map(Some);
|
||||
}
|
||||
if !self.working_advanced(prepared.working_anchor.as_deref(), anchor.as_deref())? {
|
||||
return Ok(None);
|
||||
}
|
||||
@@ -1560,35 +1646,55 @@ impl Repository {
|
||||
removed.insert(j);
|
||||
}
|
||||
}
|
||||
let files: Vec<_> = files
|
||||
prepared.files = files
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.filter_map(|(i, f)| (!removed.contains(&i)).then_some(f))
|
||||
.filter_map(|(i, file)| {
|
||||
(!removed.contains(&i)).then_some((file.path, file.old_path, file.status))
|
||||
})
|
||||
.collect();
|
||||
let editable_paths = if prepared.working_after {
|
||||
files
|
||||
Ok(self.comparison_catalog(prepared))
|
||||
}
|
||||
|
||||
fn comparison_catalog(&self, prepared: &PreparedComparison) -> Comparison {
|
||||
let files = prepared
|
||||
.files
|
||||
.iter()
|
||||
.map(|(path, old_path, status)| ChangedFile {
|
||||
path: path.clone(),
|
||||
old_path: old_path.clone(),
|
||||
status: *status,
|
||||
loaded: false,
|
||||
before: vec![],
|
||||
after: vec![],
|
||||
before_eof: false,
|
||||
after_eof: false,
|
||||
hunks: vec![],
|
||||
notice: None,
|
||||
})
|
||||
.collect();
|
||||
let editable_paths = if prepared.editable_after {
|
||||
prepared
|
||||
.files
|
||||
.iter()
|
||||
.filter(|file| {
|
||||
right_view
|
||||
.get(&file.path)
|
||||
.filter(|(path, _, _)| {
|
||||
prepared
|
||||
.right
|
||||
.get(path)
|
||||
.is_some_and(|data| matches!(data.mode.as_str(), "100644" | "100755"))
|
||||
})
|
||||
.map(|file| file.path.clone())
|
||||
.map(|(path, _, _)| path.clone())
|
||||
.collect()
|
||||
} else {
|
||||
BTreeSet::new()
|
||||
};
|
||||
prepared.files = files
|
||||
.iter()
|
||||
.map(|file| (file.path.clone(), file.old_path.clone(), file.status))
|
||||
.collect();
|
||||
Ok(Comparison {
|
||||
Comparison {
|
||||
before: prepared.before.clone(),
|
||||
after: prepared.after.clone(),
|
||||
files,
|
||||
root: self.root.clone(),
|
||||
editable_paths,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Prepare one catalog entry using its original comparison's frozen sources.
|
||||
|
||||
+27
-30
@@ -84,12 +84,13 @@ impl DiffWorker {
|
||||
} => repository
|
||||
.compare(id, &before, &after, mode, &buffers)
|
||||
.map(DiffOutcome::Comparison),
|
||||
DiffOperation::RefreshWorking {
|
||||
DiffOperation::RefreshCheckout {
|
||||
comparison_id,
|
||||
follow_working,
|
||||
buffers,
|
||||
} => repository
|
||||
.refresh_working_after(comparison_id, id, &buffers)
|
||||
.map(DiffOutcome::WorkingRefresh),
|
||||
.refresh_checkout(comparison_id, id, follow_working, &buffers)
|
||||
.map(DiffOutcome::CheckoutRefresh),
|
||||
DiffOperation::LoadFile {
|
||||
comparison_id,
|
||||
file_index,
|
||||
@@ -1151,14 +1152,13 @@ impl EditorSession {
|
||||
self.editor.refresh_files()
|
||||
}
|
||||
|
||||
fn refresh_working_diffs_at(&mut self, now: Instant) {
|
||||
if !self.diff_follow_working_copy
|
||||
|| now.saturating_duration_since(self.last_diff_refresh) < Duration::from_secs(1)
|
||||
{
|
||||
fn refresh_checkout_diffs_at(&mut self, now: Instant) {
|
||||
if now.saturating_duration_since(self.last_diff_refresh) < Duration::from_secs(1) {
|
||||
return;
|
||||
}
|
||||
self.last_diff_refresh = now;
|
||||
self.editor.queue_working_refreshes();
|
||||
self.editor
|
||||
.queue_checkout_refreshes(self.diff_follow_working_copy);
|
||||
}
|
||||
|
||||
/// Drains ready plugin work without waiting for command or server responses.
|
||||
@@ -1170,7 +1170,7 @@ impl EditorSession {
|
||||
let mut changed = self.refresh_files_at(now);
|
||||
changed |= self.synchronize_documents();
|
||||
changed |= self.poll_battery();
|
||||
self.refresh_working_diffs_at(now);
|
||||
self.refresh_checkout_diffs_at(now);
|
||||
changed |= self.poll_diff();
|
||||
for update in self.lsp.poll() {
|
||||
match self.apply_update("core", update) {
|
||||
@@ -1234,7 +1234,7 @@ mod tests {
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn working_diff_polling_uses_injected_interval_and_never_queues_historical_or_disabled_views() {
|
||||
fn checkout_diff_polling_uses_injected_interval_independently_of_endpoint_following() {
|
||||
use crate::diff::{Catalog, Category, Comparison, RepoKind, Revision, RevisionTarget};
|
||||
let fixture = Fixture::new();
|
||||
fixture.file("buffer.txt", "base");
|
||||
@@ -1279,27 +1279,24 @@ mod tests {
|
||||
})),
|
||||
);
|
||||
let now = session.last_diff_refresh;
|
||||
session.diff_follow_working_copy = false;
|
||||
session.refresh_working_diffs_at(now + Duration::from_secs(1));
|
||||
assert!(session.editor.take_next_diff_request().is_none());
|
||||
session.diff_follow_working_copy = true;
|
||||
session.refresh_working_diffs_at(now + Duration::from_millis(999));
|
||||
assert!(session.editor.take_next_diff_request().is_none());
|
||||
session.refresh_working_diffs_at(now + Duration::from_secs(1));
|
||||
let request = session.editor.take_next_diff_request();
|
||||
assert_eq!(request.is_some(), working);
|
||||
if let Some((_, refresh, operation)) = request {
|
||||
assert!(
|
||||
matches!(operation, DiffOperation::RefreshWorking { comparison_id, .. }
|
||||
if comparison_id == id)
|
||||
);
|
||||
session.refresh_working_diffs_at(now + Duration::from_secs(2));
|
||||
for (tick, follow) in [(1, false), (2, true)] {
|
||||
session.diff_follow_working_copy = follow;
|
||||
session.refresh_checkout_diffs_at(now + Duration::from_millis(tick * 1000 - 1));
|
||||
assert!(session.editor.take_next_diff_request().is_none());
|
||||
assert!(
|
||||
!session
|
||||
.editor
|
||||
.apply_diff_result(refresh, Ok(DiffOutcome::WorkingRefresh(None)))
|
||||
);
|
||||
session.refresh_checkout_diffs_at(now + Duration::from_secs(tick));
|
||||
let request = session.editor.take_next_diff_request();
|
||||
assert_eq!(request.is_some(), !working || follow);
|
||||
if let Some((_, refresh, operation)) = request {
|
||||
assert!(
|
||||
matches!(operation, DiffOperation::RefreshCheckout { comparison_id, follow_working, .. }
|
||||
if comparison_id == id && follow_working == follow)
|
||||
);
|
||||
assert!(
|
||||
!session
|
||||
.editor
|
||||
.apply_diff_result(refresh, Ok(DiffOutcome::CheckoutRefresh(None)))
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+6
-4
@@ -4864,7 +4864,7 @@ mod tests {
|
||||
fn diff_borders_keep_full_revisions_and_each_panes_own_editability() {
|
||||
let mut terminal = Terminal::new(ratatui::backend::TestBackend::new(180, 12)).unwrap();
|
||||
let mut panels = WheelPanels::default();
|
||||
for case in ["working", "unlisted", "historical", "binary"] {
|
||||
for case in ["working", "checked_out", "unlisted", "historical", "binary"] {
|
||||
let mut file = syntax_diff_file("new.rs", "fn old() {}", "fn new() {}");
|
||||
file.status = FileStatus::Renamed;
|
||||
file.old_path = Some("old.rs".into());
|
||||
@@ -4876,14 +4876,14 @@ mod tests {
|
||||
let comparison = diff.comparison.as_mut().unwrap();
|
||||
comparison.before = "abcdef0123456789 (baseline)".into();
|
||||
comparison.after = "fedcba9876543210 (destination)".into();
|
||||
if case != "unlisted" {
|
||||
if matches!(case, "working" | "checked_out" | "binary") {
|
||||
comparison.editable_paths.insert("new.rs".into());
|
||||
}
|
||||
if case == "historical" {
|
||||
if matches!(case, "historical" | "checked_out") {
|
||||
diff.after.as_mut().unwrap().target =
|
||||
crate::diff::RevisionTarget::Commit("fedcba9876543210".into());
|
||||
}
|
||||
let writable = case == "working";
|
||||
let writable = matches!(case, "working" | "checked_out");
|
||||
assert_eq!(diff.after_editable(), writable, "{case}");
|
||||
for focus in [Focus::Before, Focus::After] {
|
||||
diff.focus = focus;
|
||||
@@ -9737,6 +9737,7 @@ end
|
||||
}
|
||||
git(&["add", "."]);
|
||||
git(&["commit", "-qm", "base"]);
|
||||
git(&["commit", "--allow-empty", "-qm", "unmatched checkout"]);
|
||||
for index in 0..8 {
|
||||
directory.file(
|
||||
&format!("file-{index}.txt"),
|
||||
@@ -9763,6 +9764,7 @@ end
|
||||
ready(&mut session, Phase::Before);
|
||||
session.handle(Input::Enter);
|
||||
session.handle(Input::Down);
|
||||
session.handle(Input::Down);
|
||||
session.handle(Input::Enter);
|
||||
ready(&mut session, Phase::View);
|
||||
let mut terminal = Terminal::new(ratatui::backend::TestBackend::new(120, 18)).unwrap();
|
||||
|
||||
Reference in New Issue
Block a user