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,224 @@
|
||||
//! The commit box: message, amend, identity (TREE-05).
|
||||
|
||||
use ely_gpui_component::forms::TextInput;
|
||||
use gpui::{AppContext as _, Context, Window};
|
||||
|
||||
use crate::{
|
||||
asks::Ask,
|
||||
commit::{self, Me},
|
||||
tree::{Tree, text},
|
||||
work::Write,
|
||||
};
|
||||
|
||||
/// A repository's commit box, kept while another is shown.
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct Held {
|
||||
pub subject: String,
|
||||
pub body: String,
|
||||
pub amend: bool,
|
||||
pub draft: Option<(String, String)>,
|
||||
}
|
||||
|
||||
impl Tree {
|
||||
/// Shows repository `id`, swapping its commit box in.
|
||||
pub fn activate(&mut self, id: Option<u64>, cx: &mut Context<Self>) {
|
||||
if self.active == id {
|
||||
return cx.notify();
|
||||
}
|
||||
// A last message still loading is not carried across.
|
||||
if self.amend_loading {
|
||||
self.set_amend(false, cx);
|
||||
}
|
||||
let held = Held {
|
||||
subject: self.subject.read(cx).text().to_string(),
|
||||
body: self.body.read(cx).text().to_string(),
|
||||
amend: self.amend,
|
||||
draft: self.draft.take(),
|
||||
};
|
||||
if let Some(old) = self.active.and_then(|old| self.opened_mut(old)) {
|
||||
old.held = held;
|
||||
}
|
||||
self.active = id;
|
||||
self.amend_ask += 1;
|
||||
let held = id
|
||||
.and_then(|id| self.opened_mut(id))
|
||||
.map(|opened| std::mem::take(&mut opened.held))
|
||||
.unwrap_or_default();
|
||||
(self.amend, self.draft) = (held.amend, held.draft);
|
||||
self.fill(&held.subject, &held.body, cx);
|
||||
self.ensure_side(cx);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
/// Rereads who commits and whether Git signs.
|
||||
pub fn read_me(&mut self, id: u64, cx: &mut Context<Self>) {
|
||||
let Some(git) = self.git.as_ref().ok().cloned() else {
|
||||
return;
|
||||
};
|
||||
let Some(opened) = self.opened_mut(id) else {
|
||||
return;
|
||||
};
|
||||
opened.me_asks += 1;
|
||||
let (root, ask) = (opened.repo.root.clone(), opened.me_asks);
|
||||
let task = cx.background_spawn(async move { commit::me(&git, &root) });
|
||||
cx.spawn(async move |this, cx| {
|
||||
let me = task.await.map_err(|error| error.to_string());
|
||||
this.update(cx, |tree, cx| {
|
||||
if let Some(opened) = tree.opened_mut(id).filter(|opened| opened.me_asks == ask) {
|
||||
opened.me = Some(me);
|
||||
cx.notify();
|
||||
}
|
||||
})
|
||||
.ok();
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
fn fill(&self, subject: &str, body: &str, cx: &mut Context<Self>) {
|
||||
self.subject
|
||||
.update(cx, |field, cx| field.set_text(subject.to_string(), cx));
|
||||
self.body
|
||||
.update(cx, |field, cx| field.set_text(body.to_string(), cx));
|
||||
}
|
||||
|
||||
/// Amend loads the last message; off restores the draft.
|
||||
pub fn set_amend(&mut self, on: bool, cx: &mut Context<Self>) {
|
||||
// Any later choice voids a read still on its way.
|
||||
self.amend_ask += 1;
|
||||
if !on {
|
||||
(self.amend, self.amend_loading) = (false, false);
|
||||
if let Some((subject, body)) = self.draft.take() {
|
||||
self.fill(&subject, &body, cx);
|
||||
}
|
||||
return cx.notify();
|
||||
}
|
||||
let Some(git) = self.git(cx) else { return };
|
||||
let Some((repo, root)) = self
|
||||
.active()
|
||||
.map(|opened| (opened.id, opened.repo.root.clone()))
|
||||
else {
|
||||
return;
|
||||
};
|
||||
// The tick shows at once; the message follows.
|
||||
(self.amend, self.amend_loading) = (true, true);
|
||||
self.draft = Some((
|
||||
self.subject.read(cx).text().to_string(),
|
||||
self.body.read(cx).text().to_string(),
|
||||
));
|
||||
let ask = self.amend_ask;
|
||||
let task = cx.background_spawn(async move { commit::last_message(&git, &root) });
|
||||
cx.spawn(async move |this, cx| {
|
||||
let last = task.await;
|
||||
this.update(cx, |tree, cx| {
|
||||
if tree.amend_ask != ask || tree.active != Some(repo) {
|
||||
return log::info!("tree: an amend read came back late");
|
||||
}
|
||||
match last {
|
||||
Ok(message) => {
|
||||
let (subject, body) = commit::split(&message);
|
||||
tree.fill(&subject, &body, cx);
|
||||
tree.amend_loading = false;
|
||||
cx.notify();
|
||||
}
|
||||
Err(error) => {
|
||||
tree.set_amend(false, cx);
|
||||
tree.failed("tree.error.amend", &error.to_string(), cx);
|
||||
}
|
||||
}
|
||||
})
|
||||
.ok();
|
||||
})
|
||||
.detach();
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn commit(&mut self, cx: &mut Context<Self>) {
|
||||
let Some(id) = self.active.filter(|_| !self.amend_loading) else {
|
||||
return;
|
||||
};
|
||||
let message = commit::message(self.subject.read(cx).text(), self.body.read(cx).text());
|
||||
let amend = self.amend;
|
||||
log::info!("tree: commit asked, amend {amend}");
|
||||
self.write(id, Write::Commit { message, amend }, cx);
|
||||
}
|
||||
|
||||
/// A commit went in: its box empties if unchanged since.
|
||||
pub fn committed(&mut self, id: u64, message: &str, cx: &mut Context<Self>) {
|
||||
if self.active == Some(id) {
|
||||
let now = commit::message(self.subject.read(cx).text(), self.body.read(cx).text());
|
||||
if now == message {
|
||||
(self.amend, self.draft) = (false, None);
|
||||
self.fill("", "", cx);
|
||||
}
|
||||
} else if let Some(opened) = self.opened_mut(id) {
|
||||
if commit::message(&opened.held.subject, &opened.held.body) == message {
|
||||
opened.held = Held::default();
|
||||
}
|
||||
}
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
/// Opens the identity dialog, filled with Git's settings.
|
||||
pub fn ask_identity(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let me: Me = match self.active().and_then(|opened| opened.me.clone()) {
|
||||
Some(Ok(me)) => me,
|
||||
_ => Me::default(),
|
||||
};
|
||||
let field =
|
||||
|value: Option<String>, key: &str, window: &mut Window, cx: &mut Context<Self>| {
|
||||
// Named for assistive tech; the dialog draws the label.
|
||||
let label = text(cx, key, &[]);
|
||||
cx.new(|cx| {
|
||||
let mut field = TextInput::new(window, cx).label(label);
|
||||
field.set_text(value.unwrap_or_default(), cx);
|
||||
field
|
||||
})
|
||||
};
|
||||
let name = field(me.name, "tree.identity.name", window, cx);
|
||||
let email = field(me.email, "tree.identity.email", window, cx);
|
||||
let Some(repo) = self.active else { return };
|
||||
self.ask = Some(Ask::Identity {
|
||||
repo,
|
||||
name,
|
||||
email,
|
||||
global: false,
|
||||
});
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
/// Writes the name and address, here or for every repository.
|
||||
pub fn set_identity(&mut self, cx: &mut Context<Self>) {
|
||||
let Some(Ask::Identity {
|
||||
repo,
|
||||
name,
|
||||
email,
|
||||
global,
|
||||
}) = &self.ask
|
||||
else {
|
||||
return;
|
||||
};
|
||||
let id = *repo;
|
||||
let (name, email, global) = (
|
||||
name.read(cx).text().trim().to_string(),
|
||||
email.read(cx).text().trim().to_string(),
|
||||
*global,
|
||||
);
|
||||
if name.is_empty() || email.is_empty() {
|
||||
let words = text(cx, "tree.identity.empty", &[]);
|
||||
return self.toast(words, ely_gpui_component::primitives::Severity::Warning, cx);
|
||||
}
|
||||
self.ask = None;
|
||||
for (key, value) in [("user.name", name), ("user.email", email)] {
|
||||
self.write(
|
||||
id,
|
||||
Write::Config {
|
||||
key: key.into(),
|
||||
value,
|
||||
global,
|
||||
},
|
||||
cx,
|
||||
);
|
||||
}
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user