//! Renaming the open file in its folder (MARK-01). use std::{io, path::PathBuf}; use ely_files::SaveState; use ely_gpui_component::{forms::TextInput, overlays::PromptDialog, primitives::Severity}; use ely_office::{Host as _, files::file_name, prelude::*}; use ely_ui::{commands, i18n::text}; use gpui::{AnyElement, AppContext as _, Context, IntoElement as _, Window}; use crate::desk::Desk; /// The text key saying why `name` is refused. fn refused(name: &str) -> Option<&'static str> { let name = name.trim(); if name.is_empty() || name == "." || name == ".." || name.contains(['/', '\\', '\0']) { return Some("mark.rename.bad"); } let extension = std::path::Path::new(name) .extension() .and_then(|ext| ext.to_str()) .unwrap_or_default() .to_lowercase(); (!Desk::KIND.kept.contains(&extension.as_str())).then_some("mark.rename.format") } impl Desk { /// Asks a new name for the active file. pub fn on_rename(&mut self, _: &commands::Rename, window: &mut Window, cx: &mut Context) { let Some(tab) = self.active_tab() else { return; }; let (id, name) = (tab.id, tab.name.clone()); let field = cx.new(|cx| { let mut field = TextInput::new(window, cx); field.set_text(name, cx); field }); self.naming = Some((id, field)); cx.notify(); } pub fn naming_element(&self, cx: &mut Context) -> Option { let (id, field) = self.naming.as_ref()?; let (closed, sent, id) = (cx.entity(), cx.entity(), *id); let formats = Desk::KIND.kept.join(", ."); let bad = text(cx, "mark.rename.bad", &[]); let format = text(cx, "mark.rename.format", &[("formats", &formats)]); let prompt = PromptDialog::new( "mark-rename", text(cx, "mark.rename", &[]), field, move |_, cx| { closed.update(cx, |desk, cx| { desk.naming = None; cx.notify(); }) }, ) .label(text(cx, "mark.rename.name", &[])) .submit(text(cx, "mark.rename.apply", &[])) .check(move |name| match refused(name) { None => Ok(()), Some("mark.rename.bad") => Err(bad.clone()), Some(_) => Err(format.clone()), }) .on_submit(move |name, _, cx| { sent.update(cx, |desk, cx| { desk.naming = None; desk.rename(id, name, cx); }) }); Some(prompt.into_any_element()) } /// Renames tab `id`'s file; an existing name is refused. fn rename(&mut self, id: u64, name: &str, cx: &mut Context) { let Some(tab) = self.tab(id) else { return; }; let Some(from) = tab.session.path().map(|path| path.to_path_buf()) else { return self.toast(text(cx, "mark.rename.untitled", &[]), Severity::Warning, cx); }; if tab.exporting || tab.session.state() == SaveState::Saving { return self.toast(text(cx, "mark.rename.saving", &[]), Severity::Warning, cx); } let to = from.with_file_name(name.trim()); let (source, target) = (from.clone(), to.clone()); cx.spawn(async move |this, cx| { let done = cx .background_executor() .spawn(async move { ely_files::rename_new(&source, &target) }) .await; this.update(cx, |desk, cx| desk.renamed(id, from, to, done, cx)) .ok(); }) .detach(); } /// The tab follows only if it holds the old path. fn renamed( &mut self, id: u64, from: PathBuf, to: PathBuf, done: io::Result<()>, cx: &mut Context, ) { if let Err(error) = done { log::error!("mark: {} did not rename: {error}", from.display()); let (name, error) = (file_name(&from), error.to_string()); let said = text( cx, "mark.rename.failed", &[("name", &name), ("error", &error)], ); return self.toast(said, Severity::Danger, cx); } log::info!("mark: renamed {} to {}", from.display(), to.display()); match self.tab(id).and_then(|tab| tab.session.path()) { Some(path) if path == from => self.follow(id, to, cx), // The watcher saw the rename first. Some(path) if path == to => log::info!("mark: the tab already follows"), _ => log::info!("mark: the renamed file's tab is gone"), } } /// Tab `id`'s file now lives at `to`. pub fn follow(&mut self, id: u64, to: PathBuf, cx: &mut Context) { let Some(registry) = self.tab(id).map(|tab| tab.registry) else { return; }; // One session moves: an open in the gap focused it. if let Some(registry) = registry && let Err(error) = self.core_mut().sessions.track(registry, &to) { log::error!("mark: the renamed file did not register: {error}"); let said = text(cx, "office.error.engine", &[("error", &error.to_string())]); self.toast(said, Severity::Danger, cx); } if let Some(tab) = self.tab_mut(id) { tab.session.renamed(to.clone()); tab.name = file_name(&to); } if let Some(view) = self.tab(id).map(|tab| tab.view.clone()) { self.observed(id, &view, cx); } // The tree and folder search see the new name. self.relist(cx); cx.notify(); } } #[cfg(test)] mod tests { #[test] fn names_need_no_slashes_and_a_markdown_end() { for bad in ["", " ", "..", "a/b.md", "a\\b.md"] { assert_eq!(super::refused(bad), Some("mark.rename.bad"), "{bad:?}"); } assert_eq!(super::refused("notes.txt"), Some("mark.rename.format")); assert_eq!(super::refused("Notes.MD"), None); assert_eq!(super::refused("a.markdown"), None); } }