@@ -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, ¤t_commit, false)?;
+
+ Ok(())
+ }
}
fn read_status(status: git2::Status) -> Option<GitFileStatus> {
@@ -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)]
@@ -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(¤t_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(¤t_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(¤t_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(¤t_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
+ }
+ }
}
@@ -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
+ ),
+ },
+ }
+ }),
}
}