Display all branches and remotes by default in the branch picker (#45041)

This both matches VS Code's branch picker and makes the "Filter Remotes"
button make more sense.

<img width="584" height="496" alt="SCR-20251216-pgkv"
src="https://github.com/user-attachments/assets/e2ae5917-38dc-42e3-a1be-4b3a1f23523e"
/>

<img width="614" height="410" alt="SCR-20251216-pgqp"
src="https://github.com/user-attachments/assets/30b0a17a-1529-4f75-9781-92b08125aa0b"
/>


Release Notes:

- Display all branches and remotes by default in the branch picker
This commit is contained in:
Joseph T. Lyons
2025-12-16 17:51:33 -05:00
committed by GitHub
parent 3a013d8090
commit 2886806809

View File

@@ -316,17 +316,17 @@ impl Entry {
#[derive(Clone, Copy, PartialEq)]
enum BranchFilter {
/// Only show local branches
Local,
/// Only show remote branches
/// Show both local and remote branches.
All,
/// Only show remote branches.
Remote,
}
impl BranchFilter {
fn invert(&self) -> Self {
match self {
BranchFilter::Local => BranchFilter::Remote,
BranchFilter::Remote => BranchFilter::Local,
BranchFilter::All => BranchFilter::Remote,
BranchFilter::Remote => BranchFilter::All,
}
}
}
@@ -375,7 +375,7 @@ impl BranchListDelegate {
selected_index: 0,
last_query: Default::default(),
modifiers: Default::default(),
branch_filter: BranchFilter::Local,
branch_filter: BranchFilter::All,
state: PickerState::List,
focus_handle: cx.focus_handle(),
}
@@ -518,7 +518,7 @@ impl PickerDelegate for BranchListDelegate {
match self.state {
PickerState::List | PickerState::NewRemote | PickerState::NewBranch => {
match self.branch_filter {
BranchFilter::Local => "Select branch…",
BranchFilter::All => "Select branch or remote",
BranchFilter::Remote => "Select remote…",
}
}
@@ -560,8 +560,8 @@ impl PickerDelegate for BranchListDelegate {
self.editor_position() == PickerEditorPosition::End,
|this| {
let tooltip_label = match self.branch_filter {
BranchFilter::Local => "Turn Off Remote Filter",
BranchFilter::Remote => "Filter Remote Branches",
BranchFilter::All => "Filter Remote Branches",
BranchFilter::Remote => "Show All Branches",
};
this.gap_1().justify_between().child({
@@ -625,40 +625,38 @@ impl PickerDelegate for BranchListDelegate {
return Task::ready(());
};
let display_remotes = self.branch_filter;
let branch_filter = self.branch_filter;
cx.spawn_in(window, async move |picker, cx| {
let branch_matches_filter = |branch: &Branch| match branch_filter {
BranchFilter::All => true,
BranchFilter::Remote => branch.is_remote(),
};
let mut matches: Vec<Entry> = if query.is_empty() {
all_branches
let mut matches: Vec<Entry> = all_branches
.into_iter()
.filter(|branch| {
if display_remotes == BranchFilter::Remote {
branch.is_remote()
} else {
!branch.is_remote()
}
})
.filter(|branch| branch_matches_filter(branch))
.map(|branch| Entry::Branch {
branch,
positions: Vec::new(),
})
.collect()
.collect();
// Keep the existing recency sort within each group, but show local branches first.
matches.sort_by_key(|entry| entry.as_branch().is_some_and(|b| b.is_remote()));
matches
} else {
let branches = all_branches
.iter()
.filter(|branch| {
if display_remotes == BranchFilter::Remote {
branch.is_remote()
} else {
!branch.is_remote()
}
})
.filter(|branch| branch_matches_filter(branch))
.collect::<Vec<_>>();
let candidates = branches
.iter()
.enumerate()
.map(|(ix, branch)| StringMatchCandidate::new(ix, branch.name()))
.collect::<Vec<StringMatchCandidate>>();
fuzzy::match_strings(
let mut matches: Vec<Entry> = fuzzy::match_strings(
&candidates,
&query,
true,
@@ -673,7 +671,12 @@ impl PickerDelegate for BranchListDelegate {
branch: branches[candidate.candidate_id].clone(),
positions: candidate.positions,
})
.collect()
.collect();
// Keep fuzzy-relevance ordering within local/remote groups, but show locals first.
matches.sort_by_key(|entry| entry.as_branch().is_some_and(|b| b.is_remote()));
matches
};
picker
.update(cx, |picker, _| {
@@ -841,10 +844,13 @@ impl PickerDelegate for BranchListDelegate {
Entry::NewUrl { .. } | Entry::NewBranch { .. } | Entry::NewRemoteName { .. } => {
Icon::new(IconName::Plus).color(Color::Muted)
}
Entry::Branch { .. } => match self.branch_filter {
BranchFilter::Local => Icon::new(IconName::GitBranchAlt).color(Color::Muted),
BranchFilter::Remote => Icon::new(IconName::Screen).color(Color::Muted),
},
Entry::Branch { branch, .. } => {
if branch.is_remote() {
Icon::new(IconName::Screen).color(Color::Muted)
} else {
Icon::new(IconName::GitBranchAlt).color(Color::Muted)
}
}
};
let entry_title = match entry {
@@ -1036,8 +1042,8 @@ impl PickerDelegate for BranchListDelegate {
) -> Option<AnyElement> {
matches!(self.state, PickerState::List).then(|| {
let label = match self.branch_filter {
BranchFilter::Local => "Local",
BranchFilter::Remote => "Remote",
BranchFilter::All => "Branches",
BranchFilter::Remote => "Remotes",
};
ListHeader::new(label).inset(true).into_any_element()
@@ -1532,7 +1538,7 @@ mod tests {
}
#[gpui::test]
async fn test_update_remote_matches_with_query(cx: &mut TestAppContext) {
async fn test_branch_filter_shows_all_then_remotes_and_applies_query(cx: &mut TestAppContext) {
init_test(cx);
let branches = vec![
@@ -1547,34 +1553,49 @@ mod tests {
update_branch_list_matches_with_empty_query(&branch_list, cx).await;
// Check matches, it should match all existing branches and no option to create new branch
branch_list
.update_in(cx, |branch_list, window, cx| {
branch_list.picker.update(cx, |picker, cx| {
assert_eq!(picker.delegate.matches.len(), 2);
let branches = picker
.delegate
.matches
.iter()
.map(|be| be.name())
.collect::<HashSet<_>>();
assert_eq!(
branches,
["feature-ui", "develop"]
.into_iter()
.collect::<HashSet<_>>()
);
branch_list.update(cx, |branch_list, cx| {
branch_list.picker.update(cx, |picker, _cx| {
assert_eq!(picker.delegate.matches.len(), 4);
// Verify the last entry is NOT the "create new branch" option
let last_match = picker.delegate.matches.last().unwrap();
assert!(!last_match.is_new_branch());
assert!(!last_match.is_new_url());
picker.delegate.branch_filter = BranchFilter::Remote;
picker.delegate.update_matches(String::new(), window, cx)
})
let branches = picker
.delegate
.matches
.iter()
.map(|be| be.name())
.collect::<HashSet<_>>();
assert_eq!(
branches,
["origin/main", "fork/feature-auth", "feature-ui", "develop"]
.into_iter()
.collect::<HashSet<_>>()
);
// Locals should be listed before remotes.
let ordered = picker
.delegate
.matches
.iter()
.map(|be| be.name())
.collect::<Vec<_>>();
assert_eq!(
ordered,
vec!["feature-ui", "develop", "origin/main", "fork/feature-auth"]
);
// Verify the last entry is NOT the "create new branch" option
let last_match = picker.delegate.matches.last().unwrap();
assert!(!last_match.is_new_branch());
assert!(!last_match.is_new_url());
})
.await;
cx.run_until_parked();
});
branch_list.update(cx, |branch_list, cx| {
branch_list.picker.update(cx, |picker, _cx| {
picker.delegate.branch_filter = BranchFilter::Remote;
})
});
update_branch_list_matches_with_empty_query(&branch_list, cx).await;
branch_list
.update_in(cx, |branch_list, window, cx| {