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,263 @@
|
||||
//! Cleaning and moving back, each previewed; lock retries.
|
||||
|
||||
use ely_gpui_component::{
|
||||
buttons::{Button, ButtonVariant},
|
||||
forms::Checkbox,
|
||||
overlays::{ConfirmDialog, Dialog},
|
||||
theme::ActiveTheme as _,
|
||||
};
|
||||
use gpui::{
|
||||
AnyElement, App, AppContext as _, Context, InteractiveElement as _, IntoElement,
|
||||
ParentElement as _, StatefulInteractiveElement as _, Styled as _, Window, div, px,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
asks::Ask,
|
||||
recover,
|
||||
tree::{Tree, text},
|
||||
work::Write,
|
||||
};
|
||||
|
||||
/// Each file, then each folder, as the dialog lists them.
|
||||
fn shown(clean: &recover::Clean) -> Vec<String> {
|
||||
clean.files.iter().chain(&clean.dirs).cloned().collect()
|
||||
}
|
||||
|
||||
impl Tree {
|
||||
/// Opens the clean dialog; its list comes from Git.
|
||||
pub fn ask_clean(&mut self, cx: &mut Context<Self>) {
|
||||
let Some(repo) = self.active else { return };
|
||||
self.ask = Some(Ask::Clean {
|
||||
repo,
|
||||
ignored: false,
|
||||
preview: None,
|
||||
});
|
||||
self.preview_clean(cx);
|
||||
}
|
||||
|
||||
pub fn clean_ignored(&mut self, on: bool, cx: &mut Context<Self>) {
|
||||
if let Some(Ask::Clean {
|
||||
ignored, preview, ..
|
||||
}) = &mut self.ask
|
||||
{
|
||||
(*ignored, *preview) = (on, None);
|
||||
self.preview_clean(cx);
|
||||
}
|
||||
}
|
||||
|
||||
/// Only the answer to the dialog as it stands lands.
|
||||
fn preview_clean(&mut self, cx: &mut Context<Self>) {
|
||||
let Some(git) = self.git.as_ref().ok().cloned() else {
|
||||
return;
|
||||
};
|
||||
let Some(Ask::Clean { repo, ignored, .. }) = &self.ask else {
|
||||
return;
|
||||
};
|
||||
let (repo, ignored) = (*repo, *ignored);
|
||||
let Some(root) = self.opened(repo).map(|opened| opened.repo.root.clone()) else {
|
||||
return;
|
||||
};
|
||||
let task = cx.background_spawn(async move { recover::clean_preview(&git, &root, ignored) });
|
||||
cx.spawn(async move |this, cx| {
|
||||
let read = task.await.map_err(|error| error.to_string());
|
||||
this.update(cx, |tree, cx| {
|
||||
if let Some(Ask::Clean {
|
||||
repo: now,
|
||||
ignored: still,
|
||||
preview,
|
||||
}) = &mut tree.ask
|
||||
&& (*now, *still) == (repo, ignored)
|
||||
{
|
||||
*preview = Some(read);
|
||||
cx.notify();
|
||||
}
|
||||
})
|
||||
.ok();
|
||||
})
|
||||
.detach();
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
/// Removes exactly the previewed paths, nothing found later.
|
||||
fn clean(&mut self, cx: &mut Context<Self>) {
|
||||
let Some(Ask::Clean {
|
||||
repo,
|
||||
ignored,
|
||||
preview: Some(Ok(clean)),
|
||||
}) = &self.ask
|
||||
else {
|
||||
return;
|
||||
};
|
||||
let write = Write::Clean {
|
||||
files: clean.files.clone(),
|
||||
dirs: clean.dirs.clone(),
|
||||
ignored: *ignored,
|
||||
};
|
||||
let (repo, count) = (*repo, clean.files.len());
|
||||
self.ask = None;
|
||||
log::info!("tree: cleaning {count} files");
|
||||
self.write(repo, write, cx);
|
||||
}
|
||||
|
||||
/// Reads what moving the branch to `to` leaves behind.
|
||||
pub fn ask_move(&mut self, to: &str, cx: &mut Context<Self>) {
|
||||
let Some(git) = self.git.as_ref().ok().cloned() else {
|
||||
return;
|
||||
};
|
||||
let Some(opened) = self.active() else { return };
|
||||
let (repo, root, to) = (opened.id, opened.repo.root.clone(), to.to_string());
|
||||
let task = cx.background_spawn(async move { recover::plan_move(&git, &root, &to) });
|
||||
cx.spawn(async move |this, cx| {
|
||||
let plan = task.await;
|
||||
this.update(cx, |tree, cx| {
|
||||
if tree.active != Some(repo) {
|
||||
return;
|
||||
}
|
||||
tree.ask = Some(match plan {
|
||||
Ok(plan) => Ask::Move { repo, plan },
|
||||
Err(error) => Ask::Failed {
|
||||
title: text(cx, "tree.op.failed", &[]),
|
||||
detail: error.to_string().into(),
|
||||
},
|
||||
});
|
||||
cx.notify();
|
||||
})
|
||||
.ok();
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
pub fn recover_dialog(
|
||||
&self,
|
||||
dismiss: impl Fn(&mut Window, &mut App) + Clone + 'static,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Option<AnyElement> {
|
||||
let this = cx.weak_entity();
|
||||
let label = |key: &str| text(cx, key, &[]);
|
||||
Some(match self.ask.as_ref()? {
|
||||
Ask::Clean {
|
||||
ignored, preview, ..
|
||||
} => {
|
||||
let colors = cx.theme().colors.clone();
|
||||
let toggle = {
|
||||
let this = this.clone();
|
||||
Checkbox::new("tree-clean-ignored", *ignored)
|
||||
.label(label("tree.clean.ignored"))
|
||||
.on_change(move |on, _, cx| {
|
||||
this.update(cx, |tree, cx| tree.clean_ignored(on, cx)).ok();
|
||||
})
|
||||
};
|
||||
let (body, ready): (AnyElement, bool) = match preview {
|
||||
None => (
|
||||
div().child(label("tree.clean.reading")).into_any_element(),
|
||||
false,
|
||||
),
|
||||
Some(Err(error)) => (
|
||||
div()
|
||||
.text_color(colors.danger)
|
||||
.child(error.clone())
|
||||
.into_any_element(),
|
||||
false,
|
||||
),
|
||||
Some(Ok(clean)) if shown(clean).is_empty() => (
|
||||
div().child(label("tree.clean.nothing")).into_any_element(),
|
||||
false,
|
||||
),
|
||||
Some(Ok(clean)) => (
|
||||
div()
|
||||
.id("tree-clean-paths")
|
||||
.text_color(colors.fg)
|
||||
.max_h(px(280.0))
|
||||
.overflow_y_scroll()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.children(shown(clean).into_iter().map(|path| div().child(path)))
|
||||
.into_any_element(),
|
||||
true,
|
||||
),
|
||||
};
|
||||
let count = match preview {
|
||||
Some(Ok(clean)) => shown(clean).len(),
|
||||
_ => 0,
|
||||
};
|
||||
let (go, cancel) = (
|
||||
text(cx, "tree.clean.go", &[("n", &count.to_string())]),
|
||||
label("tree.cancel"),
|
||||
);
|
||||
Dialog::new("tree-clean", label("tree.clean.title"), dismiss)
|
||||
.child(toggle)
|
||||
.child(body)
|
||||
.action(move |close| {
|
||||
Button::new("tree-clean-cancel", cancel)
|
||||
.on_click(move |_, window, cx| close(window, cx))
|
||||
})
|
||||
.action(move |_| {
|
||||
Button::new("tree-clean-go", go)
|
||||
.variant(ButtonVariant::Danger)
|
||||
.disabled(!ready)
|
||||
.on_click(move |_, _, cx| {
|
||||
this.update(cx, |tree, cx| tree.clean(cx)).ok();
|
||||
})
|
||||
})
|
||||
.into_any_element()
|
||||
}
|
||||
Ask::Move { repo, plan } => {
|
||||
let short = |oid: &str| oid[..oid.len().min(7)].to_string();
|
||||
let left = match plan.left.len() {
|
||||
0 => label("tree.move.nothing"),
|
||||
n => text(
|
||||
cx,
|
||||
"tree.move.left",
|
||||
&[("n", &n.to_string()), ("commits", &plan.left.join("\n"))],
|
||||
),
|
||||
};
|
||||
let what = match &plan.branch {
|
||||
Some(branch) => branch.trim_start_matches("refs/heads/").to_string(),
|
||||
None => label("tree.move.detached").to_string(),
|
||||
};
|
||||
let message = text(
|
||||
cx,
|
||||
"tree.move.message",
|
||||
&[
|
||||
("what", &what),
|
||||
("from", &short(&plan.head)),
|
||||
("to", &short(&plan.to)),
|
||||
("left", &left),
|
||||
],
|
||||
);
|
||||
let (repo, write) = (
|
||||
*repo,
|
||||
Write::Move {
|
||||
branch: plan.branch.clone(),
|
||||
head: plan.head.clone(),
|
||||
to: plan.to.clone(),
|
||||
},
|
||||
);
|
||||
ConfirmDialog::new("tree-move", label("tree.move.title"), message, dismiss)
|
||||
.confirm(label("tree.move.go"))
|
||||
.destructive()
|
||||
.on_confirm(move |_, cx| {
|
||||
this.update(cx, |tree, cx| tree.write(repo, write.clone(), cx))
|
||||
.ok();
|
||||
})
|
||||
.into_any_element()
|
||||
}
|
||||
Ask::Locked {
|
||||
repo,
|
||||
write,
|
||||
detail,
|
||||
} => {
|
||||
let (repo, write) = (*repo, write.clone());
|
||||
let message = format!("{}\n\n{detail}", label("tree.locked.message"));
|
||||
ConfirmDialog::new("tree-locked", label("tree.locked.title"), message, dismiss)
|
||||
.confirm(label("tree.locked.retry"))
|
||||
.on_confirm(move |_, cx| {
|
||||
this.update(cx, |tree, cx| tree.write(repo, write.clone(), cx))
|
||||
.ok();
|
||||
})
|
||||
.into_any_element()
|
||||
}
|
||||
_ => return None,
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user