WIP: git menu

This commit is contained in:
Piotr Osiewicz
2023-06-30 14:05:55 +02:00
parent f6b64dc67a
commit d8d0bdc479
4 changed files with 311 additions and 24 deletions
+178
View File
@@ -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::<BranchListDelegate>::init(cx);
}
pub type BranchList = Picker<BranchListDelegate>;
pub fn build_branch_list(
project: ModelHandle<Project>,
cx: &mut ViewContext<BranchList>,
) -> 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<String>,
matches: Vec<StringMatch>,
project: ModelHandle<Project>,
selected_index: usize,
}
impl PickerDelegate for BranchListDelegate {
fn placeholder_text(&self) -> Arc<str> {
"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<Picker<Self>>) {
self.selected_index = ix;
}
fn update_matches(&mut self, query: String, cx: &mut ViewContext<Picker<Self>>) -> 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::<Vec<_>>()
})
.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<Picker<Self>>) {
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(&current_pick)
.log_err();
cx.emit(PickerEvent::Dismiss);
}
fn dismissed(&mut self, cx: &mut ViewContext<Picker<Self>>) {
cx.emit(PickerEvent::Dismiss);
}
fn render_match(
&self,
ix: usize,
mouse_state: &mut MouseState,
selected: bool,
cx: &gpui::AppContext,
) -> AnyElement<Picker<Self>> {
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()
}
}
+100 -23
View File
@@ -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<Client>,
workspace: WeakViewHandle<Workspace>,
contacts_popover: Option<ViewHandle<ContactsPopover>>,
branch_popover: Option<ViewHandle<BranchList>>,
user_menu: ViewHandle<ContextMenu>,
_subscriptions: Vec<Subscription>,
}
@@ -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<Theme>,
cx: &ViewContext<Self>,
cx: &mut ViewContext<Self>,
) -> AnyElement<Self> {
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) = names_and_branches.next().unwrap_or(("", None));
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())
});
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"),
Stack::new().with_child(
Label::new(name, style.clone())
.with_highlights(highlights)
.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)
.contained()
.with_margin_right(item_spacing)
.aligned()
.left()
.into_any_named("title-project-branch"),
Stack::new()
.with_child(
MouseEventHandler::<ToggleVcsMenu, Self>::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<Self>,
) -> Option<AnyElement<Self>> {
self.branch_popover.as_ref().map(|child| {
let theme = theme::current(cx).clone();
let child = ChildView::new(child, cx);
let child = MouseEventHandler::<BranchList, Self>::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<Self>) {
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()
+2
View File
@@ -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<AppState>, cx: &mut AppContext) {
branch_list::init(cx);
collab_titlebar_item::init(cx);
contact_list::init(cx);
contact_finder::init(cx);
+31 -1
View File
@@ -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<TreeMap<RepoPath, GitFileStatus>>;
fn status(&self, path: &RepoPath) -> Result<Option<GitFileStatus>>;
fn branches(&self) -> Result<Vec<String>> {
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<Vec<String>> {
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<GitFileStatus> {