Cargo.lock 🔗
@@ -5477,6 +5477,7 @@ dependencies = [
"telemetry",
"theme",
"time",
+ "time_format",
"ui",
"unindent",
"util",
Angelk90 , Danilo Leal , Marshall Bowers , and Conrad Irwin created
Final product:

Release Notes:
- Added more information about Git branches to the branch picker.
---------
Co-authored-by: Danilo Leal <daniloleal09@gmail.com>
Co-authored-by: Marshall Bowers <git@maxdeviant.com>
Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
Cargo.lock | 1
crates/git/src/repository.rs | 7 +
crates/git_ui/Cargo.toml | 1
crates/git_ui/src/branch_picker.rs | 200 +++++++++++++++++--------------
4 files changed, 120 insertions(+), 89 deletions(-)
@@ -5477,6 +5477,7 @@ dependencies = [
"telemetry",
"theme",
"time",
+ "time_format",
"ui",
"unindent",
"util",
@@ -1,4 +1,5 @@
use crate::status::FileStatus;
+use crate::SHORT_SHA_LENGTH;
use crate::{blame::Blame, status::GitStatus};
use anyhow::{anyhow, Context, Result};
use askpass::{AskPassResult, AskPassSession};
@@ -127,6 +128,12 @@ pub struct CommitDetails {
pub committer_name: SharedString,
}
+impl CommitDetails {
+ pub fn short_sha(&self) -> SharedString {
+ self.sha[..SHORT_SHA_LENGTH].to_string().into()
+ }
+}
+
#[derive(Debug, Clone, Hash, PartialEq, Eq)]
pub struct Remote {
pub name: SharedString,
@@ -50,6 +50,7 @@ strum.workspace = true
telemetry.workspace = true
theme.workspace = true
time.workspace = true
+time_format.workspace = true
ui.workspace = true
util.workspace = true
workspace.workspace = true
@@ -1,5 +1,5 @@
use anyhow::{anyhow, Context as _};
-use fuzzy::{StringMatch, StringMatchCandidate};
+use fuzzy::StringMatchCandidate;
use git::repository::Branch;
use gpui::{
@@ -10,6 +10,8 @@ use gpui::{
use picker::{Picker, PickerDelegate};
use project::git::Repository;
use std::sync::Arc;
+use time::OffsetDateTime;
+use time_format::format_local_timestamp;
use ui::{
prelude::*, HighlightedLabel, KeyBinding, ListItem, ListItemSpacing, PopoverMenuHandle, Tooltip,
};
@@ -63,7 +65,7 @@ pub fn popover(
cx: &mut App,
) -> Entity<BranchList> {
cx.new(|cx| {
- let list = BranchList::new(repository, BranchListStyle::Popover, rems(15.), window, cx);
+ let list = BranchList::new(repository, BranchListStyle::Popover, rems(20.), window, cx);
list.focus_handle(cx).focus(window);
list
})
@@ -96,10 +98,17 @@ impl BranchList {
.map(|repository| repository.read(cx).branches());
cx.spawn_in(window, |this, mut cx| async move {
- let all_branches = all_branches_request
+ let mut all_branches = all_branches_request
.context("No active repository")?
.await??;
+ all_branches.sort_by_key(|branch| {
+ branch
+ .most_recent_commit
+ .as_ref()
+ .map(|commit| 0 - commit.commit_timestamp)
+ });
+
this.update_in(&mut cx, |this, window, cx| {
this.picker.update(cx, |picker, cx| {
picker.delegate.all_branches = Some(all_branches);
@@ -111,7 +120,7 @@ impl BranchList {
})
.detach_and_log_err(cx);
- let delegate = BranchListDelegate::new(repository.clone(), style, (1.6 * width.0) as usize);
+ let delegate = BranchListDelegate::new(repository.clone(), style);
let picker = cx.new(|cx| Picker::uniform_list(delegate, window, cx));
let _subscription = cx.subscribe(&picker, |_, _, _, cx| {
@@ -162,18 +171,9 @@ impl Render for BranchList {
}
#[derive(Debug, Clone)]
-enum BranchEntry {
- Branch(StringMatch),
- History(String),
-}
-
-impl BranchEntry {
- fn name(&self) -> &str {
- match self {
- Self::Branch(branch) => &branch.string,
- Self::History(branch) => &branch,
- }
- }
+struct BranchEntry {
+ branch: Branch,
+ positions: Vec<usize>,
}
pub struct BranchListDelegate {
@@ -183,17 +183,11 @@ pub struct BranchListDelegate {
style: BranchListStyle,
selected_index: usize,
last_query: String,
- /// Max length of branch name before we truncate it and add a trailing `...`.
- branch_name_trailoff_after: usize,
modifiers: Modifiers,
}
impl BranchListDelegate {
- fn new(
- repo: Option<Entity<Repository>>,
- style: BranchListStyle,
- branch_name_trailoff_after: usize,
- ) -> Self {
+ fn new(repo: Option<Entity<Repository>>, style: BranchListStyle) -> Self {
Self {
matches: vec![],
repo,
@@ -201,16 +195,12 @@ impl BranchListDelegate {
all_branches: None,
selected_index: 0,
last_query: Default::default(),
- branch_name_trailoff_after,
modifiers: Default::default(),
}
}
fn has_exact_match(&self, target: &str) -> bool {
- self.matches.iter().any(|mat| match mat {
- BranchEntry::Branch(branch) => branch.string == target,
- _ => false,
- })
+ self.matches.iter().any(|entry| entry.branch.name == target)
}
fn create_branch(
@@ -266,37 +256,27 @@ impl PickerDelegate for BranchListDelegate {
window: &mut Window,
cx: &mut Context<Picker<Self>>,
) -> Task<()> {
- let Some(mut all_branches) = self.all_branches.clone() else {
+ let Some(all_branches) = self.all_branches.clone() else {
return Task::ready(());
};
+ const RECENT_BRANCHES_COUNT: usize = 10;
cx.spawn_in(window, move |picker, mut cx| async move {
- const RECENT_BRANCHES_COUNT: usize = 10;
- if query.is_empty() {
- if all_branches.len() > RECENT_BRANCHES_COUNT {
- // Truncate list of recent branches
- // Do a partial sort to show recent-ish branches first.
- all_branches.select_nth_unstable_by(RECENT_BRANCHES_COUNT - 1, |lhs, rhs| {
- rhs.priority_key().cmp(&lhs.priority_key())
- });
- all_branches.truncate(RECENT_BRANCHES_COUNT);
- }
- all_branches.sort_unstable_by(|lhs, rhs| {
- rhs.is_head.cmp(&lhs.is_head).then(lhs.name.cmp(&rhs.name))
- });
- }
-
- let candidates = all_branches
- .into_iter()
- .enumerate()
- .map(|(ix, command)| StringMatchCandidate::new(ix, &command.name))
- .collect::<Vec<StringMatchCandidate>>();
- let matches: Vec<BranchEntry> = if query.is_empty() {
- candidates
+ let matches = if query.is_empty() {
+ all_branches
.into_iter()
- .map(|candidate| BranchEntry::History(candidate.string))
+ .take(RECENT_BRANCHES_COUNT)
+ .map(|branch| BranchEntry {
+ branch,
+ positions: Vec::new(),
+ })
.collect()
} else {
+ let candidates = all_branches
+ .iter()
+ .enumerate()
+ .map(|(ix, command)| StringMatchCandidate::new(ix, &command.name.clone()))
+ .collect::<Vec<StringMatchCandidate>>();
fuzzy::match_strings(
&candidates,
&query,
@@ -308,7 +288,10 @@ impl PickerDelegate for BranchListDelegate {
.await
.iter()
.cloned()
- .map(BranchEntry::Branch)
+ .map(|candidate| BranchEntry {
+ branch: all_branches[candidate.candidate_id].clone(),
+ positions: candidate.positions,
+ })
.collect()
};
picker
@@ -337,7 +320,7 @@ impl PickerDelegate for BranchListDelegate {
return;
}
- let Some(branch) = self.matches.get(self.selected_index()) else {
+ let Some(entry) = self.matches.get(self.selected_index()) else {
return;
};
@@ -349,14 +332,14 @@ impl PickerDelegate for BranchListDelegate {
if current_branch
.flatten()
- .is_some_and(|current_branch| current_branch == branch.name())
+ .is_some_and(|current_branch| current_branch == entry.branch.name)
{
cx.emit(DismissEvent);
return;
}
cx.spawn_in(window, {
- let branch = branch.clone();
+ let branch = entry.branch.clone();
|picker, mut cx| async move {
let branch_change_task = picker.update(&mut cx, |this, cx| {
let repo = this
@@ -369,16 +352,8 @@ impl PickerDelegate for BranchListDelegate {
let cx = cx.to_async();
anyhow::Ok(async move {
- match branch {
- BranchEntry::Branch(StringMatch {
- string: branch_name,
- ..
- })
- | BranchEntry::History(branch_name) => {
- cx.update(|cx| repo.read(cx).change_branch(&branch_name))?
- .await?
- }
- }
+ cx.update(|cx| repo.read(cx).change_branch(&branch.name))?
+ .await?
})
})??;
@@ -398,6 +373,10 @@ impl PickerDelegate for BranchListDelegate {
cx.emit(DismissEvent);
}
+ fn render_header(&self, _: &mut Window, _cx: &mut Context<Picker<Self>>) -> Option<AnyElement> {
+ None
+ }
+
fn render_match(
&self,
ix: usize,
@@ -405,9 +384,29 @@ impl PickerDelegate for BranchListDelegate {
_window: &mut Window,
_cx: &mut Context<Picker<Self>>,
) -> Option<Self::ListItem> {
- let hit = &self.matches[ix];
- let shortened_branch_name =
- util::truncate_and_trailoff(&hit.name(), self.branch_name_trailoff_after);
+ let entry = &self.matches[ix];
+
+ let (commit_time, commit_message) = entry
+ .branch
+ .most_recent_commit
+ .as_ref()
+ .map(|commit| {
+ let commit_message = commit.subject.clone();
+ let commit_time = OffsetDateTime::from_unix_timestamp(commit.commit_timestamp)
+ .unwrap_or_else(|_| OffsetDateTime::now_utc());
+ let formatted_time = format_local_timestamp(
+ commit_time,
+ OffsetDateTime::now_utc(),
+ time_format::TimestampFormat::Relative,
+ );
+ (formatted_time, commit_message)
+ })
+ .unwrap_or_else(|| {
+ (
+ "Unknown Date".to_string(),
+ SharedString::from("No commit message available"),
+ )
+ });
Some(
ListItem::new(SharedString::from(format!("vcs-menu-{ix}")))
@@ -418,26 +417,49 @@ impl PickerDelegate for BranchListDelegate {
})
.spacing(ListItemSpacing::Sparse)
.toggle_state(selected)
- .when(matches!(hit, BranchEntry::History(_)), |el| {
- el.end_slot(
- Icon::new(IconName::HistoryRerun)
- .color(Color::Muted)
- .size(IconSize::Small),
- )
- })
- .map(|el| match hit {
- BranchEntry::Branch(branch) => {
- let highlights: Vec<_> = branch
- .positions
- .iter()
- .filter(|index| index < &&self.branch_name_trailoff_after)
- .copied()
- .collect();
-
- el.child(HighlightedLabel::new(shortened_branch_name, highlights))
- }
- BranchEntry::History(_) => el.child(Label::new(shortened_branch_name)),
- }),
+ .child(
+ v_flex()
+ .w_full()
+ .child(
+ h_flex()
+ .w_full()
+ .flex_shrink()
+ .overflow_x_hidden()
+ .gap_2()
+ .justify_between()
+ .child(
+ div().flex_shrink().overflow_x_hidden().child(
+ HighlightedLabel::new(
+ entry.branch.name.clone(),
+ entry.positions.clone(),
+ )
+ .truncate(),
+ ),
+ )
+ .child(
+ Label::new(commit_time)
+ .size(LabelSize::Small)
+ .color(Color::Muted)
+ .into_element(),
+ ),
+ )
+ .when(self.style == BranchListStyle::Modal, |el| {
+ el.child(
+ div().max_w_96().child(
+ Label::new(
+ commit_message
+ .split('\n')
+ .next()
+ .unwrap_or_default()
+ .to_string(),
+ )
+ .size(LabelSize::Small)
+ .truncate()
+ .color(Color::Muted),
+ ),
+ )
+ }),
+ ),
)
}