//! Opening user files and copies over one worker (FILE-03). use std::{ io, path::{Path, PathBuf}, sync::Arc, }; use ely_files::{Open, Session}; use gpui::{AppContext as _, Context, Entity, TaskExt as _, Window}; use crate::engine::{Engine, Load}; use crate::files::{self, file_name}; use crate::{Host, Tab, Tabs, View, in_window, text}; /// How host `H`'s engine reads text files. type ImportOf = <::Engine as Engine>::Import; /// What a load starts from. pub enum Source { /// A user file under its registry id, with its password. File(PathBuf, usize, String), /// A template copy; the result is untitled. Copy(PathBuf), /// A copy that needed a password, with the one given. Locked(PathBuf, String), /// Checked text bytes, where they go, how to read. Import(PathBuf, Arc>, I), } pub trait Opening: Host { /// An untitled copy of a local template. fn new_from_file(&mut self, template: &Path, cx: &mut Context) { let name = file_name(template); let copy = files::work_dir(&self.core().work, self.core().next).and_then(|dir| { let copy = dir.join(name.as_ref()); std::fs::copy(template, ©)?; Ok(copy) }); match copy { Ok(copy) => self.load(Source::Copy(copy), cx), Err(error) => self.failed_open(&name, &error, cx), } } /// A new document from checked text bytes named `name`. fn import_text( &mut self, name: &str, bytes: Arc>, how: ImportOf, cx: &mut Context, ) { match files::work_dir(&self.core().work, self.core().next) { Ok(dir) => self.load(Source::Import(dir.join(name), bytes, how), cx), Err(error) => self.failed_open(name, &error, cx), } } /// An untitled document from `contents` named `name`. fn new_from_text(&mut self, name: &str, contents: &str, cx: &mut Context) { let copy = files::work_dir(&self.core().work, self.core().next).and_then(|dir| { let copy = dir.join(name); std::fs::write(©, contents)?; Ok(copy) }); match copy { Ok(copy) => self.load(Source::Copy(copy), cx), Err(error) => self.failed_open(name, &error, cx), } } /// Opens a user file; an open one gets focus (FILE-03). fn open_file(&mut self, path: PathBuf, cx: &mut Context) { self.open_with(path, String::new(), cx); } /// Opens a user file with its password. fn open_with(&mut self, path: PathBuf, password: String, cx: &mut Context) { match self.core_mut().sessions.open(&path, false) { Ok(Open::Focus(registry)) => { let core = self.core_mut(); core.active = core .tabs .iter() .find(|tab| tab.registry == Some(registry)) .map(|tab| tab.id); cx.notify(); } Ok(Open::Fresh(registry) | Open::View(registry)) => { self.load(Source::File(path, registry, password), cx) } Err(error) => self.failed_open(&path.display().to_string(), &error, cx), } } fn failed_open(&self, name: &str, error: &io::Error, cx: &mut gpui::App) { log::error!("{}: {name} did not open: {error}", Self::KIND.app.slug()); let error = error.to_string(); let text = text( cx, "office.error.open", &[("name", name), ("error", &error)], ); self.toast(text, ely_gpui_component::primitives::Severity::Danger, cx); } /// Loads off the main thread; the tab takes focus. fn load(&mut self, source: Source>, cx: &mut Context) { let id = self.core().next; self.core_mut().next += 1; self.load_into(id, source, cx); } /// Loads into tab id `id`, taken earlier from `next`. fn load_into(&mut self, id: u64, source: Source>, cx: &mut Context) { let (path, password, text) = match &source { Source::File(path, _, password) => (path.clone(), password.clone(), None), Source::Copy(path) => (path.clone(), String::new(), None), Source::Locked(path, password) => (path.clone(), password.clone(), None), Source::Import(path, bytes, how) => ( path.clone(), String::new(), Some((Arc::clone(bytes), how.clone())), ), }; let written = path.clone(); self.core_mut().loading.push((id, file_name(&path))); cx.notify(); let worker = Arc::clone(&self.core().worker); cx.spawn(async move |this, cx| { let loaded = cx .background_spawn(async move { if let Some((bytes, _)) = &text { std::fs::write(&written, bytes.as_slice())?; } let mut worker = worker.lock().expect("the worker lock"); let from = match &text { None => Load::File(&written, &password), Some((_, how)) => Load::Import(&written, how), }; Self::Engine::load(&mut worker, from) }) .await; in_window(&this, cx, |host, window, cx| { host.loaded(id, source, loaded, window, cx) }) }) .detach_and_log_err(cx); } fn loaded( &mut self, id: u64, source: Source>, loaded: io::Result<::Loaded>, window: &mut Window, cx: &mut Context, ) { self.core_mut() .loading .retain(|(loading, _)| *loading != id); let imported = matches!(source, Source::Import(..)); let (path, registry, tried) = match source { Source::File(path, registry, password) => (path, Some(registry), !password.is_empty()), Source::Copy(path) | Source::Import(path, ..) => (path, None, false), Source::Locked(path, _) => (path, None, true), }; let opened = loaded.and_then(|loaded| { let risk = match files::macro_risk(&Self::KIND, &path)? { true => Some(crate::Risk::Macros), false => Self::Engine::risk(&loaded), }; let (work, session) = match registry { Some(_) => ( files::work_dir(&self.core().work, id)?, Session::load(path.clone())?, ), None => { let dir = path.parent().expect("a copy sits in its work dir"); (dir.to_path_buf(), Session::new_file()) } }; let (doc, view, mark) = Self::Engine::open(&self.core().worker, loaded, window, cx)?; Ok((doc, work, session, risk, view, mark)) }); let (doc, work, session, risk, view, mark) = match opened { Ok(opened) => opened, Err(error) => { if let Some(registry) = registry { self.core_mut().sessions.close(registry); } cx.notify(); // A locked copy asks too, and stays a copy. if error.kind() == io::ErrorKind::PermissionDenied && !imported { return crate::Dialogs::ask_password(self, path, tried, registry.is_none(), window, cx); } return self.failed_open(&path.display().to_string(), &error, cx); } }; let name = match registry { Some(_) => file_name(&path), None if imported => file_name(&path), None => { self.core_mut().untitled += 1; let n = self.core().untitled.to_string(); text(cx, "office.untitled", &[("n", &n)]) } }; log::info!( "{}: opened {name} as tab {id}, risk {risk:?}", Self::KIND.app.slug() ); self.watch(id, &view, cx); window.focus(&Self::Engine::focus(view.read(cx)), cx); self.core_mut().tabs.push(Tab { id, view, doc, session, registry, name, modified: false, risk, mark, work, exporting: false, reopen: false, extra: Default::default(), }); self.core_mut().active = Some(id); // Points from now on; a crash copy comes back unsaved. let named = self.tab(id).expect("just opened").name.clone(); crate::Recovering::keep_tab(self, id, &named, tried, cx); crate::Recovering::reopened(self, id, &path, cx); cx.notify(); } /// Engine errors become toasts; kernel edits mark the session. fn watch(&mut self, id: u64, view: &Entity>, cx: &mut Context) { let reported = move |host: &mut Self, view: &Entity>, modified: bool, cx: &mut Context| { if let Some(tab) = host.tab_mut(id) { match (modified, tab.modified) { (true, false) => tab.session.edit(), (false, true) => tab.session.reverted(), _ => {} } tab.modified = modified; } host.observed(id, view, cx); if Self::Engine::settled(view.read(cx)) { let (ready, rest): (Vec<_>, Vec<_>) = host .core_mut() .waiting .drain(..) .partition(|(tab, _)| *tab == id); host.core_mut().waiting = rest; for (_, then) in ready { cx.spawn(async move |this, cx| in_window(&this, cx, then)) .detach_and_log_err(cx); } } cx.notify(); }; Self::Engine::watch(view, cx, reported, move |host, error, cx| { // A released tab's view may finish its queue late. if host.tab(id).is_none() { log::info!("office: tab {id} failed after it closed"); return; } host.engine_failed(error, cx); crate::Stopping::check_engine(host, cx); }); } } impl Opening for H {}