Piotr/z 2556 add create branch button (#2696)

Piotr Osiewicz created

Release Notes:

- N/A

Change summary

crates/fs/src/repository.rs     |   9 ++
crates/theme/src/theme.rs       |   2 
crates/vcs_menu/src/lib.rs      | 137 ++++++++++++++++++++++++++--------
styles/src/style_tree/picker.ts |  42 ++++++++--
4 files changed, 147 insertions(+), 43 deletions(-)

Detailed changes

crates/fs/src/repository.rs 🔗

@@ -39,6 +39,9 @@ pub trait GitRepository: Send {
     fn change_branch(&self, _: &str) -> Result<()> {
         Ok(())
     }
+    fn create_branch(&self, _: &str) -> Result<()> {
+        Ok(())
+    }
 }
 
 impl std::fmt::Debug for dyn GitRepository {
@@ -152,6 +155,12 @@ impl GitRepository for LibGitRepository {
         )?;
         Ok(())
     }
+    fn create_branch(&self, name: &str) -> Result<()> {
+        let current_commit = self.head()?.peel_to_commit()?;
+        self.branch(name, &current_commit, false)?;
+
+        Ok(())
+    }
 }
 
 fn read_status(status: git2::Status) -> Option<GitFileStatus> {

crates/theme/src/theme.rs 🔗

@@ -586,7 +586,7 @@ pub struct Picker {
     pub no_matches: ContainedLabel,
     pub item: Toggleable<Interactive<ContainedLabel>>,
     pub header: ContainedLabel,
-    pub footer: ContainedLabel,
+    pub footer: Interactive<ContainedLabel>,
 }
 
 #[derive(Clone, Debug, Deserialize, Default, JsonSchema)]

crates/vcs_menu/src/lib.rs 🔗

@@ -1,6 +1,11 @@
 use anyhow::{anyhow, bail, Result};
 use fuzzy::{StringMatch, StringMatchCandidate};
-use gpui::{actions, elements::*, AppContext, MouseState, Task, ViewContext, ViewHandle};
+use gpui::{
+    actions,
+    elements::*,
+    platform::{CursorStyle, MouseButton},
+    AppContext, MouseState, Task, ViewContext, ViewHandle,
+};
 use picker::{Picker, PickerDelegate, PickerEvent};
 use std::{ops::Not, sync::Arc};
 use util::ResultExt;
@@ -70,6 +75,14 @@ pub struct BranchListDelegate {
     branch_name_trailoff_after: usize,
 }
 
+impl BranchListDelegate {
+    fn display_error_toast(&self, message: String, cx: &mut ViewContext<BranchList>) {
+        const GIT_CHECKOUT_FAILURE_ID: usize = 2048;
+        self.workspace.update(cx, |model, ctx| {
+            model.show_toast(Toast::new(GIT_CHECKOUT_FAILURE_ID, message), ctx)
+        });
+    }
+}
 impl PickerDelegate for BranchListDelegate {
     fn placeholder_text(&self) -> Arc<str> {
         "Select branch...".into()
@@ -171,40 +184,39 @@ impl PickerDelegate for BranchListDelegate {
         let current_pick = self.selected_index();
         let current_pick = self.matches[current_pick].string.clone();
         cx.spawn(|picker, mut cx| async move {
-            picker.update(&mut cx, |this, cx| {
-                let project = this.delegate().workspace.read(cx).project().read(cx);
-                let mut cwd = project
-                .visible_worktrees(cx)
-                .next()
-                .ok_or_else(|| anyhow!("There are no visisible worktrees."))?
-                .read(cx)
-                .abs_path()
-                .to_path_buf();
-                cwd.push(".git");
-                let status = project
-                    .fs()
-                    .open_repo(&cwd)
-                    .ok_or_else(|| anyhow!("Could not open repository at path `{}`", cwd.as_os_str().to_string_lossy()))?
-                    .lock()
-                    .change_branch(&current_pick);
-                if status.is_err() {
-                    const GIT_CHECKOUT_FAILURE_ID: usize = 2048;
-                    this.delegate().workspace.update(cx, |model, ctx| {
-                        model.show_toast(
-                            Toast::new(
-                                GIT_CHECKOUT_FAILURE_ID,
-                                format!("Failed to checkout branch '{current_pick}', check for conflicts or unstashed files"),
-                            ),
-                            ctx,
-                        )
-                    });
-                    status?;
-                }
-                cx.emit(PickerEvent::Dismiss);
+            picker
+                .update(&mut cx, |this, cx| {
+                    let project = this.delegate().workspace.read(cx).project().read(cx);
+                    let mut cwd = project
+                        .visible_worktrees(cx)
+                        .next()
+                        .ok_or_else(|| anyhow!("There are no visisible worktrees."))?
+                        .read(cx)
+                        .abs_path()
+                        .to_path_buf();
+                    cwd.push(".git");
+                    let status = project
+                        .fs()
+                        .open_repo(&cwd)
+                        .ok_or_else(|| {
+                            anyhow!(
+                                "Could not open repository at path `{}`",
+                                cwd.as_os_str().to_string_lossy()
+                            )
+                        })?
+                        .lock()
+                        .change_branch(&current_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(PickerEvent::Dismiss);
 
-                Ok::<(), anyhow::Error>(())
-            }).log_err();
-        }).detach();
+                    Ok::<(), anyhow::Error>(())
+                })
+                .log_err();
+        })
+        .detach();
     }
 
     fn dismissed(&mut self, cx: &mut ViewContext<Picker<Self>>) {
@@ -270,4 +282,61 @@ impl PickerDelegate for BranchListDelegate {
         };
         Some(label.into_any())
     }
+    fn render_footer(
+        &self,
+        cx: &mut ViewContext<Picker<Self>>,
+    ) -> Option<AnyElement<Picker<Self>>> {
+        if !self.last_query.is_empty() {
+            let theme = &theme::current(cx);
+            let style = theme.picker.footer.clone();
+            enum BranchCreateButton {}
+            Some(
+                Flex::row().with_child(MouseEventHandler::<BranchCreateButton, _>::new(0, cx, |state, _| {
+                    let style = style.style_for(state);
+                    Label::new("Create branch", style.label.clone())
+                        .contained()
+                        .with_style(style.container)
+                })
+                .with_cursor_style(CursorStyle::PointingHand)
+                .on_down(MouseButton::Left, |_, _, 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 mut cwd = project
+                            .visible_worktrees(cx)
+                            .next()
+                            .ok_or_else(|| anyhow!("There are no visisible worktrees."))?
+                            .read(cx)
+                            .abs_path()
+                            .to_path_buf();
+                            cwd.push(".git");
+                            let repo = project
+                                .fs()
+                                .open_repo(&cwd)
+                                .ok_or_else(|| anyhow!("Could not open repository at path `{}`", cwd.as_os_str().to_string_lossy()))?;
+                            let repo = repo
+                                .lock();
+                            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 chec branch '{current_pick}', check for conflicts or unstashed files"), cx);
+                                status?;
+                            }
+                            cx.emit(PickerEvent::Dismiss);
+                            Ok::<(), anyhow::Error>(())
+                })
+                    }).detach();
+                })).aligned().right()
+                .into_any(),
+            )
+        } else {
+            None
+        }
+    }
 }

styles/src/style_tree/picker.ts 🔗

@@ -119,14 +119,40 @@ export default function picker(): any {
                 right: 8,
             },
         },
-        footer: {
-            text: text(theme.lowest, "sans", "variant", { size: "xs" }),
-            margin: {
-                top: 1,
-                left: 8,
-                right: 8,
+        footer: interactive({
+            base: {
+                text: text(theme.lowest, "sans", "base", { size: "xs" }),
+                padding: {
+                    bottom: 4,
+                    left: 12,
+                    right: 12,
+                    top: 4,
+                },
+                margin: {
+                    top: 1,
+                    left: 4,
+                    right: 4,
+                },
+                corner_radius: 8,
+                background: with_opacity(
+                    background(theme.lowest, "active"),
+                    0.5
+                ),
             },
-
-        }
+            state: {
+                hovered: {
+                    background: with_opacity(
+                        background(theme.lowest, "hovered"),
+                        0.5
+                    ),
+                },
+                clicked: {
+                    background: with_opacity(
+                        background(theme.lowest, "pressed"),
+                        0.5
+                    ),
+                },
+            }
+        }),
     }
 }