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 | 92 | pub struct BranchListDelegate { |
| 78 | − matches: Vec<StringMatch>, | |
| 93 | + matches: Vec<BranchEntry>, | |
| 79 | 94 | all_branches: Vec<Branch>, |
| 80 | 95 | workspace: View<Workspace>, |
| 81 | 96 | selected_index: usize, |
| @@ -194,8 +209,14 @@ impl PickerDelegate for BranchListDelegate { · 112 unmodified lines | ||
| 194 | 209 | picker |
| 195 | 210 | .update(&mut cx, |picker, _| { |
| 196 | 211 | let delegate = &mut picker.delegate; |
| 197 | − delegate.matches = matches; | |
| 212 | + delegate.matches = matches.into_iter().map(BranchEntry::Branch).collect(); | |
| 198 | 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 | 220 | delegate.selected_index = 0; |
| 200 | 221 | } else { |
| 201 | 222 | delegate.selected_index = |
| @@ -208,32 +229,44 @@ impl PickerDelegate for BranchListDelegate { · 6 unmodified lines | ||
| 208 | 229 | } |
| 209 | 230 | |
| 210 | 231 | 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 { | |
| 217 | 233 | return; |
| 218 | 234 | }; |
| 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(¤t_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")?; | |
| 233 | 244 | |
| 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 | + } | |
| 237 | 270 | }) |
| 238 | 271 | .detach(); |
| 239 | 272 | } |
| @@ -250,19 +283,28 @@ impl PickerDelegate for BranchListDelegate { · 10 unmodified lines | ||
| 250 | 283 | ) -> Option<Self::ListItem> { |
| 251 | 284 | let hit = &self.matches[ix]; |
| 252 | 285 | 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 | + | |
| 260 | 288 | Some( |
| 261 | 289 | ListItem::new(SharedString::from(format!("vcs-menu-{ix}"))) |
| 262 | 290 | .inset(true) |
| 263 | 291 | .spacing(ListItemSpacing::Sparse) |
| 264 | 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 | 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 | } |