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
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
// (C) 2026 - Enzo Lombardi
//! Window-level editor contracts that mirror Borland's TEditor / TFileEditor
//! hierarchy.
//!
//! `View::valid(cmd)` already covers no-app close validation, but a save-on-close
//! prompt needs `&mut Application` to display a modal dialog. These traits add
//! `valid_close(app, cmd)` for that purpose; the IDE event loop calls it when an
//! editor's frame requests a close (CM_CLOSE).
//!
//! Hierarchy:
//! - [`Editor`] — basic editor window. Default `valid_close` always allows close.
//! - [`FileEditor`] — adds an optional file path, dirty state, and load/save.
//! Implementors typically delegate `valid_close` to [`confirm_save_on_close`].
use std::path::PathBuf;
use std::time::SystemTime;
use crate::app::Application;
use crate::core::command::CommandId;
use crate::views::view::View;
/// Result of probing the on-disk file behind a [`FileEditor`].
///
/// `last_known_mtime` is what the editor recorded the last time it touched
/// the file (load/save). [`FileEditor::poll_external_changes`] stats the path
/// and compares against that snapshot.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ExternalState {
/// No file path is bound yet, so there's nothing to compare.
NoFile,
/// File on disk matches the editor's last-known mtime.
Unchanged,
/// File on disk has a newer mtime than the editor's snapshot.
Modified,
/// File path is set but the file no longer exists on disk.
Deleted,
}
/// Window-level editor contract. The default `valid_close` allows the close to
/// proceed; override to abort it (e.g. after asking the user to save).
///
/// The clipboard / undo / selection methods carry no-op defaults so non-text
/// editors that implement this trait (e.g. read-only output panes) don't have
/// to bother. Real text editors override every method to delegate to the
/// underlying [`crate::views::editor::EditorWindow`]. The IDE event loop
/// dispatches `CM_UNDO` / `CM_CUT` / ... to whichever editor is focused via
/// these methods, so the Edit menu and the editor's own keyboard shortcuts
/// (Ctrl+Z, Ctrl+X, ...) end up calling the same code paths.
pub trait Editor: View {
/// Called by the event loop when this editor's frame requests a close.
/// Returns true if the window should be removed.
fn valid_close(&mut self, _app: &mut Application, _command: CommandId) -> bool {
true
}
/// Pop the most recent edit from the undo stack and revert it.
fn undo(&mut self) {}
/// Re-apply the most recent undone edit.
fn redo(&mut self) {}
/// True when the undo stack has at least one entry. Used by the IDE
/// to grey out the Undo menu item when there's nothing to revert.
fn can_undo(&self) -> bool {
false
}
/// True when the redo stack has at least one entry. Cleared by any
/// fresh edit, so this is only briefly true between an undo and the
/// next mutation.
fn can_redo(&self) -> bool {
false
}
/// Cut the current selection to the clipboard. Returns false when there
/// was nothing to cut (no selection or read-only buffer).
fn cut(&mut self) -> bool {
false
}
/// Copy the current selection to the clipboard. Returns false when there
/// was no selection.
fn copy(&mut self) -> bool {
false
}
/// Insert the clipboard contents at the cursor (replacing any active
/// selection). Returns false on read-only editors or when the clipboard
/// is empty.
fn paste(&mut self) -> bool {
false
}
/// Select the entire buffer.
fn select_all(&mut self) {}
/// Delete the current selection without copying it to the clipboard.
/// No-op when there's no selection.
fn clear_selection(&mut self) {}
/// True when there's a non-empty selection. Used by the IDE to grey out
/// Cut / Copy / Clear menu entries.
fn has_selection(&self) -> bool {
false
}
}
/// Editor that is backed by an on-disk file. The path may be `None` for a new
/// untitled buffer; in that case [`FileEditor::prompt_save_as`] is invoked
/// when the user chooses to save.
pub trait FileEditor: Editor {
/// Current file path. Cloned per call so implementations may store the path
/// behind interior mutability (e.g. `Rc<RefCell<Option<PathBuf>>>`).
fn file_path(&self) -> Option<PathBuf>;
fn set_file_path(&mut self, path: Option<PathBuf>);
/// True when in-memory contents differ from the on-disk file (or no file yet).
fn is_dirty(&self) -> bool;
fn save(&mut self) -> std::io::Result<()>;
fn save_as(&mut self, path: PathBuf) -> std::io::Result<()>;
fn load(&mut self, path: PathBuf) -> std::io::Result<()>;
/// Reset to an empty Untitled buffer (no file path, no breakpoints, clean).
fn new_buffer(&mut self);
/// mtime recorded the last time the editor read or wrote the file. `None`
/// when there's no file or the editor has never touched disk yet.
fn last_known_mtime(&self) -> Option<SystemTime>;
/// Probe the on-disk file and report whether it has changed since the last
/// load/save. Pure metadata (`stat`); does not read contents or modify the
/// editor's last-known mtime — call [`FileEditor::reload`] to do that.
fn poll_external_changes(&self) -> ExternalState;
/// Re-read the file from disk into the buffer and refresh the last-known
/// mtime. Caller is responsible for prompting on dirty buffers.
fn reload(&mut self) -> std::io::Result<()>;
/// Friendly name used in dialogs and titles. Defaults to the file's basename
/// or `"Untitled"`.
fn display_name(&self) -> String {
self.file_path()
.as_deref()
.and_then(|p| p.file_name())
.and_then(|n| n.to_str())
.map(|s| s.to_string())
.unwrap_or_else(|| "Untitled".to_string())
}
/// Show a Save As dialog and persist the buffer to the chosen path.
/// Returns true on a successful save, false on cancel or I/O error.
fn prompt_save_as(&mut self, app: &mut Application) -> bool;
}
/// Standard "do you want to save?" dialog used by [`FileEditor`] implementors
/// from their [`Editor::valid_close`] override. Mirrors Borland's
/// `TFileEditor::valid(cmClose)`: YES → save, NO → discard, CANCEL → abort close.
pub fn confirm_save_on_close<E: FileEditor + ?Sized>(
editor: &mut E,
app: &mut Application,
command: CommandId,
) -> bool {
use crate::core::command::{CM_CLOSE, CM_NO, CM_YES};
use crate::views::msgbox::confirmation_box;
if command != CM_CLOSE || !editor.is_dirty() {
return true;
}
let message = format!(
"{} has been modified.\n\nSave changes?",
editor.display_name()
);
match confirmation_box(app, &message) {
c if c == CM_YES => {
if editor.file_path().is_some() {
editor.save().is_ok()
} else {
editor.prompt_save_as(app)
}
}
c if c == CM_NO => true,
_ => false,
}
}