feat: add guarded and forceful editor restart actions

This commit is contained in:
2026-10-12 23:17:35 +02:00
parent 298d586c3e
commit d85998e1cb
6 changed files with 284 additions and 5 deletions
+99 -1
View File
@@ -736,6 +736,15 @@ core_actions![
false,
true
),
("core.restart", Action::Restart, &[Mode::Normal], true),
(
"core.force_restart",
Action::ForceRestart,
&[Mode::Normal],
true,
false,
true
),
(
"core.save_and_quit",
Action::SaveAndQuit,
@@ -788,6 +797,7 @@ const COMMAND_ALIASES: &[(&str, Action)] = &[
("w!", Action::ForceSave),
("q", Action::Quit),
("q!", Action::ForceQuit),
("restart!", Action::ForceRestart),
("wq", Action::SaveAndQuit),
("wq!", Action::ForceSaveAndQuit),
("bnext", Action::BufferNext),
@@ -1609,6 +1619,7 @@ pub struct Editor {
lsp_names: Vec<String>,
lsp_label: String,
quit: bool,
restart: bool,
capabilities: Vec<String>,
features: HashMap<String, Feature>,
bindings: HashMap<Mode, HashMap<Input, Binding>>,
@@ -1705,6 +1716,7 @@ impl Editor {
lsp_names: Vec::new(),
lsp_label: String::new(),
quit: false,
restart: false,
capabilities,
features: HashMap::new(),
bindings: default_bindings(),
@@ -3113,6 +3125,8 @@ impl Editor {
| Action::WordWrapToggle
| Action::Quit
| Action::ForceQuit
| Action::Restart
| Action::ForceRestart
| Action::Plugins
| Action::Buffers
)
@@ -4549,7 +4563,12 @@ impl Editor {
self.active.dirty()
}
pub fn should_quit(&self) -> bool {
self.quit
self.quit || self.restart
}
/// Whether frontend shutdown should relaunch the executable rather than exit.
pub fn should_restart(&self) -> bool {
self.restart
}
pub fn context(&self) -> Context {
@@ -5848,6 +5867,8 @@ impl Editor {
| Action::ForceSave
| Action::Quit
| Action::ForceQuit
| Action::Restart
| Action::ForceRestart
| Action::SaveAndQuit
| Action::ForceSaveAndQuit
| Action::Plugins
@@ -6523,6 +6544,42 @@ impl Editor {
self.active.dirty() || self.parked.values().any(Buffer::dirty)
}
fn restart(&mut self, force: bool) {
if force || !self.any_dirty() {
self.restart = true;
return;
}
let owners: Vec<_> = self
.diff_owner
.into_iter()
.chain(self.tabs.values().filter_map(|tab| tab.diff_owner))
.collect();
for owner in owners {
self.with_pane(owner, |editor| {
editor.execute_action(Action::DiffClose.entry(), "");
});
}
if !self.active.dirty() {
let dirty = self.buffers().find(|buffer| buffer.dirty).unwrap().id;
self.switch_buffer(dirty);
}
let clean: Vec<_> = self
.parked
.values()
.filter(|buffer| !buffer.dirty())
.map(|buffer| buffer.id)
.collect();
for id in clean {
if self.has_buffer(id) {
self.delete_buffer(id, false);
}
}
let count = self.buffers().filter(|buffer| buffer.dirty).count();
self.set_message(format!(
"Restart blocked: {count} buffer(s) have unsaved changes; clean buffers closed. Save them or use :restart!"
));
}
pub fn execute(&mut self, command: &str) -> Option<Invocation> {
self.pending_lsp_navigation = None;
self.finish_diff_prompt();
@@ -6748,6 +6805,12 @@ impl Editor {
}
Action::Quit if self.any_dirty() => self.set_message("Unsaved changes; use :w or :q!"),
Action::Quit | Action::ForceQuit => self.quit = true,
Action::Restart | Action::ForceRestart if !args.is_empty() => {
self.set_message("Restart commands take no arguments");
}
Action::Restart | Action::ForceRestart => {
self.restart(action == Action::ForceRestart);
}
Action::Plugins if !args.is_empty() => {
self.set_message("The plugins command takes no arguments")
}
@@ -9888,6 +9951,41 @@ mod tests {
assert_eq!(fs::read_to_string(path).unwrap(), "new disk");
}
#[test]
fn guarded_restart_closes_historical_views_without_losing_dirty_source_history() {
let directory = Directory::new();
let path = directory.path("file.txt");
fs::write(&path, "disk").unwrap();
let mut editor = Editor::open(Some(path.clone()), vec![]).unwrap();
editor
.apply(vec![Effect::Replace("unsaved".into())])
.unwrap();
let dirty = editor.buffer_id();
historical_diff(
&mut editor,
&directory.0,
vec!["before".into()],
vec!["after".into()],
);
editor.execute("tab.new");
let snapshot = editor.buffer_id();
assert!(editor.active.diff_snapshot.is_some());
editor.execute("restart");
assert!(!editor.should_restart());
assert!(!editor.has_buffer(snapshot));
assert_eq!(editor.buffer_id(), dirty);
assert_eq!(editor.lines(), ["unsaved"]);
assert!(editor.diff().is_none());
assert!(editor.tabs.values().all(|tab| tab.diff().is_none()));
editor.handle(Input::Char('u'));
assert_eq!(editor.lines(), ["disk"]);
assert!(!editor.dirty());
assert_eq!(fs::read_to_string(path).unwrap(), "disk");
editor.execute("restart");
assert!(editor.should_restart());
assert!(editor.should_quit());
}
#[test]
fn notification_escape_preserves_carets_then_ordinary_escape_collapses_them() {
let mut editor = editor();
+33 -1
View File
@@ -53,6 +53,10 @@ Save actions: core.save, core.force_save, core.save_and_quit, core.force_save_an
Buffers: :edit PATH, :buffers, :buffer N, :buffer_next, :buffer_previous, :buffer_delete [N], :buffer_force_delete [N]
Aliases: bnext, bprev, bdelete, bdelete!; arguments and safety rules are unchanged
:quit refuses any dirty buffer; both save-and-quit commands require all buffers clean
Restart: :restart closes clean buffers; unsaved buffers remain open and block restart with a warning
:force_restart / :restart! discards unsaved text without writing
Actions: core.restart, core.force_restart (no default binding)
Relaunches the executable with original CLI args/config/startup file; tabs/views reset
Tabs: :tab.new, :tab.previous, :tab.next, :tab.close, :tab.force_close
Tabs retain pane layouts and independent views; close protects exclusively displayed unsaved edits
Markdown links: :follow_link (primary caret; local files only)
@@ -134,7 +138,35 @@ fn run() -> Result<(), String> {
options.config.as_deref(),
)
.map_err(|error| format!("cannot open file: {error}"))?;
tui::run(&mut session).map_err(|error| format!("terminal error: {error}"))
tui::run(&mut session).map_err(|error| format!("terminal error: {error}"))?;
let restart = session.editor().should_restart();
drop(session);
if restart {
restart_editor()?;
}
Ok(())
}
fn restart_editor() -> Result<(), String> {
let executable = env::current_exe().map_err(|error| format!("Restart failed: {error}"))?;
let mut command = std::process::Command::new(executable);
command.args(env::args_os().skip(1));
#[cfg(unix)]
{
use std::os::unix::process::CommandExt;
Err(format!("Restart failed: {}", command.exec()))
}
#[cfg(not(unix))]
{
let status = command
.status()
.map_err(|error| format!("Restart failed: {error}"))?;
if status.success() {
Ok(())
} else {
Err(format!("Restarted editor exited with {status}"))
}
}
}
fn main() {