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.
Unified · Split
| @@ -74,8 +74,23 @@ impl Render for BranchList { · 73 unmodified lines | |||
| 74 | } | 74 | } |
| 75 | } | 75 | } |
| 76 | 76 | ||
| 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 | |||
| 77 | pub struct BranchListDelegate { | 92 | pub 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 | picker | 209 | 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 | picker | 236 | 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 = project | 239 | .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 = repo | 242 | .get_first_worktree_root_repo(cx) |
| 227 | .change_branch(¤t_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<_> = hit | 287 | |
| 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 | } |