feat: add local and remote repository diff views
This commit is contained in:
+163
@@ -1,4 +1,5 @@
|
||||
use crate::clipboard::{ClipboardMode, SystemClipboard};
|
||||
use crate::diff::{DiffState, Operation, Outcome, Phase};
|
||||
use crate::files::{DockSide, FilePicker, FilePresentation, FileView};
|
||||
use crate::plugin::{CompletionItem, Diagnostic, SemanticToken, SnippetTabstop, TextPosition};
|
||||
use serde::Serialize;
|
||||
@@ -553,6 +554,7 @@ core_actions![
|
||||
"lsp.diagnostics",
|
||||
),
|
||||
("core.save", Action::Save, &[Mode::Normal], "w"),
|
||||
("core.diff", Action::Diff, &[Mode::Normal], "diff"),
|
||||
("core.quit", Action::Quit, &[Mode::Normal], "q"),
|
||||
(
|
||||
"core.force_quit",
|
||||
@@ -1061,6 +1063,9 @@ pub struct Editor {
|
||||
file_picker_view: FileView,
|
||||
file_explorer_view: FileView,
|
||||
count: Option<usize>,
|
||||
diff: Option<DiffState>,
|
||||
next_diff_request: u64,
|
||||
outstanding_diff_request: Option<u64>,
|
||||
}
|
||||
|
||||
impl Editor {
|
||||
@@ -1128,6 +1133,9 @@ impl Editor {
|
||||
file_picker_view: FileView::Tree,
|
||||
file_explorer_view: FileView::Tree,
|
||||
count: None,
|
||||
diff: None,
|
||||
next_diff_request: 0,
|
||||
outstanding_diff_request: None,
|
||||
};
|
||||
editor.refresh_selection_navigation_hint();
|
||||
editor
|
||||
@@ -1228,6 +1236,77 @@ impl Editor {
|
||||
&self.project_root
|
||||
}
|
||||
|
||||
pub fn diff(&self) -> Option<&DiffState> {
|
||||
self.diff.as_ref()
|
||||
}
|
||||
|
||||
pub(crate) fn take_diff_request(&mut self) -> Option<(u64, Operation)> {
|
||||
let operation = self.diff.as_mut()?.take_request()?;
|
||||
self.next_diff_request = self
|
||||
.next_diff_request
|
||||
.checked_add(1)
|
||||
.expect("diff request IDs exhausted");
|
||||
let id = self.next_diff_request;
|
||||
self.outstanding_diff_request = Some(id);
|
||||
Some((id, operation))
|
||||
}
|
||||
|
||||
pub(crate) fn apply_diff_result(&mut self, id: u64, result: Result<Outcome, String>) -> bool {
|
||||
if self.outstanding_diff_request != Some(id) {
|
||||
return false;
|
||||
}
|
||||
self.outstanding_diff_request = None;
|
||||
let Some(diff) = &mut self.diff else {
|
||||
return false;
|
||||
};
|
||||
diff.apply_result(result);
|
||||
true
|
||||
}
|
||||
|
||||
fn open_diff(&mut self) {
|
||||
self.count = None;
|
||||
self.clear_leader();
|
||||
self.palette = None;
|
||||
self.hover = None;
|
||||
self.startup_diagnostic = None;
|
||||
self.blur_file_picker();
|
||||
if self.active.mode == Mode::Command {
|
||||
self.command_line.clear();
|
||||
self.active.mode = Mode::Normal;
|
||||
}
|
||||
self.outstanding_diff_request = None;
|
||||
self.diff = Some(DiffState::new());
|
||||
}
|
||||
|
||||
fn handle_diff(&mut self, input: Input) {
|
||||
// Escape and Tab always retain their modal close/back/focus semantics.
|
||||
if !matches!(input, Input::Escape | Input::Char('\t') | Input::BackTab) {
|
||||
match self.selection_bindings.get(&input) {
|
||||
Some(Binding::Action(Action::Down)) => {
|
||||
self.diff.as_mut().expect("active diff").navigate(true);
|
||||
return;
|
||||
}
|
||||
Some(Binding::Action(Action::Up)) => {
|
||||
self.diff.as_mut().expect("active diff").navigate(false);
|
||||
return;
|
||||
}
|
||||
Some(Binding::Disabled) => return,
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
let back = input == Input::Escape
|
||||
&& self
|
||||
.diff
|
||||
.as_ref()
|
||||
.is_some_and(|diff| diff.phase == Phase::After);
|
||||
if self.diff.as_mut().expect("active diff").handle(input) {
|
||||
self.diff = None;
|
||||
self.outstanding_diff_request = None;
|
||||
} else if back {
|
||||
self.outstanding_diff_request = None;
|
||||
}
|
||||
}
|
||||
|
||||
fn buffer(&self, id: usize) -> Option<&Buffer> {
|
||||
if id == self.active.id {
|
||||
Some(&self.active)
|
||||
@@ -2857,6 +2936,10 @@ impl Editor {
|
||||
Input::Ctrl(ch) => control_key(ch),
|
||||
input => input,
|
||||
};
|
||||
if self.diff.is_some() {
|
||||
self.handle_diff(input);
|
||||
return None;
|
||||
}
|
||||
if self.completion.is_some() {
|
||||
let request = matches!(
|
||||
self.bindings
|
||||
@@ -3140,6 +3223,7 @@ impl Editor {
|
||||
| Action::BufferPrevious
|
||||
| Action::BufferDelete
|
||||
| Action::BufferForceDelete
|
||||
| Action::Diff
|
||||
| Action::LspDefinition
|
||||
| Action::LspHover
|
||||
| Action::LspDiagnostics => unreachable!("catalog Ex actions dispatch above"),
|
||||
@@ -3653,6 +3737,9 @@ impl Editor {
|
||||
}
|
||||
|
||||
pub fn execute(&mut self, command: &str) -> Option<Invocation> {
|
||||
if self.diff.is_some() {
|
||||
return None;
|
||||
}
|
||||
let command = command.trim().strip_prefix(':').unwrap_or(command.trim());
|
||||
let (name, args) = command
|
||||
.split_once(char::is_whitespace)
|
||||
@@ -3694,6 +3781,14 @@ impl Editor {
|
||||
}
|
||||
|
||||
fn execute_action(&mut self, entry: &CoreAction, args: &str) -> Option<Invocation> {
|
||||
if entry.action == Action::Diff {
|
||||
if args.is_empty() {
|
||||
self.open_diff();
|
||||
} else {
|
||||
self.set_message("The diff command takes no arguments");
|
||||
}
|
||||
return None;
|
||||
}
|
||||
self.prepare_command();
|
||||
let action = entry.action;
|
||||
let name = entry.ex.expect("Ex dispatcher requires an alias");
|
||||
@@ -3775,6 +3870,13 @@ impl Editor {
|
||||
/// to the primary caret: Insert runs once there, Replace replaces the buffer.
|
||||
/// Rejected responses and message-only effects retain the complete set.
|
||||
pub fn apply(&mut self, effects: Vec<Effect>) -> Result<(), String> {
|
||||
if self.diff.is_some()
|
||||
&& effects
|
||||
.iter()
|
||||
.any(|effect| matches!(effect, Effect::Insert(_) | Effect::Replace(_)))
|
||||
{
|
||||
return Err("Diff view is read-only".into());
|
||||
}
|
||||
let mut total = 0usize;
|
||||
for effect in &effects {
|
||||
let text = match effect {
|
||||
@@ -6274,6 +6376,67 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn diff_preserves_parked_buffers_visual_carets_and_insert_history() {
|
||||
let directory = Directory::new();
|
||||
let a = directory.path("a.txt");
|
||||
let b = directory.path("b.txt");
|
||||
fs::write(&a, "alpha").unwrap();
|
||||
fs::write(&b, "beta").unwrap();
|
||||
let mut editor = Editor::open(Some(a), vec![]).unwrap();
|
||||
type_text(&mut editor, "iunsaved");
|
||||
editor.handle(Input::Escape);
|
||||
editor.execute(&format!("edit {}", b.display()));
|
||||
type_text(&mut editor, "iother");
|
||||
editor.handle(Input::Escape);
|
||||
type_text(&mut editor, "vl");
|
||||
let carets = editor.carets();
|
||||
let selections = editor.selections();
|
||||
let documents = editor
|
||||
.documents()
|
||||
.map(|document| (document.id, document.revision, document.lines.to_vec()))
|
||||
.collect::<Vec<_>>();
|
||||
editor.execute("diff");
|
||||
for input in [
|
||||
Input::Paste("bad".into()),
|
||||
Input::Delete,
|
||||
Input::Char('d'),
|
||||
Input::Enter,
|
||||
] {
|
||||
editor.handle(input);
|
||||
}
|
||||
editor.handle(Input::Escape);
|
||||
assert_eq!(editor.mode(), Mode::Visual);
|
||||
assert_eq!(editor.carets(), carets);
|
||||
assert_eq!(editor.selections(), selections);
|
||||
assert_eq!(
|
||||
editor
|
||||
.documents()
|
||||
.map(|document| (document.id, document.revision, document.lines.to_vec()))
|
||||
.collect::<Vec<_>>(),
|
||||
documents
|
||||
);
|
||||
editor.handle(Input::Escape);
|
||||
editor.handle(Input::Char('u'));
|
||||
assert_eq!(editor.context().text, "beta");
|
||||
editor.execute("buffer 1");
|
||||
editor.handle(Input::Char('u'));
|
||||
assert_eq!(editor.context().text, "alpha");
|
||||
|
||||
editor.handle(Input::Char('i'));
|
||||
type_text(&mut editor, "start");
|
||||
let caret = editor.cursor();
|
||||
editor.execute("diff");
|
||||
editor.handle(Input::Paste("ignored".into()));
|
||||
editor.handle(Input::Escape);
|
||||
assert_eq!(editor.mode(), Mode::Insert);
|
||||
assert_eq!(editor.cursor(), caret);
|
||||
type_text(&mut editor, "finish");
|
||||
editor.handle(Input::Escape);
|
||||
editor.handle(Input::Char('u'));
|
||||
assert_eq!(editor.context().text, "alpha");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn line_actions_counted_delete_restores_caret_and_pastes_whole_rows() {
|
||||
let mut editor = editor();
|
||||
|
||||
Reference in New Issue
Block a user