From 144ff825e26e2090c4bb15ab4ff7f5174a9fe6a2 Mon Sep 17 00:00:00 2001 From: Danilo Leal <67129314+danilo-leal@users.noreply.github.com> Date: Thu, 8 Jan 2026 21:40:41 -0300 Subject: [PATCH] git_ui: Add some fixes and improvements to the worktree picker (#46407) This PR adds some clean up to the worktree picker: making sure that worktree names render just one line, even when they might have multiple lines (that was making each line item break for me), removing superfluous icons, and making the footer controls more consistent with the branch picker, where the buttons change to "Create..." if you're in a state where the only match is the one to create a new worktree. https://github.com/user-attachments/assets/e63aa8c9-b5a0-443d-a2ab-7759daed10d1 Release Notes: - Git: Fixed some UI bugs in the worktree picker as well as improved the UX for creating worktrees based off of the current or default branch. --- crates/git_ui/src/worktree_picker.rs | 196 ++++++++++++++------------- 1 file changed, 101 insertions(+), 95 deletions(-) diff --git a/crates/git_ui/src/worktree_picker.rs b/crates/git_ui/src/worktree_picker.rs index 94c37378cca301f84abcaf3ccc12b238bdfb8f37..6e58de1ae141bd3770b57b93c4a89b9ea2af56bb 100644 --- a/crates/git_ui/src/worktree_picker.rs +++ b/crates/git_ui/src/worktree_picker.rs @@ -18,7 +18,7 @@ use project::{ use recent_projects::{RemoteConnectionModal, connect}; use remote::{RemoteConnectionOptions, remote_client::ConnectionIdentifier}; use std::{path::PathBuf, sync::Arc}; -use ui::{HighlightedLabel, KeyBinding, ListItem, ListItemSpacing, Tooltip, prelude::*}; +use ui::{HighlightedLabel, KeyBinding, ListItem, ListItemSpacing, prelude::*}; use util::ResultExt; use workspace::{ModalView, Workspace, notifications::DetachAndPromptErr}; @@ -632,68 +632,32 @@ impl PickerDelegate for WorktreeListDelegate { .take(7) .collect::(); - let focus_handle = self.focus_handle.clone(); - let icon = if let Some(default_branch) = self.default_branch.clone() - && entry.is_new - { - Some( - IconButton::new("worktree-from-default", IconName::GitBranchAlt) - .on_click(|_, window, cx| { - window.dispatch_action(WorktreeFromDefault.boxed_clone(), cx) - }) - .on_right_click(|_, window, cx| { - window.dispatch_action(WorktreeFromDefaultOnWindow.boxed_clone(), cx) - }) - .tooltip(move |_, cx| { - Tooltip::for_action_in( - format!("From default branch {default_branch}"), - &WorktreeFromDefault, - &focus_handle, - cx, - ) - }), + let (branch_name, sublabel) = if entry.is_new { + ( + Label::new(format!("Create Worktree: \"{}\"…", entry.worktree.branch())) + .truncate() + .into_any_element(), + format!( + "based off {}", + self.base_branch(cx).unwrap_or("the current branch") + ), ) } else { - None - }; - - let branch_name = if entry.is_new { - h_flex() - .gap_1() - .child( - Icon::new(IconName::Plus) - .size(IconSize::Small) - .color(Color::Muted), - ) - .child( - Label::new(format!("Create worktree \"{}\"…", entry.worktree.branch())) - .single_line() - .truncate(), - ) - .into_any_element() - } else { - h_flex() - .gap_1() - .child( - Icon::new(IconName::GitBranch) - .size(IconSize::Small) - .color(Color::Muted), - ) - .child(HighlightedLabel::new( - entry.worktree.branch().to_owned(), - entry.positions.clone(), - )) - .truncate() - .into_any_element() - }; - - let sublabel = if entry.is_new { - format!( - "based off {}", - self.base_branch(cx).unwrap_or("the current branch") + let branch = entry.worktree.branch(); + let branch_first_line = branch.lines().next().unwrap_or(branch); + let positions: Vec<_> = entry + .positions + .iter() + .copied() + .filter(|&pos| pos < branch_first_line.len()) + .collect(); + + ( + HighlightedLabel::new(branch_first_line.to_owned(), positions) + .truncate() + .into_any_element(), + path, ) - } else { - format!("at {}", path) }; Some( @@ -704,33 +668,30 @@ impl PickerDelegate for WorktreeListDelegate { .child( v_flex() .w_full() - .overflow_hidden() .child( h_flex() - .gap_6() + .gap_2() .justify_between() .overflow_x_hidden() .child(branch_name) - .when(!entry.is_new, |el| { - el.child( + .when(!entry.is_new, |this| { + this.child( Label::new(sha) .size(LabelSize::Small) .color(Color::Muted) + .buffer_font(cx) .into_element(), ) }), ) .child( - div().max_w_96().child( - Label::new(sublabel) - .size(LabelSize::Small) - .color(Color::Muted) - .truncate() - .into_any_element(), - ), + Label::new(sublabel) + .size(LabelSize::Small) + .color(Color::Muted) + .truncate() + .into_any_element(), ), - ) - .end_slot::(icon), + ), ) } @@ -740,17 +701,42 @@ impl PickerDelegate for WorktreeListDelegate { fn render_footer(&self, _: &mut Window, cx: &mut Context>) -> Option { let focus_handle = self.focus_handle.clone(); + let selected_entry = self.matches.get(self.selected_index); + let is_creating = selected_entry.is_some_and(|entry| entry.is_new); + + let footer_container = h_flex() + .w_full() + .p_1p5() + .gap_0p5() + .justify_end() + .border_t_1() + .border_color(cx.theme().colors().border_variant); + + if is_creating { + let from_default_button = self.default_branch.as_ref().map(|default_branch| { + Button::new( + "worktree-from-default", + format!("Create from: {default_branch}"), + ) + .key_binding( + KeyBinding::for_action_in(&WorktreeFromDefault, &focus_handle, cx) + .map(|kb| kb.size(rems_from_px(12.))), + ) + .on_click(|_, window, cx| { + window.dispatch_action(WorktreeFromDefault.boxed_clone(), cx) + }) + }); - Some( - h_flex() - .w_full() - .p_1p5() - .gap_0p5() - .justify_end() - .border_t_1() - .border_color(cx.theme().colors().border_variant) - .child( - Button::new("open-in-new-window", "Open in new window") + let current_branch = self.base_branch(cx).unwrap_or("current branch"); + + Some( + footer_container + .when_some(from_default_button, |this, button| this.child(button)) + .child( + Button::new( + "worktree-from-current", + format!("Create from: {current_branch}"), + ) .key_binding( KeyBinding::for_action_in(&menu::Confirm, &focus_handle, cx) .map(|kb| kb.size(rems_from_px(12.))), @@ -758,18 +744,38 @@ impl PickerDelegate for WorktreeListDelegate { .on_click(|_, window, cx| { window.dispatch_action(menu::Confirm.boxed_clone(), cx) }), - ) - .child( - Button::new("open-in-window", "Open") - .key_binding( - KeyBinding::for_action_in(&menu::SecondaryConfirm, &focus_handle, cx) + ) + .into_any(), + ) + } else { + Some( + footer_container + .child( + Button::new("open-in-new-window", "Open in New Window") + .key_binding( + KeyBinding::for_action_in(&menu::Confirm, &focus_handle, cx) + .map(|kb| kb.size(rems_from_px(12.))), + ) + .on_click(|_, window, cx| { + window.dispatch_action(menu::Confirm.boxed_clone(), cx) + }), + ) + .child( + Button::new("open-in-window", "Open") + .key_binding( + KeyBinding::for_action_in( + &menu::SecondaryConfirm, + &focus_handle, + cx, + ) .map(|kb| kb.size(rems_from_px(12.))), - ) - .on_click(|_, window, cx| { - window.dispatch_action(menu::SecondaryConfirm.boxed_clone(), cx) - }), - ) - .into_any(), - ) + ) + .on_click(|_, window, cx| { + window.dispatch_action(menu::SecondaryConfirm.boxed_clone(), cx) + }), + ) + .into_any(), + ) + } } }