feat: edit working-checkout comparisons with normal editor actions

This commit is contained in:
2026-10-11 02:13:16 +02:00
parent 704dce2539
commit 9a34669c08
16 changed files with 2277 additions and 647 deletions
+151 -10
View File
@@ -11,7 +11,7 @@ use std::{
};
use crate::{
diff::{Operation as DiffOperation, Outcome as DiffOutcome},
diff::{Focus, Operation as DiffOperation, Outcome as DiffOutcome},
editor::{Cursor, Editor, Effect, Input, Invocation, Mode},
lsp::Lsp,
plugin::{
@@ -71,8 +71,12 @@ impl DiffWorker {
DiffOperation::FetchRemote(remote) => {
repository.fetch_remote(&remote).map(DiffOutcome::Catalog)
}
DiffOperation::Compare { before, after } => repository
.compare(&before, &after)
DiffOperation::Compare {
before,
after,
buffers,
} => repository
.compare(&before, &after, &buffers)
.map(DiffOutcome::Comparison),
}
})();
@@ -254,6 +258,10 @@ impl EditorSession {
&self.editor
}
pub(crate) fn scroll_diff(&mut self, focus: Focus, down: bool) {
self.editor.scroll_diff(focus, down);
}
pub fn handle(&mut self, input: Input) {
self.cancel_pending_popup_request();
let invocation = self.editor.handle(input);
@@ -386,6 +394,7 @@ impl EditorSession {
return;
}
let context = self.editor.context();
let read_only = self.editor.diff().is_some() && !self.editor.diff_editing();
let result = match self.plugins.get_mut(&invocation.plugin) {
Some(plugin) if plugin.is_async() => {
let requests = self.requests.entry(invocation.plugin.clone()).or_default();
@@ -402,7 +411,18 @@ impl EditorSession {
}
Some(plugin) => plugin
.invoke(&invocation.command, &invocation.args, &context)
.and_then(|effects| self.editor.apply(effects)),
.and_then(|effects| {
if read_only
&& effects
.iter()
.any(|effect| matches!(effect, Effect::Insert(_) | Effect::Replace(_)))
{
self.editor.set_message("Diff pane is read-only");
Ok(())
} else {
self.editor.apply(effects)
}
}),
None => Err("plugin host is unavailable".into()),
};
if let Err(error) = result {
@@ -555,7 +575,7 @@ impl EditorSession {
request_id: Option<u64>,
source: InvocationSource,
) -> bool {
if self.editor.diff().is_some() {
if self.editor.diff().is_some() && !self.editor.diff_editing() {
return false;
}
self.editor.buffer_id() == source.buffer_id
@@ -791,6 +811,7 @@ impl EditorSession {
}
}
}
self.editor.refresh_live_diff();
Ok(changed)
}
@@ -807,10 +828,7 @@ impl EditorSession {
let Some(source) = source else {
return Err("plugin response has an unknown invocation ID".into());
};
if !self.source_is_current(source)
|| self.editor.buffer_id() != source.buffer_id
|| self.editor.diff().is_some()
{
if !self.source_is_current(source) || self.editor.buffer_id() != source.buffer_id {
return Ok(false);
}
if effects
@@ -826,6 +844,9 @@ impl EditorSession {
}
return Ok(changed);
}
if self.editor.diff().is_some() && !self.editor.diff_editing() {
return Ok(false);
}
let changed = !effects.is_empty();
self.editor.apply(effects)?;
Ok(changed)
@@ -3314,11 +3335,131 @@ end
});
assert_eq!(session.editor().context().text, "original");
assert_eq!(fs::read_to_string(&source).unwrap(), "original");
session.handle(Input::Escape);
session.execute("diff.close");
session.execute("ping");
drain_until(&mut session, |session| {
session.editor().message() == "still alive"
});
assert_eq!(session.editor().context().text, "original");
}
#[test]
fn async_responses_edit_only_current_bound_working_after_and_refresh_projection() {
use crate::diff::{
Catalog, Category, ChangedFile, Comparison, FileStatus, RepoKind, Revision,
RevisionTarget,
};
let fixture = Fixture::new();
fixture.file("buffer.txt", "base");
let mut session = fixture.open("");
session.editor.execute("diff");
let (id, _) = session.editor.take_diff_request().unwrap();
let revision = |target, label: &str| Revision {
target,
label: label.into(),
description: String::new(),
category: Category::Commits,
};
session.editor.apply_diff_result(
id,
Ok(DiffOutcome::Catalog(Catalog {
kind: RepoKind::Git,
notice: None,
entries: vec![
revision(RevisionTarget::Commit("base".into()), "base"),
revision(RevisionTarget::Working, "Working"),
],
})),
);
for input in [Input::Down, Input::Enter, Input::Enter] {
session.editor.handle(input);
}
let (id, _) = session.editor.take_diff_request().unwrap();
session.editor.apply_diff_result(
id,
Ok(DiffOutcome::Comparison(Comparison {
root: fixture.0.canonicalize().unwrap(),
before: "base".into(),
after: "Working".into(),
editable_paths: [PathBuf::from("buffer.txt")].into_iter().collect(),
files: vec![ChangedFile {
path: PathBuf::from("buffer.txt"),
old_path: None,
status: FileStatus::Modified,
before: vec!["base".into()],
after: vec!["base".into()],
before_eof: false,
after_eof: false,
hunks: Vec::new(),
notice: None,
}],
})),
);
session.editor.handle(Input::BackTab);
let source = InvocationSource {
buffer_id: session.editor.buffer_id(),
revision: session.editor.text_revision(),
};
session
.requests
.entry("fixture".into())
.or_default()
.pending
.insert(1, source);
assert!(
session
.apply_response("fixture", 1, vec![Effect::Insert("accepted".into())])
.unwrap()
);
assert_eq!(session.editor.lines(), ["acceptedbase"]);
let diff = session.editor.diff().unwrap();
assert_eq!(diff.live_revision, Some(session.editor.text_revision()));
assert_eq!(
diff.comparison.as_ref().unwrap().files[0].after,
session.editor.lines()
);
session
.requests
.get_mut("fixture")
.unwrap()
.pending
.insert(2, source);
assert!(
!session
.apply_response("fixture", 2, vec![Effect::Insert("stale".into())])
.unwrap()
);
let current = InvocationSource {
buffer_id: session.editor.buffer_id(),
revision: session.editor.text_revision(),
};
session.editor.scroll_diff(Focus::Before, false);
session
.requests
.get_mut("fixture")
.unwrap()
.pending
.insert(3, current);
assert!(
!session
.apply_response("fixture", 3, vec![Effect::Insert("readonly".into())])
.unwrap()
);
assert_eq!(session.editor.lines(), ["acceptedbase"]);
session
.requests
.get_mut("fixture")
.unwrap()
.pending
.insert(4, current);
assert!(
session
.apply_response("fixture", 4, vec![Effect::Message("allowed".into())])
.unwrap()
);
assert_eq!(session.editor.message(), "allowed");
session.editor.execute("diff.close");
assert!(session.editor.dirty());
}
}