Files
ElyOffice/apps/ely-tree/src/hist_pane.rs
T
ZacharyZhang-NYandClaude Opus 5.5 9c9f9ab8f9 Zero warnings on Windows and Linux, clippy included (W-02, L-02)
rustc and clippy are clean for the whole workspace, every target, on
both. clippy --fix moved test modules last and took the shorter
forms; long types got named aliases; ElyTree's form takes its title
and button words as one pair.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 18:36:06 -04:00

321 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());
type Act = Box<dyn Fn(&mut Tree, &mut gpui::Window, &mut Context<Tree>)>;
let acts: [(&str, Act); 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()
}
}