Fixes for M-02 to M-14, G-01 to G-14, K-02 to K-04, X-04 to X-14, not yet reviewed
Committed before review so Windows and WSL builds can start. Each fix was tested and mutation-checked here; the codex review of each item is still to come, so tasks/BUGS.md stays unticked. M-02 is mid-review: round 1 findings are fixed (apps/ely-mark/src/tally.rs). K-01 stays deferred. The ElyMark window test mark-outside.sh failed once at its crash-recovery step in the last run; not yet looked into.
This commit is contained in:
75 files changed
+1482
-354
No files matched your search
@@ -265,6 +265,11 @@ pub fn print_later<H: Tabs>(pdf: std::path::PathBuf, cx: &mut Context<H>) {
|
||||
.detach();
|
||||
}
|
||||
|
||||
/// Addresses a click may open outside: web and mail.
|
||||
pub fn outward(uri: &str) -> bool {
|
||||
["https://", "http://", "mailto:"].iter().any(|scheme| uri.starts_with(scheme))
|
||||
}
|
||||
|
||||
/// A localized message.
|
||||
pub fn text(cx: &App, key: &str, args: &[(&str, &str)]) -> SharedString {
|
||||
ely_ui::i18n::text(cx, key, args)
|
||||
|
||||
@@ -238,13 +238,18 @@ pub trait Opening: Host {
|
||||
|
||||
/// Engine errors become toasts; kernel edits mark the session.
|
||||
fn watch(&mut self, id: u64, view: &Entity<View<Self>>, cx: &mut Context<Self>) {
|
||||
let seen = std::cell::Cell::new(Self::Engine::edited(view.read(cx)));
|
||||
let reported = move |host: &mut Self,
|
||||
view: &Entity<View<Self>>,
|
||||
modified: bool,
|
||||
cx: &mut Context<Self>| {
|
||||
let edited = Self::Engine::edited(view.read(cx));
|
||||
let moved = seen.replace(edited) != edited;
|
||||
if let Some(tab) = host.tab_mut(id) {
|
||||
match (modified, tab.modified) {
|
||||
(true, false) => tab.session.edit(),
|
||||
// An edit landing mid-save outdates its ticket.
|
||||
(true, true) if moved => tab.session.edit(),
|
||||
(false, true) => tab.session.reverted(),
|
||||
_ => {}
|
||||
}
|
||||
|
||||
@@ -225,7 +225,7 @@ pub fn size_menu<P: Panel>(panel: &P, id: &'static str, cx: &Context<P>) -> AnyE
|
||||
if let Some(size) = size.clone().filter(|size| !sizes.contains(size)) {
|
||||
sizes.push(size);
|
||||
}
|
||||
sizes.sort_by(|a, b| points(a).total_cmp(&points(b)));
|
||||
let sizes = ordered(sizes);
|
||||
let mut menu = Select::new(
|
||||
id,
|
||||
sizes
|
||||
@@ -279,7 +279,28 @@ pub fn known(state: &str) -> bool {
|
||||
!state.is_empty() && state != ely_lok::MIXED
|
||||
}
|
||||
|
||||
/// A size label's points, for ordering.
|
||||
fn points(size: &str) -> f64 {
|
||||
size.parse().expect("sizes are numbers")
|
||||
/// Sizes by points; one the kernel words oddly shows last.
|
||||
fn ordered(sizes: Vec<SharedString>) -> Vec<SharedString> {
|
||||
let (mut sized, mut odd) = (Vec::new(), Vec::new());
|
||||
for size in sizes {
|
||||
match size.parse::<f64>() {
|
||||
Ok(points) => sized.push((points, size)),
|
||||
Err(_) => odd.push(size),
|
||||
}
|
||||
}
|
||||
sized.sort_by(|a, b| a.0.total_cmp(&b.0));
|
||||
let mut sizes: Vec<SharedString> = sized.into_iter().map(|(_, size)| size).collect();
|
||||
sizes.append(&mut odd);
|
||||
sizes
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#[test]
|
||||
fn an_odd_size_shows_last_and_panics_nothing() {
|
||||
let sizes = ["12", "disabled", "9", "10.5"]
|
||||
.map(gpui::SharedString::from)
|
||||
.to_vec();
|
||||
assert_eq!(super::ordered(sizes), ["9", "10.5", "12", "disabled"]);
|
||||
}
|
||||
}
|
||||
@@ -9,16 +9,35 @@ use std::{
|
||||
|
||||
use ely_files::{Crash, Marker, Recovery, RecoveryKey};
|
||||
use ely_gpui_component::primitives::Severity;
|
||||
use gpui::{AsyncApp, Context, WeakEntity};
|
||||
use gpui::{AppContext as _, AsyncApp, Context, WeakEntity};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{Engine, Host, Opening as _, Source, Tabs as _, text};
|
||||
use crate::{Engine, Host, Tabs as _, text};
|
||||
|
||||
mod reopen;
|
||||
|
||||
/// A tab's points, and the edit the last holds.
|
||||
struct Slot {
|
||||
kept: Kept,
|
||||
written: Option<u64>,
|
||||
writing: bool,
|
||||
/// The last failure, shown once until it changes.
|
||||
failed: Option<String>,
|
||||
}
|
||||
|
||||
impl Slot {
|
||||
/// Notes a failed point; words to show, once each.
|
||||
fn fail(&mut self, edited: u64, error: &io::Error) -> Option<String> {
|
||||
// Too big for the cap: wait for the next edit.
|
||||
if error.kind() == io::ErrorKind::InvalidInput {
|
||||
self.written = Some(edited);
|
||||
}
|
||||
let said = error.to_string();
|
||||
(self.failed.as_ref() != Some(&said)).then(|| {
|
||||
self.failed = Some(said.clone());
|
||||
said
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// What reopening a slot needs, kept beside its points.
|
||||
@@ -47,6 +66,9 @@ pub struct Keeping {
|
||||
pub(crate) last: Option<SystemTime>,
|
||||
/// The index on disk is behind; the failure shown.
|
||||
behind: Option<String>,
|
||||
/// An index write runs; another follows it if asked.
|
||||
flushing: bool,
|
||||
flush_again: bool,
|
||||
}
|
||||
|
||||
impl Keeping {
|
||||
@@ -70,7 +92,7 @@ impl Keeping {
|
||||
};
|
||||
let crash = Crash::open(&dir, std::process::id())?;
|
||||
let (reopening, held) = (Vec::new(), Vec::new());
|
||||
Ok(Self { dir, crash: Some(crash), slots: HashMap::new(), crashed, reopening, held, last: None, behind: None })
|
||||
Ok(Self { dir, crash: Some(crash), slots: HashMap::new(), crashed, reopening, held, last: None, behind: None, flushing: false, flush_again: false })
|
||||
}
|
||||
|
||||
/// A clean quit: the next start finds nothing to recover.
|
||||
@@ -87,17 +109,27 @@ impl Keeping {
|
||||
}
|
||||
|
||||
fn save_index(&self) -> io::Result<()> {
|
||||
let (path, bytes) = self.index()?;
|
||||
write_index(&path, &bytes)
|
||||
}
|
||||
|
||||
/// The index file and what it should hold now.
|
||||
fn index(&self) -> io::Result<(PathBuf, Vec<u8>)> {
|
||||
let waiting = self.reopening.iter().map(|(_, _, kept)| kept).chain(&self.held);
|
||||
let waiting = waiting.map(|kept| Kept { held: true, ..kept.clone() });
|
||||
let slots = self.slots.values().map(|slot| slot.kept.clone());
|
||||
let kept: Vec<Kept> = slots.chain(self.crashed.iter().cloned()).chain(waiting).collect();
|
||||
let path = self.dir.join("recovery").join("index.json");
|
||||
std::fs::create_dir_all(self.dir.join("recovery"))?;
|
||||
// Staged then renamed: a failed write keeps the last index.
|
||||
ely_files::save_atomic(&path, &serde_json::to_vec(&kept).map_err(io::Error::other)?, 0).map(|_| ())
|
||||
let bytes = serde_json::to_vec(&kept).map_err(io::Error::other)?;
|
||||
Ok((self.dir.join("recovery").join("index.json"), bytes))
|
||||
}
|
||||
}
|
||||
|
||||
/// Staged then renamed: a failed write keeps the last index.
|
||||
fn write_index(path: &Path, bytes: &[u8]) -> io::Result<()> {
|
||||
std::fs::create_dir_all(path.parent().expect("the index has a folder"))?;
|
||||
ely_files::save_atomic(path, bytes, 0).map(|_| ())
|
||||
}
|
||||
|
||||
fn read_index(dir: &Path) -> io::Result<Vec<Kept>> {
|
||||
match std::fs::read(dir.join("recovery").join("index.json")) {
|
||||
Ok(bytes) => serde_json::from_slice(&bytes).map_err(|error| io::Error::new(io::ErrorKind::InvalidData, error)),
|
||||
@@ -128,6 +160,9 @@ fn remove(path: &Path) -> io::Result<()> {
|
||||
}
|
||||
}
|
||||
|
||||
/// A slot taken from a released tab, waiting to reopen.
|
||||
pub struct Taken(Kept, String);
|
||||
|
||||
/// When recovery points are written and crashes come back.
|
||||
pub trait Recovering: Host {
|
||||
/// From the app's window, never its tests.
|
||||
@@ -140,8 +175,17 @@ pub trait Recovering: Host {
|
||||
}
|
||||
}
|
||||
Self::keep_points_every(cx);
|
||||
cx.on_app_quit(|host, _| {
|
||||
if let Some(keeping) = host.core_mut().keeping.as_mut() {
|
||||
cx.on_app_quit(|host, cx| {
|
||||
// A write still in flight must not be the last.
|
||||
if let Some(keeping) = host.core().keeping.as_ref()
|
||||
&& let Err(error) = keeping.save_index()
|
||||
{
|
||||
log::error!("recovery: the index is not saved at quit: {error}");
|
||||
}
|
||||
// A system end skips asking: unsaved tabs keep points.
|
||||
if host.core().tabs.iter().any(|tab| tab.dirty(cx)) {
|
||||
log::warn!("recovery: quit with unsaved tabs; points stay");
|
||||
} else if let Some(keeping) = host.core_mut().keeping.as_mut() {
|
||||
keeping.end();
|
||||
}
|
||||
async {}
|
||||
@@ -219,6 +263,7 @@ pub trait Recovering: Host {
|
||||
match written {
|
||||
Ok(()) => {
|
||||
slot.written = Some(edited);
|
||||
slot.failed = None;
|
||||
keeping.last = Some(SystemTime::now());
|
||||
if keeping.behind.is_some() {
|
||||
host.flush_index(cx);
|
||||
@@ -226,7 +271,8 @@ pub trait Recovering: Host {
|
||||
}
|
||||
Err(error) => {
|
||||
log::error!("recovery: tab {id} point failed ({:?})", error.kind());
|
||||
let shown = text(cx, "office.recovery.failed", &[("error", &error.to_string())]);
|
||||
let Some(said) = slot.fail(edited, &error) else { return };
|
||||
let shown = text(cx, "office.recovery.failed", &[("error", &said)]);
|
||||
host.toast_unlogged(shown, Severity::Warning, cx);
|
||||
}
|
||||
}
|
||||
@@ -236,13 +282,40 @@ pub trait Recovering: Host {
|
||||
}
|
||||
}
|
||||
|
||||
/// Writes the index; a failure shows once and retries.
|
||||
/// Writes the index in the background, one at a time.
|
||||
fn flush_index(&mut self, cx: &mut Context<Self>) {
|
||||
let Some(keeping) = self.core_mut().keeping.as_mut() else { return };
|
||||
let error = match keeping.save_index() {
|
||||
Ok(()) => return keeping.behind = None,
|
||||
Err(error) => error.to_string(),
|
||||
if keeping.flushing {
|
||||
return keeping.flush_again = true;
|
||||
}
|
||||
let (path, bytes) = match keeping.index() {
|
||||
Ok(index) => index,
|
||||
Err(error) => return self.index_failed(error.to_string(), cx),
|
||||
};
|
||||
keeping.flushing = true;
|
||||
let task = cx.background_spawn(async move { write_index(&path, &bytes) });
|
||||
cx.spawn(async move |this: WeakEntity<Self>, cx: &mut AsyncApp| {
|
||||
let written = task.await;
|
||||
this.update(cx, |host, cx| {
|
||||
let Some(keeping) = host.core_mut().keeping.as_mut() else { return };
|
||||
keeping.flushing = false;
|
||||
match written {
|
||||
Ok(()) => keeping.behind = None,
|
||||
Err(error) => host.index_failed(error.to_string(), cx),
|
||||
}
|
||||
let again = host.core_mut().keeping.as_mut().is_some_and(|keeping| std::mem::take(&mut keeping.flush_again));
|
||||
if again {
|
||||
host.flush_index(cx);
|
||||
}
|
||||
})
|
||||
.ok();
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
/// An index failure shows once and retries.
|
||||
fn index_failed(&mut self, error: String, cx: &mut Context<Self>) {
|
||||
let Some(keeping) = self.core_mut().keeping.as_mut() else { return };
|
||||
if keeping.behind.as_ref() == Some(&error) {
|
||||
return;
|
||||
}
|
||||
@@ -261,7 +334,7 @@ pub trait Recovering: Host {
|
||||
let Some(keeping) = self.core_mut().keeping.as_mut() else { return };
|
||||
let slot = format!("{}-{id}", std::process::id());
|
||||
let kept = Kept { slot, name: name.to_string(), format, sealed, held: false };
|
||||
keeping.slots.insert(id, Slot { kept, written: None, writing: false });
|
||||
keeping.slots.insert(id, Slot { kept, written: None, writing: false, failed: None });
|
||||
self.flush_index(cx);
|
||||
}
|
||||
|
||||
@@ -275,60 +348,50 @@ pub trait Recovering: Host {
|
||||
self.flush_index(cx);
|
||||
}
|
||||
|
||||
/// Opens each crashed slot's newest point as unsaved copy.
|
||||
/// Reopens each crashed slot's newest point as an unsaved copy.
|
||||
fn reopen_crashed(&mut self, cx: &mut Context<Self>) {
|
||||
let Some(keeping) = self.core_mut().keeping.as_mut() else { return };
|
||||
let crashed = std::mem::take(&mut keeping.crashed);
|
||||
let crashed: Vec<(Kept, String)> = std::mem::take(&mut keeping.crashed).into_iter().map(|kept| { let name = kept.name.clone(); (kept, name) }).collect();
|
||||
if crashed.is_empty() {
|
||||
return;
|
||||
}
|
||||
for kept in crashed {
|
||||
let folder = self.core().keeping.as_ref().expect("keeping").folder(&kept.slot);
|
||||
match restore(&kept, &folder, &self.core().work, self.core().next, false) {
|
||||
Ok(Some((copy, at))) => {
|
||||
log::info!("recovery: reopening a crashed {}", kept.format);
|
||||
self.core_mut().keeping.as_mut().expect("keeping").reopening.push((copy.clone(), at, kept));
|
||||
self.load(Source::Copy(copy), cx);
|
||||
}
|
||||
Ok(None) => {
|
||||
if let Err(error) = remove(&folder) {
|
||||
log::error!("recovery: an empty slot stays: {error}");
|
||||
}
|
||||
}
|
||||
Err(error) => {
|
||||
reopen::restore_off(self, crashed, false, |host, done, cx| {
|
||||
for (name, restored) in done {
|
||||
if let Err(error) = restored {
|
||||
log::error!("recovery: a crashed copy cannot open ({:?})", error.kind());
|
||||
let shown = text(cx, "office.recovery.lost", &[("name", &kept.name), ("error", &error.to_string())]);
|
||||
self.toast_unlogged(shown, Severity::Danger, cx);
|
||||
// Its points stay for a later start.
|
||||
self.core_mut().keeping.as_mut().expect("keeping").held.push(kept);
|
||||
let shown = text(cx, "office.recovery.lost", &[("name", &name), ("error", &error.to_string())]);
|
||||
host.toast_unlogged(shown, Severity::Danger, cx);
|
||||
}
|
||||
}
|
||||
}
|
||||
self.flush_index(cx);
|
||||
}, cx);
|
||||
}
|
||||
|
||||
/// Opens `id`'s newest point as a copy; false if none.
|
||||
fn reopen_point(&mut self, id: u64, cx: &mut Context<Self>) -> io::Result<bool> {
|
||||
let (work, next) = (self.core().work.clone(), self.core().next);
|
||||
let Some(keeping) = self.core_mut().keeping.as_mut() else { return Ok(false) };
|
||||
// Out of the tab's hands: releasing it keeps these points.
|
||||
let Some(slot) = keeping.slots.remove(&id) else { return Ok(false) };
|
||||
let restored = restore(&slot.kept, &keeping.folder(&slot.kept.slot), &work, next, true);
|
||||
match restored {
|
||||
Ok(Some((copy, at))) => {
|
||||
keeping.reopening.push((copy.clone(), at, slot.kept));
|
||||
self.load(Source::Copy(copy), cx);
|
||||
Ok(true)
|
||||
/// Takes tab `id`'s slot; releasing it keeps the points.
|
||||
fn take_slot(&mut self, id: u64, name: String) -> Option<Taken> {
|
||||
let slot = self.core_mut().keeping.as_mut()?.slots.remove(&id)?;
|
||||
Some(Taken(slot.kept, name))
|
||||
}
|
||||
|
||||
/// Reopens taken slots as copies; names any lost.
|
||||
fn reopen_taken(&mut self, taken: Vec<Taken>, mut lost: Vec<String>, cx: &mut Context<Self>) {
|
||||
let taken = taken.into_iter().map(|Taken(kept, name)| (kept, name)).collect();
|
||||
reopen::restore_off(self, taken, true, move |host, done, cx| {
|
||||
for (name, restored) in done {
|
||||
match restored {
|
||||
Ok(true) => {}
|
||||
Ok(false) => lost.push(name),
|
||||
Err(error) => {
|
||||
log::error!("office: a point does not open ({:?})", error.kind());
|
||||
lost.push(format!("{name} ({error})"));
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(None) => {
|
||||
keeping.slots.insert(id, slot);
|
||||
Ok(false)
|
||||
log::info!("{}: the engine is back, {} tab(s) lost", Self::KIND.app.slug(), lost.len());
|
||||
if !lost.is_empty() {
|
||||
let shown = text(cx, "office.stopped.lost", &[("names", &lost.join(", "))]);
|
||||
host.toast_unlogged(shown, Severity::Danger, cx);
|
||||
}
|
||||
Err(error) => {
|
||||
keeping.held.push(slot.kept);
|
||||
Err(error)
|
||||
}
|
||||
}
|
||||
}, cx);
|
||||
}
|
||||
|
||||
/// A reopened copy takes over its old points, unsaved.
|
||||
@@ -355,28 +418,6 @@ pub trait Recovering: Host {
|
||||
|
||||
impl<H: Host> Recovering for H {}
|
||||
|
||||
/// The newest point copied to the work folder, and when.
|
||||
fn restore(kept: &Kept, folder: &Path, work: &Path, next: u64, this_run: bool) -> io::Result<Option<(PathBuf, SystemTime)>> {
|
||||
let recovery = match kept.sealed {
|
||||
true => {
|
||||
let key = RecoveryKey::load();
|
||||
// This run's points open with this run's key.
|
||||
if !key.lasting && !this_run {
|
||||
return Err(io::Error::new(io::ErrorKind::PermissionDenied, "the recovery key is not in the key store"));
|
||||
}
|
||||
Recovery::sensitive(folder.to_path_buf(), key.key)
|
||||
}
|
||||
false => Recovery::new(folder.to_path_buf()),
|
||||
};
|
||||
let Some(point) = recovery.points()?.into_iter().next() else {
|
||||
return Ok(None);
|
||||
};
|
||||
let dir = crate::files::work_dir(work, next)?;
|
||||
let copy = dir.join(&kept.name).with_extension(&kept.format);
|
||||
recovery.restore(&point, ©)?;
|
||||
Ok(Some((copy, point.at)))
|
||||
}
|
||||
|
||||
/// A recovery time as this machine's clock reads it.
|
||||
pub(crate) fn clock(at: SystemTime) -> String {
|
||||
match jiff::Timestamp::try_from(at) {
|
||||
@@ -387,7 +428,18 @@ pub(crate) fn clock(at: SystemTime) -> String {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{Keeping, Kept};
|
||||
use super::{Keeping, Kept, Slot};
|
||||
|
||||
#[test]
|
||||
fn a_failing_point_says_so_once_until_the_reason_changes() {
|
||||
let mut slot = Slot { kept: kept("s", false), written: None, writing: false, failed: None };
|
||||
let full = std::io::Error::other("disk full");
|
||||
assert_eq!(slot.fail(1, &full).as_deref(), Some("disk full"));
|
||||
assert_eq!(slot.fail(2, &full), None, "the same reason stays quiet");
|
||||
let big = std::io::Error::new(std::io::ErrorKind::InvalidInput, "over the cap");
|
||||
assert!(slot.fail(3, &big).is_some(), "a new reason shows");
|
||||
assert_eq!(slot.written, Some(3), "too big waits for the next edit");
|
||||
}
|
||||
|
||||
fn kept(slot: &str, held: bool) -> Kept {
|
||||
Kept { slot: slot.into(), name: "a.docx".into(), format: "docx".into(), sealed: false, held }
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
//! Points restored off the UI thread, then opened (§6.3).
|
||||
|
||||
use std::{
|
||||
io,
|
||||
path::{Path, PathBuf},
|
||||
time::SystemTime,
|
||||
};
|
||||
|
||||
use ely_files::{Recovery, RecoveryKey};
|
||||
use gpui::{AppContext as _, AsyncApp, Context, WeakEntity};
|
||||
|
||||
use super::{Kept, Recovering, remove};
|
||||
use crate::{Opening as _, Source};
|
||||
|
||||
/// Restores each slot's newest point, then opens the copy.
|
||||
pub(super) fn restore_off<H: Recovering>(
|
||||
host: &mut H,
|
||||
slots: Vec<(Kept, String)>,
|
||||
this_run: bool,
|
||||
then: impl FnOnce(&mut H, Vec<(String, io::Result<bool>)>, &mut Context<H>) + 'static,
|
||||
cx: &mut Context<H>,
|
||||
) {
|
||||
let Some(keeping) = host.core_mut().keeping.as_mut() else { return };
|
||||
// Held meanwhile: a crash now still finds them.
|
||||
keeping.held.extend(slots.iter().map(|(kept, _)| kept.clone()));
|
||||
let jobs: Vec<(u64, PathBuf, Kept, String)> = slots
|
||||
.into_iter()
|
||||
.map(|(kept, name)| {
|
||||
let folder = host.core().keeping.as_ref().expect("keeping").folder(&kept.slot);
|
||||
let id = host.core().next;
|
||||
// Each copy's folder is its future tab's.
|
||||
host.core_mut().next += 1;
|
||||
(id, folder, kept, name)
|
||||
})
|
||||
.collect();
|
||||
host.flush_index(cx);
|
||||
let work = host.core().work.clone();
|
||||
let task = cx.background_spawn(async move {
|
||||
jobs.into_iter()
|
||||
.map(|(id, folder, kept, name)| {
|
||||
let restored = restore(&kept, &folder, &work, id, this_run);
|
||||
// An empty slot has nothing to keep.
|
||||
let restored = match restored {
|
||||
Ok(None) => remove(&folder).map(|()| None),
|
||||
other => other,
|
||||
};
|
||||
(id, kept, name, restored)
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
});
|
||||
cx.spawn(async move |this: WeakEntity<H>, cx: &mut AsyncApp| {
|
||||
let done = task.await;
|
||||
this.update(cx, |host, cx| {
|
||||
let Some(keeping) = host.core_mut().keeping.as_mut() else { return };
|
||||
keeping.held.retain(|held| !done.iter().any(|(_, kept, ..)| kept.slot == held.slot));
|
||||
let mut said = Vec::new();
|
||||
for (id, kept, name, restored) in done {
|
||||
let keeping = host.core_mut().keeping.as_mut().expect("keeping");
|
||||
let opened = match restored {
|
||||
Ok(Some((copy, at))) => {
|
||||
log::info!("recovery: reopening a {}", kept.format);
|
||||
keeping.reopening.push((copy.clone(), at, kept));
|
||||
host.load_into(id, Source::Copy(copy), cx);
|
||||
Ok(true)
|
||||
}
|
||||
Ok(None) => Ok(false),
|
||||
Err(error) => {
|
||||
// Its points stay for a later start.
|
||||
keeping.held.push(kept);
|
||||
Err(error)
|
||||
}
|
||||
};
|
||||
said.push((name, opened));
|
||||
}
|
||||
host.flush_index(cx);
|
||||
then(host, said, cx);
|
||||
})
|
||||
.ok();
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
/// The newest point copied to the work folder, and when.
|
||||
fn restore(kept: &Kept, folder: &Path, work: &Path, next: u64, this_run: bool) -> io::Result<Option<(PathBuf, SystemTime)>> {
|
||||
let recovery = match kept.sealed {
|
||||
true => {
|
||||
let key = RecoveryKey::load();
|
||||
// This run's points open with this run's key.
|
||||
if !key.lasting && !this_run {
|
||||
return Err(io::Error::new(io::ErrorKind::PermissionDenied, "the recovery key is not in the key store"));
|
||||
}
|
||||
Recovery::sensitive(folder.to_path_buf(), key.key)
|
||||
}
|
||||
false => Recovery::new(folder.to_path_buf()),
|
||||
};
|
||||
let Some(point) = recovery.points()?.into_iter().next() else {
|
||||
return Ok(None);
|
||||
};
|
||||
let dir = crate::files::work_dir(work, next)?;
|
||||
let copy = dir.join(&kept.name).with_extension(&kept.format);
|
||||
recovery.restore(&point, ©)?;
|
||||
Ok(Some((copy, point.at)))
|
||||
}
|
||||
|
||||
@@ -30,16 +30,27 @@ fn identity(path: &Path) -> io::Result<PathBuf> {
|
||||
pub trait Saving: Host {
|
||||
/// The tab whose source file is `target`.
|
||||
fn holder(&self, target: &Path) -> io::Result<Option<u64>> {
|
||||
// Target errors stand; a tab's gone volume does not.
|
||||
let target = identity(target)?;
|
||||
for tab in &self.core().tabs {
|
||||
if let Some(open) = tab.session.path()
|
||||
&& ely_files::same_file(open, target)?
|
||||
{
|
||||
return Ok(Some(tab.id));
|
||||
let Some(open) = tab.session.path() else { continue };
|
||||
match ely_files::same_file(open, &target) {
|
||||
Ok(true) => return Ok(Some(tab.id)),
|
||||
Ok(false) => {}
|
||||
Err(error) => log::warn!("office: tab {} has no file to compare: {error}", tab.id),
|
||||
}
|
||||
}
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
/// A save as refused unwritten; a waiting close stops.
|
||||
fn refuse_save(&mut self, then: &Option<After>, words: gpui::SharedString, cx: &mut Context<Self>) {
|
||||
self.toast(words, Severity::Danger, cx);
|
||||
if then.is_some() {
|
||||
self.close_stopped(cx);
|
||||
}
|
||||
}
|
||||
|
||||
/// Saves in place in its own format.
|
||||
fn save(&mut self, id: u64, then: Option<After>, window: &mut Window, cx: &mut Context<Self>) {
|
||||
if !self.settled(id, cx) {
|
||||
@@ -216,25 +227,29 @@ pub trait Saving: Host {
|
||||
host.write_as(id, path, then, window, cx)
|
||||
});
|
||||
}
|
||||
let name = self.tab(id).map(|tab| tab.name.clone()).unwrap_or_default();
|
||||
let unsaved = |error: io::Error, cx: &mut Context<Self>| {
|
||||
text(cx, "office.error.save", &[("name", &name), ("error", &error.to_string())])
|
||||
};
|
||||
let target = match identity(&path) {
|
||||
Ok(target) => target,
|
||||
Err(error) => return self.report(id, Err(SaveError::Io(error)), cx),
|
||||
Err(error) => return self.refuse_save(&then, unsaved(error, cx), cx),
|
||||
};
|
||||
let Some(format) = files::save_as_format(&Self::KIND, &target) else {
|
||||
let formats = Self::KIND.save_as.join(", .");
|
||||
let text = text(cx, "office.error.format", &[("formats", &formats)]);
|
||||
return self.toast(text, Severity::Danger, cx);
|
||||
return self.refuse_save(&then, text, cx);
|
||||
};
|
||||
let Some(tab) = self.tab(id) else {
|
||||
return log::info!("office: tab {id} closed before its save");
|
||||
};
|
||||
let holder = match self.holder(&target) {
|
||||
Ok(holder) => holder,
|
||||
Err(error) => return self.report(id, Err(SaveError::Io(error)), cx),
|
||||
Err(error) => return self.refuse_save(&then, unsaved(error, cx), cx),
|
||||
};
|
||||
if holder.is_some_and(|other| other != id) {
|
||||
let text = text(cx, "office.error.taken", &[("name", &file_name(&target))]);
|
||||
return self.toast(text, Severity::Danger, cx);
|
||||
return self.refuse_save(&then, text, cx);
|
||||
}
|
||||
let own_format = tab
|
||||
.session
|
||||
@@ -242,7 +257,7 @@ pub trait Saving: Host {
|
||||
.and_then(|open| files::kept_format(&Self::KIND, open).ok());
|
||||
if holder == Some(id) && (tab.risk.is_some() || own_format != Some(format)) {
|
||||
let text = text(cx, "office.error.convert", &[]);
|
||||
return self.toast(text, Severity::Danger, cx);
|
||||
return self.refuse_save(&then, text, cx);
|
||||
}
|
||||
// The written file differs from the open one; reopen it.
|
||||
let reopen =
|
||||
|
||||
@@ -5,7 +5,7 @@ use std::{path::PathBuf, sync::Arc};
|
||||
use ely_gpui_component::{feedback::Banner, primitives::Severity};
|
||||
use gpui::{AppContext as _, Context};
|
||||
|
||||
use crate::{Closing as _, Dialog, Engine, Host, Opening as _, Recovering as _, Tabs as _, recover::clock, text};
|
||||
use crate::{Closing as _, Dialog, Engine, Host, Opening as _, Recovering as _, recover::clock, text};
|
||||
|
||||
/// Why the engine ended, and whether it is coming back.
|
||||
pub struct Stopped {
|
||||
@@ -93,7 +93,7 @@ pub trait Stopping: Host {
|
||||
.iter()
|
||||
.map(|tab| (tab.id, tab.name.to_string(), tab.session.path().map(PathBuf::from), tab.dirty(cx)))
|
||||
.collect();
|
||||
let mut lost = Vec::new();
|
||||
let (mut taken, mut lost) = (Vec::new(), Vec::new());
|
||||
for (id, name, path, dirty) in tabs {
|
||||
if !dirty {
|
||||
self.release_tab(id, cx);
|
||||
@@ -102,13 +102,9 @@ pub trait Stopping: Host {
|
||||
}
|
||||
continue;
|
||||
}
|
||||
match self.reopen_point(id, cx) {
|
||||
Ok(true) => {}
|
||||
Ok(false) => lost.push(name),
|
||||
Err(error) => {
|
||||
log::error!("office: tab {id} point does not open ({:?})", error.kind());
|
||||
lost.push(format!("{name} ({error})"));
|
||||
}
|
||||
match self.take_slot(id, name.clone()) {
|
||||
Some(slot) => taken.push(slot),
|
||||
None => lost.push(name),
|
||||
}
|
||||
self.release_tab(id, cx);
|
||||
}
|
||||
@@ -116,11 +112,7 @@ pub trait Stopping: Host {
|
||||
if matches!(self.core().dialog, Some(Dialog::Unsaved { .. } | Dialog::Conflict { .. })) {
|
||||
self.core_mut().dialog = None;
|
||||
}
|
||||
log::info!("{}: the engine is back, {} tab(s) lost", Self::KIND.app.slug(), lost.len());
|
||||
if !lost.is_empty() {
|
||||
let shown = text(cx, "office.stopped.lost", &[("names", &lost.join(", "))]);
|
||||
self.toast_unlogged(shown, Severity::Danger, cx);
|
||||
}
|
||||
self.reopen_taken(taken, lost, cx);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user