From 6635758009842dcf901fe341082e496549cb8577 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Thu, 3 Oct 2024 16:18:28 -0400 Subject: [PATCH] vcs_menu: Streamline branch creation from branch selector (#18712) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR streamlines the branch creation from the branch selector when searching for a branch that does not exist. The branch selector will show the available branches, as it does today: Screenshot 2024-10-03 at 4 01 25 PM When entering the name of a branch that does not exist, the picker will be populated with an entry to create a new branch: Screenshot 2024-10-03 at 4 01 37 PM Selecting that entry will create the branch and switch to it. Release Notes: - Streamlined creating a new branch from the branch selector. --- crates/vcs_menu/src/lib.rs | 156 ++++++++++++++++++------------------- 1 file changed, 75 insertions(+), 81 deletions(-) diff --git a/crates/vcs_menu/src/lib.rs b/crates/vcs_menu/src/lib.rs index 091c378ca70c5a0f1b0bfe13fad0574c54055a6e..720a427ae90efedf3004a2b1c062a3090517e85f 100644 --- a/crates/vcs_menu/src/lib.rs +++ b/crates/vcs_menu/src/lib.rs @@ -74,8 +74,23 @@ impl Render for BranchList { } } +#[derive(Debug, Clone)] +enum BranchEntry { + Branch(StringMatch), + NewBranch { name: String }, +} + +impl BranchEntry { + fn name(&self) -> &str { + match self { + Self::Branch(branch) => &branch.string, + Self::NewBranch { name } => &name, + } + } +} + pub struct BranchListDelegate { - matches: Vec, + matches: Vec, all_branches: Vec, workspace: View, selected_index: usize, @@ -194,8 +209,14 @@ impl PickerDelegate for BranchListDelegate { picker .update(&mut cx, |picker, _| { let delegate = &mut picker.delegate; - delegate.matches = matches; + delegate.matches = matches.into_iter().map(BranchEntry::Branch).collect(); if delegate.matches.is_empty() { + if !query.is_empty() { + delegate.matches.push(BranchEntry::NewBranch { + name: query.trim().replace(' ', "-"), + }); + } + delegate.selected_index = 0; } else { delegate.selected_index = @@ -208,32 +229,44 @@ impl PickerDelegate for BranchListDelegate { } fn confirm(&mut self, _: bool, cx: &mut ViewContext>) { - let current_pick = self.selected_index(); - let Some(current_pick) = self - .matches - .get(current_pick) - .map(|pick| pick.string.clone()) - else { + let Some(branch) = self.matches.get(self.selected_index()) else { return; }; - cx.spawn(|picker, mut cx| async move { - picker - .update(&mut cx, |this, cx| { - let project = this.delegate.workspace.read(cx).project().read(cx); - let repo = project - .get_first_worktree_root_repo(cx) - .context("failed to get root repository for first worktree")?; - let status = repo - .change_branch(¤t_pick); - if status.is_err() { - this.delegate.display_error_toast(format!("Failed to checkout branch '{current_pick}', check for conflicts or unstashed files"), cx); - status?; - } - cx.emit(DismissEvent); + cx.spawn({ + let branch = branch.clone(); + |picker, mut cx| async move { + picker + .update(&mut cx, |this, cx| { + let project = this.delegate.workspace.read(cx).project().read(cx); + let repo = project + .get_first_worktree_root_repo(cx) + .context("failed to get root repository for first worktree")?; - Ok::<(), anyhow::Error>(()) - }) - .log_err(); + let branch_to_checkout = match branch { + BranchEntry::Branch(branch) => branch.string, + BranchEntry::NewBranch { name: branch_name } => { + let status = repo.create_branch(&branch_name); + if status.is_err() { + this.delegate.display_error_toast(format!("Failed to create branch '{branch_name}', check for conflicts or unstashed files"), cx); + status?; + } + + branch_name + } + }; + + let status = repo.change_branch(&branch_to_checkout); + if status.is_err() { + this.delegate.display_error_toast(format!("Failed to checkout branch '{branch_to_checkout}', check for conflicts or unstashed files"), cx); + status?; + } + + cx.emit(DismissEvent); + + Ok::<(), anyhow::Error>(()) + }) + .log_err(); + } }) .detach(); } @@ -250,19 +283,28 @@ impl PickerDelegate for BranchListDelegate { ) -> Option { let hit = &self.matches[ix]; let shortened_branch_name = - util::truncate_and_trailoff(&hit.string, self.branch_name_trailoff_after); - let highlights: Vec<_> = hit - .positions - .iter() - .filter(|index| index < &&self.branch_name_trailoff_after) - .copied() - .collect(); + util::truncate_and_trailoff(&hit.name(), self.branch_name_trailoff_after); + Some( ListItem::new(SharedString::from(format!("vcs-menu-{ix}"))) .inset(true) .spacing(ListItemSpacing::Sparse) .selected(selected) - .start_slot(HighlightedLabel::new(shortened_branch_name, highlights)), + .map(|parent| match hit { + BranchEntry::Branch(branch) => { + let highlights: Vec<_> = branch + .positions + .iter() + .filter(|index| index < &&self.branch_name_trailoff_after) + .copied() + .collect(); + + parent.child(HighlightedLabel::new(shortened_branch_name, highlights)) + } + BranchEntry::NewBranch { name } => { + parent.child(Label::new(format!("Create branch '{name}'"))) + } + }), ) } @@ -289,52 +331,4 @@ impl PickerDelegate for BranchListDelegate { }; Some(v_flex().mt_1().child(label).into_any_element()) } - - fn render_footer(&self, cx: &mut ViewContext>) -> Option { - if self.last_query.is_empty() { - return None; - } - - Some( - h_flex() - .p_2() - .border_t_1() - .border_color(cx.theme().colors().border_variant) - .justify_end() - .child(h_flex().w_full()) - .child( - Button::new("branch-picker-create-branch-button", "Create Branch") - .icon(IconName::Plus) - .icon_size(IconSize::Small) - .icon_color(Color::Muted) - .icon_position(IconPosition::Start) - .on_click(cx.listener(|_, _, cx| { - cx.spawn(|picker, mut cx| async move { - picker.update(&mut cx, |this, cx| { - let project = - this.delegate.workspace.read(cx).project().read(cx); - let current_pick = &this.delegate.last_query; - let repo = project.get_first_worktree_root_repo(cx).context( - "failed to get root repository for first worktree", - )?; - let status = repo.create_branch(current_pick); - if status.is_err() { - this.delegate.display_error_toast(format!("Failed to create branch '{current_pick}', check for conflicts or unstashed files"), cx); - status?; - } - let status = repo.change_branch(current_pick); - if status.is_err() { - this.delegate.display_error_toast(format!("Failed to check branch '{current_pick}', check for conflicts or unstashed files"), cx); - status?; - } - this.cancel(&Default::default(), cx); - Ok::<(), anyhow::Error>(()) - }) - }) - .detach_and_log_err(cx); - })) - ) - .into_any_element(), - ) - } }