1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
//! Path-entry modal pushed atop [`super::DirtyConflictModal`] when the
//! user picks `[Save a copy]`. Reuses the shared [`SaveCopyState`] +
//! [`SaveCopyView`] path-entry widget (as [`super::SaveAsModal`] does)
//! but its post-save effect is "save buffer to the
//! chosen path, then reload the on-disk contents into the editor's
//! buffer" — the in-flight conflict resolution flow. Carries the
//! on-disk contents already read by the watcher worker so the
//! reload skips a disk re-read.
use std::any::Any;
use std::path::Path;
use crossterm::event::KeyEvent;
use ratatui::layout::Rect;
use ratatui::Frame;
use super::dirty_conflict::DirtyConflictModal;
use super::types::{Modal, ModalOutcome, ModalRenderCtx};
use crate::app::flash::MessageKind;
use crate::app::App;
use crate::ui::{SaveCopyResponse, SaveCopyState, SaveCopyView};
pub struct DirtyConflictSaveCopyModal {
state: SaveCopyState,
on_disk_contents: String,
}
impl DirtyConflictSaveCopyModal {
pub fn new(default_path: String, on_disk_contents: String) -> Self {
Self {
state: SaveCopyState::new(default_path),
on_disk_contents,
}
}
/// Replace the carried on-disk contents with the bytes from a
/// freshly-arrived external write. Called from
/// `App::handle_file_changed` when a new change is observed while
/// this modal is open, so the user's eventual `Save` confirmation
/// reloads against the *current* disk state rather than the stale
/// snapshot that originally opened the modal.
pub fn set_on_disk_contents(&mut self, contents: String) {
self.on_disk_contents = contents;
}
}
impl Modal for DirtyConflictSaveCopyModal {
fn render(&mut self, frame: &mut Frame<'_>, area: Rect, ctx: &ModalRenderCtx<'_>) {
let view = SaveCopyView {
theme: ctx.theme,
cursor_visible: ctx.cursor_visible,
title: "Save a Copy",
};
frame.render_stateful_widget(view, area, &mut self.state);
}
fn handle_key(
&mut self,
key: KeyEvent,
app: &mut App,
_doc_height: usize,
_doc_width: usize,
) -> ModalOutcome {
match self.state.handle_key(&key) {
SaveCopyResponse::Continue => ModalOutcome::Continue,
// Cancel returns to the underlying DirtyConflictModal —
// intact, so the user can pick a different action.
SaveCopyResponse::Cancelled => ModalOutcome::Close,
SaveCopyResponse::Save(path_str) => {
let path = Path::new(&path_str).to_owned();
match app.editor.buffer.save_copy(&path) {
Ok(()) => {
// After the copy is on disk, replace the
// in-memory buffer with the on-disk contents
// that triggered the conflict. The user
// explicitly chose "save my edits aside and
// load the disk version."
let contents = std::mem::take(&mut self.on_disk_contents);
let display = path_str.clone();
ModalOutcome::CloseAnd(Box::new(move |app| {
// Close the parent DirtyConflictModal
// underneath; the post-reload "Reloaded
// from disk" flash from
// `reload_buffer_from_disk` is the
// primary signal, so we deliberately do
// not double up with a "copy saved" toast
// — keep the chrome quiet. We do flash
// the path so the user can find the file
// they just wrote.
app.modal_stack.remove_first::<DirtyConflictModal>();
app.flash(format!("Buffer saved to {display}"), MessageKind::Success);
app.reload_buffer_from_disk(contents);
}))
}
Err(e) => {
// Stay open so the user can correct the
// path; show the validation error inline.
self.state.last_error = Some(format!("{e}"));
ModalOutcome::Continue
}
}
}
}
}
fn handle_click(&mut self, col: u16, row: u16, _app: &mut App) -> ModalOutcome {
super::types::close_if_esc_clicked(self.state.esc_button_rect, col, row)
}
fn as_any(&self) -> &dyn Any {
self
}
fn as_any_mut(&mut self) -> &mut dyn Any {
self
}
}