Files
ElyOffice/apps/ely-tree/src/hist_pane.rs
T
ZacharyZhang-NY b7fca0146d 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.
2026-10-08 21:20:08 -04:00

323 lines
12 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! The middle in History: the picked commit (TREE-06).
use ely_gpui_component::{
buttons::{Button, ButtonVariant, IconButton},
data_display::DescriptionList,
feedback::{EmptyState, ErrorView},
lists::{List, ListItem},
primitives::IconName,
theme::{ActiveTheme as _, ControlSize},
};
use gpui::{
AnyElement, Context, FontWeight, InteractiveElement as _, IntoElement, ParentElement as _,
Role, SharedString, StatefulInteractiveElement as _, Styled as _, div,
prelude::FluentBuilder as _, px,
};
use crate::{
detail::{Detail, Person},
hist::{Hist, Picked, Whose, when},
tree::{Tree, text},
};
/// `%G?` in words.
fn signature(letter: char) -> &'static str {
match letter {
'G' => "tree.sig.good",
'B' => "tree.sig.bad",
'U' => "tree.sig.untrusted",
'X' => "tree.sig.expired",
'Y' => "tree.sig.expired_key",
'R' => "tree.sig.revoked",
'E' => "tree.sig.unchecked",
_ => "tree.sig.none",
}
}
fn person(who: &Person) -> SharedString {
format!("{} <{}>, {}", who.name, who.email, when(who.at)).into()
}
impl Tree {
/// A file's history and blame, as two buttons.
pub fn look_buttons(&self, path: &str, cx: &mut Context<Self>) -> [IconButton; 2] {
let (log, blame, at, blamed) = (
cx.weak_entity(),
cx.weak_entity(),
path.to_string(),
path.to_string(),
);
[
IconButton::new("tree-file-log", IconName::History)
.variant(ButtonVariant::Ghost)
.size(ControlSize::Sm)
.tooltip(text(cx, "tree.file_log", &[]))
.on_click(move |_, _, cx| {
log.update(cx, |tree, cx| tree.file_log(&at, cx)).ok();
}),
IconButton::new("tree-blame", IconName::User)
.variant(ButtonVariant::Ghost)
.size(ControlSize::Sm)
.tooltip(text(cx, "tree.blame", &[]))
.on_click(move |_, _, cx| {
blame.update(cx, |tree, cx| tree.blame(&blamed, cx)).ok();
}),
]
}
/// A committed file's path, its history and its blame.
fn file_head(&self, path: &str, cx: &mut Context<Self>) -> AnyElement {
let colors = cx.theme().colors.clone();
div()
.flex()
.items_center()
.gap_2()
.px_2()
.py_1p5()
.border_b_1()
.border_color(colors.border)
.child(
div()
.flex_1()
.min_w_0()
.text_color(colors.fg)
.child(path.to_string()),
)
.children(self.look_buttons(path, cx))
.into_any_element()
}
pub fn commit_pane(&self, hist: &Hist, cx: &mut Context<Self>) -> AnyElement {
self.picked_pane(&hist.pick, Whose::History, cx)
}
/// A history commit or a stash, as read so far.
pub fn picked_pane(&self, picked: &Picked, whose: Whose, cx: &mut Context<Self>) -> AnyElement {
match (&picked.commit, &picked.detail) {
(None, _) => EmptyState::new(
"tree-commit-none",
IconName::GitCommitHorizontal,
text(cx, "tree.commit.pick", &[]),
)
.into_any_element(),
(Some(_), None) => div().into_any_element(),
(Some(_), Some(Err(error))) => ErrorView::new(
"tree-commit-error",
IconName::TriangleAlert,
text(cx, "tree.commit.unread", &[]),
)
.detail(error.clone())
.into_any_element(),
(Some(_), Some(Ok(detail))) => {
let head = match whose {
Whose::History => self.commit_head(detail, cx),
Whose::Stash => self.stash_head(detail, cx),
Whose::Reflog => self.reflog_head(detail, cx),
};
self.commit_detail(detail, picked, whose, head, cx)
}
}
}
fn commit_head(&self, detail: &Detail, cx: &mut Context<Self>) -> AnyElement {
let colors = cx.theme().colors.clone();
let (subject, body) = detail
.message
.split_once('\n')
.unwrap_or((&detail.message, ""));
let label = |key: &str| text(cx, key, &[]);
let mut rows =
DescriptionList::new().text(label("tree.commit.author"), person(&detail.author));
if (&detail.committer.name, &detail.committer.email)
!= (&detail.author.name, &detail.author.email)
{
rows = rows.text(label("tree.commit.committer"), person(&detail.committer));
}
let parents = detail.parents.iter().map(|parent| {
let (this, id) = (cx.weak_entity(), parent.clone());
Button::new(
SharedString::from(format!("tree-parent-{parent}")),
parent[..parent.len().min(7)].to_string(),
)
.variant(ButtonVariant::Ghost)
.size(ControlSize::Sm)
.on_click(move |_, _, cx| {
this.update(cx, |tree, cx| tree.pick_commit(&id, cx)).ok();
})
});
let signed = match detail.signer.as_str() {
"" => label(signature(detail.signed)),
signer => format!("{}, {signer}", label(signature(detail.signed))).into(),
};
rows = rows
.text(label("tree.commit.id"), detail.id.clone())
.item(label("tree.commit.parents"), div().flex().children(parents))
.text(label("tree.commit.signature"), signed);
if !detail.refs.is_empty() {
rows = rows.text(label("tree.commit.refs"), detail.refs.clone());
}
div()
.id("tree-commit-head")
.flex_none()
.max_h(px(280.0))
.overflow_y_scroll()
.flex()
.flex_col()
.gap_2()
.p_3()
.border_b_1()
.border_color(colors.border)
.child(
div()
.id("tree-commit-subject")
.role(Role::Heading)
.aria_label(SharedString::from(subject.to_string()))
.font_weight(FontWeight::SEMIBOLD)
.text_color(colors.fg)
.child(subject.to_string()),
)
.when(!body.trim().is_empty(), |head| {
let body = SharedString::from(body.trim().to_string());
head.child(
div()
.id("tree-commit-body")
.role(Role::Label)
.aria_label(body.clone())
.text_color(colors.fg_muted)
.child(body),
)
})
.child(rows)
.child(self.commit_actions(detail, cx))
.into_any_element()
}
/// Pick, revert, or branch or tag at this commit.
fn commit_actions(&self, detail: &Detail, cx: &mut Context<Self>) -> AnyElement {
let (id, parents) = (detail.id.clone(), detail.parents.len());
let acts: [(
&str,
Box<dyn Fn(&mut Tree, &mut gpui::Window, &mut Context<Tree>)>,
); 4] = [
("tree.commit.pick_it", {
let id = id.clone();
Box::new(move |tree, _, cx| tree.cherry_pick(&id, parents, cx))
}),
("tree.commit.revert_it", {
let id = id.clone();
Box::new(move |tree, _, cx| tree.revert(&id, parents, cx))
}),
("tree.ref.branch_here", {
let id = id.clone();
Box::new(move |tree, window, cx| tree.ask_branch(Some(id.clone()), window, cx))
}),
("tree.ref.tag_here", {
let id = id.clone();
Box::new(move |tree, window, cx| tree.ask_tag(Some(id.clone()), window, cx))
}),
];
div()
.flex()
.flex_wrap()
.gap_2()
.children(acts.into_iter().map(|(key, act)| {
let this = cx.weak_entity();
Button::new(
SharedString::from(format!("{key}-button")),
text(cx, key, &[]),
)
.size(ControlSize::Sm)
.on_click(move |_, window, cx| {
this.update(cx, |tree, cx| act(tree, window, cx)).ok();
})
}))
.into_any_element()
}
fn commit_detail(
&self,
detail: &Detail,
pick: &Picked,
whose: Whose,
head: AnyElement,
cx: &mut Context<Self>,
) -> AnyElement {
let colors = cx.theme().colors.clone();
let picked = pick.file.as_ref().map(|(at, _)| *at);
let weak = cx.weak_entity();
let files: Vec<AnyElement> = detail
.files
.iter()
.enumerate()
.map(|(at, file)| {
let this = weak.clone();
let title = match &file.from {
Some(from) => format!("{from} → {}", file.path),
None => file.path.clone(),
};
let about = match file.lines {
Some((added, removed)) => format!("{} · +{added} −{removed}", file.status),
None => file.status.to_string(),
};
ListItem::new(SharedString::from(format!("tree-touched-{at}")), title)
.description(about)
.selected(picked == Some(at))
.on_click(move |_, _, cx| {
this.update(cx, |tree, cx| tree.pick_touched(whose, at, cx))
.ok();
})
.into_any_element()
})
.collect();
let shown = match &pick.file {
Some((at, view)) => {
let path = &detail.files[*at].path;
let diff = self.view_only(
SharedString::from(format!("tree-commit-diff-{}", detail.id)).into(),
path,
view.as_ref(),
cx,
);
div()
.size_full()
.flex()
.flex_col()
.child(self.file_head(path, cx))
.child(div().flex_1().min_h_0().child(diff))
.into_any_element()
}
None => EmptyState::new(
"tree-file-none",
IconName::FileText,
text(cx, "tree.commit.pick_file", &[]),
)
.into_any_element(),
};
div()
.size_full()
.flex()
.flex_col()
.child(head)
.child(
div()
.flex_1()
.min_h_0()
.flex()
.child(
div()
.id("tree-touched")
.flex_none()
.w_72()
.h_full()
.overflow_y_scroll()
.p_2()
.border_r_1()
.border_color(colors.border)
.child(List::new().children(files)),
)
.child(div().flex_1().min_w_0().h_full().child(shown)),
)
.into_any_element()
}
}