Files
ElyOffice/crates/ely-office/src/close.rs
T

255 lines
9.1 KiB
Rust

//! File actions, the system file panel, close and quit (FILE-03).
use std::path::PathBuf;
use ely_gpui_component::primitives::Severity;
use ely_ui::commands::{CloseTab, Open, Save, SaveAs};
use gpui::{App, AppContext as _, Context, PathPromptOptions, TaskExt as _, Window};
use crate::{Dialog, Engine, Host, Opening, Saving, Tabs, in_window, text};
/// What a finished close or save goes on to.
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum After {
Tab,
Window,
Quit,
}
pub trait Closing: Host {
fn on_open(&mut self, _: &Open, _: &mut Window, cx: &mut Context<Self>) {
self.pick(false, cx, |host, path, cx| host.open_file(path, cx));
}
fn on_save(&mut self, _: &Save, window: &mut Window, cx: &mut Context<Self>) {
match self.core().active {
Some(id) => self.save(id, None, window, cx),
None => self.no_document(cx),
}
}
fn on_save_as(&mut self, _: &SaveAs, _: &mut Window, cx: &mut Context<Self>) {
match self.core().active {
Some(id) => self.save_as(id, None, cx),
None => self.no_document(cx),
}
}
fn on_close_tab(&mut self, _: &CloseTab, window: &mut Window, cx: &mut Context<Self>) {
if let Some(id) = self.core().active {
self.request_close(id, After::Tab, window, cx);
}
}
/// Takes `window` as home; quit anywhere asks here first.
fn own_window(&mut self, window: &Window, cx: &mut Context<Self>) {
let handle = window.window_handle();
self.core_mut().window = Some(handle);
let host = cx.entity().downgrade();
ely_ui::commands::route_quit(cx, move |cx| {
let host = host.clone();
// The asking window is busy until its action ends.
cx.defer(move |cx| {
let asked = handle.update(cx, |_, window, cx| {
// The question shows in front, with the keys.
window.activate_window();
host.update(cx, |host, cx| {
host.quitting(cx);
host.proceed(After::Quit, window, cx);
})
});
match asked {
Ok(Ok(())) => {}
// No home window is left to ask in.
_ if handle.update(cx, |_, _, _| ()).is_err() => cx.quit(),
Ok(Err(error)) | Err(error) => log::error!("office: the quit was not asked: {error}"),
}
});
});
}
/// A close or quit stopped: no update installs on it.
fn close_stopped(&mut self, cx: &mut Context<Self>) {
ely_ui::updates::quit_stopped(cx);
self.close_cancelled(cx);
}
fn no_document(&self, cx: &mut Context<Self>) {
let text = text(cx, "command.blocked.no_document", &[]);
self.toast(text, Severity::Warning, cx);
}
/// A failed system panel is a failure, never a cancel.
fn panel_failed(&self, error: &str, cx: &mut App) {
let text = text(cx, "office.error.panel", &[("error", error)]);
self.toast(text, Severity::Danger, cx);
}
/// Asks for one file, or one folder when `folder`.
fn pick(
&mut self,
folder: bool,
cx: &mut Context<Self>,
then: impl FnOnce(&mut Self, PathBuf, &mut Context<Self>) + 'static,
) {
let picked = cx.prompt_for_paths(PathPromptOptions {
files: !folder,
directories: folder,
multiple: false,
prompt: None,
});
cx.spawn(async move |this, cx| {
let picked = match picked.await {
Ok(Ok(paths)) => Ok(paths.and_then(|paths| paths.into_iter().next())),
Ok(Err(error)) => Err(error.to_string()),
Err(error) => Err(error.to_string()),
};
in_window(&this, cx, |host, _, cx| match picked {
Ok(Some(path)) => then(host, path, cx),
Ok(None) => log::info!("office: the file panel was cancelled"),
Err(error) => host.panel_failed(&error, cx),
})
})
.detach_and_log_err(cx);
}
/// Closes `id`, asking first when it holds unsaved changes.
fn request_close(
&mut self,
id: u64,
after: After,
_window: &mut Window,
cx: &mut Context<Self>,
) {
log::info!("office: closing tab {id} asked, then {after:?}");
if !self.settled(id, cx) {
return self.when_settled(id, move |host, window, cx| {
host.request_close(id, after, window, cx)
});
}
let Some(tab) = self.tab(id) else {
return;
};
if tab.dirty(cx) {
return self.ask_unsaved(id, after, cx);
}
// The undo history decides; it is read in the background.
let edited = Self::Engine::edited(tab.view.read(cx));
self.read_in(
id,
Self::Engine::history,
move |host, read, window, cx| {
let Some(tab) = host.tab(id) else {
return;
};
if Self::Engine::edited(tab.view.read(cx)) != edited {
return host.request_close(id, after, window, cx);
}
let moved = match read {
Ok(history) => history != tab.mark,
Err(error) => {
log::error!("office: the undo history of tab {id} is unreadable: {error}");
true
}
};
if moved || tab.dirty(cx) {
return host.ask_unsaved(id, after, cx);
}
host.close_tab(id, cx);
host.proceed(after, window, cx);
},
cx,
);
}
fn ask_unsaved(&mut self, id: u64, after: After, cx: &mut Context<Self>) {
self.core_mut().dialog = Some(Dialog::Unsaved { tab: id, after });
cx.notify();
}
/// Window and quit close each tab, asking for dirty ones.
fn proceed(&mut self, after: After, window: &mut Window, cx: &mut Context<Self>) {
if after == After::Tab {
return;
}
if let Some(id) = self.core().tabs.first().map(|tab| tab.id) {
return self.request_close(id, after, window, cx);
}
match after {
After::Window => window.remove_window(),
After::Quit => cx.quit(),
After::Tab => {}
}
}
/// The close button: tabs close in turn, off the window.
fn should_close(&mut self, window: &mut Window, cx: &mut Context<Self>) -> bool {
if self.core().tabs.is_empty() {
return true;
}
// Each tab's history is read in the background.
self.proceed(After::Window, window, cx);
false
}
/// Releases the kernel document, registry entry and scratch folder.
fn close_tab(&mut self, id: u64, cx: &mut Context<Self>) {
let Some(doc) = self.release_tab(id, cx) else {
return;
};
// The kernel lets go in the background.
let worker = std::sync::Arc::clone(&self.core().worker);
cx.spawn(async move |this, cx| {
let closed = cx
.background_spawn(async move {
Self::Engine::close(&mut worker.lock().expect("the worker lock"), doc)
})
.await;
if let Err(error) = closed
&& this
.update(cx, |host, cx| host.engine_failed(&error.to_string(), cx))
.is_err()
{
log::error!("office: closing a document failed after its window: {error}");
}
})
.detach();
}
/// Drops `id` from the window; its kernel document is returned.
fn release_tab(&mut self, id: u64, cx: &mut Context<Self>) -> Option<crate::DocOf<Self>> {
let core = self.core_mut();
let at = core.tabs.iter().position(|tab| tab.id == id)?;
let tab = core.tabs.remove(at);
self.tab_closed(&tab, cx);
// Its images leave GPUI's atlas next frame (PERF-08).
let mut images = Self::Engine::retire(&tab.view, cx);
cx.spawn(async move |this, cx| {
let freed = crate::in_window(&this, cx, |_, window, _| {
window.on_next_frame(move |window, _| images.release_all(window));
});
if freed.is_err() {
log::info!("office: the window went, and its images with it");
}
})
.detach();
crate::Recovering::forget_tab(self, id, cx);
let core = self.core_mut();
core.waiting.retain(|(waiting, _)| *waiting != id);
if let Some(registry) = tab.registry {
core.sessions.close(registry);
}
if core.active == Some(id) {
core.active = core.tabs.last().map(|tab| tab.id);
}
if let Err(error) = std::fs::remove_dir_all(&tab.work) {
log::warn!("office: scratch {} stays: {error}", tab.work.display());
}
log::info!("office: closed {}", tab.name);
cx.notify();
Some(tab.doc)
}
}
impl<H: Host> Closing for H {}