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:
ZacharyZhang-NY committed 2026-10-10 02:29:19 -04:00
1 parent 6505f35669
commit d59159cea2
75 files changed
+1482 -354

No files matched your search

+5
View File
@@ -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)
+5
View File
@@ -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(),
_ => {}
}
+25 -4
View File
@@ -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"]);
}
}
+132 -80
View File
@@ -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, &copy)?;
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 }
+104
View File
@@ -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, &copy)?;
Ok(Some((copy, point.at)))
}
+24 -9
View File
@@ -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 =
+6 -14
View File
@@ -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);
}
}