feat: follow forward working-checkout changes in diffs

This commit is contained in:
Valdemar Lorenzen
2026-10-13 13:38:46 +02:00
parent b16232aa1d
commit bfdb7c544f
12 changed files with 1118 additions and 62 deletions
+113 -21
View File
@@ -84,6 +84,12 @@ impl DiffWorker {
} => repository
.compare(id, &before, &after, mode, &buffers)
.map(DiffOutcome::Comparison),
DiffOperation::RefreshWorking {
comparison_id,
buffers,
} => repository
.refresh_working_after(comparison_id, id, &buffers)
.map(DiffOutcome::WorkingRefresh),
DiffOperation::LoadFile {
comparison_id,
file_index,
@@ -181,6 +187,8 @@ pub struct EditorSession {
battery: Option<crate::battery::Battery>,
battery_enabled: bool,
last_file_refresh: Instant,
last_diff_refresh: Instant,
pub(crate) diff_follow_working_copy: bool,
}
impl EditorSession {
@@ -287,6 +295,8 @@ impl EditorSession {
battery: None,
battery_enabled: false,
last_file_refresh: Instant::now(),
last_diff_refresh: Instant::now(),
diff_follow_working_copy: config.diff_follow_working_copy,
};
session.synchronize_documents();
session.poll_battery();
@@ -352,6 +362,7 @@ impl EditorSession {
return false;
};
if !diff.loading
&& !diff.refresh_pending()
&& let Some(id) = worker.pending.take()
{
worker.cancelled_request.store(id, Ordering::Release);
@@ -359,29 +370,28 @@ impl EditorSession {
}
true
});
let Some((id, operation)) = self.editor.take_diff_request() else {
return;
};
let tab = self.editor.tab_id();
if let std::collections::btree_map::Entry::Vacant(entry) = self.diff_workers.entry(tab) {
match DiffWorker::start(self.editor.diff_root()) {
Ok(worker) => {
entry.insert(worker);
}
Err(error) => {
self.editor.apply_diff_result(id, Err(error));
return;
while let Some((tab, id, operation)) = self.editor.take_next_diff_request() {
if let std::collections::btree_map::Entry::Vacant(entry) = self.diff_workers.entry(tab)
{
match DiffWorker::start(self.editor.diff_root(tab)) {
Ok(worker) => {
entry.insert(worker);
}
Err(error) => {
self.editor.apply_diff_result(id, Err(error));
continue;
}
}
}
}
if let Err(error) = self
.diff_workers
.get_mut(&tab)
.expect("diff worker started")
.request(id, operation)
{
self.editor.apply_diff_result(id, Err(error));
self.diff_workers.remove(&tab);
if let Err(error) = self
.diff_workers
.get_mut(&tab)
.expect("diff worker started")
.request(id, operation)
{
self.editor.apply_diff_result(id, Err(error));
self.diff_workers.remove(&tab);
}
}
}
@@ -1141,6 +1151,16 @@ 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)
{
return;
}
self.last_diff_refresh = now;
self.editor.queue_working_refreshes();
}
/// Drains ready plugin work without waiting for command or server responses.
pub fn poll(&mut self) -> bool {
self.poll_at(Instant::now())
@@ -1150,6 +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);
changed |= self.poll_diff();
for update in self.lsp.poll() {
match self.apply_update("core", update) {
@@ -1212,6 +1233,77 @@ mod tests {
sync::atomic::{AtomicU64, Ordering},
};
#[test]
fn working_diff_polling_uses_injected_interval_and_never_queues_historical_or_disabled_views() {
use crate::diff::{Catalog, Category, Comparison, RepoKind, Revision, RevisionTarget};
let fixture = Fixture::new();
fixture.file("buffer.txt", "base");
for working in [true, false] {
let mut session = fixture.open("");
session.editor.execute("diff");
let (id, _) = session.editor.take_diff_request().unwrap();
session.editor.apply_diff_result(
id,
Ok(DiffOutcome::Catalog(Catalog {
kind: RepoKind::Git,
notice: None,
entries: [
RevisionTarget::Working,
RevisionTarget::Commit("base".into()),
]
.into_iter()
.map(|target| Revision {
target,
label: "revision".into(),
description: String::new(),
category: Category::Commits,
})
.collect(),
})),
);
session.editor.handle(Input::Down);
session.editor.handle(Input::Enter);
if !working {
session.editor.handle(Input::Down);
}
session.editor.handle(Input::Enter);
let (id, _) = session.editor.take_diff_request().unwrap();
session.editor.apply_diff_result(
id,
Ok(DiffOutcome::Comparison(Comparison {
root: fixture.0.clone(),
before: "base".into(),
after: "after".into(),
files: Vec::new(),
editable_paths: Default::default(),
})),
);
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));
assert!(session.editor.take_next_diff_request().is_none());
assert!(
!session
.editor
.apply_diff_result(refresh, Ok(DiffOutcome::WorkingRefresh(None)))
);
}
}
}
#[test]
fn blame_sources_are_deduplicated_and_stale_results_cannot_reappear() {
let fixture = Fixture::new();