History

vcs_menu: Streamline branch creation from branch selector (#18712)

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: <img width="576" alt="Screenshot 2024-10-03 at 4 01 25 PM" src="https://github.com/user-attachments/assets/e1904f5b-4aad-4f88-901d-ab9422ec18bb"> When entering the name of a branch that does not exist, the picker will be populated with an entry to create a new branch: <img width="570" alt="Screenshot 2024-10-03 at 4 01 37 PM" src="https://github.com/user-attachments/assets/07f8d12c-9422-4fd8-a6dc-ae450e297a13"> Selecting that entry will create the branch and switch to it. Release Notes: - Streamlined creating a new branch from the branch selector.

Marshall Bowers committed 2 years ago

Commit 66357581 parent 8d6fa951 file · +75 −81Browse files

Unified · Split

crates/vcs_menu/src/lib.rs+75−81
@@ -74,8 +74,23 @@ impl Render for BranchList { · 73 unmodified lines
7474 }
7575 }
7676
77+#[derive(Debug, Clone)]
78+enum BranchEntry {
79+ Branch(StringMatch),
80+ NewBranch { name: String },
81+}
82+
83+impl BranchEntry {
84+ fn name(&self) -> &str {
85+ match self {
86+ Self::Branch(branch) => &branch.string,
87+ Self::NewBranch { name } => &name,
88+ }
89+ }
90+}
91+
7792 pub struct BranchListDelegate {
78− matches: Vec<StringMatch>,
93+ matches: Vec<BranchEntry>,
7994 all_branches: Vec<Branch>,
8095 workspace: View<Workspace>,
8196 selected_index: usize,
@@ -194,8 +209,14 @@ impl PickerDelegate for BranchListDelegate { · 112 unmodified lines
194209 picker
195210 .update(&mut cx, |picker, _| {
196211 let delegate = &mut picker.delegate;
197− delegate.matches = matches;
212+ delegate.matches = matches.into_iter().map(BranchEntry::Branch).collect();
198213 if delegate.matches.is_empty() {
214+ if !query.is_empty() {
215+ delegate.matches.push(BranchEntry::NewBranch {
216+ name: query.trim().replace(' ', "-"),
217+ });
218+ }
219+
199220 delegate.selected_index = 0;
200221 } else {
201222 delegate.selected_index =
@@ -208,32 +229,44 @@ impl PickerDelegate for BranchListDelegate { · 6 unmodified lines
208229 }
209230
210231 fn confirm(&mut self, _: bool, cx: &mut ViewContext<Picker<Self>>) {
211− let current_pick = self.selected_index();
212− let Some(current_pick) = self
213− .matches
214− .get(current_pick)
215− .map(|pick| pick.string.clone())
216− else {
232+ let Some(branch) = self.matches.get(self.selected_index()) else {
217233 return;
218234 };
219− cx.spawn(|picker, mut cx| async move {
220− picker
221− .update(&mut cx, |this, cx| {
222− let project = this.delegate.workspace.read(cx).project().read(cx);
223− let repo = project
224− .get_first_worktree_root_repo(cx)
225− .context("failed to get root repository for first worktree")?;
226− let status = repo
227− .change_branch(&current_pick);
228− if status.is_err() {
229− this.delegate.display_error_toast(format!("Failed to checkout branch '{current_pick}', check for conflicts or unstashed files"), cx);
230− status?;
231− }
232− cx.emit(DismissEvent);
235+ cx.spawn({
236+ let branch = branch.clone();
237+ |picker, mut cx| async move {
238+ picker
239+ .update(&mut cx, |this, cx| {
240+ let project = this.delegate.workspace.read(cx).project().read(cx);
241+ let repo = project
242+ .get_first_worktree_root_repo(cx)
243+ .context("failed to get root repository for first worktree")?;
233244
234− Ok::<(), anyhow::Error>(())
235− })
236− .log_err();
245+ let branch_to_checkout = match branch {
246+ BranchEntry::Branch(branch) => branch.string,
247+ BranchEntry::NewBranch { name: branch_name } => {
248+ let status = repo.create_branch(&branch_name);
249+ if status.is_err() {
250+ this.delegate.display_error_toast(format!("Failed to create branch '{branch_name}', check for conflicts or unstashed files"), cx);
251+ status?;
252+ }
253+
254+ branch_name
255+ }
256+ };
257+
258+ let status = repo.change_branch(&branch_to_checkout);
259+ if status.is_err() {
260+ this.delegate.display_error_toast(format!("Failed to checkout branch '{branch_to_checkout}', check for conflicts or unstashed files"), cx);
261+ status?;
262+ }
263+
264+ cx.emit(DismissEvent);
265+
266+ Ok::<(), anyhow::Error>(())
267+ })
268+ .log_err();
269+ }
237270 })
238271 .detach();
239272 }
@@ -250,19 +283,28 @@ impl PickerDelegate for BranchListDelegate { · 10 unmodified lines
250283 ) -> Option<Self::ListItem> {
251284 let hit = &self.matches[ix];
252285 let shortened_branch_name =
253− util::truncate_and_trailoff(&hit.string, self.branch_name_trailoff_after);
254− let highlights: Vec<_> = hit
255− .positions
256− .iter()
257− .filter(|index| index < &&self.branch_name_trailoff_after)
258− .copied()
259− .collect();
286+ util::truncate_and_trailoff(&hit.name(), self.branch_name_trailoff_after);
287+
260288 Some(
261289 ListItem::new(SharedString::from(format!("vcs-menu-{ix}")))
262290 .inset(true)
263291 .spacing(ListItemSpacing::Sparse)
264292 .selected(selected)
265− .start_slot(HighlightedLabel::new(shortened_branch_name, highlights)),
293+ .map(|parent| match hit {
294+ BranchEntry::Branch(branch) => {
295+ let highlights: Vec<_> = branch
296+ .positions
297+ .iter()
298+ .filter(|index| index < &&self.branch_name_trailoff_after)
299+ .copied()
300+ .collect();
301+
302+ parent.child(HighlightedLabel::new(shortened_branch_name, highlights))
303+ }
304+ BranchEntry::NewBranch { name } => {
305+ parent.child(Label::new(format!("Create branch '{name}'")))
306+ }
307+ }),
266308 )
267309 }
268310
@@ -289,52 +331,4 @@ impl PickerDelegate for BranchListDelegate { · 20 unmodified lines
289331 };
290332 Some(v_flex().mt_1().child(label).into_any_element())
291333 }
292−
293− fn render_footer(&self, cx: &mut ViewContext<Picker<Self>>) -> Option<AnyElement> {
294− if self.last_query.is_empty() {
295− return None;
296− }
297−
298− Some(
299− h_flex()
300− .p_2()
301− .border_t_1()
302− .border_color(cx.theme().colors().border_variant)
303− .justify_end()
304− .child(h_flex().w_full())
305− .child(
306− Button::new("branch-picker-create-branch-button", "Create Branch")
307− .icon(IconName::Plus)
308− .icon_size(IconSize::Small)
309− .icon_color(Color::Muted)
310− .icon_position(IconPosition::Start)
311− .on_click(cx.listener(|_, _, cx| {
312− cx.spawn(|picker, mut cx| async move {
313− picker.update(&mut cx, |this, cx| {
314− let project =
315− this.delegate.workspace.read(cx).project().read(cx);
316− let current_pick = &this.delegate.last_query;
317− let repo = project.get_first_worktree_root_repo(cx).context(
318− "failed to get root repository for first worktree",
319− )?;
320− let status = repo.create_branch(current_pick);
321− if status.is_err() {
322− this.delegate.display_error_toast(format!("Failed to create branch '{current_pick}', check for conflicts or unstashed files"), cx);
323− status?;
324− }
325− let status = repo.change_branch(current_pick);
326− if status.is_err() {
327− this.delegate.display_error_toast(format!("Failed to check branch '{current_pick}', check for conflicts or unstashed files"), cx);
328− status?;
329− }
330− this.cancel(&Default::default(), cx);
331− Ok::<(), anyhow::Error>(())
332− })
333− })
334− .detach_and_log_err(cx);
335− }))
336− )
337− .into_any_element(),
338− )
339− }
340334 }