Ely Office: six offline-first desktop apps in Rust and GPUI
ElyDoc, ElyTable, ElyShow (LibreOfficeKit), ElyPDF (MuPDF), ElyTree (Git), ElyMark (Milkdown and CodeMirror in CEF). ElyMail and ElyAgent follow.
This commit is contained in:
817 files changed
+143101
No files matched your search
@@ -0,0 +1,165 @@
|
||||
//! 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<Self>) {
|
||||
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<Self>) -> Option<AnyElement> {
|
||||
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<Self>) {
|
||||
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<Self>,
|
||||
) {
|
||||
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<Self>) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user