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
74 }74 }
75}75}
76 76
77#[derive(Debug, Clone)]
78enum BranchEntry {
79 Branch(StringMatch),
80 NewBranch { name: String },
81}
82
83impl BranchEntry {
84 fn name(&self) -> &str {
85 match self {
86 Self::Branch(branch) => &branch.string,
87 Self::NewBranch { name } => &name,
88 }
89 }
90}
91
77pub struct BranchListDelegate {92pub struct BranchListDelegate {
78 matches: Vec<StringMatch>,93 matches: Vec<BranchEntry>,
79 all_branches: Vec<Branch>,94 all_branches: Vec<Branch>,
80 workspace: View<Workspace>,95 workspace: View<Workspace>,
81 selected_index: usize,96 selected_index: usize,
@@ -194,8 +209,14 @@ impl PickerDelegate for BranchListDelegate { · 112 unmodified lines
194 picker209 picker
195 .update(&mut cx, |picker, _| {210 .update(&mut cx, |picker, _| {
196 let delegate = &mut picker.delegate;211 let delegate = &mut picker.delegate;
197 delegate.matches = matches;212 delegate.matches = matches.into_iter().map(BranchEntry::Branch).collect();
198 if delegate.matches.is_empty() {213 if delegate.matches.is_empty() {
214 if !query.is_empty() {
215 delegate.matches.push(BranchEntry::NewBranch {
216 name: query.trim().replace(' ', "-"),
217 });
218 }
219
199 delegate.selected_index = 0;220 delegate.selected_index = 0;
200 } else {221 } else {
201 delegate.selected_index =222 delegate.selected_index =
@@ -208,32 +229,44 @@ impl PickerDelegate for BranchListDelegate { · 6 unmodified lines
208 }229 }
209 230
210 fn confirm(&mut self, _: bool, cx: &mut ViewContext<Picker<Self>>) {231 fn confirm(&mut self, _: bool, cx: &mut ViewContext<Picker<Self>>) {
211 let current_pick = self.selected_index();232 let Some(branch) = self.matches.get(self.selected_index()) else {
212 let Some(current_pick) = self
213 .matches
214 .get(current_pick)
215 .map(|pick| pick.string.clone())
216 else {
217 return;233 return;
218 };234 };
219 cx.spawn(|picker, mut cx| async move {235 cx.spawn({
220 picker236 let branch = branch.clone();
221 .update(&mut cx, |this, cx| {237 |picker, mut cx| async move {
222 let project = this.delegate.workspace.read(cx).project().read(cx);238 picker
223 let repo = project239 .update(&mut cx, |this, cx| {
224 .get_first_worktree_root_repo(cx)240 let project = this.delegate.workspace.read(cx).project().read(cx);
225 .context("failed to get root repository for first worktree")?;241 let repo = project
226 let status = repo242 .get_first_worktree_root_repo(cx)
227 .change_branch(&current_pick);243 .context("failed to get root repository for first worktree")?;
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);
233 244
234 Ok::<(), anyhow::Error>(())245 let branch_to_checkout = match branch {
235 })246 BranchEntry::Branch(branch) => branch.string,
236 .log_err();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 }
237 })270 })
238 .detach();271 .detach();
239 }272 }
@@ -250,19 +283,28 @@ impl PickerDelegate for BranchListDelegate { · 10 unmodified lines
250 ) -> Option<Self::ListItem> {283 ) -> Option<Self::ListItem> {
251 let hit = &self.matches[ix];284 let hit = &self.matches[ix];
252 let shortened_branch_name =285 let shortened_branch_name =
253 util::truncate_and_trailoff(&hit.string, self.branch_name_trailoff_after);286 util::truncate_and_trailoff(&hit.name(), self.branch_name_trailoff_after);
254 let highlights: Vec<_> = hit287
255 .positions
256 .iter()
257 .filter(|index| index < &&self.branch_name_trailoff_after)
258 .copied()
259 .collect();
260 Some(288 Some(
261 ListItem::new(SharedString::from(format!("vcs-menu-{ix}")))289 ListItem::new(SharedString::from(format!("vcs-menu-{ix}")))
262 .inset(true)290 .inset(true)
263 .spacing(ListItemSpacing::Sparse)291 .spacing(ListItemSpacing::Sparse)
264 .selected(selected)292 .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 }),
266 )308 )
267 }309 }
268 310
@@ -289,52 +331,4 @@ impl PickerDelegate for BranchListDelegate { · 20 unmodified lines
289 };331 };
290 Some(v_flex().mt_1().child(label).into_any_element())332 Some(v_flex().mt_1().child(label).into_any_element())
291 }333 }
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 }
340}334}