diff --git a/crates/collab_ui/src/branch_list.rs b/crates/collab_ui/src/branch_list.rs new file mode 100644 index 0000000000000000000000000000000000000000..8d755e8aa456abdfc4cc7656178b25060147ef46 --- /dev/null +++ b/crates/collab_ui/src/branch_list.rs @@ -0,0 +1,178 @@ +use client::{ContactRequestStatus, User, UserStore}; +use fuzzy::{StringMatch, StringMatchCandidate}; +use gpui::{elements::*, AppContext, ModelHandle, MouseState, Task, ViewContext}; +use picker::{Picker, PickerDelegate, PickerEvent}; +use project::Project; +use std::sync::Arc; +use util::{ResultExt, TryFutureExt}; + +pub fn init(cx: &mut AppContext) { + Picker::::init(cx); +} + +pub type BranchList = Picker; + +pub fn build_branch_list( + project: ModelHandle, + cx: &mut ViewContext, +) -> BranchList { + Picker::new( + BranchListDelegate { + branches: vec!["Foo".into(), "bar/baz".into()], + matches: vec![], + project, + selected_index: 0, + }, + cx, + ) + .with_theme(|theme| theme.picker.clone()) +} + +pub struct BranchListDelegate { + branches: Vec, + matches: Vec, + project: ModelHandle, + selected_index: usize, +} + +impl PickerDelegate for BranchListDelegate { + fn placeholder_text(&self) -> Arc { + "Select branch...".into() + } + + fn match_count(&self) -> usize { + self.matches.len() + } + + fn selected_index(&self) -> usize { + self.selected_index + } + + fn set_selected_index(&mut self, ix: usize, _: &mut ViewContext>) { + self.selected_index = ix; + } + + fn update_matches(&mut self, query: String, cx: &mut ViewContext>) -> Task<()> { + cx.spawn(move |picker, mut cx| async move { + let candidates = picker + .read_with(&mut cx, |view, cx| { + let delegate = view.delegate(); + let project = delegate.project.read(&cx); + let mut cwd = project + .visible_worktrees(cx) + .next() + .unwrap() + .read(cx) + .root_entry() + .unwrap() + .path + .to_path_buf(); + cwd.push(".git"); + let branches = project.fs().open_repo(&cwd).unwrap().lock().branches(); + branches + .unwrap() + .iter() + .cloned() + .enumerate() + .map(|(ix, command)| StringMatchCandidate { + id: ix, + string: command.clone(), + char_bag: command.chars().collect(), + }) + .collect::>() + }) + .unwrap(); + let matches = if query.is_empty() { + candidates + .into_iter() + .enumerate() + .map(|(index, candidate)| StringMatch { + candidate_id: index, + string: candidate.string, + positions: Vec::new(), + score: 0.0, + }) + .collect() + } else { + fuzzy::match_strings( + &candidates, + &query, + true, + 10000, + &Default::default(), + cx.background(), + ) + .await + }; + picker + .update(&mut cx, |picker, _| { + let delegate = picker.delegate_mut(); + //delegate.branches = actions; + delegate.matches = matches; + if delegate.matches.is_empty() { + delegate.selected_index = 0; + } else { + delegate.selected_index = + core::cmp::min(delegate.selected_index, delegate.matches.len() - 1); + } + }) + .log_err(); + }) + } + + fn confirm(&mut self, cx: &mut ViewContext>) { + log::error!("confirm {}", self.selected_index()); + let current_pick = self.selected_index(); + let current_pick = self.matches[current_pick].string.clone(); + log::error!("Hi? {current_pick}"); + let project = self.project.read(cx); + let mut cwd = project + .visible_worktrees(cx) + .next() + .unwrap() + .read(cx) + .root_entry() + .unwrap() + .path + .to_path_buf(); + cwd.push(".git"); + log::error!("{current_pick}"); + project + .fs() + .open_repo(&cwd) + .unwrap() + .lock() + .change_branch(¤t_pick) + .log_err(); + cx.emit(PickerEvent::Dismiss); + } + + fn dismissed(&mut self, cx: &mut ViewContext>) { + cx.emit(PickerEvent::Dismiss); + } + + fn render_match( + &self, + ix: usize, + mouse_state: &mut MouseState, + selected: bool, + cx: &gpui::AppContext, + ) -> AnyElement> { + let theme = &theme::current(cx); + let user = &self.matches[ix]; + let style = theme.picker.item.in_state(selected).style_for(mouse_state); + Flex::row() + .with_child( + Label::new(user.string.clone(), style.label.clone()) + .with_highlights(user.positions.clone()) + .contained() + .aligned() + .left(), + ) + .contained() + .with_style(style.container) + .constrained() + .with_height(theme.contact_finder.row_height) + .into_any() + } +} diff --git a/crates/collab_ui/src/collab_titlebar_item.rs b/crates/collab_ui/src/collab_titlebar_item.rs index 5caebb9f0c033d9fda6aed87d155f4e1e5b9c655..338ba6cf27de82c786834e584510b211a89d2583 100644 --- a/crates/collab_ui/src/collab_titlebar_item.rs +++ b/crates/collab_ui/src/collab_titlebar_item.rs @@ -1,5 +1,8 @@ use crate::{ - contact_notification::ContactNotification, contacts_popover, face_pile::FacePile, + branch_list::{build_branch_list, BranchList}, + contact_notification::ContactNotification, + contacts_popover, + face_pile::FacePile, toggle_deafen, toggle_mute, toggle_screen_sharing, LeaveCall, ToggleDeafen, ToggleMute, ToggleScreenSharing, }; @@ -12,12 +15,14 @@ use gpui::{ actions, color::Color, elements::*, + fonts::TextStyle, geometry::{rect::RectF, vector::vec2f, PathBuilder}, json::{self, ToJson}, platform::{CursorStyle, MouseButton}, AppContext, Entity, ImageData, LayoutContext, ModelHandle, SceneBuilder, Subscription, View, ViewContext, ViewHandle, WeakViewHandle, }; +use picker::PickerEvent; use project::{Project, RepositoryEntry}; use std::{ops::Range, sync::Arc}; use theme::{AvatarStyle, Theme}; @@ -31,6 +36,8 @@ actions!( [ ToggleContactsMenu, ToggleUserMenu, + ToggleVcsMenu, + SwitchBranch, ShareProject, UnshareProject, ] @@ -41,6 +48,7 @@ pub fn init(cx: &mut AppContext) { cx.add_action(CollabTitlebarItem::share_project); cx.add_action(CollabTitlebarItem::unshare_project); cx.add_action(CollabTitlebarItem::toggle_user_menu); + cx.add_action(CollabTitlebarItem::toggle_vcs_menu); } pub struct CollabTitlebarItem { @@ -49,6 +57,7 @@ pub struct CollabTitlebarItem { client: Arc, workspace: WeakViewHandle, contacts_popover: Option>, + branch_popover: Option>, user_menu: ViewHandle, _subscriptions: Vec, } @@ -69,12 +78,11 @@ impl View for CollabTitlebarItem { return Empty::new().into_any(); }; - let project = self.project.read(cx); let theme = theme::current(cx).clone(); let mut left_container = Flex::row(); let mut right_container = Flex::row().align_children_center(); - left_container.add_child(self.collect_title_root_names(&project, theme.clone(), cx)); + left_container.add_child(self.collect_title_root_names(theme.clone(), cx)); let user = self.user_store.read(cx).current_user(); let peer_id = self.client.peer_id(); @@ -182,22 +190,28 @@ impl CollabTitlebarItem { menu.set_position_mode(OverlayPositionMode::Local); menu }), + branch_popover: None, _subscriptions: subscriptions, } } fn collect_title_root_names( &self, - project: &Project, theme: Arc, - cx: &ViewContext, + cx: &mut ViewContext, ) -> AnyElement { - let mut names_and_branches = project.visible_worktrees(cx).map(|worktree| { - let worktree = worktree.read(cx); - (worktree.root_name(), worktree.root_git_entry()) - }); + let project = self.project.read(cx); + + let (name, entry) = { + let mut names_and_branches = project.visible_worktrees(cx).map(|worktree| { + let worktree = worktree.read(cx); + (worktree.root_name(), worktree.root_git_entry()) + }); - let (name, entry) = names_and_branches.next().unwrap_or(("", None)); + names_and_branches.next().unwrap_or(("", None)) + }; + + let name = name.to_owned(); let branch_prepended = entry .as_ref() .and_then(RepositoryEntry::branch) @@ -212,22 +226,37 @@ impl CollabTitlebarItem { text: text_style, highlight_text: Some(highlight), }; + let highlights = (0..name.len()).into_iter().collect(); let mut ret = Flex::row().with_child( - Label::new(name.to_owned(), style.clone()) - .with_highlights((0..name.len()).into_iter().collect()) - .contained() - .aligned() - .left() - .into_any_named("title-project-name"), - ); - if let Some(git_branch) = branch_prepended { - ret = ret.with_child( - Label::new(git_branch, style) + Stack::new().with_child( + Label::new(name, style.clone()) + .with_highlights(highlights) .contained() - .with_margin_right(item_spacing) .aligned() .left() - .into_any_named("title-project-branch"), + .into_any_named("title-project-name"), + ), + ); + if let Some(git_branch) = branch_prepended { + ret = ret.with_child( + Stack::new() + .with_child( + MouseEventHandler::::new(0, cx, |state, _| { + Label::new(git_branch, style) + .contained() + .with_margin_right(item_spacing) + .aligned() + .left() + .into_any_named("title-project-branch") + }) + .with_cursor_style(CursorStyle::PointingHand) + .on_click(MouseButton::Left, move |_, this, cx| { + this.toggle_vcs_menu(&Default::default(), cx) + }), + ) + .with_children( + self.render_branches_popover_host(&theme.workspace.titlebar, cx), + ), ) } ret.into_any() @@ -320,7 +349,55 @@ impl CollabTitlebarItem { user_menu.toggle(Default::default(), AnchorCorner::TopRight, items, cx); }); } + fn render_branches_popover_host<'a>( + &'a self, + _theme: &'a theme::Titlebar, + cx: &'a mut ViewContext, + ) -> Option> { + self.branch_popover.as_ref().map(|child| { + let theme = theme::current(cx).clone(); + let child = ChildView::new(child, cx); + let child = MouseEventHandler::::new(0, cx, |_, _| { + Flex::column() + .with_child(child.flex(1., true)) + .contained() + .with_style(theme.contacts_popover.container) + .constrained() + .with_width(theme.contacts_popover.width) + .with_height(theme.contacts_popover.height) + }) + .on_click(MouseButton::Left, |_, _, _| {}) + .on_down_out(MouseButton::Left, move |_, _, cx| cx.emit(())) + .into_any(); + + Overlay::new(child) + .with_fit_mode(OverlayFitMode::SwitchAnchor) + .with_anchor_corner(AnchorCorner::TopLeft) + .with_z_index(999) + .aligned() + .bottom() + .left() + .into_any() + }) + } + pub fn toggle_vcs_menu(&mut self, _: &ToggleVcsMenu, cx: &mut ViewContext) { + if self.branch_popover.take().is_none() { + let view = cx.add_view(|cx| build_branch_list(self.project.clone(), cx)); + cx.subscribe(&view, |this, _, event, cx| { + match event { + PickerEvent::Dismiss => { + this.contacts_popover = None; + } + } + cx.notify(); + }) + .detach(); + self.branch_popover = Some(view); + } + + cx.notify(); + } fn render_toggle_contacts_button( &self, theme: &Theme, @@ -733,7 +810,7 @@ impl CollabTitlebarItem { self.contacts_popover.as_ref().map(|popover| { Overlay::new(ChildView::new(popover, cx)) .with_fit_mode(OverlayFitMode::SwitchAnchor) - .with_anchor_corner(AnchorCorner::TopRight) + .with_anchor_corner(AnchorCorner::TopLeft) .with_z_index(999) .aligned() .bottom() diff --git a/crates/collab_ui/src/collab_ui.rs b/crates/collab_ui/src/collab_ui.rs index a809b9c7e6d54b2504aa1f4a95e7257396e930c4..26d9c70a4378af9e8ff90fa4a0af8babf5a45539 100644 --- a/crates/collab_ui/src/collab_ui.rs +++ b/crates/collab_ui/src/collab_ui.rs @@ -1,3 +1,4 @@ +mod branch_list; mod collab_titlebar_item; mod contact_finder; mod contact_list; @@ -28,6 +29,7 @@ actions!( ); pub fn init(app_state: &Arc, cx: &mut AppContext) { + branch_list::init(cx); collab_titlebar_item::init(cx); contact_list::init(cx); contact_finder::init(cx); diff --git a/crates/fs/src/repository.rs b/crates/fs/src/repository.rs index 488262887fd6c5c47ceee129840d72a5201531ca..af6198eda444ec772901ed6a856c57100232e2a3 100644 --- a/crates/fs/src/repository.rs +++ b/crates/fs/src/repository.rs @@ -1,6 +1,6 @@ use anyhow::Result; use collections::HashMap; -use git2::ErrorCode; +use git2::{BranchType, ErrorCode}; use parking_lot::Mutex; use rpc::proto; use serde_derive::{Deserialize, Serialize}; @@ -27,6 +27,12 @@ pub trait GitRepository: Send { fn statuses(&self) -> Option>; fn status(&self, path: &RepoPath) -> Result>; + fn branches(&self) -> Result> { + Ok(vec![]) + } + fn change_branch(&self, _: &str) -> Result<()> { + Ok(()) + } } impl std::fmt::Debug for dyn GitRepository { @@ -106,6 +112,30 @@ impl GitRepository for LibGitRepository { } } } + fn branches(&self) -> Result> { + let local_branches = self.branches(Some(BranchType::Local))?; + let valid_branches = local_branches + .filter_map(|branch| { + branch + .ok() + .map(|(branch, _)| branch.name().ok().flatten().map(String::from)) + .flatten() + }) + .collect(); + Ok(valid_branches) + } + fn change_branch(&self, name: &str) -> Result<()> { + let revision = self.find_branch(name, BranchType::Local)?; + let revision = revision.get(); + let as_tree = revision.peel_to_tree()?; + self.checkout_tree(as_tree.as_object(), None)?; + self.set_head( + revision + .name() + .ok_or_else(|| anyhow::anyhow!("Branch name could not be retrieved"))?, + )?; + Ok(()) + } } fn read_status(status: git2::Status) -> Option {