Merge pull request #2420 from zed-industries/simplify-action-dispatch

Remove `impl_internal_actions!` macro
This commit is contained in:
Antonio Scandurra 2023-04-28 17:39:56 +02:00 committed by GitHub
commit dc999f719b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
45 changed files with 1316 additions and 2150 deletions

View File

@ -38,9 +38,7 @@ use std::{
},
};
use unindent::Unindent as _;
use workspace::{
item::ItemHandle as _, shared_screen::SharedScreen, SplitDirection, ToggleFollow, Workspace,
};
use workspace::{item::ItemHandle as _, shared_screen::SharedScreen, SplitDirection, Workspace};
#[ctor::ctor]
fn init_logger() {
@ -6119,9 +6117,7 @@ async fn test_basic_following(
// When client B starts following client A, all visible view states are replicated to client B.
workspace_b
.update(cx_b, |workspace, cx| {
workspace
.toggle_follow(&ToggleFollow(peer_id_a), cx)
.unwrap()
workspace.toggle_follow(peer_id_a, cx).unwrap()
})
.await
.unwrap();
@ -6160,9 +6156,7 @@ async fn test_basic_following(
// Client C also follows client A.
workspace_c
.update(cx_c, |workspace, cx| {
workspace
.toggle_follow(&ToggleFollow(peer_id_a), cx)
.unwrap()
workspace.toggle_follow(peer_id_a, cx).unwrap()
})
.await
.unwrap();
@ -6197,7 +6191,7 @@ async fn test_basic_following(
// Client C unfollows client A.
workspace_c.update(cx_c, |workspace, cx| {
workspace.toggle_follow(&ToggleFollow(peer_id_a), cx);
workspace.toggle_follow(peer_id_a, cx);
});
// All clients see that clients B is following client A.
@ -6220,7 +6214,7 @@ async fn test_basic_following(
// Client C re-follows client A.
workspace_c.update(cx_c, |workspace, cx| {
workspace.toggle_follow(&ToggleFollow(peer_id_a), cx);
workspace.toggle_follow(peer_id_a, cx);
});
// All clients see that clients B and C are following client A.
@ -6244,9 +6238,7 @@ async fn test_basic_following(
// Client D follows client C.
workspace_d
.update(cx_d, |workspace, cx| {
workspace
.toggle_follow(&ToggleFollow(peer_id_c), cx)
.unwrap()
workspace.toggle_follow(peer_id_c, cx).unwrap()
})
.await
.unwrap();
@ -6436,9 +6428,7 @@ async fn test_basic_following(
// Client A starts following client B.
workspace_a
.update(cx_a, |workspace, cx| {
workspace
.toggle_follow(&ToggleFollow(peer_id_b), cx)
.unwrap()
workspace.toggle_follow(peer_id_b, cx).unwrap()
})
.await
.unwrap();
@ -6707,9 +6697,7 @@ async fn test_following_tab_order(
//Follow client B as client A
workspace_a
.update(cx_a, |workspace, cx| {
workspace
.toggle_follow(&ToggleFollow(client_b_id), cx)
.unwrap()
workspace.toggle_follow(client_b_id, cx).unwrap()
})
.await
.unwrap();
@ -6824,9 +6812,7 @@ async fn test_peers_following_each_other(
workspace_a
.update(cx_a, |workspace, cx| {
let leader_id = *project_a.read(cx).collaborators().keys().next().unwrap();
workspace
.toggle_follow(&workspace::ToggleFollow(leader_id), cx)
.unwrap()
workspace.toggle_follow(leader_id, cx).unwrap()
})
.await
.unwrap();
@ -6840,9 +6826,7 @@ async fn test_peers_following_each_other(
workspace_b
.update(cx_b, |workspace, cx| {
let leader_id = *project_b.read(cx).collaborators().keys().next().unwrap();
workspace
.toggle_follow(&workspace::ToggleFollow(leader_id), cx)
.unwrap()
workspace.toggle_follow(leader_id, cx).unwrap()
})
.await
.unwrap();
@ -6988,9 +6972,7 @@ async fn test_auto_unfollowing(
});
workspace_b
.update(cx_b, |workspace, cx| {
workspace
.toggle_follow(&ToggleFollow(leader_id), cx)
.unwrap()
workspace.toggle_follow(leader_id, cx).unwrap()
})
.await
.unwrap();
@ -7015,9 +6997,7 @@ async fn test_auto_unfollowing(
workspace_b
.update(cx_b, |workspace, cx| {
workspace
.toggle_follow(&ToggleFollow(leader_id), cx)
.unwrap()
workspace.toggle_follow(leader_id, cx).unwrap()
})
.await
.unwrap();
@ -7035,9 +7015,7 @@ async fn test_auto_unfollowing(
workspace_b
.update(cx_b, |workspace, cx| {
workspace
.toggle_follow(&ToggleFollow(leader_id), cx)
.unwrap()
workspace.toggle_follow(leader_id, cx).unwrap()
})
.await
.unwrap();
@ -7057,9 +7035,7 @@ async fn test_auto_unfollowing(
workspace_b
.update(cx_b, |workspace, cx| {
workspace
.toggle_follow(&ToggleFollow(leader_id), cx)
.unwrap()
workspace.toggle_follow(leader_id, cx).unwrap()
})
.await
.unwrap();
@ -7134,14 +7110,10 @@ async fn test_peers_simultaneously_following_each_other(
});
let a_follow_b = workspace_a.update(cx_a, |workspace, cx| {
workspace
.toggle_follow(&ToggleFollow(client_b_id), cx)
.unwrap()
workspace.toggle_follow(client_b_id, cx).unwrap()
});
let b_follow_a = workspace_b.update(cx_b, |workspace, cx| {
workspace
.toggle_follow(&ToggleFollow(client_a_id), cx)
.unwrap()
workspace.toggle_follow(client_a_id, cx).unwrap()
});
futures::try_join!(a_follow_b, b_follow_a).unwrap();

View File

@ -13,7 +13,6 @@ use gpui::{
color::Color,
elements::*,
geometry::{rect::RectF, vector::vec2f, PathBuilder},
impl_internal_actions,
json::{self, ToJson},
platform::{CursorStyle, MouseButton},
AppContext, Entity, ImageData, ModelHandle, SceneBuilder, Subscription, View, ViewContext,
@ -23,7 +22,7 @@ use settings::Settings;
use std::{ops::Range, sync::Arc};
use theme::{AvatarStyle, Theme};
use util::ResultExt;
use workspace::{FollowNextCollaborator, JoinProject, ToggleFollow, Workspace};
use workspace::{FollowNextCollaborator, Workspace};
actions!(
collab,
@ -36,17 +35,11 @@ actions!(
]
);
impl_internal_actions!(collab, [LeaveCall]);
#[derive(Copy, Clone, PartialEq)]
pub(crate) struct LeaveCall;
pub fn init(cx: &mut AppContext) {
cx.add_action(CollabTitlebarItem::toggle_collaborator_list_popover);
cx.add_action(CollabTitlebarItem::toggle_contacts_popover);
cx.add_action(CollabTitlebarItem::share_project);
cx.add_action(CollabTitlebarItem::unshare_project);
cx.add_action(CollabTitlebarItem::leave_call);
cx.add_action(CollabTitlebarItem::toggle_user_menu);
}
@ -136,7 +129,7 @@ impl View for CollabTitlebarItem {
impl CollabTitlebarItem {
pub fn new(
workspace: &ViewHandle<Workspace>,
user_store: &ModelHandle<UserStore>,
user_store: ModelHandle<UserStore>,
cx: &mut ViewContext<Self>,
) -> Self {
let active_call = ActiveCall::global(cx);
@ -146,9 +139,9 @@ impl CollabTitlebarItem {
subscriptions.push(cx.observe_window_activation(|this, active, cx| {
this.window_activation_changed(active, cx)
}));
subscriptions.push(cx.observe(user_store, |_, _, cx| cx.notify()));
subscriptions.push(cx.observe(&user_store, |_, _, cx| cx.notify()));
subscriptions.push(
cx.subscribe(user_store, move |this, user_store, event, cx| {
cx.subscribe(&user_store, move |this, user_store, event, cx| {
if let Some(workspace) = this.workspace.upgrade(cx) {
workspace.update(cx, |workspace, cx| {
if let client::Event::Contact { user, kind } = event {
@ -257,9 +250,7 @@ impl CollabTitlebarItem {
pub fn toggle_contacts_popover(&mut self, _: &ToggleContactsMenu, cx: &mut ViewContext<Self>) {
if self.contacts_popover.take().is_none() {
if let Some(workspace) = self.workspace.upgrade(cx) {
let project = workspace.read(cx).project().clone();
let user_store = workspace.read(cx).user_store().clone();
let view = cx.add_view(|cx| ContactsPopover::new(project, user_store, cx));
let view = cx.add_view(|cx| ContactsPopover::new(&workspace, cx));
cx.subscribe(&view, |this, _, event, cx| {
match event {
contacts_popover::Event::Dismissed => {
@ -301,13 +292,19 @@ impl CollabTitlebarItem {
.with_style(item_style.container)
.into_any()
})),
ContextMenuItem::item("Sign out", SignOut),
ContextMenuItem::item("Send Feedback", feedback::feedback_editor::GiveFeedback),
ContextMenuItem::action("Sign out", SignOut),
ContextMenuItem::action(
"Send Feedback",
feedback::feedback_editor::GiveFeedback,
),
]
} else {
vec![
ContextMenuItem::item("Sign in", SignIn),
ContextMenuItem::item("Send Feedback", feedback::feedback_editor::GiveFeedback),
ContextMenuItem::action("Sign in", SignIn),
ContextMenuItem::action(
"Send Feedback",
feedback::feedback_editor::GiveFeedback,
),
]
};
@ -315,12 +312,6 @@ impl CollabTitlebarItem {
});
}
fn leave_call(&mut self, _: &LeaveCall, cx: &mut ViewContext<Self>) {
ActiveCall::global(cx)
.update(cx, |call, cx| call.hang_up(cx))
.detach_and_log_err(cx);
}
fn render_toggle_contacts_button(
&self,
theme: &Theme,
@ -740,14 +731,22 @@ impl CollabTitlebarItem {
if let Some(location) = location {
if let Some(replica_id) = replica_id {
enum ToggleFollow {}
content = MouseEventHandler::<ToggleFollow, Self>::new(
replica_id.into(),
cx,
move |_, _| content,
)
.with_cursor_style(CursorStyle::PointingHand)
.on_click(MouseButton::Left, move |_, _, cx| {
cx.dispatch_action(ToggleFollow(peer_id))
.on_click(MouseButton::Left, move |_, item, cx| {
if let Some(workspace) = item.workspace.upgrade(cx) {
if let Some(task) = workspace
.update(cx, |workspace, cx| workspace.toggle_follow(peer_id, cx))
{
task.detach_and_log_err(cx);
}
}
})
.with_tooltip::<ToggleFollow>(
peer_id.as_u64() as usize,
@ -762,6 +761,8 @@ impl CollabTitlebarItem {
)
.into_any();
} else if let ParticipantLocation::SharedProject { project_id } = location {
enum JoinProject {}
let user_id = user.id;
content = MouseEventHandler::<JoinProject, Self>::new(
peer_id.as_u64() as usize,
@ -769,11 +770,12 @@ impl CollabTitlebarItem {
move |_, _| content,
)
.with_cursor_style(CursorStyle::PointingHand)
.on_click(MouseButton::Left, move |_, _, cx| {
cx.dispatch_action(JoinProject {
project_id,
follow_user_id: user_id,
})
.on_click(MouseButton::Left, move |_, this, cx| {
if let Some(workspace) = this.workspace.upgrade(cx) {
let app_state = workspace.read(cx).app_state().clone();
workspace::join_remote_project(project_id, user_id, app_state, cx)
.detach_and_log_err(cx);
}
})
.with_tooltip::<JoinProject>(
peer_id.as_u64() as usize,

View File

@ -10,29 +10,24 @@ mod notifications;
mod project_shared_notification;
mod sharing_status_indicator;
use anyhow::anyhow;
use call::ActiveCall;
pub use collab_titlebar_item::{CollabTitlebarItem, ToggleContactsMenu};
use gpui::{actions, AppContext, Task};
use std::sync::Arc;
use workspace::{AppState, JoinProject, ToggleFollow, Workspace};
use workspace::AppState;
actions!(collab, [ToggleScreenSharing]);
pub fn init(app_state: Arc<AppState>, cx: &mut AppContext) {
pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
collab_titlebar_item::init(cx);
contact_notification::init(cx);
contact_list::init(cx);
contact_finder::init(cx);
contacts_popover::init(cx);
incoming_call_notification::init(cx);
project_shared_notification::init(cx);
incoming_call_notification::init(&app_state, cx);
project_shared_notification::init(&app_state, cx);
sharing_status_indicator::init(cx);
cx.add_global_action(toggle_screen_sharing);
cx.add_global_action(move |action: &JoinProject, cx| {
join_project(action, app_state.clone(), cx);
});
}
pub fn toggle_screen_sharing(_: &ToggleScreenSharing, cx: &mut AppContext) {
@ -47,88 +42,3 @@ pub fn toggle_screen_sharing(_: &ToggleScreenSharing, cx: &mut AppContext) {
toggle_screen_sharing.detach_and_log_err(cx);
}
}
fn join_project(action: &JoinProject, app_state: Arc<AppState>, cx: &mut AppContext) {
let project_id = action.project_id;
let follow_user_id = action.follow_user_id;
cx.spawn(|mut cx| async move {
let existing_workspace = cx.update(|cx| {
cx.window_ids()
.filter_map(|window_id| cx.root_view(window_id)?.clone().downcast::<Workspace>())
.find(|workspace| {
workspace.read(cx).project().read(cx).remote_id() == Some(project_id)
})
});
let workspace = if let Some(existing_workspace) = existing_workspace {
existing_workspace.downgrade()
} else {
let active_call = cx.read(ActiveCall::global);
let room = active_call
.read_with(&cx, |call, _| call.room().cloned())
.ok_or_else(|| anyhow!("not in a call"))?;
let project = room
.update(&mut cx, |room, cx| {
room.join_project(
project_id,
app_state.languages.clone(),
app_state.fs.clone(),
cx,
)
})
.await?;
let (_, workspace) = cx.add_window(
(app_state.build_window_options)(None, None, cx.platform().as_ref()),
|cx| {
let mut workspace = Workspace::new(
Default::default(),
0,
project,
app_state.dock_default_item_factory,
app_state.background_actions,
cx,
);
(app_state.initialize_workspace)(&mut workspace, &app_state, cx);
workspace
},
);
workspace.downgrade()
};
cx.activate_window(workspace.window_id());
cx.platform().activate(true);
workspace.update(&mut cx, |workspace, cx| {
if let Some(room) = ActiveCall::global(cx).read(cx).room().cloned() {
let follow_peer_id = room
.read(cx)
.remote_participants()
.iter()
.find(|(_, participant)| participant.user.id == follow_user_id)
.map(|(_, p)| p.peer_id)
.or_else(|| {
// If we couldn't follow the given user, follow the host instead.
let collaborator = workspace
.project()
.read(cx)
.collaborators()
.values()
.find(|collaborator| collaborator.replica_id == 0)?;
Some(collaborator.peer_id)
});
if let Some(follow_peer_id) = follow_peer_id {
if !workspace.is_being_followed(follow_peer_id) {
workspace
.toggle_follow(&ToggleFollow(follow_peer_id), cx)
.map(|follow| follow.detach_and_log_err(cx));
}
}
}
})?;
anyhow::Ok(())
})
.detach_and_log_err(cx);
}

View File

@ -1,4 +1,3 @@
use super::collab_titlebar_item::LeaveCall;
use crate::contacts_popover;
use call::ActiveCall;
use client::{proto::PeerId, Contact, User, UserStore};
@ -8,10 +7,10 @@ use fuzzy::{match_strings, StringMatchCandidate};
use gpui::{
elements::*,
geometry::{rect::RectF, vector::vec2f},
impl_actions, impl_internal_actions,
impl_actions,
keymap_matcher::KeymapContext,
platform::{CursorStyle, MouseButton, PromptLevel},
AppContext, Entity, ModelHandle, Subscription, View, ViewContext, ViewHandle,
AppContext, Entity, ModelHandle, Subscription, View, ViewContext, ViewHandle, WeakViewHandle,
};
use menu::{Confirm, SelectNext, SelectPrev};
use project::Project;
@ -19,10 +18,9 @@ use serde::Deserialize;
use settings::Settings;
use std::{mem, sync::Arc};
use theme::IconButton;
use workspace::{JoinProject, OpenSharedScreen};
use workspace::Workspace;
impl_actions!(contact_list, [RemoveContact, RespondToContactRequest]);
impl_internal_actions!(contact_list, [ToggleExpanded, Call]);
pub fn init(cx: &mut AppContext) {
cx.add_action(ContactList::remove_contact);
@ -31,17 +29,6 @@ pub fn init(cx: &mut AppContext) {
cx.add_action(ContactList::select_next);
cx.add_action(ContactList::select_prev);
cx.add_action(ContactList::confirm);
cx.add_action(ContactList::toggle_expanded);
cx.add_action(ContactList::call);
}
#[derive(Clone, PartialEq)]
struct ToggleExpanded(Section);
#[derive(Clone, PartialEq)]
struct Call {
recipient_user_id: u64,
initial_project: Option<ModelHandle<Project>>,
}
#[derive(Clone, Copy, PartialEq, Eq, Debug, PartialOrd, Ord)]
@ -161,6 +148,7 @@ pub struct ContactList {
match_candidates: Vec<StringMatchCandidate>,
list_state: ListState<Self>,
project: ModelHandle<Project>,
workspace: WeakViewHandle<Workspace>,
user_store: ModelHandle<UserStore>,
filter_editor: ViewHandle<Editor>,
collapsed_sections: Vec<Section>,
@ -169,11 +157,7 @@ pub struct ContactList {
}
impl ContactList {
pub fn new(
project: ModelHandle<Project>,
user_store: ModelHandle<UserStore>,
cx: &mut ViewContext<Self>,
) -> Self {
pub fn new(workspace: &ViewHandle<Workspace>, cx: &mut ViewContext<Self>) -> Self {
let filter_editor = cx.add_view(|cx| {
let mut editor = Editor::single_line(
Some(Arc::new(|theme| {
@ -278,6 +262,7 @@ impl ContactList {
});
let active_call = ActiveCall::global(cx);
let user_store = workspace.read(cx).user_store().clone();
let mut subscriptions = Vec::new();
subscriptions.push(cx.observe(&user_store, |this, _, cx| this.update_entries(cx)));
subscriptions.push(cx.observe(&active_call, |this, _, cx| this.update_entries(cx)));
@ -290,7 +275,8 @@ impl ContactList {
match_candidates: Default::default(),
filter_editor,
_subscriptions: subscriptions,
project,
project: workspace.read(cx).project().clone(),
workspace: workspace.downgrade(),
user_store,
};
this.update_entries(cx);
@ -403,18 +389,11 @@ impl ContactList {
if let Some(entry) = self.entries.get(selection) {
match entry {
ContactEntry::Header(section) => {
let section = *section;
self.toggle_expanded(&ToggleExpanded(section), cx);
self.toggle_expanded(*section, cx);
}
ContactEntry::Contact { contact, calling } => {
if contact.online && !contact.busy && !calling {
self.call(
&Call {
recipient_user_id: contact.user.id,
initial_project: Some(self.project.clone()),
},
cx,
);
self.call(contact.user.id, Some(self.project.clone()), cx);
}
}
ContactEntry::ParticipantProject {
@ -422,13 +401,23 @@ impl ContactList {
host_user_id,
..
} => {
cx.dispatch_global_action(JoinProject {
project_id: *project_id,
follow_user_id: *host_user_id,
});
if let Some(workspace) = self.workspace.upgrade(cx) {
let app_state = workspace.read(cx).app_state().clone();
workspace::join_remote_project(
*project_id,
*host_user_id,
app_state,
cx,
)
.detach_and_log_err(cx);
}
}
ContactEntry::ParticipantScreen { peer_id, .. } => {
cx.dispatch_action(OpenSharedScreen { peer_id: *peer_id });
if let Some(workspace) = self.workspace.upgrade(cx) {
workspace.update(cx, |workspace, cx| {
workspace.open_shared_screen(*peer_id, cx)
});
}
}
_ => {}
}
@ -436,8 +425,7 @@ impl ContactList {
}
}
fn toggle_expanded(&mut self, action: &ToggleExpanded, cx: &mut ViewContext<Self>) {
let section = action.0;
fn toggle_expanded(&mut self, section: Section, cx: &mut ViewContext<Self>) {
if let Some(ix) = self.collapsed_sections.iter().position(|s| *s == section) {
self.collapsed_sections.remove(ix);
} else {
@ -798,6 +786,8 @@ impl ContactList {
theme: &theme::ContactList,
cx: &mut ViewContext<Self>,
) -> AnyElement<Self> {
enum JoinProject {}
let font_cache = cx.font_cache();
let host_avatar_height = theme
.contact_avatar
@ -873,12 +863,13 @@ impl ContactList {
} else {
CursorStyle::Arrow
})
.on_click(MouseButton::Left, move |_, _, cx| {
.on_click(MouseButton::Left, move |_, this, cx| {
if !is_current {
cx.dispatch_global_action(JoinProject {
project_id,
follow_user_id: host_user_id,
});
if let Some(workspace) = this.workspace.upgrade(cx) {
let app_state = workspace.read(cx).app_state().clone();
workspace::join_remote_project(project_id, host_user_id, app_state, cx)
.detach_and_log_err(cx);
}
}
})
.into_any()
@ -891,6 +882,8 @@ impl ContactList {
theme: &theme::ContactList,
cx: &mut ViewContext<Self>,
) -> AnyElement<Self> {
enum OpenSharedScreen {}
let font_cache = cx.font_cache();
let host_avatar_height = theme
.contact_avatar
@ -971,8 +964,12 @@ impl ContactList {
},
)
.with_cursor_style(CursorStyle::PointingHand)
.on_click(MouseButton::Left, move |_, _, cx| {
cx.dispatch_action(OpenSharedScreen { peer_id });
.on_click(MouseButton::Left, move |_, this, cx| {
if let Some(workspace) = this.workspace.upgrade(cx) {
workspace.update(cx, |workspace, cx| {
workspace.open_shared_screen(peer_id, cx)
});
}
})
.into_any()
}
@ -1004,7 +1001,11 @@ impl ContactList {
.contained()
.with_style(style.container)
})
.on_click(MouseButton::Left, |_, _, cx| cx.dispatch_action(LeaveCall))
.on_click(MouseButton::Left, |_, _, cx| {
ActiveCall::global(cx)
.update(cx, |call, cx| call.hang_up(cx))
.detach_and_log_err(cx);
})
.aligned(),
)
} else {
@ -1043,8 +1044,8 @@ impl ContactList {
.with_style(header_style.container)
})
.with_cursor_style(CursorStyle::PointingHand)
.on_click(MouseButton::Left, move |_, _, cx| {
cx.dispatch_action(ToggleExpanded(section))
.on_click(MouseButton::Left, move |_, this, cx| {
this.toggle_expanded(section, cx);
})
.into_any()
}
@ -1142,12 +1143,9 @@ impl ContactList {
.style_for(&mut Default::default(), is_selected),
)
})
.on_click(MouseButton::Left, move |_, _, cx| {
.on_click(MouseButton::Left, move |_, this, cx| {
if online && !busy {
cx.dispatch_action(Call {
recipient_user_id: user_id,
initial_project: Some(initial_project.clone()),
});
this.call(user_id, Some(initial_project.clone()), cx);
}
});
@ -1269,9 +1267,12 @@ impl ContactList {
.into_any()
}
fn call(&mut self, action: &Call, cx: &mut ViewContext<Self>) {
let recipient_user_id = action.recipient_user_id;
let initial_project = action.initial_project.clone();
fn call(
&mut self,
recipient_user_id: u64,
initial_project: Option<ModelHandle<Project>>,
cx: &mut ViewContext<Self>,
) {
ActiveCall::global(cx)
.update(cx, |call, cx| {
call.invite(recipient_user_id, initial_project, cx)

View File

@ -2,18 +2,9 @@ use std::sync::Arc;
use crate::notifications::render_user_notification;
use client::{ContactEventKind, User, UserStore};
use gpui::{
elements::*, impl_internal_actions, AppContext, Entity, ModelHandle, View, ViewContext,
};
use gpui::{elements::*, Entity, ModelHandle, View, ViewContext};
use workspace::notifications::Notification;
impl_internal_actions!(contact_notifications, [Dismiss, RespondToContactRequest]);
pub fn init(cx: &mut AppContext) {
cx.add_action(ContactNotification::dismiss);
cx.add_action(ContactNotification::respond_to_contact_request);
}
pub struct ContactNotification {
user_store: ModelHandle<UserStore>,
user: Arc<User>,
@ -48,20 +39,18 @@ impl View for ContactNotification {
self.user.clone(),
"wants to add you as a contact",
Some("They won't be alerted if you decline."),
Dismiss(self.user.id),
|notification, cx| notification.dismiss(cx),
vec![
(
"Decline",
Box::new(RespondToContactRequest {
user_id: self.user.id,
accept: false,
Box::new(|notification, cx| {
notification.respond_to_contact_request(false, cx)
}),
),
(
"Accept",
Box::new(RespondToContactRequest {
user_id: self.user.id,
accept: true,
Box::new(|notification, cx| {
notification.respond_to_contact_request(true, cx)
}),
),
],
@ -71,7 +60,7 @@ impl View for ContactNotification {
self.user.clone(),
"accepted your contact request",
None,
Dismiss(self.user.id),
|notification, cx| notification.dismiss(cx),
vec![],
cx,
),
@ -113,7 +102,7 @@ impl ContactNotification {
}
}
fn dismiss(&mut self, _: &Dismiss, cx: &mut ViewContext<Self>) {
fn dismiss(&mut self, cx: &mut ViewContext<Self>) {
self.user_store.update(cx, |store, cx| {
store
.dismiss_contact_request(self.user.id, cx)
@ -122,14 +111,10 @@ impl ContactNotification {
cx.emit(Event::Dismiss);
}
fn respond_to_contact_request(
&mut self,
action: &RespondToContactRequest,
cx: &mut ViewContext<Self>,
) {
fn respond_to_contact_request(&mut self, accept: bool, cx: &mut ViewContext<Self>) {
self.user_store
.update(cx, |store, cx| {
store.respond_to_contact_request(action.user_id, action.accept, cx)
store.respond_to_contact_request(self.user.id, accept, cx)
})
.detach();
}

View File

@ -6,11 +6,11 @@ use crate::{
use client::UserStore;
use gpui::{
actions, elements::*, platform::MouseButton, AppContext, Entity, ModelHandle, View,
ViewContext, ViewHandle,
ViewContext, ViewHandle, WeakViewHandle,
};
use picker::PickerEvent;
use project::Project;
use settings::Settings;
use workspace::Workspace;
actions!(contacts_popover, [ToggleContactFinder]);
@ -29,23 +29,17 @@ enum Child {
pub struct ContactsPopover {
child: Child,
project: ModelHandle<Project>,
user_store: ModelHandle<UserStore>,
workspace: WeakViewHandle<Workspace>,
_subscription: Option<gpui::Subscription>,
}
impl ContactsPopover {
pub fn new(
project: ModelHandle<Project>,
user_store: ModelHandle<UserStore>,
cx: &mut ViewContext<Self>,
) -> Self {
pub fn new(workspace: &ViewHandle<Workspace>, cx: &mut ViewContext<Self>) -> Self {
let mut this = Self {
child: Child::ContactList(
cx.add_view(|cx| ContactList::new(project.clone(), user_store.clone(), cx)),
),
project,
user_store,
child: Child::ContactList(cx.add_view(|cx| ContactList::new(workspace, cx))),
user_store: workspace.read(cx).user_store().clone(),
workspace: workspace.downgrade(),
_subscription: None,
};
this.show_contact_list(String::new(), cx);
@ -74,16 +68,16 @@ impl ContactsPopover {
}
fn show_contact_list(&mut self, editor_text: String, cx: &mut ViewContext<ContactsPopover>) {
let child = cx.add_view(|cx| {
ContactList::new(self.project.clone(), self.user_store.clone(), cx)
.with_editor_text(editor_text, cx)
});
cx.focus(&child);
self._subscription = Some(cx.subscribe(&child, |_, _, event, cx| match event {
crate::contact_list::Event::Dismissed => cx.emit(Event::Dismissed),
}));
self.child = Child::ContactList(child);
cx.notify();
if let Some(workspace) = self.workspace.upgrade(cx) {
let child = cx
.add_view(|cx| ContactList::new(&workspace, cx).with_editor_text(editor_text, cx));
cx.focus(&child);
self._subscription = Some(cx.subscribe(&child, |_, _, event, cx| match event {
crate::contact_list::Event::Dismissed => cx.emit(Event::Dismissed),
}));
self.child = Child::ContactList(child);
cx.notify();
}
}
}

View File

@ -1,22 +1,20 @@
use std::sync::{Arc, Weak};
use call::{ActiveCall, IncomingCall};
use client::proto;
use futures::StreamExt;
use gpui::{
elements::*,
geometry::{rect::RectF, vector::vec2f},
impl_internal_actions,
platform::{CursorStyle, MouseButton, WindowBounds, WindowKind, WindowOptions},
AnyElement, AppContext, Entity, View, ViewContext,
};
use settings::Settings;
use util::ResultExt;
use workspace::JoinProject;
impl_internal_actions!(incoming_call_notification, [RespondToCall]);
pub fn init(cx: &mut AppContext) {
cx.add_action(IncomingCallNotification::respond_to_call);
use workspace::AppState;
pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
let app_state = Arc::downgrade(app_state);
let mut incoming_call = ActiveCall::global(cx).read(cx).incoming();
cx.spawn(|mut cx| async move {
let mut notification_windows = Vec::new();
@ -48,7 +46,7 @@ pub fn init(cx: &mut AppContext) {
is_movable: false,
screen: Some(screen),
},
|_| IncomingCallNotification::new(incoming_call.clone()),
|_| IncomingCallNotification::new(incoming_call.clone(), app_state.clone()),
);
notification_windows.push(window_id);
@ -66,28 +64,34 @@ struct RespondToCall {
pub struct IncomingCallNotification {
call: IncomingCall,
app_state: Weak<AppState>,
}
impl IncomingCallNotification {
pub fn new(call: IncomingCall) -> Self {
Self { call }
pub fn new(call: IncomingCall, app_state: Weak<AppState>) -> Self {
Self { call, app_state }
}
fn respond_to_call(&mut self, action: &RespondToCall, cx: &mut ViewContext<Self>) {
fn respond(&mut self, accept: bool, cx: &mut ViewContext<Self>) {
let active_call = ActiveCall::global(cx);
if action.accept {
if accept {
let join = active_call.update(cx, |active_call, cx| active_call.accept_incoming(cx));
let caller_user_id = self.call.calling_user.id;
let initial_project_id = self.call.initial_project.as_ref().map(|project| project.id);
cx.spawn(|_, mut cx| async move {
cx.spawn(|this, mut cx| async move {
join.await?;
if let Some(project_id) = initial_project_id {
cx.update(|cx| {
cx.dispatch_global_action(JoinProject {
project_id,
follow_user_id: caller_user_id,
})
});
this.update(&mut cx, |this, cx| {
if let Some(app_state) = this.app_state.upgrade() {
workspace::join_remote_project(
project_id,
caller_user_id,
app_state,
cx,
)
.detach_and_log_err(cx);
}
})?;
}
anyhow::Ok(())
})
@ -174,8 +178,8 @@ impl IncomingCallNotification {
.with_style(theme.accept_button.container)
})
.with_cursor_style(CursorStyle::PointingHand)
.on_click(MouseButton::Left, |_, _, cx| {
cx.dispatch_action(RespondToCall { accept: true });
.on_click(MouseButton::Left, |_, this, cx| {
this.respond(true, cx);
})
.flex(1., true),
)
@ -188,8 +192,8 @@ impl IncomingCallNotification {
.with_style(theme.decline_button.container)
})
.with_cursor_style(CursorStyle::PointingHand)
.on_click(MouseButton::Left, |_, _, cx| {
cx.dispatch_action(RespondToCall { accept: false });
.on_click(MouseButton::Left, |_, this, cx| {
this.respond(false, cx);
})
.flex(1., true),
)

View File

@ -2,7 +2,7 @@ use client::User;
use gpui::{
elements::*,
platform::{CursorStyle, MouseButton},
Action, AnyElement, Element, View, ViewContext,
AnyElement, Element, View, ViewContext,
};
use settings::Settings;
use std::sync::Arc;
@ -10,14 +10,18 @@ use std::sync::Arc;
enum Dismiss {}
enum Button {}
pub fn render_user_notification<V: View, A: Action + Clone>(
pub fn render_user_notification<F, V>(
user: Arc<User>,
title: &'static str,
body: Option<&'static str>,
dismiss_action: A,
buttons: Vec<(&'static str, Box<dyn Action>)>,
on_dismiss: F,
buttons: Vec<(&'static str, Box<dyn Fn(&mut V, &mut ViewContext<V>)>)>,
cx: &mut ViewContext<V>,
) -> AnyElement<V> {
) -> AnyElement<V>
where
F: 'static + Fn(&mut V, &mut ViewContext<V>),
V: View,
{
let theme = cx.global::<Settings>().theme.clone();
let theme = &theme.contact_notification;
@ -64,9 +68,7 @@ pub fn render_user_notification<V: View, A: Action + Clone>(
})
.with_cursor_style(CursorStyle::PointingHand)
.with_padding(Padding::uniform(5.))
.on_click(MouseButton::Left, move |_, _, cx| {
cx.dispatch_any_action(dismiss_action.boxed_clone())
})
.on_click(MouseButton::Left, move |_, view, cx| on_dismiss(view, cx))
.aligned()
.constrained()
.with_height(
@ -90,7 +92,7 @@ pub fn render_user_notification<V: View, A: Action + Clone>(
Some(
Flex::row()
.with_children(buttons.into_iter().enumerate().map(
|(ix, (message, action))| {
|(ix, (message, handler))| {
MouseEventHandler::<Button, V>::new(ix, cx, |state, _| {
let button = theme.button.style_for(state, false);
Label::new(message, button.text.clone())
@ -98,9 +100,7 @@ pub fn render_user_notification<V: View, A: Action + Clone>(
.with_style(button.container)
})
.with_cursor_style(CursorStyle::PointingHand)
.on_click(MouseButton::Left, move |_, _, cx| {
cx.dispatch_any_action(action.boxed_clone())
})
.on_click(MouseButton::Left, move |_, view, cx| handler(view, cx))
},
))
.aligned()

View File

@ -2,22 +2,17 @@ use call::{room, ActiveCall};
use client::User;
use collections::HashMap;
use gpui::{
actions,
elements::*,
geometry::{rect::RectF, vector::vec2f},
platform::{CursorStyle, MouseButton, WindowBounds, WindowKind, WindowOptions},
AppContext, Entity, View, ViewContext,
};
use settings::Settings;
use std::sync::Arc;
use workspace::JoinProject;
actions!(project_shared_notification, [DismissProject]);
pub fn init(cx: &mut AppContext) {
cx.add_action(ProjectSharedNotification::join);
cx.add_action(ProjectSharedNotification::dismiss);
use std::sync::{Arc, Weak};
use workspace::AppState;
pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
let app_state = Arc::downgrade(app_state);
let active_call = ActiveCall::global(cx);
let mut notification_windows = HashMap::default();
cx.subscribe(&active_call, move |_, event, cx| match event {
@ -50,6 +45,7 @@ pub fn init(cx: &mut AppContext) {
owner.clone(),
*project_id,
worktree_root_names.clone(),
app_state.clone(),
)
},
);
@ -82,23 +78,33 @@ pub struct ProjectSharedNotification {
project_id: u64,
worktree_root_names: Vec<String>,
owner: Arc<User>,
app_state: Weak<AppState>,
}
impl ProjectSharedNotification {
fn new(owner: Arc<User>, project_id: u64, worktree_root_names: Vec<String>) -> Self {
fn new(
owner: Arc<User>,
project_id: u64,
worktree_root_names: Vec<String>,
app_state: Weak<AppState>,
) -> Self {
Self {
project_id,
worktree_root_names,
owner,
app_state,
}
}
fn join(&mut self, _: &JoinProject, cx: &mut ViewContext<Self>) {
fn join(&mut self, cx: &mut ViewContext<Self>) {
cx.remove_window();
cx.propagate_action();
if let Some(app_state) = self.app_state.upgrade() {
workspace::join_remote_project(self.project_id, self.owner.id, app_state, cx)
.detach_and_log_err(cx);
}
}
fn dismiss(&mut self, _: &DismissProject, cx: &mut ViewContext<Self>) {
fn dismiss(&mut self, cx: &mut ViewContext<Self>) {
cx.remove_window();
}
@ -161,9 +167,6 @@ impl ProjectSharedNotification {
enum Open {}
enum Dismiss {}
let project_id = self.project_id;
let owner_user_id = self.owner.id;
Flex::column()
.with_child(
MouseEventHandler::<Open, Self>::new(0, cx, |_, cx| {
@ -174,12 +177,7 @@ impl ProjectSharedNotification {
.with_style(theme.open_button.container)
})
.with_cursor_style(CursorStyle::PointingHand)
.on_click(MouseButton::Left, move |_, _, cx| {
cx.dispatch_action(JoinProject {
project_id,
follow_user_id: owner_user_id,
});
})
.on_click(MouseButton::Left, move |_, this, cx| this.join(cx))
.flex(1., true),
)
.with_child(
@ -191,8 +189,8 @@ impl ProjectSharedNotification {
.with_style(theme.dismiss_button.container)
})
.with_cursor_style(CursorStyle::PointingHand)
.on_click(MouseButton::Left, |_, _, cx| {
cx.dispatch_action(DismissProject);
.on_click(MouseButton::Left, |_, this, cx| {
this.dismiss(cx);
})
.flex(1., true),
)

View File

@ -2,7 +2,6 @@ use gpui::{
anyhow,
elements::*,
geometry::vector::Vector2F,
impl_internal_actions,
keymap_matcher::KeymapContext,
platform::{CursorStyle, MouseButton},
Action, AnyViewHandle, AppContext, Axis, Entity, MouseState, SizeConstraint, Subscription,
@ -10,19 +9,13 @@ use gpui::{
};
use menu::*;
use settings::Settings;
use std::{any::TypeId, borrow::Cow, time::Duration};
#[derive(Copy, Clone, PartialEq)]
struct Clicked;
impl_internal_actions!(context_menu, [Clicked]);
use std::{any::TypeId, borrow::Cow, sync::Arc, time::Duration};
pub fn init(cx: &mut AppContext) {
cx.add_action(ContextMenu::select_first);
cx.add_action(ContextMenu::select_last);
cx.add_action(ContextMenu::select_next);
cx.add_action(ContextMenu::select_prev);
cx.add_action(ContextMenu::clicked);
cx.add_action(ContextMenu::confirm);
cx.add_action(ContextMenu::cancel);
}
@ -37,21 +30,43 @@ pub enum ContextMenuItemLabel {
Element(ContextMenuItemBuilder),
}
pub enum ContextMenuAction {
ParentAction {
action: Box<dyn Action>,
},
ViewAction {
action: Box<dyn Action>,
for_view: usize,
},
impl From<Cow<'static, str>> for ContextMenuItemLabel {
fn from(s: Cow<'static, str>) -> Self {
Self::String(s)
}
}
impl ContextMenuAction {
fn id(&self) -> TypeId {
impl From<&'static str> for ContextMenuItemLabel {
fn from(s: &'static str) -> Self {
Self::String(s.into())
}
}
impl From<String> for ContextMenuItemLabel {
fn from(s: String) -> Self {
Self::String(s.into())
}
}
impl<T> From<T> for ContextMenuItemLabel
where
T: 'static + Fn(&mut MouseState, &theme::ContextMenuItem) -> AnyElement<ContextMenu>,
{
fn from(f: T) -> Self {
Self::Element(Box::new(f))
}
}
pub enum ContextMenuItemAction {
Action(Box<dyn Action>),
Handler(Arc<dyn Fn(&mut ViewContext<ContextMenu>)>),
}
impl Clone for ContextMenuItemAction {
fn clone(&self) -> Self {
match self {
ContextMenuAction::ParentAction { action } => action.id(),
ContextMenuAction::ViewAction { action, .. } => action.id(),
Self::Action(action) => Self::Action(action.boxed_clone()),
Self::Handler(handler) => Self::Handler(handler.clone()),
}
}
}
@ -59,42 +74,27 @@ impl ContextMenuAction {
pub enum ContextMenuItem {
Item {
label: ContextMenuItemLabel,
action: ContextMenuAction,
action: ContextMenuItemAction,
},
Static(StaticItem),
Separator,
}
impl ContextMenuItem {
pub fn element_item(label: ContextMenuItemBuilder, action: impl 'static + Action) -> Self {
pub fn action(label: impl Into<ContextMenuItemLabel>, action: impl 'static + Action) -> Self {
Self::Item {
label: ContextMenuItemLabel::Element(label),
action: ContextMenuAction::ParentAction {
action: Box::new(action),
},
label: label.into(),
action: ContextMenuItemAction::Action(Box::new(action)),
}
}
pub fn item(label: impl Into<Cow<'static, str>>, action: impl 'static + Action) -> Self {
Self::Item {
label: ContextMenuItemLabel::String(label.into()),
action: ContextMenuAction::ParentAction {
action: Box::new(action),
},
}
}
pub fn item_for_view(
label: impl Into<Cow<'static, str>>,
view_id: usize,
action: impl 'static + Action,
pub fn handler(
label: impl Into<ContextMenuItemLabel>,
handler: impl 'static + Fn(&mut ViewContext<ContextMenu>),
) -> Self {
Self::Item {
label: ContextMenuItemLabel::String(label.into()),
action: ContextMenuAction::ViewAction {
action: Box::new(action),
for_view: view_id,
},
label: label.into(),
action: ContextMenuItemAction::Handler(Arc::new(handler)),
}
}
@ -108,7 +108,10 @@ impl ContextMenuItem {
fn action_id(&self) -> Option<TypeId> {
match self {
ContextMenuItem::Item { action, .. } => Some(action.id()),
ContextMenuItem::Item { action, .. } => match action {
ContextMenuItemAction::Action(action) => Some(action.id()),
ContextMenuItemAction::Handler(_) => None,
},
ContextMenuItem::Static(..) | ContextMenuItem::Separator => None,
}
}
@ -218,22 +221,20 @@ impl ContextMenu {
}
}
fn clicked(&mut self, _: &Clicked, _: &mut ViewContext<Self>) {
self.clicked = true;
}
fn confirm(&mut self, _: &Confirm, cx: &mut ViewContext<Self>) {
if let Some(ix) = self.selected_index {
if let Some(ContextMenuItem::Item { action, .. }) = self.items.get(ix) {
match action {
ContextMenuAction::ParentAction { action } => {
cx.dispatch_any_action(action.boxed_clone())
}
ContextMenuAction::ViewAction { action, for_view } => {
ContextMenuItemAction::Action(action) => {
let window_id = cx.window_id();
cx.dispatch_any_action_at(window_id, *for_view, action.boxed_clone())
cx.dispatch_any_action_at(
window_id,
self.parent_view_id,
action.boxed_clone(),
);
}
};
ContextMenuItemAction::Handler(handler) => handler(cx),
}
self.reset(cx);
}
}
@ -375,22 +376,17 @@ impl ContextMenu {
&mut Default::default(),
Some(ix) == self.selected_index,
);
let (action, view_id) = match action {
ContextMenuAction::ParentAction { action } => {
(action.boxed_clone(), self.parent_view_id)
}
ContextMenuAction::ViewAction { action, for_view } => {
(action.boxed_clone(), *for_view)
}
};
KeystrokeLabel::new(
view_id,
action.boxed_clone(),
style.keystroke.container,
style.keystroke.text.clone(),
)
.into_any()
match action {
ContextMenuItemAction::Action(action) => KeystrokeLabel::new(
self.parent_view_id,
action.boxed_clone(),
style.keystroke.container,
style.keystroke.text.clone(),
)
.into_any(),
ContextMenuItemAction::Handler(_) => Empty::new().into_any(),
}
}
ContextMenuItem::Static(_) => Empty::new().into_any(),
@ -422,18 +418,23 @@ impl ContextMenu {
.with_children(self.items.iter().enumerate().map(|(ix, item)| {
match item {
ContextMenuItem::Item { label, action } => {
let (action, view_id) = match action {
ContextMenuAction::ParentAction { action } => {
(action.boxed_clone(), self.parent_view_id)
}
ContextMenuAction::ViewAction { action, for_view } => {
(action.boxed_clone(), *for_view)
}
};
let action = action.clone();
let view_id = self.parent_view_id;
MouseEventHandler::<MenuItem, ContextMenu>::new(ix, cx, |state, _| {
let style =
style.item.style_for(state, Some(ix) == self.selected_index);
let keystroke = match &action {
ContextMenuItemAction::Action(action) => Some(
KeystrokeLabel::new(
view_id,
action.boxed_clone(),
style.keystroke.container,
style.keystroke.text.clone(),
)
.flex_float(),
),
ContextMenuItemAction::Handler(_) => None,
};
Flex::row()
.with_child(match label {
@ -446,25 +447,26 @@ impl ContextMenu {
element(state, style)
}
})
.with_child({
KeystrokeLabel::new(
view_id,
action.boxed_clone(),
style.keystroke.container,
style.keystroke.text.clone(),
)
.flex_float()
})
.with_children(keystroke)
.contained()
.with_style(style.container)
})
.with_cursor_style(CursorStyle::PointingHand)
.on_up(MouseButton::Left, |_, _, _| {}) // Capture these events
.on_down(MouseButton::Left, |_, _, _| {}) // Capture these events
.on_click(MouseButton::Left, move |_, _, cx| {
cx.dispatch_action(Clicked);
.on_click(MouseButton::Left, move |_, menu, cx| {
menu.clicked = true;
let window_id = cx.window_id();
cx.dispatch_any_action_at(window_id, view_id, action.boxed_clone());
match &action {
ContextMenuItemAction::Action(action) => {
cx.dispatch_any_action_at(
window_id,
view_id,
action.boxed_clone(),
);
}
ContextMenuItemAction::Handler(handler) => handler(cx),
}
})
.on_drag(MouseButton::Left, |_, _, _| {})
.into_any()

View File

@ -458,7 +458,7 @@ impl Copilot {
}
}
fn sign_in(&mut self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
pub fn sign_in(&mut self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
if let CopilotServer::Running(server) = &mut self.server {
let task = match &server.sign_in_status {
SignInStatus::Authorized { .. } | SignInStatus::Unauthorized { .. } => {

View File

@ -2,7 +2,6 @@ use crate::{request::PromptUserDeviceFlow, Copilot, Status};
use gpui::{
elements::*,
geometry::rect::RectF,
impl_internal_actions,
platform::{WindowBounds, WindowKind, WindowOptions},
AnyElement, AnyViewHandle, AppContext, ClipboardItem, Element, Entity, View, ViewContext,
ViewHandle,
@ -10,11 +9,6 @@ use gpui::{
use settings::Settings;
use theme::ui::modal;
#[derive(PartialEq, Eq, Debug, Clone)]
struct ClickedConnect;
impl_internal_actions!(copilot_verification, [ClickedConnect]);
#[derive(PartialEq, Eq, Debug, Clone)]
struct CopyUserCode;
@ -68,12 +62,6 @@ pub fn init(cx: &mut AppContext) {
}
})
.detach();
cx.add_action(
|code_verification: &mut CopilotCodeVerification, _: &ClickedConnect, _| {
code_verification.connect_clicked = true;
},
);
}
}
@ -219,9 +207,9 @@ impl CopilotCodeVerification {
cx,
{
let verification_uri = data.verification_uri.clone();
move |_, _, cx| {
move |_, verification, cx| {
cx.platform().open_url(&verification_uri);
cx.dispatch_action(ClickedConnect)
verification.connect_clicked = true;
}
},
))

View File

@ -1,145 +1,24 @@
use std::sync::Arc;
use context_menu::{ContextMenu, ContextMenuItem};
use copilot::{Copilot, Reinstall, SignOut, Status};
use editor::Editor;
use gpui::{
elements::*,
impl_internal_actions,
platform::{CursorStyle, MouseButton},
AnyElement, AppContext, Element, Entity, MouseState, Subscription, View, ViewContext,
ViewHandle,
ViewHandle, WindowContext,
};
use settings::{settings_file::SettingsFile, Settings};
use std::sync::Arc;
use util::ResultExt;
use workspace::{
item::ItemHandle, notifications::simple_message_notification::OsOpen, DismissToast,
StatusItemView,
item::ItemHandle, notifications::simple_message_notification::OsOpen, StatusItemView, Toast,
Workspace,
};
use copilot::{Copilot, Reinstall, SignIn, SignOut, Status};
const COPILOT_SETTINGS_URL: &str = "https://github.com/settings/copilot";
const COPILOT_STARTING_TOAST_ID: usize = 1337;
const COPILOT_ERROR_TOAST_ID: usize = 1338;
#[derive(Clone, PartialEq)]
pub struct DeployCopilotMenu;
#[derive(Clone, PartialEq)]
pub struct DeployCopilotStartMenu;
#[derive(Clone, PartialEq)]
pub struct HideCopilot;
#[derive(Clone, PartialEq)]
pub struct InitiateSignIn;
#[derive(Clone, PartialEq)]
pub struct ToggleCopilotForLanguage {
language: Arc<str>,
}
#[derive(Clone, PartialEq)]
pub struct ToggleCopilotGlobally;
// TODO: Make the other code path use `get_or_insert` logic for this modal
#[derive(Clone, PartialEq)]
pub struct DeployCopilotModal;
impl_internal_actions!(
copilot,
[
DeployCopilotMenu,
DeployCopilotStartMenu,
HideCopilot,
InitiateSignIn,
DeployCopilotModal,
ToggleCopilotForLanguage,
ToggleCopilotGlobally,
]
);
pub fn init(cx: &mut AppContext) {
cx.add_action(CopilotButton::deploy_copilot_menu);
cx.add_action(CopilotButton::deploy_copilot_start_menu);
cx.add_action(
|_: &mut CopilotButton, action: &ToggleCopilotForLanguage, cx| {
let language = action.language.clone();
let show_copilot_suggestions = cx
.global::<Settings>()
.show_copilot_suggestions(Some(&language));
SettingsFile::update(cx, move |file_contents| {
file_contents.languages.insert(
language,
settings::EditorSettings {
show_copilot_suggestions: Some((!show_copilot_suggestions).into()),
..Default::default()
},
);
})
},
);
cx.add_action(|_: &mut CopilotButton, _: &ToggleCopilotGlobally, cx| {
let show_copilot_suggestions = cx.global::<Settings>().show_copilot_suggestions(None);
SettingsFile::update(cx, move |file_contents| {
file_contents.editor.show_copilot_suggestions = Some((!show_copilot_suggestions).into())
})
});
cx.add_action(|_: &mut CopilotButton, _: &HideCopilot, cx| {
SettingsFile::update(cx, move |file_contents| {
file_contents.features.copilot = Some(false)
})
});
cx.add_action(|_: &mut CopilotButton, _: &InitiateSignIn, cx| {
let Some(copilot) = Copilot::global(cx) else {
return;
};
let status = copilot.read(cx).status();
match status {
Status::Starting { task } => {
cx.dispatch_action(workspace::Toast::new(
COPILOT_STARTING_TOAST_ID,
"Copilot is starting...",
));
let window_id = cx.window_id();
let task = task.to_owned();
cx.spawn(|handle, mut cx| async move {
task.await;
cx.update(|cx| {
if let Some(copilot) = Copilot::global(cx) {
let status = copilot.read(cx).status();
match status {
Status::Authorized => cx.dispatch_action_at(
window_id,
handle.id(),
workspace::Toast::new(
COPILOT_STARTING_TOAST_ID,
"Copilot has started!",
),
),
_ => {
cx.dispatch_action_at(
window_id,
handle.id(),
DismissToast::new(COPILOT_STARTING_TOAST_ID),
);
cx.dispatch_action_at(window_id, handle.id(), SignIn)
}
}
}
})
})
.detach();
}
_ => cx.dispatch_action(SignIn),
}
})
}
pub struct CopilotButton {
popup_menu: ViewHandle<ContextMenu>,
editor_subscription: Option<(Subscription, usize)>,
@ -217,15 +96,25 @@ impl View for CopilotButton {
.with_cursor_style(CursorStyle::PointingHand)
.on_click(MouseButton::Left, {
let status = status.clone();
move |_, _, cx| match status {
Status::Authorized => cx.dispatch_action(DeployCopilotMenu),
Status::Error(ref e) => cx.dispatch_action(workspace::Toast::new_action(
COPILOT_ERROR_TOAST_ID,
format!("Copilot can't be started: {}", e),
"Reinstall Copilot",
Reinstall,
)),
_ => cx.dispatch_action(DeployCopilotStartMenu),
move |_, this, cx| match status {
Status::Authorized => this.deploy_copilot_menu(cx),
Status::Error(ref e) => {
if let Some(workspace) = cx.root_view().clone().downcast::<Workspace>()
{
workspace.update(cx, |workspace, cx| {
workspace.show_toast(
Toast::new_action(
COPILOT_ERROR_TOAST_ID,
format!("Copilot can't be started: {}", e),
"Reinstall Copilot",
Reinstall,
),
cx,
);
});
}
}
_ => this.deploy_copilot_start_menu(cx),
}
})
.with_tooltip::<Self>(
@ -264,15 +153,15 @@ impl CopilotButton {
}
}
pub fn deploy_copilot_start_menu(
&mut self,
_: &DeployCopilotStartMenu,
cx: &mut ViewContext<Self>,
) {
pub fn deploy_copilot_start_menu(&mut self, cx: &mut ViewContext<Self>) {
let mut menu_options = Vec::with_capacity(2);
menu_options.push(ContextMenuItem::item("Sign In", InitiateSignIn));
menu_options.push(ContextMenuItem::item("Disable Copilot", HideCopilot));
menu_options.push(ContextMenuItem::handler("Sign In", |cx| {
initiate_sign_in(cx)
}));
menu_options.push(ContextMenuItem::handler("Disable Copilot", |cx| {
hide_copilot(cx)
}));
self.popup_menu.update(cx, |menu, cx| {
menu.show(
@ -284,53 +173,48 @@ impl CopilotButton {
});
}
pub fn deploy_copilot_menu(&mut self, _: &DeployCopilotMenu, cx: &mut ViewContext<Self>) {
pub fn deploy_copilot_menu(&mut self, cx: &mut ViewContext<Self>) {
let settings = cx.global::<Settings>();
let mut menu_options = Vec::with_capacity(6);
if let Some(language) = &self.language {
if let Some(language) = self.language.clone() {
let language_enabled = settings.show_copilot_suggestions(Some(language.as_ref()));
menu_options.push(ContextMenuItem::item(
menu_options.push(ContextMenuItem::handler(
format!(
"{} Suggestions for {}",
if language_enabled { "Hide" } else { "Show" },
language
),
ToggleCopilotForLanguage {
language: language.to_owned(),
},
move |cx| toggle_copilot_for_language(language.clone(), cx),
));
}
let globally_enabled = cx.global::<Settings>().show_copilot_suggestions(None);
menu_options.push(ContextMenuItem::item(
menu_options.push(ContextMenuItem::handler(
if globally_enabled {
"Hide Suggestions for All Files"
} else {
"Show Suggestions for All Files"
},
ToggleCopilotGlobally,
|cx| toggle_copilot_globally(cx),
));
menu_options.push(ContextMenuItem::Separator);
let icon_style = settings.theme.copilot.out_link_icon.clone();
menu_options.push(ContextMenuItem::element_item(
Box::new(
move |state: &mut MouseState, style: &theme::ContextMenuItem| {
Flex::row()
.with_child(Label::new("Copilot Settings", style.label.clone()))
.with_child(theme::ui::icon(icon_style.style_for(state, false)))
.align_children_center()
.into_any()
},
),
menu_options.push(ContextMenuItem::action(
move |state: &mut MouseState, style: &theme::ContextMenuItem| {
Flex::row()
.with_child(Label::new("Copilot Settings", style.label.clone()))
.with_child(theme::ui::icon(icon_style.style_for(state, false)))
.align_children_center()
.into_any()
},
OsOpen::new(COPILOT_SETTINGS_URL),
));
menu_options.push(ContextMenuItem::item("Sign Out", SignOut));
menu_options.push(ContextMenuItem::action("Sign Out", SignOut));
self.popup_menu.update(cx, |menu, cx| {
menu.show(
@ -375,3 +259,80 @@ impl StatusItemView for CopilotButton {
cx.notify();
}
}
fn toggle_copilot_globally(cx: &mut AppContext) {
let show_copilot_suggestions = cx.global::<Settings>().show_copilot_suggestions(None);
SettingsFile::update(cx, move |file_contents| {
file_contents.editor.show_copilot_suggestions = Some((!show_copilot_suggestions).into())
});
}
fn toggle_copilot_for_language(language: Arc<str>, cx: &mut AppContext) {
let show_copilot_suggestions = cx
.global::<Settings>()
.show_copilot_suggestions(Some(&language));
SettingsFile::update(cx, move |file_contents| {
file_contents.languages.insert(
language,
settings::EditorSettings {
show_copilot_suggestions: Some((!show_copilot_suggestions).into()),
..Default::default()
},
);
})
}
fn hide_copilot(cx: &mut AppContext) {
SettingsFile::update(cx, move |file_contents| {
file_contents.features.copilot = Some(false)
})
}
fn initiate_sign_in(cx: &mut WindowContext) {
let Some(copilot) = Copilot::global(cx) else {
return;
};
let status = copilot.read(cx).status();
match status {
Status::Starting { task } => {
let Some(workspace) = cx.root_view().clone().downcast::<Workspace>() else {
return;
};
workspace.update(cx, |workspace, cx| {
workspace.show_toast(
Toast::new(COPILOT_STARTING_TOAST_ID, "Copilot is starting..."),
cx,
)
});
let workspace = workspace.downgrade();
cx.spawn(|mut cx| async move {
task.await;
if let Some(copilot) = cx.read(Copilot::global) {
workspace
.update(&mut cx, |workspace, cx| match copilot.read(cx).status() {
Status::Authorized => workspace.show_toast(
Toast::new(COPILOT_STARTING_TOAST_ID, "Copilot has started!"),
cx,
),
_ => {
workspace.dismiss_toast(COPILOT_STARTING_TOAST_ID, cx);
copilot
.update(cx, |copilot, cx| copilot.sign_in(cx))
.detach_and_log_err(cx);
}
})
.log_err();
}
})
.detach();
}
_ => {
copilot
.update(cx, |copilot, cx| copilot.sign_in(cx))
.detach_and_log_err(cx);
}
}
}

View File

@ -10,8 +10,8 @@ use editor::{
Editor, ExcerptId, ExcerptRange, MultiBuffer, ToOffset,
};
use gpui::{
actions, elements::*, fonts::TextStyle, impl_internal_actions, serde_json, AnyViewHandle,
AppContext, Entity, ModelHandle, Task, View, ViewContext, ViewHandle, WeakViewHandle,
actions, elements::*, fonts::TextStyle, serde_json, AnyViewHandle, AppContext, Entity,
ModelHandle, Task, View, ViewContext, ViewHandle, WeakViewHandle,
};
use language::{
Anchor, Bias, Buffer, Diagnostic, DiagnosticEntry, DiagnosticSeverity, Point, Selection,
@ -38,8 +38,6 @@ use workspace::{
actions!(diagnostics, [Deploy]);
impl_internal_actions!(diagnostics, [Jump]);
const CONTEXT_LINE_COUNT: u32 = 1;
pub fn init(cx: &mut AppContext) {
@ -551,6 +549,11 @@ impl Item for ProjectDiagnosticsEditor {
false
}
fn added_to_workspace(&mut self, workspace: &mut Workspace, cx: &mut ViewContext<Self>) {
self.editor
.update(cx, |editor, cx| editor.added_to_workspace(workspace, cx));
}
fn navigate(&mut self, data: Box<dyn Any>, cx: &mut ViewContext<Self>) -> bool {
self.editor
.update(cx, |editor, cx| editor.navigate(data, cx))

View File

@ -37,7 +37,7 @@ use gpui::{
executor,
fonts::{self, HighlightStyle, TextStyle},
geometry::vector::Vector2F,
impl_actions, impl_internal_actions,
impl_actions,
keymap_matcher::KeymapContext,
platform::{CursorStyle, MouseButton},
serde_json::{self, json},
@ -45,7 +45,7 @@ use gpui::{
ModelHandle, Subscription, Task, View, ViewContext, ViewHandle, WeakViewHandle, WindowContext,
};
use highlight_matching_bracket::refresh_matching_bracket_highlights;
use hover_popover::{hide_hover, HideHover, HoverState};
use hover_popover::{hide_hover, HoverState};
pub use items::MAX_TAB_TITLE_LEN;
use itertools::Itertools;
pub use language::{char_kind, CharKind};
@ -86,7 +86,7 @@ use std::{
pub use sum_tree::Bias;
use theme::{DiagnosticStyle, Theme};
use util::{post_inc, RangeExt, ResultExt, TryFutureExt};
use workspace::{ItemNavHistory, ViewId, Workspace, WorkspaceId};
use workspace::{ItemNavHistory, ViewId, Workspace};
use crate::git::diff_hunk_to_display;
@ -104,16 +104,6 @@ pub struct SelectNext {
pub replace_newest: bool,
}
#[derive(Clone, PartialEq)]
pub struct Select(pub SelectPhase);
#[derive(Clone, Debug, PartialEq)]
pub struct Jump {
path: ProjectPath,
position: Point,
anchor: language::Anchor,
}
#[derive(Clone, Deserialize, PartialEq)]
pub struct SelectToBeginningOfLine {
#[serde(default)]
@ -285,8 +275,6 @@ impl_actions!(
]
);
impl_internal_actions!(editor, [Select, Jump]);
enum DocumentHighlightRead {}
enum DocumentHighlightWrite {}
enum InputComposition {}
@ -299,7 +287,6 @@ pub enum Direction {
pub fn init(cx: &mut AppContext) {
cx.add_action(Editor::new_file);
cx.add_action(Editor::select);
cx.add_action(Editor::cancel);
cx.add_action(Editor::newline);
cx.add_action(Editor::newline_above);
@ -381,7 +368,6 @@ pub fn init(cx: &mut AppContext) {
cx.add_action(Editor::show_completions);
cx.add_action(Editor::toggle_code_actions);
cx.add_action(Editor::open_excerpts);
cx.add_action(Editor::jump);
cx.add_action(Editor::toggle_soft_wrap);
cx.add_action(Editor::reveal_in_finder);
cx.add_action(Editor::copy_path);
@ -400,8 +386,6 @@ pub fn init(cx: &mut AppContext) {
cx.add_action(Editor::copilot_suggest);
hover_popover::init(cx);
link_go_to_definition::init(cx);
mouse_context_menu::init(cx);
scroll::actions::init(cx);
workspace::register_project_item::<Editor>(cx);
@ -509,7 +493,7 @@ pub struct Editor {
pending_rename: Option<RenameState>,
searchable: bool,
cursor_shape: CursorShape,
workspace_id: Option<WorkspaceId>,
workspace: Option<(WeakViewHandle<Workspace>, i64)>,
keymap_context_layers: BTreeMap<TypeId, KeymapContext>,
input_enabled: bool,
read_only: bool,
@ -1282,7 +1266,7 @@ impl Editor {
searchable: true,
override_text_style: None,
cursor_shape: Default::default(),
workspace_id: None,
workspace: None,
keymap_context_layers: Default::default(),
input_enabled: true,
read_only: false,
@ -1495,7 +1479,7 @@ impl Editor {
}
}
hide_hover(self, &HideHover, cx);
hide_hover(self, cx);
if old_cursor_position.to_display_point(&display_map).row()
!= new_cursor_position.to_display_point(&display_map).row()
@ -1563,7 +1547,7 @@ impl Editor {
});
}
fn select(&mut self, Select(phase): &Select, cx: &mut ViewContext<Self>) {
fn select(&mut self, phase: SelectPhase, cx: &mut ViewContext<Self>) {
self.hide_context_menu(cx);
match phase {
@ -1571,20 +1555,20 @@ impl Editor {
position,
add,
click_count,
} => self.begin_selection(*position, *add, *click_count, cx),
} => self.begin_selection(position, add, click_count, cx),
SelectPhase::BeginColumnar {
position,
goal_column,
} => self.begin_columnar_selection(*position, *goal_column, cx),
} => self.begin_columnar_selection(position, goal_column, cx),
SelectPhase::Extend {
position,
click_count,
} => self.extend_selection(*position, *click_count, cx),
} => self.extend_selection(position, click_count, cx),
SelectPhase::Update {
position,
goal_column,
scroll_position,
} => self.update_selection(*position, *goal_column, *scroll_position, cx),
} => self.update_selection(position, goal_column, scroll_position, cx),
SelectPhase::End => self.end_selection(cx),
}
}
@ -1879,7 +1863,7 @@ impl Editor {
return;
}
if hide_hover(self, &HideHover, cx) {
if hide_hover(self, cx) {
return;
}
@ -6756,10 +6740,14 @@ impl Editor {
});
}
fn jump(workspace: &mut Workspace, action: &Jump, cx: &mut ViewContext<Workspace>) {
let editor = workspace.open_path(action.path.clone(), None, true, cx);
let position = action.position;
let anchor = action.anchor;
fn jump(
workspace: &mut Workspace,
path: ProjectPath,
position: Point,
anchor: language::Anchor,
cx: &mut ViewContext<Workspace>,
) {
let editor = workspace.open_path(path, None, true, cx);
cx.spawn(|_, mut cx| async move {
let editor = editor
.await?
@ -7025,7 +7013,7 @@ impl View for Editor {
if font_changed {
cx.defer(move |editor, cx: &mut ViewContext<Editor>| {
hide_hover(editor, &HideHover, cx);
hide_hover(editor, cx);
hide_link_definition(editor, cx);
});
}
@ -7074,7 +7062,7 @@ impl View for Editor {
self.buffer
.update(cx, |buffer, cx| buffer.remove_active_selections(cx));
self.hide_context_menu(cx);
hide_hover(self, &HideHover, cx);
hide_hover(self, cx);
cx.emit(Event::Blurred);
cx.notify();
}

View File

@ -24,7 +24,7 @@ use util::{
};
use workspace::{
item::{FollowableItem, Item, ItemHandle},
NavigationEntry, Pane, ViewId,
NavigationEntry, ViewId,
};
#[gpui::test]
@ -486,12 +486,15 @@ fn test_clone(cx: &mut TestAppContext) {
}
#[gpui::test]
fn test_navigation_history(cx: &mut TestAppContext) {
async fn test_navigation_history(cx: &mut TestAppContext) {
cx.update(|cx| cx.set_global(Settings::test(cx)));
cx.set_global(DragAndDrop::<Workspace>::default());
use workspace::item::Item;
let (_, pane) = cx.add_window(|cx| Pane::new(0, None, || &[], cx));
let fs = FakeFs::new(cx.background());
let project = Project::test(fs, [], cx).await;
let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
cx.add_view(&pane, |cx| {
let buffer = MultiBuffer::build_simple(&sample_text(300, 5, 'a'), cx);
let mut editor = build_editor(buffer.clone(), cx);
@ -5576,7 +5579,8 @@ async fn test_following_with_multiple_excerpts(cx: &mut gpui::TestAppContext) {
Settings::test_async(cx);
let fs = FakeFs::new(cx.background());
let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
let (_, pane) = cx.add_window(|cx| Pane::new(0, None, || &[], cx));
let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
let leader = pane.update(cx, |_, cx| {
let multibuffer = cx.add_model(|_| MultiBuffer::new(0));

View File

@ -1,20 +1,19 @@
use super::{
display_map::{BlockContext, ToDisplayPoint},
Anchor, DisplayPoint, Editor, EditorMode, EditorSnapshot, Select, SelectPhase, SoftWrap,
ToPoint, MAX_LINE_LEN,
Anchor, DisplayPoint, Editor, EditorMode, EditorSnapshot, SelectPhase, SoftWrap, ToPoint,
MAX_LINE_LEN,
};
use crate::{
display_map::{BlockStyle, DisplaySnapshot, FoldStatus, TransformBlock},
git::{diff_hunk_to_display, DisplayDiffHunk},
hover_popover::{
HideHover, HoverAt, HOVER_POPOVER_GAP, MIN_POPOVER_CHARACTER_WIDTH, MIN_POPOVER_LINE_HEIGHT,
hide_hover, hover_at, HOVER_POPOVER_GAP, MIN_POPOVER_CHARACTER_WIDTH,
MIN_POPOVER_LINE_HEIGHT,
},
link_go_to_definition::{
GoToFetchedDefinition, GoToFetchedTypeDefinition, UpdateGoToDefinitionLink,
go_to_fetched_definition, go_to_fetched_type_definition, update_go_to_definition_link,
},
mouse_context_menu::DeployMouseContextMenu,
scroll::actions::Scroll,
EditorStyle, GutterHover, UnfoldAt,
mouse_context_menu, EditorStyle, GutterHover, UnfoldAt,
};
use clock::ReplicaId;
use collections::{BTreeMap, HashMap};
@ -115,9 +114,10 @@ impl EditorElement {
)
.on_down(MouseButton::Left, {
let position_map = position_map.clone();
move |e, _, cx| {
move |event, editor, cx| {
if !Self::mouse_down(
e.platform_event,
editor,
event.platform_event,
position_map.as_ref(),
text_bounds,
gutter_bounds,
@ -129,8 +129,9 @@ impl EditorElement {
})
.on_down(MouseButton::Right, {
let position_map = position_map.clone();
move |event, _, cx| {
move |event, editor, cx| {
if !Self::mouse_right_down(
editor,
event.position,
position_map.as_ref(),
text_bounds,
@ -144,12 +145,12 @@ impl EditorElement {
let position_map = position_map.clone();
move |event, editor, cx| {
if !Self::mouse_up(
editor,
event.position,
event.cmd,
event.shift,
position_map.as_ref(),
text_bounds,
editor,
cx,
) {
cx.propagate_event()
@ -160,10 +161,10 @@ impl EditorElement {
let position_map = position_map.clone();
move |event, editor, cx| {
if !Self::mouse_dragged(
editor,
event.platform_event,
position_map.as_ref(),
text_bounds,
editor,
cx,
) {
cx.propagate_event()
@ -172,24 +173,31 @@ impl EditorElement {
})
.on_move({
let position_map = position_map.clone();
move |e, _, cx| {
if !Self::mouse_moved(e.platform_event, &position_map, text_bounds, cx) {
move |event, editor, cx| {
if !Self::mouse_moved(
editor,
event.platform_event,
&position_map,
text_bounds,
cx,
) {
cx.propagate_event()
}
}
})
.on_move_out(move |_, _: &mut Editor, cx| {
.on_move_out(move |_, editor: &mut Editor, cx| {
if has_popovers {
cx.dispatch_action(HideHover);
hide_hover(editor, cx);
}
})
.on_scroll({
let position_map = position_map.clone();
move |e, _, cx| {
move |event, editor, cx| {
if !Self::scroll(
e.position,
*e.delta.raw(),
e.delta.precise(),
editor,
event.position,
*event.delta.raw(),
event.delta.precise(),
&position_map,
bounds,
cx,
@ -212,6 +220,7 @@ impl EditorElement {
}
fn mouse_down(
editor: &mut Editor,
MouseButtonEvent {
position,
modifiers:
@ -239,27 +248,37 @@ impl EditorElement {
let (position, target_position) = position_map.point_for_position(text_bounds, position);
if shift && alt {
cx.dispatch_action(Select(SelectPhase::BeginColumnar {
position,
goal_column: target_position.column(),
}));
editor.select(
SelectPhase::BeginColumnar {
position,
goal_column: target_position.column(),
},
cx,
);
} else if shift && !ctrl && !alt && !cmd {
cx.dispatch_action(Select(SelectPhase::Extend {
position,
click_count,
}));
editor.select(
SelectPhase::Extend {
position,
click_count,
},
cx,
);
} else {
cx.dispatch_action(Select(SelectPhase::Begin {
position,
add: alt,
click_count,
}));
editor.select(
SelectPhase::Begin {
position,
add: alt,
click_count,
},
cx,
);
}
true
}
fn mouse_right_down(
editor: &mut Editor,
position: Vector2F,
position_map: &PositionMap,
text_bounds: RectF,
@ -270,38 +289,45 @@ impl EditorElement {
}
let (point, _) = position_map.point_for_position(text_bounds, position);
cx.dispatch_action(DeployMouseContextMenu { position, point });
mouse_context_menu::deploy_context_menu(editor, position, point, cx);
true
}
fn mouse_up(
editor: &mut Editor,
position: Vector2F,
cmd: bool,
shift: bool,
position_map: &PositionMap,
text_bounds: RectF,
editor: &mut Editor,
cx: &mut EventContext<Editor>,
) -> bool {
let end_selection = editor.has_pending_selection();
let pending_nonempty_selections = editor.has_pending_nonempty_selection();
if end_selection {
cx.dispatch_action(Select(SelectPhase::End));
editor.select(SelectPhase::End, cx);
}
if !pending_nonempty_selections && cmd && text_bounds.contains_point(position) {
let (point, target_point) = position_map.point_for_position(text_bounds, position);
if let Some(workspace) = editor
.workspace
.as_ref()
.and_then(|(workspace, _)| workspace.upgrade(cx))
{
if !pending_nonempty_selections && cmd && text_bounds.contains_point(position) {
let (point, target_point) = position_map.point_for_position(text_bounds, position);
if point == target_point {
if shift {
cx.dispatch_action(GoToFetchedTypeDefinition { point });
} else {
cx.dispatch_action(GoToFetchedDefinition { point });
if point == target_point {
workspace.update(cx, |workspace, cx| {
if shift {
go_to_fetched_type_definition(workspace, point, cx);
} else {
go_to_fetched_definition(workspace, point, cx);
}
});
return true;
}
return true;
}
}
@ -309,6 +335,7 @@ impl EditorElement {
}
fn mouse_dragged(
editor: &mut Editor,
MouseMovedEvent {
modifiers: Modifiers { cmd, shift, .. },
position,
@ -316,7 +343,6 @@ impl EditorElement {
}: MouseMovedEvent,
position_map: &PositionMap,
text_bounds: RectF,
editor: &mut Editor,
cx: &mut EventContext<Editor>,
) -> bool {
// This will be handled more correctly once https://github.com/zed-industries/zed/issues/1218 is completed
@ -332,11 +358,7 @@ impl EditorElement {
None
};
cx.dispatch_action(UpdateGoToDefinitionLink {
point,
cmd_held: cmd,
shift_held: shift,
});
update_go_to_definition_link(editor, point, cmd, shift, cx);
if editor.has_pending_selection() {
let mut scroll_delta = Vector2F::zero();
@ -368,22 +390,25 @@ impl EditorElement {
let (position, target_position) =
position_map.point_for_position(text_bounds, position);
cx.dispatch_action(Select(SelectPhase::Update {
position,
goal_column: target_position.column(),
scroll_position: (position_map.snapshot.scroll_position() + scroll_delta)
.clamp(Vector2F::zero(), position_map.scroll_max),
}));
cx.dispatch_action(HoverAt { point });
editor.select(
SelectPhase::Update {
position,
goal_column: target_position.column(),
scroll_position: (position_map.snapshot.scroll_position() + scroll_delta)
.clamp(Vector2F::zero(), position_map.scroll_max),
},
cx,
);
hover_at(editor, point, cx);
true
} else {
cx.dispatch_action(HoverAt { point });
hover_at(editor, point, cx);
false
}
}
fn mouse_moved(
editor: &mut Editor,
MouseMovedEvent {
modifiers: Modifiers { shift, cmd, .. },
position,
@ -397,18 +422,14 @@ impl EditorElement {
// Don't trigger hover popover if mouse is hovering over context menu
let point = position_to_display_point(position, text_bounds, position_map);
cx.dispatch_action(UpdateGoToDefinitionLink {
point,
cmd_held: cmd,
shift_held: shift,
});
cx.dispatch_action(HoverAt { point });
update_go_to_definition_link(editor, point, cmd, shift, cx);
hover_at(editor, point, cx);
true
}
fn scroll(
editor: &mut Editor,
position: Vector2F,
mut delta: Vector2F,
precise: bool,
@ -436,11 +457,7 @@ impl EditorElement {
let x = (scroll_position.x() * max_glyph_width - delta.x()) / max_glyph_width;
let y = (scroll_position.y() * line_height - delta.y()) / line_height;
let scroll_position = vec2f(x, y).clamp(Vector2F::zero(), position_map.scroll_max);
cx.dispatch_action(Scroll {
scroll_position,
axis,
});
editor.scroll(scroll_position, axis, cx);
true
}
@ -1421,18 +1438,15 @@ impl EditorElement {
} => {
let id = *id;
let jump_icon = project::File::from_dyn(buffer.file()).map(|file| {
let jump_position = range
let jump_path = ProjectPath {
worktree_id: file.worktree_id(cx),
path: file.path.clone(),
};
let jump_anchor = range
.primary
.as_ref()
.map_or(range.context.start, |primary| primary.start);
let jump_action = crate::Jump {
path: ProjectPath {
worktree_id: file.worktree_id(cx),
path: file.path.clone(),
},
position: language::ToPoint::to_point(&jump_position, buffer),
anchor: jump_position,
};
let jump_position = language::ToPoint::to_point(&jump_anchor, buffer);
enum JumpIcon {}
MouseEventHandler::<JumpIcon, _>::new(id.into(), cx, |state, _| {
@ -1449,8 +1463,22 @@ impl EditorElement {
.with_height(style.button_width)
})
.with_cursor_style(CursorStyle::PointingHand)
.on_click(MouseButton::Left, move |_, _, cx| {
cx.dispatch_action(jump_action.clone())
.on_click(MouseButton::Left, move |_, editor, cx| {
if let Some(workspace) = editor
.workspace
.as_ref()
.and_then(|(workspace, _)| workspace.upgrade(cx))
{
workspace.update(cx, |workspace, cx| {
Editor::jump(
workspace,
jump_path.clone(),
jump_position,
jump_anchor,
cx,
);
});
}
})
.with_tooltip::<JumpIcon>(
id.into(),

View File

@ -3,7 +3,6 @@ use gpui::{
actions,
elements::{Flex, MouseEventHandler, Padding, ParentElement, Text},
fonts::{HighlightStyle, Underline, Weight},
impl_internal_actions,
platform::{CursorStyle, MouseButton},
AnyElement, AppContext, CursorRegion, Element, ModelHandle, MouseRegion, Task, ViewContext,
};
@ -25,21 +24,10 @@ pub const MIN_POPOVER_CHARACTER_WIDTH: f32 = 20.;
pub const MIN_POPOVER_LINE_HEIGHT: f32 = 4.;
pub const HOVER_POPOVER_GAP: f32 = 10.;
#[derive(Clone, PartialEq)]
pub struct HoverAt {
pub point: Option<DisplayPoint>,
}
#[derive(Copy, Clone, PartialEq)]
pub struct HideHover;
actions!(editor, [Hover]);
impl_internal_actions!(editor, [HoverAt, HideHover]);
pub fn init(cx: &mut AppContext) {
cx.add_action(hover);
cx.add_action(hover_at);
cx.add_action(hide_hover);
}
/// Bindable action which uses the most recent selection head to trigger a hover
@ -50,12 +38,12 @@ pub fn hover(editor: &mut Editor, _: &Hover, cx: &mut ViewContext<Editor>) {
/// The internal hover action dispatches between `show_hover` or `hide_hover`
/// depending on whether a point to hover over is provided.
pub fn hover_at(editor: &mut Editor, action: &HoverAt, cx: &mut ViewContext<Editor>) {
pub fn hover_at(editor: &mut Editor, point: Option<DisplayPoint>, cx: &mut ViewContext<Editor>) {
if cx.global::<Settings>().hover_popover_enabled {
if let Some(point) = action.point {
if let Some(point) = point {
show_hover(editor, point, false, cx);
} else {
hide_hover(editor, &HideHover, cx);
hide_hover(editor, cx);
}
}
}
@ -63,7 +51,7 @@ pub fn hover_at(editor: &mut Editor, action: &HoverAt, cx: &mut ViewContext<Edit
/// Hides the type information popup.
/// Triggered by the `Hover` action when the cursor is not over a symbol or when the
/// selections changed.
pub fn hide_hover(editor: &mut Editor, _: &HideHover, cx: &mut ViewContext<Editor>) -> bool {
pub fn hide_hover(editor: &mut Editor, cx: &mut ViewContext<Editor>) -> bool {
let did_hide = editor.hover_state.info_popover.take().is_some()
| editor.hover_state.diagnostic_popover.take().is_some();
@ -130,7 +118,7 @@ fn show_hover(
// Hover triggered from same location as last time. Don't show again.
return;
} else {
hide_hover(editor, &HideHover, cx);
hide_hover(editor, cx);
}
}
}
@ -736,15 +724,7 @@ mod tests {
fn test() { printˇln!(); }
"});
cx.update_editor(|editor, cx| {
hover_at(
editor,
&HoverAt {
point: Some(hover_point),
},
cx,
)
});
cx.update_editor(|editor, cx| hover_at(editor, Some(hover_point), cx));
assert!(!cx.editor(|editor, _| editor.hover_state.visible()));
// After delay, hover should be visible.
@ -783,15 +763,7 @@ mod tests {
let mut request = cx
.lsp
.handle_request::<lsp::request::HoverRequest, _, _>(|_, _| async move { Ok(None) });
cx.update_editor(|editor, cx| {
hover_at(
editor,
&HoverAt {
point: Some(hover_point),
},
cx,
)
});
cx.update_editor(|editor, cx| hover_at(editor, Some(hover_point), cx));
cx.foreground()
.advance_clock(Duration::from_millis(HOVER_DELAY_MILLIS + 100));
request.next().await;

View File

@ -794,7 +794,7 @@ impl Item for Editor {
fn added_to_workspace(&mut self, workspace: &mut Workspace, cx: &mut ViewContext<Self>) {
let workspace_id = workspace.database_id();
let item_id = cx.view_id();
self.workspace_id = Some(workspace_id);
self.workspace = Some((workspace.weak_handle(), workspace.database_id()));
fn serialize(
buffer: ModelHandle<Buffer>,
@ -819,9 +819,9 @@ impl Item for Editor {
serialize(buffer.clone(), workspace_id, item_id, cx);
cx.subscribe(&buffer, |this, buffer, event, cx| {
if let Some(workspace_id) = this.workspace_id {
if let Some((_, workspace_id)) = this.workspace.as_ref() {
if let language::Event::FileHandleChanged = event {
serialize(buffer, workspace_id, cx.view_id(), cx);
serialize(buffer, *workspace_id, cx.view_id(), cx);
}
}
})

View File

@ -1,6 +1,6 @@
use std::ops::Range;
use gpui::{impl_internal_actions, AppContext, Task, ViewContext};
use gpui::{Task, ViewContext};
use language::{Bias, ToOffset};
use project::LocationLink;
use settings::Settings;
@ -8,42 +8,9 @@ use util::TryFutureExt;
use workspace::Workspace;
use crate::{
Anchor, DisplayPoint, Editor, EditorSnapshot, GoToDefinition, GoToTypeDefinition, Select,
SelectPhase,
Anchor, DisplayPoint, Editor, EditorSnapshot, GoToDefinition, GoToTypeDefinition, SelectPhase,
};
#[derive(Clone, PartialEq)]
pub struct UpdateGoToDefinitionLink {
pub point: Option<DisplayPoint>,
pub cmd_held: bool,
pub shift_held: bool,
}
#[derive(Clone, PartialEq)]
pub struct GoToFetchedDefinition {
pub point: DisplayPoint,
}
#[derive(Clone, PartialEq)]
pub struct GoToFetchedTypeDefinition {
pub point: DisplayPoint,
}
impl_internal_actions!(
editor,
[
UpdateGoToDefinitionLink,
GoToFetchedDefinition,
GoToFetchedTypeDefinition
]
);
pub fn init(cx: &mut AppContext) {
cx.add_action(update_go_to_definition_link);
cx.add_action(go_to_fetched_definition);
cx.add_action(go_to_fetched_type_definition);
}
#[derive(Debug, Default)]
pub struct LinkGoToDefinitionState {
pub last_mouse_location: Option<Anchor>,
@ -55,11 +22,9 @@ pub struct LinkGoToDefinitionState {
pub fn update_go_to_definition_link(
editor: &mut Editor,
&UpdateGoToDefinitionLink {
point,
cmd_held,
shift_held,
}: &UpdateGoToDefinitionLink,
point: Option<DisplayPoint>,
cmd_held: bool,
shift_held: bool,
cx: &mut ViewContext<Editor>,
) {
let pending_nonempty_selection = editor.has_pending_nonempty_selection();
@ -286,7 +251,7 @@ pub fn hide_link_definition(editor: &mut Editor, cx: &mut ViewContext<Editor>) {
pub fn go_to_fetched_definition(
workspace: &mut Workspace,
&GoToFetchedDefinition { point }: &GoToFetchedDefinition,
point: DisplayPoint,
cx: &mut ViewContext<Workspace>,
) {
go_to_fetched_definition_of_kind(LinkDefinitionKind::Symbol, workspace, point, cx);
@ -294,7 +259,7 @@ pub fn go_to_fetched_definition(
pub fn go_to_fetched_type_definition(
workspace: &mut Workspace,
&GoToFetchedTypeDefinition { point }: &GoToFetchedTypeDefinition,
point: DisplayPoint,
cx: &mut ViewContext<Workspace>,
) {
go_to_fetched_definition_of_kind(LinkDefinitionKind::Type, workspace, point, cx);
@ -334,11 +299,11 @@ fn go_to_fetched_definition_of_kind(
} else {
editor_handle.update(cx, |editor, cx| {
editor.select(
&Select(SelectPhase::Begin {
SelectPhase::Begin {
position: point,
add: false,
click_count: 1,
}),
},
cx,
);
});
@ -411,15 +376,7 @@ mod tests {
// Press cmd+shift to trigger highlight
cx.update_editor(|editor, cx| {
update_go_to_definition_link(
editor,
&UpdateGoToDefinitionLink {
point: Some(hover_point),
cmd_held: true,
shift_held: true,
},
cx,
);
update_go_to_definition_link(editor, Some(hover_point), true, true, cx);
});
requests.next().await;
cx.foreground().run_until_parked();
@ -470,11 +427,7 @@ mod tests {
});
cx.update_workspace(|workspace, cx| {
go_to_fetched_type_definition(
workspace,
&GoToFetchedTypeDefinition { point: hover_point },
cx,
);
go_to_fetched_type_definition(workspace, hover_point, cx);
});
requests.next().await;
cx.foreground().run_until_parked();
@ -527,15 +480,7 @@ mod tests {
});
cx.update_editor(|editor, cx| {
update_go_to_definition_link(
editor,
&UpdateGoToDefinitionLink {
point: Some(hover_point),
cmd_held: true,
shift_held: false,
},
cx,
);
update_go_to_definition_link(editor, Some(hover_point), true, false, cx);
});
requests.next().await;
cx.foreground().run_until_parked();
@ -569,15 +514,7 @@ mod tests {
])))
});
cx.update_editor(|editor, cx| {
update_go_to_definition_link(
editor,
&UpdateGoToDefinitionLink {
point: Some(hover_point),
cmd_held: true,
shift_held: false,
},
cx,
);
update_go_to_definition_link(editor, Some(hover_point), true, false, cx);
});
requests.next().await;
cx.foreground().run_until_parked();
@ -599,15 +536,7 @@ mod tests {
Ok(Some(lsp::GotoDefinitionResponse::Link(vec![])))
});
cx.update_editor(|editor, cx| {
update_go_to_definition_link(
editor,
&UpdateGoToDefinitionLink {
point: Some(hover_point),
cmd_held: true,
shift_held: false,
},
cx,
);
update_go_to_definition_link(editor, Some(hover_point), true, false, cx);
});
requests.next().await;
cx.foreground().run_until_parked();
@ -624,15 +553,7 @@ mod tests {
fn do_work() { teˇst(); }
"});
cx.update_editor(|editor, cx| {
update_go_to_definition_link(
editor,
&UpdateGoToDefinitionLink {
point: Some(hover_point),
cmd_held: false,
shift_held: false,
},
cx,
);
update_go_to_definition_link(editor, Some(hover_point), false, false, cx);
});
cx.foreground().run_until_parked();
@ -691,15 +612,7 @@ mod tests {
// Moving the mouse restores the highlights.
cx.update_editor(|editor, cx| {
update_go_to_definition_link(
editor,
&UpdateGoToDefinitionLink {
point: Some(hover_point),
cmd_held: true,
shift_held: false,
},
cx,
);
update_go_to_definition_link(editor, Some(hover_point), true, false, cx);
});
cx.foreground().run_until_parked();
cx.assert_editor_text_highlights::<LinkGoToDefinitionState>(indoc! {"
@ -713,15 +626,7 @@ mod tests {
fn do_work() { tesˇt(); }
"});
cx.update_editor(|editor, cx| {
update_go_to_definition_link(
editor,
&UpdateGoToDefinitionLink {
point: Some(hover_point),
cmd_held: true,
shift_held: false,
},
cx,
);
update_go_to_definition_link(editor, Some(hover_point), true, false, cx);
});
cx.foreground().run_until_parked();
cx.assert_editor_text_highlights::<LinkGoToDefinitionState>(indoc! {"
@ -731,7 +636,7 @@ mod tests {
// Cmd click with existing definition doesn't re-request and dismisses highlight
cx.update_workspace(|workspace, cx| {
go_to_fetched_definition(workspace, &GoToFetchedDefinition { point: hover_point }, cx);
go_to_fetched_definition(workspace, hover_point, cx);
});
// Assert selection moved to to definition
cx.lsp
@ -772,7 +677,7 @@ mod tests {
])))
});
cx.update_workspace(|workspace, cx| {
go_to_fetched_definition(workspace, &GoToFetchedDefinition { point: hover_point }, cx);
go_to_fetched_definition(workspace, hover_point, cx);
});
requests.next().await;
cx.foreground().run_until_parked();
@ -817,15 +722,7 @@ mod tests {
});
});
cx.update_editor(|editor, cx| {
update_go_to_definition_link(
editor,
&UpdateGoToDefinitionLink {
point: Some(hover_point),
cmd_held: true,
shift_held: false,
},
cx,
);
update_go_to_definition_link(editor, Some(hover_point), true, false, cx);
});
cx.foreground().run_until_parked();
assert!(requests.try_next().is_err());

View File

@ -1,29 +1,14 @@
use context_menu::ContextMenuItem;
use gpui::{
elements::AnchorCorner, geometry::vector::Vector2F, impl_internal_actions, AppContext,
ViewContext,
};
use crate::{
DisplayPoint, Editor, EditorMode, FindAllReferences, GoToDefinition, GoToTypeDefinition,
Rename, RevealInFinder, SelectMode, ToggleCodeActions,
};
#[derive(Clone, PartialEq)]
pub struct DeployMouseContextMenu {
pub position: Vector2F,
pub point: DisplayPoint,
}
impl_internal_actions!(editor, [DeployMouseContextMenu]);
pub fn init(cx: &mut AppContext) {
cx.add_action(deploy_context_menu);
}
use context_menu::ContextMenuItem;
use gpui::{elements::AnchorCorner, geometry::vector::Vector2F, ViewContext};
pub fn deploy_context_menu(
editor: &mut Editor,
&DeployMouseContextMenu { position, point }: &DeployMouseContextMenu,
position: Vector2F,
point: DisplayPoint,
cx: &mut ViewContext<Editor>,
) {
if !editor.focused {
@ -51,18 +36,18 @@ pub fn deploy_context_menu(
position,
AnchorCorner::TopLeft,
vec![
ContextMenuItem::item("Rename Symbol", Rename),
ContextMenuItem::item("Go to Definition", GoToDefinition),
ContextMenuItem::item("Go to Type Definition", GoToTypeDefinition),
ContextMenuItem::item("Find All References", FindAllReferences),
ContextMenuItem::item(
ContextMenuItem::action("Rename Symbol", Rename),
ContextMenuItem::action("Go to Definition", GoToDefinition),
ContextMenuItem::action("Go to Type Definition", GoToTypeDefinition),
ContextMenuItem::action("Find All References", FindAllReferences),
ContextMenuItem::action(
"Code Actions",
ToggleCodeActions {
deployed_from_indicator: false,
},
),
ContextMenuItem::Separator,
ContextMenuItem::item("Reveal in Finder", RevealInFinder),
ContextMenuItem::action("Reveal in Finder", RevealInFinder),
],
cx,
);
@ -98,16 +83,7 @@ mod tests {
do_wˇork();
}
"});
cx.update_editor(|editor, cx| {
deploy_context_menu(
editor,
&DeployMouseContextMenu {
position: Default::default(),
point,
},
cx,
)
});
cx.update_editor(|editor, cx| deploy_context_menu(editor, Default::default(), point, cx));
cx.assert_editor_state(indoc! {"
fn test() {

View File

@ -17,7 +17,7 @@ use workspace::WorkspaceId;
use crate::{
display_map::{DisplaySnapshot, ToDisplayPoint},
hover_popover::{hide_hover, HideHover},
hover_popover::hide_hover,
persistence::DB,
Anchor, DisplayPoint, Editor, EditorMode, Event, MultiBufferSnapshot, ToPoint,
};
@ -307,14 +307,10 @@ impl Editor {
) {
let map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
hide_hover(self, &HideHover, cx);
self.scroll_manager.set_scroll_position(
scroll_position,
&map,
local,
self.workspace_id,
cx,
);
hide_hover(self, cx);
let workspace_id = self.workspace.as_ref().map(|workspace| workspace.1);
self.scroll_manager
.set_scroll_position(scroll_position, &map, local, workspace_id, cx);
}
pub fn scroll_position(&self, cx: &mut ViewContext<Self>) -> Vector2F {
@ -323,13 +319,14 @@ impl Editor {
}
pub fn set_scroll_anchor(&mut self, scroll_anchor: ScrollAnchor, cx: &mut ViewContext<Self>) {
hide_hover(self, &HideHover, cx);
hide_hover(self, cx);
let workspace_id = self.workspace.as_ref().map(|workspace| workspace.1);
let top_row = scroll_anchor
.top_anchor
.to_point(&self.buffer().read(cx).snapshot(cx))
.row;
self.scroll_manager
.set_anchor(scroll_anchor, top_row, true, self.workspace_id, cx);
.set_anchor(scroll_anchor, top_row, true, workspace_id, cx);
}
pub(crate) fn set_scroll_anchor_remote(
@ -337,13 +334,14 @@ impl Editor {
scroll_anchor: ScrollAnchor,
cx: &mut ViewContext<Self>,
) {
hide_hover(self, &HideHover, cx);
hide_hover(self, cx);
let workspace_id = self.workspace.as_ref().map(|workspace| workspace.1);
let top_row = scroll_anchor
.top_anchor
.to_point(&self.buffer().read(cx).snapshot(cx))
.row;
self.scroll_manager
.set_anchor(scroll_anchor, top_row, false, self.workspace_id, cx);
.set_anchor(scroll_anchor, top_row, false, workspace_id, cx);
}
pub fn scroll_screen(&mut self, amount: &ScrollAmount, cx: &mut ViewContext<Self>) {

View File

@ -1,6 +1,4 @@
use gpui::{
actions, geometry::vector::Vector2F, impl_internal_actions, AppContext, Axis, ViewContext,
};
use gpui::{actions, geometry::vector::Vector2F, AppContext, Axis, ViewContext};
use language::Bias;
use crate::{Editor, EditorMode};
@ -23,17 +21,8 @@ actions!(
]
);
#[derive(Clone, PartialEq)]
pub struct Scroll {
pub scroll_position: Vector2F,
pub axis: Option<Axis>,
}
impl_internal_actions!(editor, [Scroll]);
pub fn init(cx: &mut AppContext) {
cx.add_action(Editor::next_screen);
cx.add_action(Editor::scroll);
cx.add_action(Editor::scroll_cursor_top);
cx.add_action(Editor::scroll_cursor_center);
cx.add_action(Editor::scroll_cursor_bottom);
@ -75,9 +64,14 @@ impl Editor {
Some(())
}
fn scroll(&mut self, action: &Scroll, cx: &mut ViewContext<Self>) {
self.scroll_manager.update_ongoing_scroll(action.axis);
self.set_scroll_position(action.scroll_position, cx);
pub fn scroll(
&mut self,
scroll_position: Vector2F,
axis: Option<Axis>,
cx: &mut ViewContext<Self>,
) {
self.scroll_manager.update_ongoing_scroll(axis);
self.set_scroll_position(scroll_position, cx);
}
fn scroll_cursor_top(editor: &mut Editor, _: &ScrollCursorTop, cx: &mut ViewContext<Editor>) {

View File

@ -66,24 +66,6 @@ macro_rules! impl_actions {
};
}
/// Implement the `Action` trait for a set of existing types that are
/// not intended to be constructed via a keymap file, but only dispatched
/// internally.
#[macro_export]
macro_rules! impl_internal_actions {
($namespace:path, [ $($name:ident),* $(,)? ]) => {
$(
$crate::__impl_action! {
$namespace,
$name,
fn from_json_str(_: &str) -> $crate::anyhow::Result<Box<dyn $crate::Action>> {
Err($crate::anyhow::anyhow!("internal action"))
}
}
)*
};
}
#[doc(hidden)]
#[macro_export]
macro_rules! __impl_action {

View File

@ -1,100 +0,0 @@
use std::ops::{Deref, DerefMut};
use collections::{HashMap, HashSet};
use crate::{Action, ElementBox, Event, FontCache, MutableAppContext, TextLayoutCache};
pub struct EventContext<'a> {
rendered_views: &'a mut HashMap<usize, ElementBox>,
pub font_cache: &'a FontCache,
pub text_layout_cache: &'a TextLayoutCache,
pub app: &'a mut MutableAppContext,
pub window_id: usize,
pub notify_count: usize,
view_stack: Vec<usize>,
pub(crate) handled: bool,
pub(crate) invalidated_views: HashSet<usize>,
}
impl<'a> EventContext<'a> {
pub(crate) fn dispatch_event(&mut self, view_id: usize, event: &Event) -> bool {
if let Some(mut element) = self.rendered_views.remove(&view_id) {
let result =
self.with_current_view(view_id, |this| element.dispatch_event(event, this));
self.rendered_views.insert(view_id, element);
result
} else {
false
}
}
pub(crate) fn with_current_view<F, T>(&mut self, view_id: usize, f: F) -> T
where
F: FnOnce(&mut Self) -> T,
{
self.view_stack.push(view_id);
let result = f(self);
self.view_stack.pop();
result
}
pub fn window_id(&self) -> usize {
self.window_id
}
pub fn view_id(&self) -> Option<usize> {
self.view_stack.last().copied()
}
pub fn is_parent_view_focused(&self) -> bool {
if let Some(parent_view_id) = self.view_stack.last() {
self.app.focused_view_id(self.window_id) == Some(*parent_view_id)
} else {
false
}
}
pub fn focus_parent_view(&mut self) {
if let Some(parent_view_id) = self.view_stack.last() {
self.app.focus(self.window_id, Some(*parent_view_id))
}
}
pub fn dispatch_any_action(&mut self, action: Box<dyn Action>) {
self.app
.dispatch_any_action_at(self.window_id, *self.view_stack.last().unwrap(), action)
}
pub fn dispatch_action<A: Action>(&mut self, action: A) {
self.dispatch_any_action(Box::new(action));
}
pub fn notify(&mut self) {
self.notify_count += 1;
if let Some(view_id) = self.view_stack.last() {
self.invalidated_views.insert(*view_id);
}
}
pub fn notify_count(&self) -> usize {
self.notify_count
}
pub fn propogate_event(&mut self) {
self.handled = false;
}
}
impl<'a> Deref for EventContext<'a> {
type Target = MutableAppContext;
fn deref(&self) -> &Self::Target {
self.app
}
}
impl<'a> DerefMut for EventContext<'a> {
fn deref_mut(&mut self) -> &mut Self::Target {
self.app
}
}

View File

@ -1,309 +0,0 @@
use std::sync::Arc;
use collections::{HashMap, HashSet};
use pathfinder_geometry::vector::Vector2F;
use crate::{
scene::{
ClickRegionEvent, DownOutRegionEvent, DownRegionEvent, DragRegionEvent, HoverRegionEvent,
MouseRegionEvent, MoveRegionEvent, UpOutRegionEvent, UpRegionEvent,
},
CursorRegion, CursorStyle, ElementBox, Event, EventContext, FontCache, MouseButton,
MouseMovedEvent, MouseRegion, MouseRegionId, MutableAppContext, Scene, TextLayoutCache,
};
pub struct EventDispatcher {
window_id: usize,
font_cache: Arc<FontCache>,
last_mouse_moved_event: Option<Event>,
cursor_regions: Vec<CursorRegion>,
mouse_regions: Vec<(MouseRegion, usize)>,
clicked_regions: Vec<MouseRegion>,
clicked_button: Option<MouseButton>,
mouse_position: Vector2F,
hovered_region_ids: HashSet<MouseRegionId>,
}
impl EventDispatcher {
pub fn new(window_id: usize, font_cache: Arc<FontCache>) -> Self {
Self {
window_id,
font_cache,
last_mouse_moved_event: Default::default(),
cursor_regions: Default::default(),
mouse_regions: Default::default(),
clicked_regions: Default::default(),
clicked_button: Default::default(),
mouse_position: Default::default(),
hovered_region_ids: Default::default(),
}
}
pub fn clicked_region_ids(&self) -> Option<(Vec<MouseRegionId>, MouseButton)> {
self.clicked_button.map(|button| {
(
self.clicked_regions
.iter()
.filter_map(MouseRegion::id)
.collect(),
button,
)
})
}
pub fn hovered_region_ids(&self) -> HashSet<MouseRegionId> {
self.hovered_region_ids.clone()
}
pub fn update_mouse_regions(&mut self, scene: &Scene) {
self.cursor_regions = scene.cursor_regions();
self.mouse_regions = scene.mouse_regions();
}
pub fn redispatch_mouse_moved_event<'a>(&'a mut self, cx: &mut EventContext<'a>) {
if let Some(event) = self.last_mouse_moved_event.clone() {
self.dispatch_event(event, true, cx);
}
}
pub fn dispatch_event<'a>(
&'a mut self,
event: Event,
event_reused: bool,
cx: &mut EventContext<'a>,
) -> bool {
let root_view_id = cx.root_view_id(self.window_id);
if root_view_id.is_none() {
return false;
}
let root_view_id = root_view_id.unwrap();
//1. Allocate the correct set of GPUI events generated from the platform events
// -> These are usually small: [Mouse Down] or [Mouse up, Click] or [Mouse Moved, Mouse Dragged?]
// -> Also moves around mouse related state
let events_to_send = self.select_region_events(&event, cx, event_reused);
// For a given platform event, potentially multiple mouse region events can be created. For a given
// region event, dispatch continues until a mouse region callback fails to propogate (handled is set to true)
// If no region handles any of the produced platform events, we fallback to the old dispatch event style.
let mut invalidated_views: HashSet<usize> = Default::default();
let mut any_event_handled = false;
for mut region_event in events_to_send {
//2. Find mouse regions relevant to each region_event. For example, if the event is click, select
// the clicked_regions that overlap with the mouse position
let valid_regions = self.select_relevant_mouse_regions(&region_event);
let hovered_region_ids = self.hovered_region_ids.clone();
//3. Dispatch region event ot each valid mouse region
for valid_region in valid_regions.into_iter() {
region_event.set_region(valid_region.bounds);
if let MouseRegionEvent::Hover(e) = &mut region_event {
e.started = valid_region
.id()
.map(|region_id| hovered_region_ids.contains(&region_id))
.unwrap_or(false)
}
if let Some(callback) = valid_region.handlers.get(&region_event.handler_key()) {
if !event_reused {
invalidated_views.insert(valid_region.view_id);
}
cx.handled = true;
cx.with_current_view(valid_region.view_id, {
let region_event = region_event.clone();
|cx| {
callback(region_event, cx);
}
});
// For bubbling events, if the event was handled, don't continue dispatching
// This only makes sense for local events.
if cx.handled && region_event.is_local() {
break;
}
}
}
// Keep track if any platform event was handled
any_event_handled = any_event_handled && cx.handled;
}
if !any_event_handled {
// No platform event was handled, so fall back to old mouse event dispatch style
any_event_handled = cx.dispatch_event(root_view_id, &event);
}
// Notify any views which have been validated from event callbacks
for view_id in invalidated_views {
cx.notify_view(self.window_id, view_id);
}
any_event_handled
}
fn select_region_events(
&mut self,
event: &Event,
cx: &mut MutableAppContext,
event_reused: bool,
) -> Vec<MouseRegionEvent> {
let mut events_to_send = Vec::new();
match event {
Event::MouseDown(e) => {
//Click events are weird because they can be fired after a drag event.
//MDN says that browsers handle this by starting from 'the most
//specific ancestor element that contained both [positions]'
//So we need to store the overlapping regions on mouse down.
self.clicked_regions = self
.mouse_regions
.iter()
.filter_map(|(region, _)| {
region
.bounds
.contains_point(e.position)
.then(|| region.clone())
})
.collect();
self.clicked_button = Some(e.button);
events_to_send.push(MouseRegionEvent::Down(DownRegionEvent {
region: Default::default(),
platform_event: e.clone(),
}));
events_to_send.push(MouseRegionEvent::DownOut(DownOutRegionEvent {
region: Default::default(),
platform_event: e.clone(),
}));
}
Event::MouseUp(e) => {
//NOTE: The order of event pushes is important! MouseUp events MUST be fired
//before click events, and so the UpRegionEvent events need to be pushed before
//ClickRegionEvents
events_to_send.push(MouseRegionEvent::Up(UpRegionEvent {
region: Default::default(),
platform_event: e.clone(),
}));
events_to_send.push(MouseRegionEvent::UpOut(UpOutRegionEvent {
region: Default::default(),
platform_event: e.clone(),
}));
events_to_send.push(MouseRegionEvent::Click(ClickRegionEvent {
region: Default::default(),
platform_event: e.clone(),
}));
}
Event::MouseMoved(
e @ MouseMovedEvent {
position,
pressed_button,
..
},
) => {
let mut style_to_assign = CursorStyle::Arrow;
for region in self.cursor_regions.iter().rev() {
if region.bounds.contains_point(*position) {
style_to_assign = region.style;
break;
}
}
cx.platform().set_cursor_style(style_to_assign);
if !event_reused {
if pressed_button.is_some() {
events_to_send.push(MouseRegionEvent::Drag(DragRegionEvent {
region: Default::default(),
prev_mouse_position: self.mouse_position,
platform_event: e.clone(),
}));
}
events_to_send.push(MouseRegionEvent::Move(MoveRegionEvent {
region: Default::default(),
platform_event: e.clone(),
}));
}
events_to_send.push(MouseRegionEvent::Hover(HoverRegionEvent {
region: Default::default(),
platform_event: e.clone(),
started: false,
}));
self.last_mouse_moved_event = Some(event.clone());
}
_ => {}
}
if let Some(position) = event.position() {
self.mouse_position = position;
}
events_to_send
}
fn select_relevant_mouse_regions(
&mut self,
region_event: &MouseRegionEvent,
) -> Vec<MouseRegion> {
let mut valid_regions = Vec::new();
//GPUI elements are arranged by depth but sibling elements can register overlapping
//mouse regions. As such, hover events are only fired on overlapping elements which
//are at the same depth as the deepest element which overlaps with the mouse.
if let MouseRegionEvent::Hover(_) = *region_event {
let mut top_most_depth = None;
let mouse_position = self.mouse_position.clone();
for (region, depth) in self.mouse_regions.iter().rev() {
let contains_mouse = region.bounds.contains_point(mouse_position);
if contains_mouse && top_most_depth.is_none() {
top_most_depth = Some(depth);
}
if let Some(region_id) = region.id() {
//This unwrap relies on short circuiting boolean expressions
//The right side of the && is only executed when contains_mouse
//is true, and we know above that when contains_mouse is true
//top_most_depth is set
if contains_mouse && depth == top_most_depth.unwrap() {
//Ensure that hover entrance events aren't sent twice
if self.hovered_region_ids.insert(region_id) {
valid_regions.push(region.clone());
}
} else {
//Ensure that hover exit events aren't sent twice
if self.hovered_region_ids.remove(&region_id) {
valid_regions.push(region.clone());
}
}
}
}
} else if let MouseRegionEvent::Click(e) = region_event {
//Clear stored clicked_regions
let clicked_regions = std::mem::replace(&mut self.clicked_regions, Vec::new());
self.clicked_button = None;
//Find regions which still overlap with the mouse since the last MouseDown happened
for clicked_region in clicked_regions.into_iter().rev() {
if clicked_region.bounds.contains_point(e.position) {
valid_regions.push(clicked_region);
}
}
} else if region_event.is_local() {
for (mouse_region, _) in self.mouse_regions.iter().rev() {
//Contains
if mouse_region.bounds.contains_point(self.mouse_position) {
valid_regions.push(mouse_region.clone());
}
}
} else {
for (mouse_region, _) in self.mouse_regions.iter().rev() {
//NOT contains
if !mouse_region.bounds.contains_point(self.mouse_position) {
valid_regions.push(mouse_region.clone());
}
}
}
valid_regions
}
}

View File

@ -1,6 +1,3 @@
#[derive(Clone, PartialEq)]
pub struct SelectIndex(pub usize);
gpui::actions!(
menu,
[
@ -12,5 +9,3 @@ gpui::actions!(
SelectLast
]
);
gpui::impl_internal_actions!(menu, [SelectIndex]);

View File

@ -7,7 +7,7 @@ use gpui::{
AnyElement, AnyViewHandle, AppContext, Axis, Entity, MouseState, Task, View, ViewContext,
ViewHandle,
};
use menu::{Cancel, Confirm, SelectFirst, SelectIndex, SelectLast, SelectNext, SelectPrev};
use menu::{Cancel, Confirm, SelectFirst, SelectLast, SelectNext, SelectPrev};
use parking_lot::Mutex;
use std::{cmp, sync::Arc};
use util::ResultExt;
@ -104,8 +104,8 @@ impl<D: PickerDelegate> View for Picker<D> {
// Capture mouse events
.on_down(MouseButton::Left, |_, _, _| {})
.on_up(MouseButton::Left, |_, _, _| {})
.on_click(MouseButton::Left, move |_, _, cx| {
cx.dispatch_action(SelectIndex(ix))
.on_click(MouseButton::Left, move |_, picker, cx| {
picker.select_index(ix, cx);
})
.with_cursor_style(CursorStyle::PointingHand)
.into_any()
@ -151,7 +151,6 @@ impl<D: PickerDelegate> Picker<D> {
cx.add_action(Self::select_last);
cx.add_action(Self::select_next);
cx.add_action(Self::select_prev);
cx.add_action(Self::select_index);
cx.add_action(Self::confirm);
cx.add_action(Self::cancel);
}
@ -265,8 +264,7 @@ impl<D: PickerDelegate> Picker<D> {
cx.notify();
}
pub fn select_index(&mut self, action: &SelectIndex, cx: &mut ViewContext<Self>) {
let index = action.0;
pub fn select_index(&mut self, index: usize, cx: &mut ViewContext<Self>) {
if self.delegate.match_count() > 0 {
self.confirmed = true;
self.delegate.set_selected_index(index, cx);

View File

@ -10,7 +10,6 @@ use gpui::{
ParentElement, ScrollTarget, Stack, Svg, UniformList, UniformListState,
},
geometry::vector::Vector2F,
impl_internal_actions,
keymap_matcher::KeymapContext,
platform::{CursorStyle, MouseButton, PromptLevel},
AnyElement, AppContext, ClipboardItem, Element, Entity, ModelHandle, Task, View, ViewContext,
@ -88,28 +87,6 @@ pub struct EntryDetails {
is_cut: bool,
}
#[derive(Clone, PartialEq)]
pub struct ToggleExpanded(pub ProjectEntryId);
#[derive(Clone, PartialEq)]
pub struct Open {
pub entry_id: ProjectEntryId,
pub change_focus: bool,
}
#[derive(Clone, PartialEq)]
pub struct MoveProjectEntry {
pub entry_to_move: ProjectEntryId,
pub destination: ProjectEntryId,
pub destination_is_file: bool,
}
#[derive(Clone, PartialEq)]
pub struct DeployContextMenu {
pub position: Vector2F,
pub entry_id: ProjectEntryId,
}
actions!(
project_panel,
[
@ -128,19 +105,12 @@ actions!(
ToggleFocus
]
);
impl_internal_actions!(
project_panel,
[Open, ToggleExpanded, DeployContextMenu, MoveProjectEntry]
);
pub fn init(cx: &mut AppContext) {
cx.add_action(ProjectPanel::deploy_context_menu);
cx.add_action(ProjectPanel::expand_selected_entry);
cx.add_action(ProjectPanel::collapse_selected_entry);
cx.add_action(ProjectPanel::toggle_expanded);
cx.add_action(ProjectPanel::select_prev);
cx.add_action(ProjectPanel::select_next);
cx.add_action(ProjectPanel::open_entry);
cx.add_action(ProjectPanel::new_file);
cx.add_action(ProjectPanel::new_directory);
cx.add_action(ProjectPanel::rename);
@ -157,7 +127,6 @@ pub fn init(cx: &mut AppContext) {
this.paste(action, cx);
},
);
cx.add_action(ProjectPanel::move_entry);
}
pub enum Event {
@ -277,10 +246,14 @@ impl ProjectPanel {
project_panel
}
fn deploy_context_menu(&mut self, action: &DeployContextMenu, cx: &mut ViewContext<Self>) {
fn deploy_context_menu(
&mut self,
position: Vector2F,
entry_id: ProjectEntryId,
cx: &mut ViewContext<Self>,
) {
let project = self.project.read(cx);
let entry_id = action.entry_id;
let worktree_id = if let Some(id) = project.worktree_id_for_entry(entry_id, cx) {
id
} else {
@ -296,43 +269,43 @@ impl ProjectPanel {
if let Some((worktree, entry)) = self.selected_entry(cx) {
let is_root = Some(entry) == worktree.root_entry();
if !project.is_remote() {
menu_entries.push(ContextMenuItem::item(
menu_entries.push(ContextMenuItem::action(
"Add Folder to Project",
workspace::AddFolderToProject,
));
if is_root {
menu_entries.push(ContextMenuItem::item(
"Remove from Project",
workspace::RemoveWorktreeFromProject(worktree_id),
));
let project = self.project.clone();
menu_entries.push(ContextMenuItem::handler("Remove from Project", move |cx| {
project.update(cx, |project, cx| project.remove_worktree(worktree_id, cx));
}));
}
}
menu_entries.push(ContextMenuItem::item("New File", NewFile));
menu_entries.push(ContextMenuItem::item("New Folder", NewDirectory));
menu_entries.push(ContextMenuItem::action("New File", NewFile));
menu_entries.push(ContextMenuItem::action("New Folder", NewDirectory));
menu_entries.push(ContextMenuItem::Separator);
menu_entries.push(ContextMenuItem::item("Cut", Cut));
menu_entries.push(ContextMenuItem::item("Copy", Copy));
menu_entries.push(ContextMenuItem::action("Cut", Cut));
menu_entries.push(ContextMenuItem::action("Copy", Copy));
menu_entries.push(ContextMenuItem::Separator);
menu_entries.push(ContextMenuItem::item("Copy Path", CopyPath));
menu_entries.push(ContextMenuItem::item(
menu_entries.push(ContextMenuItem::action("Copy Path", CopyPath));
menu_entries.push(ContextMenuItem::action(
"Copy Relative Path",
CopyRelativePath,
));
menu_entries.push(ContextMenuItem::item("Reveal in Finder", RevealInFinder));
menu_entries.push(ContextMenuItem::action("Reveal in Finder", RevealInFinder));
if let Some(clipboard_entry) = self.clipboard_entry {
if clipboard_entry.worktree_id() == worktree.id() {
menu_entries.push(ContextMenuItem::item("Paste", Paste));
menu_entries.push(ContextMenuItem::action("Paste", Paste));
}
}
menu_entries.push(ContextMenuItem::Separator);
menu_entries.push(ContextMenuItem::item("Rename", Rename));
menu_entries.push(ContextMenuItem::action("Rename", Rename));
if !is_root {
menu_entries.push(ContextMenuItem::item("Delete", Delete));
menu_entries.push(ContextMenuItem::action("Delete", Delete));
}
}
self.context_menu.update(cx, |menu, cx| {
menu.show(action.position, AnchorCorner::TopLeft, menu_entries, cx);
menu.show(position, AnchorCorner::TopLeft, menu_entries, cx);
});
cx.notify();
@ -391,8 +364,7 @@ impl ProjectPanel {
}
}
fn toggle_expanded(&mut self, action: &ToggleExpanded, cx: &mut ViewContext<Self>) {
let entry_id = action.0;
fn toggle_expanded(&mut self, entry_id: ProjectEntryId, cx: &mut ViewContext<Self>) {
if let Some(worktree_id) = self.project.read(cx).worktree_id_for_entry(entry_id, cx) {
if let Some(expanded_dir_ids) = self.expanded_dir_ids.get_mut(&worktree_id) {
match expanded_dir_ids.binary_search(&entry_id) {
@ -440,13 +412,7 @@ impl ProjectPanel {
Some(task)
} else if let Some((_, entry)) = self.selected_entry(cx) {
if entry.is_file() {
self.open_entry(
&Open {
entry_id: entry.id,
change_focus: true,
},
cx,
);
self.open_entry(entry.id, true, cx);
}
None
} else {
@ -510,13 +476,7 @@ impl ProjectPanel {
}
this.update_visible_entries(None, cx);
if is_new_entry && !is_dir {
this.open_entry(
&Open {
entry_id: new_entry.id,
change_focus: true,
},
cx,
);
this.open_entry(new_entry.id, true, cx);
}
cx.notify();
})?;
@ -531,10 +491,15 @@ impl ProjectPanel {
cx.notify();
}
fn open_entry(&mut self, action: &Open, cx: &mut ViewContext<Self>) {
fn open_entry(
&mut self,
entry_id: ProjectEntryId,
focus_opened_item: bool,
cx: &mut ViewContext<Self>,
) {
cx.emit(Event::OpenedEntry {
entry_id: action.entry_id,
focus_opened_item: action.change_focus,
entry_id,
focus_opened_item,
});
}
@ -816,11 +781,9 @@ impl ProjectPanel {
fn move_entry(
&mut self,
&MoveProjectEntry {
entry_to_move,
destination,
destination_is_file,
}: &MoveProjectEntry,
entry_to_move: ProjectEntryId,
destination: ProjectEntryId,
destination_is_file: bool,
cx: &mut ViewContext<Self>,
) {
let destination_worktree = self.project.update(cx, |project, cx| {
@ -1196,34 +1159,29 @@ impl ProjectPanel {
cx,
)
})
.on_click(MouseButton::Left, move |e, _, cx| {
.on_click(MouseButton::Left, move |event, this, cx| {
if !show_editor {
if kind == EntryKind::Dir {
cx.dispatch_action(ToggleExpanded(entry_id))
this.toggle_expanded(entry_id, cx);
} else {
cx.dispatch_action(Open {
entry_id,
change_focus: e.click_count > 1,
})
this.open_entry(entry_id, event.click_count > 1, cx);
}
}
})
.on_down(MouseButton::Right, move |e, _, cx| {
cx.dispatch_action(DeployContextMenu {
entry_id,
position: e.position,
})
.on_down(MouseButton::Right, move |event, this, cx| {
this.deploy_context_menu(event.position, entry_id, cx);
})
.on_up(MouseButton::Left, move |_, _, cx| {
.on_up(MouseButton::Left, move |_, this, cx| {
if let Some((_, dragged_entry)) = cx
.global::<DragAndDrop<Workspace>>()
.currently_dragged::<ProjectEntryId>(cx.window_id())
{
cx.dispatch_action(MoveProjectEntry {
entry_to_move: *dragged_entry,
destination: entry_id,
destination_is_file: matches!(details.kind, EntryKind::File(_)),
});
this.move_entry(
*dragged_entry,
entry_id,
matches!(details.kind, EntryKind::File(_)),
cx,
);
}
})
.on_move(move |_, this, cx| {
@ -1307,14 +1265,11 @@ impl View for ProjectPanel {
.with_style(container_style)
.expanded()
})
.on_down(MouseButton::Right, move |e, _, cx| {
.on_down(MouseButton::Right, move |event, this, cx| {
// When deploying the context menu anywhere below the last project entry,
// act as if the user clicked the root of the last worktree.
if let Some(entry_id) = last_worktree_root_id {
cx.dispatch_action(DeployContextMenu {
entry_id,
position: e.position,
})
this.deploy_context_menu(event.position, entry_id, cx);
}
}),
)
@ -1895,7 +1850,7 @@ mod tests {
let worktree = worktree.read(cx);
if let Ok(relative_path) = path.strip_prefix(worktree.root_name()) {
let entry_id = worktree.entry_for_path(relative_path).unwrap().id;
panel.toggle_expanded(&ToggleExpanded(entry_id), cx);
panel.toggle_expanded(entry_id, cx);
return;
}
}

View File

@ -5,30 +5,30 @@ use gpui::{
actions,
anyhow::Result,
elements::{Flex, ParentElement},
AnyElement, AppContext, Element, Task, ViewContext,
AnyElement, AppContext, Element, Task, ViewContext, WeakViewHandle,
};
use highlighted_workspace_location::HighlightedWorkspaceLocation;
use ordered_float::OrderedFloat;
use picker::{Picker, PickerDelegate, PickerEvent};
use settings::Settings;
use std::sync::Arc;
use std::sync::{Arc, Weak};
use workspace::{
notifications::simple_message_notification::MessageNotification, OpenPaths, Workspace,
notifications::simple_message_notification::MessageNotification, AppState, Workspace,
WorkspaceLocation, WORKSPACE_DB,
};
actions!(projects, [OpenRecent]);
pub fn init(cx: &mut AppContext) {
cx.add_async_action(toggle);
pub fn init(cx: &mut AppContext, app_state: Weak<AppState>) {
cx.add_async_action(
move |_: &mut Workspace, _: &OpenRecent, cx: &mut ViewContext<Workspace>| {
toggle(app_state.clone(), cx)
},
);
RecentProjects::init(cx);
}
fn toggle(
_: &mut Workspace,
_: &OpenRecent,
cx: &mut ViewContext<Workspace>,
) -> Option<Task<Result<()>>> {
fn toggle(app_state: Weak<AppState>, cx: &mut ViewContext<Workspace>) -> Option<Task<Result<()>>> {
Some(cx.spawn(|workspace, mut cx| async move {
let workspace_locations: Vec<_> = cx
.background()
@ -46,9 +46,17 @@ fn toggle(
workspace.update(&mut cx, |workspace, cx| {
if !workspace_locations.is_empty() {
workspace.toggle_modal(cx, |_, cx| {
let workspace = cx.weak_handle();
cx.add_view(|cx| {
RecentProjects::new(RecentProjectsDelegate::new(workspace_locations), cx)
.with_max_size(800., 1200.)
RecentProjects::new(
RecentProjectsDelegate::new(
workspace,
workspace_locations,
app_state.clone(),
),
cx,
)
.with_max_size(800., 1200.)
})
});
} else {
@ -64,15 +72,23 @@ fn toggle(
type RecentProjects = Picker<RecentProjectsDelegate>;
struct RecentProjectsDelegate {
workspace: WeakViewHandle<Workspace>,
workspace_locations: Vec<WorkspaceLocation>,
app_state: Weak<AppState>,
selected_match_index: usize,
matches: Vec<StringMatch>,
}
impl RecentProjectsDelegate {
fn new(workspace_locations: Vec<WorkspaceLocation>) -> Self {
fn new(
workspace: WeakViewHandle<Workspace>,
workspace_locations: Vec<WorkspaceLocation>,
app_state: Weak<AppState>,
) -> Self {
Self {
workspace,
workspace_locations,
app_state,
selected_match_index: 0,
matches: Default::default(),
}
@ -139,11 +155,22 @@ impl PickerDelegate for RecentProjectsDelegate {
}
fn confirm(&mut self, cx: &mut ViewContext<RecentProjects>) {
if let Some(selected_match) = &self.matches.get(self.selected_index()) {
if let Some(((selected_match, workspace), app_state)) = self
.matches
.get(self.selected_index())
.zip(self.workspace.upgrade(cx))
.zip(self.app_state.upgrade())
{
let workspace_location = &self.workspace_locations[selected_match.candidate_id];
cx.dispatch_action(OpenPaths {
paths: workspace_location.paths().as_ref().clone(),
});
workspace
.update(cx, |workspace, cx| {
workspace.open_workspace_for_paths(
workspace_location.paths().as_ref().clone(),
app_state,
cx,
)
})
.detach_and_log_err(cx);
cx.emit(PickerEvent::Dismiss);
}
}

View File

@ -332,6 +332,11 @@ impl Item for ProjectSearchView {
Some(Self::new(model, cx))
}
fn added_to_workspace(&mut self, workspace: &mut Workspace, cx: &mut ViewContext<Self>) {
self.results_editor
.update(cx, |editor, cx| editor.added_to_workspace(workspace, cx));
}
fn set_nav_history(&mut self, nav_history: ItemNavHistory, cx: &mut ViewContext<Self>) {
self.results_editor.update(cx, |editor, _| {
editor.set_nav_history(Some(nav_history));

View File

@ -1,33 +1,14 @@
use crate::TerminalView;
use context_menu::{ContextMenu, ContextMenuItem};
use gpui::{
elements::*,
impl_internal_actions,
platform::{CursorStyle, MouseButton},
AnyElement, AppContext, Element, Entity, View, ViewContext, ViewHandle, WeakModelHandle,
WeakViewHandle,
AnyElement, Element, Entity, View, ViewContext, ViewHandle, WeakViewHandle,
};
use settings::Settings;
use std::any::TypeId;
use terminal::Terminal;
use workspace::{dock::FocusDock, item::ItemHandle, NewTerminal, StatusItemView, Workspace};
use crate::TerminalView;
#[derive(Clone, PartialEq)]
pub struct DeployTerminalMenu;
#[derive(Clone, PartialEq)]
pub struct FocusTerminal {
terminal_handle: WeakModelHandle<Terminal>,
}
impl_internal_actions!(terminal, [FocusTerminal, DeployTerminalMenu]);
pub fn init(cx: &mut AppContext) {
cx.add_action(TerminalButton::deploy_terminal_menu);
cx.add_action(TerminalButton::focus_terminal);
}
pub struct TerminalButton {
workspace: WeakViewHandle<Workspace>,
popup_menu: ViewHandle<ContextMenu>,
@ -94,9 +75,9 @@ impl View for TerminalButton {
}
})
.with_cursor_style(CursorStyle::PointingHand)
.on_click(MouseButton::Left, move |_, _, cx| {
.on_click(MouseButton::Left, move |_, this, cx| {
if has_terminals {
cx.dispatch_action(DeployTerminalMenu);
this.deploy_terminal_menu(cx);
} else {
if !active {
cx.dispatch_action(FocusDock);
@ -129,12 +110,8 @@ impl TerminalButton {
}
}
pub fn deploy_terminal_menu(
&mut self,
_action: &DeployTerminalMenu,
cx: &mut ViewContext<Self>,
) {
let mut menu_options = vec![ContextMenuItem::item("New Terminal", NewTerminal)];
pub fn deploy_terminal_menu(&mut self, cx: &mut ViewContext<Self>) {
let mut menu_options = vec![ContextMenuItem::action("New Terminal", NewTerminal)];
if let Some(workspace) = self.workspace.upgrade(cx) {
let project = workspace.read(cx).project().read(cx);
@ -146,10 +123,24 @@ impl TerminalButton {
for local_terminal_handle in local_terminal_handles {
if let Some(terminal) = local_terminal_handle.upgrade(cx) {
menu_options.push(ContextMenuItem::item(
let workspace = self.workspace.clone();
let local_terminal_handle = local_terminal_handle.clone();
menu_options.push(ContextMenuItem::handler(
terminal.read(cx).title(),
FocusTerminal {
terminal_handle: local_terminal_handle.clone(),
move |cx| {
if let Some(workspace) = workspace.upgrade(cx) {
workspace.update(cx, |workspace, cx| {
let terminal = workspace
.items_of_type::<TerminalView>(cx)
.find(|terminal| {
terminal.read(cx).model().downgrade()
== local_terminal_handle
});
if let Some(terminal) = terminal {
workspace.activate_item(&terminal, cx);
}
});
}
},
))
}
@ -165,21 +156,6 @@ impl TerminalButton {
);
});
}
pub fn focus_terminal(&mut self, action: &FocusTerminal, cx: &mut ViewContext<Self>) {
if let Some(workspace) = self.workspace.upgrade(cx) {
workspace.update(cx, |workspace, cx| {
let terminal = workspace
.items_of_type::<TerminalView>(cx)
.find(|terminal| {
terminal.read(cx).model().downgrade() == action.terminal_handle
});
if let Some(terminal) = terminal {
workspace.activate_item(&terminal, cx);
}
});
}
}
}
impl StatusItemView for TerminalButton {

View File

@ -33,7 +33,7 @@ use util::ResultExt;
use std::{fmt::Debug, ops::RangeInclusive};
use std::{mem, ops::Range};
use crate::{DeployContextMenu, TerminalView};
use crate::TerminalView;
///The information generated during layout that is nescessary for painting
pub struct LayoutState {
@ -429,19 +429,20 @@ impl TerminalElement {
),
)
// Context menu
.on_click(MouseButton::Right, move |e, _: &mut TerminalView, cx| {
let mouse_mode = if let Some(conn_handle) = connection.upgrade(cx) {
conn_handle.update(cx, |terminal, _cx| terminal.mouse_mode(e.shift))
} else {
// If we can't get the model handle, probably can't deploy the context menu
true
};
if !mouse_mode {
cx.dispatch_action(DeployContextMenu {
position: e.position,
});
}
})
.on_click(
MouseButton::Right,
move |event, view: &mut TerminalView, cx| {
let mouse_mode = if let Some(conn_handle) = connection.upgrade(cx) {
conn_handle.update(cx, |terminal, _cx| terminal.mouse_mode(event.shift))
} else {
// If we can't get the model handle, probably can't deploy the context menu
true
};
if !mouse_mode {
view.deploy_context_menu(event.position, cx);
}
},
)
.on_move(move |event, _: &mut TerminalView, cx| {
if cx.is_parent_view_focused() {
if let Some(conn_handle) = connection.upgrade(cx) {

View File

@ -9,7 +9,7 @@ use gpui::{
actions,
elements::{AnchorCorner, ChildView, Flex, Label, ParentElement, Stack},
geometry::vector::Vector2F,
impl_actions, impl_internal_actions,
impl_actions,
keymap_matcher::{KeymapContext, Keystroke},
platform::KeyDownEvent,
AnyElement, AnyViewHandle, AppContext, Element, Entity, ModelHandle, Task, View, ViewContext,
@ -50,11 +50,6 @@ const CURSOR_BLINK_INTERVAL: Duration = Duration::from_millis(500);
#[derive(Clone, Debug, PartialEq)]
pub struct ScrollTerminal(pub i32);
#[derive(Clone, PartialEq)]
pub struct DeployContextMenu {
pub position: Vector2F,
}
#[derive(Clone, Default, Deserialize, PartialEq)]
pub struct SendText(String);
@ -68,8 +63,6 @@ actions!(
impl_actions!(terminal, [SendText, SendKeystroke]);
impl_internal_actions!(project_panel, [DeployContextMenu]);
pub fn init(cx: &mut AppContext) {
cx.add_action(TerminalView::deploy);
@ -78,7 +71,6 @@ pub fn init(cx: &mut AppContext) {
//Useful terminal views
cx.add_action(TerminalView::send_text);
cx.add_action(TerminalView::send_keystroke);
cx.add_action(TerminalView::deploy_context_menu);
cx.add_action(TerminalView::copy);
cx.add_action(TerminalView::paste);
cx.add_action(TerminalView::clear);
@ -197,14 +189,14 @@ impl TerminalView {
cx.emit(Event::Wakeup);
}
pub fn deploy_context_menu(&mut self, action: &DeployContextMenu, cx: &mut ViewContext<Self>) {
pub fn deploy_context_menu(&mut self, position: Vector2F, cx: &mut ViewContext<Self>) {
let menu_entries = vec![
ContextMenuItem::item("Clear", Clear),
ContextMenuItem::item("Close", pane::CloseActiveItem),
ContextMenuItem::action("Clear", Clear),
ContextMenuItem::action("Close", pane::CloseActiveItem),
];
self.context_menu.update(cx, |menu, cx| {
menu.show(action.position, AnchorCorner::TopLeft, menu_entries, cx)
menu.show(position, AnchorCorner::TopLeft, menu_entries, cx)
});
cx.notify();

View File

@ -139,27 +139,11 @@ pub fn keystroke_label<V: View>(
) -> Container<V> {
// FIXME: Put the theme in it's own global so we can
// query the keystroke style on our own
keystroke_label_for(
cx.handle().id(),
label_text,
label_style,
keystroke_style,
action,
)
}
pub fn keystroke_label_for<V: View>(
view_id: usize,
label_text: &'static str,
label_style: &ContainedText,
keystroke_style: &ContainedText,
action: Box<dyn Action>,
) -> Container<V> {
Flex::row()
.with_child(Label::new(label_text, label_style.text.clone()).contained())
.with_child(
KeystrokeLabel::new(
view_id,
cx.view_id(),
action,
keystroke_style.container,
keystroke_style.text.clone(),

View File

@ -1,13 +1,10 @@
mod toggle_dock_button;
use serde::Deserialize;
use collections::HashMap;
use gpui::{
actions,
elements::{ChildView, Empty, MouseEventHandler, ParentElement, Side, Stack},
geometry::vector::Vector2F,
impl_internal_actions,
platform::{CursorStyle, MouseButton},
AnyElement, AppContext, Border, Element, SizeConstraint, ViewContext, ViewHandle,
};
@ -17,12 +14,6 @@ use theme::Theme;
use crate::{sidebar::SidebarSide, BackgroundActions, ItemHandle, Pane, Workspace};
pub use toggle_dock_button::ToggleDockButton;
#[derive(PartialEq, Clone, Deserialize)]
pub struct MoveDock(pub DockAnchor);
#[derive(PartialEq, Clone)]
pub struct AddDefaultItemToDock;
actions!(
dock,
[
@ -35,16 +26,10 @@ actions!(
RemoveTabFromDock,
]
);
impl_internal_actions!(dock, [MoveDock, AddDefaultItemToDock]);
pub fn init(cx: &mut AppContext) {
cx.add_action(Dock::focus_dock);
cx.add_action(Dock::hide_dock);
cx.add_action(
|workspace: &mut Workspace, &MoveDock(dock_anchor), cx: &mut ViewContext<Workspace>| {
Dock::move_dock(workspace, dock_anchor, true, cx);
},
);
cx.add_action(
|workspace: &mut Workspace, _: &AnchorDockRight, cx: &mut ViewContext<Workspace>| {
Dock::move_dock(workspace, DockAnchor::Right, true, cx);
@ -182,21 +167,14 @@ pub struct Dock {
impl Dock {
pub fn new(
workspace_id: usize,
default_item_factory: DockDefaultItemFactory,
background_actions: BackgroundActions,
cx: &mut ViewContext<Workspace>,
) -> Self {
let position = DockPosition::Hidden(cx.global::<Settings>().default_dock_anchor);
let pane = cx.add_view(|cx| {
Pane::new(
workspace_id,
Some(position.anchor()),
background_actions,
cx,
)
});
let workspace = cx.weak_handle();
let pane =
cx.add_view(|cx| Pane::new(workspace, Some(position.anchor()), background_actions, cx));
pane.update(cx, |pane, cx| {
pane.set_active(false, cx);
});
@ -426,11 +404,13 @@ mod tests {
use std::{
ops::{Deref, DerefMut},
path::PathBuf,
sync::Arc,
};
use gpui::{AppContext, BorrowWindowContext, TestAppContext, ViewContext, WindowContext};
use project::{FakeFs, Project};
use settings::Settings;
use theme::ThemeRegistry;
use super::*;
use crate::{
@ -441,7 +421,7 @@ mod tests {
},
register_deserializable_item,
sidebar::Sidebar,
ItemHandle, Workspace,
AppState, ItemHandle, Workspace,
};
pub fn default_item_factory(
@ -489,8 +469,17 @@ mod tests {
Some(serialized_workspace),
0,
project.clone(),
default_item_factory,
|| &[],
Arc::new(AppState {
languages: project.read(cx).languages().clone(),
themes: ThemeRegistry::new((), cx.font_cache().clone()),
client: project.read(cx).client(),
user_store: project.read(cx).user_store(),
fs: project.read(cx).fs().clone(),
build_window_options: |_, _, _| Default::default(),
initialize_workspace: |_, _, _| {},
dock_default_item_factory: default_item_factory,
background_actions: || &[],
}),
cx,
)
});
@ -582,7 +571,7 @@ mod tests {
#[gpui::test]
async fn test_toggle_dock_focus(cx: &mut TestAppContext) {
let cx = DockTestContext::new(cx).await;
let mut cx = DockTestContext::new(cx).await;
cx.move_dock(DockAnchor::Right);
cx.assert_dock_pane_active();
@ -620,11 +609,20 @@ mod tests {
let project = Project::test(fs, [], cx).await;
let (window_id, workspace) = cx.add_window(|cx| {
Workspace::new(
Default::default(),
None,
0,
project,
default_item_factory,
|| &[],
project.clone(),
Arc::new(AppState {
languages: project.read(cx).languages().clone(),
themes: ThemeRegistry::new((), cx.font_cache().clone()),
client: project.read(cx).client(),
user_store: project.read(cx).user_store(),
fs: project.read(cx).fs().clone(),
build_window_options: |_, _, _| Default::default(),
initialize_workspace: |_, _, _| {},
dock_default_item_factory: default_item_factory,
background_actions: || &[],
}),
cx,
)
});
@ -728,8 +726,8 @@ mod tests {
})
}
pub fn move_dock(&self, anchor: DockAnchor) {
self.cx.dispatch_action(self.window_id, MoveDock(anchor));
pub fn move_dock(&mut self, anchor: DockAnchor) {
self.update_workspace(|workspace, cx| Dock::move_dock(workspace, anchor, true, cx));
}
pub fn hide_dock(&self) {

View File

@ -67,9 +67,17 @@ impl View for ToggleDockButton {
}
})
.with_cursor_style(CursorStyle::PointingHand)
.on_up(MouseButton::Left, move |event, _, cx| {
.on_up(MouseButton::Left, move |event, this, cx| {
let drop_index = dock_pane.read(cx).items_len() + 1;
handle_dropped_item(event, &dock_pane.downgrade(), drop_index, false, None, cx);
handle_dropped_item(
event,
this.workspace.clone(),
&dock_pane.downgrade(),
drop_index,
false,
None,
cx,
);
});
if dock_position.is_visible() {

View File

@ -365,7 +365,7 @@ impl<T: Item> ItemHandle for ViewHandle<T> {
workspace.update_followers(
proto::update_followers::Variant::CreateView(proto::View {
id: followed_item
.remote_id(&workspace.client, cx)
.remote_id(&workspace.app_state.client, cx)
.map(|id| id.to_proto()),
variant: Some(message),
leader_id: workspace.leader_for_pane(&pane),
@ -421,7 +421,7 @@ impl<T: Item> ItemHandle for ViewHandle<T> {
proto::update_followers::Variant::UpdateView(
proto::UpdateView {
id: item
.remote_id(&this.client, cx)
.remote_id(&this.app_state.client, cx)
.map(|id| id.to_proto()),
variant: pending_update.borrow_mut().take(),
leader_id,

View File

@ -1,9 +1,7 @@
use std::{any::TypeId, ops::DerefMut};
use crate::{Toast, Workspace};
use collections::HashSet;
use gpui::{AnyViewHandle, AppContext, Entity, View, ViewContext, ViewHandle};
use crate::Workspace;
use std::{any::TypeId, ops::DerefMut};
pub fn init(cx: &mut AppContext) {
cx.set_global(NotificationTracker::new());
@ -113,6 +111,28 @@ impl Workspace {
self.dismiss_notification_internal(type_id, id, cx)
}
pub fn show_toast(&mut self, toast: Toast, cx: &mut ViewContext<Self>) {
self.dismiss_notification::<simple_message_notification::MessageNotification>(toast.id, cx);
self.show_notification(toast.id, cx, |cx| {
cx.add_view(|_cx| match &toast.click {
Some((click_msg, action)) => {
simple_message_notification::MessageNotification::new_boxed_action(
toast.msg.clone(),
action.boxed_clone(),
click_msg.clone(),
)
}
None => {
simple_message_notification::MessageNotification::new_message(toast.msg.clone())
}
})
})
}
pub fn dismiss_toast(&mut self, id: usize, cx: &mut ViewContext<Self>) {
self.dismiss_notification::<simple_message_notification::MessageNotification>(id, cx);
}
fn dismiss_notification_internal(
&mut self,
type_id: TypeId,

View File

@ -20,11 +20,12 @@ use gpui::{
rect::RectF,
vector::{vec2f, Vector2F},
},
impl_actions, impl_internal_actions,
impl_actions,
keymap_matcher::KeymapContext,
platform::{CursorStyle, MouseButton, NavigationDirection, PromptLevel},
Action, AnyViewHandle, AnyWeakViewHandle, AppContext, AsyncAppContext, Entity, ModelHandle,
MouseRegion, Quad, Task, View, ViewContext, ViewHandle, WeakViewHandle, WindowContext,
Action, AnyViewHandle, AnyWeakViewHandle, AppContext, AsyncAppContext, Entity, EventContext,
ModelHandle, MouseRegion, Quad, Task, View, ViewContext, ViewHandle, WeakViewHandle,
WindowContext,
};
use project::{Project, ProjectEntryId, ProjectPath};
use serde::Deserialize;
@ -74,14 +75,6 @@ actions!(
]
);
#[derive(Clone, PartialEq)]
pub struct MoveItem {
pub item_id: usize,
pub from: WeakViewHandle<Pane>,
pub to: WeakViewHandle<Pane>,
pub destination_index: usize,
}
#[derive(Clone, Deserialize, PartialEq)]
pub struct GoBack {
#[serde(skip_deserializing)]
@ -94,36 +87,7 @@ pub struct GoForward {
pub pane: Option<WeakViewHandle<Pane>>,
}
#[derive(Clone, PartialEq)]
pub struct DeploySplitMenu;
#[derive(Clone, PartialEq)]
pub struct DeployDockMenu;
#[derive(Clone, PartialEq)]
pub struct DeployNewMenu;
#[derive(Clone, PartialEq)]
pub struct DeployTabContextMenu {
pub position: Vector2F,
pub item_id: usize,
pub pane: WeakViewHandle<Pane>,
}
impl_actions!(pane, [GoBack, GoForward, ActivateItem]);
impl_internal_actions!(
pane,
[
CloseItemById,
CloseItemsToTheLeftById,
CloseItemsToTheRightById,
DeployTabContextMenu,
DeploySplitMenu,
DeployNewMenu,
DeployDockMenu,
MoveItem
]
);
const MAX_NAVIGATION_HISTORY_LEN: usize = 1024;
@ -148,68 +112,10 @@ pub fn init(cx: &mut AppContext) {
cx.add_async_action(Pane::close_items_to_the_left);
cx.add_async_action(Pane::close_items_to_the_right);
cx.add_async_action(Pane::close_all_items);
cx.add_async_action(|workspace: &mut Workspace, action: &CloseItemById, cx| {
let pane = action.pane.upgrade(cx)?;
let task = Pane::close_item_by_id(workspace, pane, action.item_id, cx);
Some(cx.foreground().spawn(async move {
task.await?;
Ok(())
}))
});
cx.add_async_action(
|workspace: &mut Workspace, action: &CloseItemsToTheLeftById, cx| {
let pane = action.pane.upgrade(cx)?;
let task = Pane::close_items_to_the_left_by_id(workspace, pane, action.item_id, cx);
Some(cx.foreground().spawn(async move {
task.await?;
Ok(())
}))
},
);
cx.add_async_action(
|workspace: &mut Workspace, action: &CloseItemsToTheRightById, cx| {
let pane = action.pane.upgrade(cx)?;
let task = Pane::close_items_to_the_right_by_id(workspace, pane, action.item_id, cx);
Some(cx.foreground().spawn(async move {
task.await?;
Ok(())
}))
},
);
cx.add_action(
|workspace,
MoveItem {
from,
to,
item_id,
destination_index,
},
cx| {
// Get item handle to move
let from = if let Some(from) = from.upgrade(cx) {
from
} else {
return;
};
// Add item to new pane at given index
let to = if let Some(to) = to.upgrade(cx) {
to
} else {
return;
};
Pane::move_item(workspace, from, to, *item_id, *destination_index, cx)
},
);
cx.add_action(|pane: &mut Pane, _: &SplitLeft, cx| pane.split(SplitDirection::Left, cx));
cx.add_action(|pane: &mut Pane, _: &SplitUp, cx| pane.split(SplitDirection::Up, cx));
cx.add_action(|pane: &mut Pane, _: &SplitRight, cx| pane.split(SplitDirection::Right, cx));
cx.add_action(|pane: &mut Pane, _: &SplitDown, cx| pane.split(SplitDirection::Down, cx));
cx.add_action(Pane::deploy_split_menu);
cx.add_action(Pane::deploy_dock_menu);
cx.add_action(Pane::deploy_new_menu);
cx.add_action(Pane::deploy_tab_context_menu);
cx.add_action(|workspace: &mut Workspace, _: &ReopenClosedItem, cx| {
Pane::reopen_closed_item(workspace, cx).detach();
});
@ -243,7 +149,7 @@ pub struct Pane {
tab_context_menu: ViewHandle<ContextMenu>,
docked: Option<DockAnchor>,
_background_actions: BackgroundActions,
_workspace_id: usize,
workspace: WeakViewHandle<Workspace>,
}
pub struct ItemNavHistory {
@ -315,7 +221,7 @@ impl TabBarContextMenu {
impl Pane {
pub fn new(
workspace_id: usize,
workspace: WeakViewHandle<Workspace>,
docked: Option<DockAnchor>,
background_actions: BackgroundActions,
cx: &mut ViewContext<Self>,
@ -349,7 +255,7 @@ impl Pane {
tab_context_menu: cx.add_view(ContextMenu::new),
docked,
_background_actions: background_actions,
_workspace_id: workspace_id,
workspace,
}
}
@ -1223,16 +1129,16 @@ impl Pane {
cx.emit(Event::Split(direction));
}
fn deploy_split_menu(&mut self, _: &DeploySplitMenu, cx: &mut ViewContext<Self>) {
fn deploy_split_menu(&mut self, cx: &mut ViewContext<Self>) {
self.tab_bar_context_menu.handle.update(cx, |menu, cx| {
menu.show(
Default::default(),
AnchorCorner::TopRight,
vec![
ContextMenuItem::item("Split Right", SplitRight),
ContextMenuItem::item("Split Left", SplitLeft),
ContextMenuItem::item("Split Up", SplitUp),
ContextMenuItem::item("Split Down", SplitDown),
ContextMenuItem::action("Split Right", SplitRight),
ContextMenuItem::action("Split Left", SplitLeft),
ContextMenuItem::action("Split Up", SplitUp),
ContextMenuItem::action("Split Down", SplitDown),
],
cx,
);
@ -1241,15 +1147,15 @@ impl Pane {
self.tab_bar_context_menu.kind = TabBarContextMenuKind::Split;
}
fn deploy_dock_menu(&mut self, _: &DeployDockMenu, cx: &mut ViewContext<Self>) {
fn deploy_dock_menu(&mut self, cx: &mut ViewContext<Self>) {
self.tab_bar_context_menu.handle.update(cx, |menu, cx| {
menu.show(
Default::default(),
AnchorCorner::TopRight,
vec![
ContextMenuItem::item("Anchor Dock Right", AnchorDockRight),
ContextMenuItem::item("Anchor Dock Bottom", AnchorDockBottom),
ContextMenuItem::item("Expand Dock", ExpandDock),
ContextMenuItem::action("Anchor Dock Right", AnchorDockRight),
ContextMenuItem::action("Anchor Dock Bottom", AnchorDockBottom),
ContextMenuItem::action("Expand Dock", ExpandDock),
],
cx,
);
@ -1258,15 +1164,15 @@ impl Pane {
self.tab_bar_context_menu.kind = TabBarContextMenuKind::Dock;
}
fn deploy_new_menu(&mut self, _: &DeployNewMenu, cx: &mut ViewContext<Self>) {
fn deploy_new_menu(&mut self, cx: &mut ViewContext<Self>) {
self.tab_bar_context_menu.handle.update(cx, |menu, cx| {
menu.show(
Default::default(),
AnchorCorner::TopRight,
vec![
ContextMenuItem::item("New File", NewFile),
ContextMenuItem::item("New Terminal", NewTerminal),
ContextMenuItem::item("New Search", NewSearch),
ContextMenuItem::action("New File", NewFile),
ContextMenuItem::action("New Terminal", NewTerminal),
ContextMenuItem::action("New Search", NewSearch),
],
cx,
);
@ -1277,56 +1183,87 @@ impl Pane {
fn deploy_tab_context_menu(
&mut self,
action: &DeployTabContextMenu,
position: Vector2F,
target_item_id: usize,
cx: &mut ViewContext<Self>,
) {
let target_item_id = action.item_id;
let target_pane = action.pane.clone();
let active_item_id = self.items[self.active_item_index].id();
let is_active_item = target_item_id == active_item_id;
let target_pane = cx.weak_handle();
// The `CloseInactiveItems` action should really be called "CloseOthers" and the behaviour should be dynamically based on the tab the action is ran on. Currenlty, this is a weird action because you can run it on a non-active tab and it will close everything by the actual active tab
self.tab_context_menu.update(cx, |menu, cx| {
menu.show(
action.position,
position,
AnchorCorner::TopLeft,
if is_active_item {
vec![
ContextMenuItem::item("Close Active Item", CloseActiveItem),
ContextMenuItem::item("Close Inactive Items", CloseInactiveItems),
ContextMenuItem::item("Close Clean Items", CloseCleanItems),
ContextMenuItem::item("Close Items To The Left", CloseItemsToTheLeft),
ContextMenuItem::item("Close Items To The Right", CloseItemsToTheRight),
ContextMenuItem::item("Close All Items", CloseAllItems),
ContextMenuItem::action("Close Active Item", CloseActiveItem),
ContextMenuItem::action("Close Inactive Items", CloseInactiveItems),
ContextMenuItem::action("Close Clean Items", CloseCleanItems),
ContextMenuItem::action("Close Items To The Left", CloseItemsToTheLeft),
ContextMenuItem::action("Close Items To The Right", CloseItemsToTheRight),
ContextMenuItem::action("Close All Items", CloseAllItems),
]
} else {
// In the case of the user right clicking on a non-active tab, for some item-closing commands, we need to provide the id of the tab, for the others, we can reuse the existing command.
vec![
ContextMenuItem::item(
"Close Inactive Item",
CloseItemById {
item_id: target_item_id,
pane: target_pane.clone(),
},
),
ContextMenuItem::item("Close Inactive Items", CloseInactiveItems),
ContextMenuItem::item("Close Clean Items", CloseCleanItems),
ContextMenuItem::item(
"Close Items To The Left",
CloseItemsToTheLeftById {
item_id: target_item_id,
pane: target_pane.clone(),
},
),
ContextMenuItem::item(
"Close Items To The Right",
CloseItemsToTheRightById {
item_id: target_item_id,
pane: target_pane.clone(),
},
),
ContextMenuItem::item("Close All Items", CloseAllItems),
ContextMenuItem::handler("Close Inactive Item", {
let workspace = self.workspace.clone();
let pane = target_pane.clone();
move |cx| {
if let Some((workspace, pane)) =
workspace.upgrade(cx).zip(pane.upgrade(cx))
{
workspace.update(cx, |workspace, cx| {
Self::close_item_by_id(workspace, pane, target_item_id, cx)
.detach_and_log_err(cx);
})
}
}
}),
ContextMenuItem::action("Close Inactive Items", CloseInactiveItems),
ContextMenuItem::action("Close Clean Items", CloseCleanItems),
ContextMenuItem::handler("Close Items To The Left", {
let workspace = self.workspace.clone();
let pane = target_pane.clone();
move |cx| {
if let Some((workspace, pane)) =
workspace.upgrade(cx).zip(pane.upgrade(cx))
{
workspace.update(cx, |workspace, cx| {
Self::close_items_to_the_left_by_id(
workspace,
pane,
target_item_id,
cx,
)
.detach_and_log_err(cx);
})
}
}
}),
ContextMenuItem::handler("Close Items To The Right", {
let workspace = self.workspace.clone();
let pane = target_pane.clone();
move |cx| {
if let Some((workspace, pane)) =
workspace.upgrade(cx).zip(pane.upgrade(cx))
{
workspace.update(cx, |workspace, cx| {
Self::close_items_to_the_right_by_id(
workspace,
pane,
target_item_id,
cx,
)
.detach_and_log_err(cx);
})
}
}
}),
ContextMenuItem::action("Close All Items", CloseAllItems),
]
},
cx,
@ -1407,24 +1344,28 @@ impl Pane {
cx.dispatch_action(ActivateItem(ix));
})
.on_click(MouseButton::Middle, {
let item = item.clone();
let pane = pane.clone();
move |_, _, cx| {
cx.dispatch_action(CloseItemById {
item_id: item.id(),
pane: pane.clone(),
})
let item_id = item.id();
move |_, pane, cx| {
let workspace = pane.workspace.clone();
let pane = cx.weak_handle();
cx.window_context().defer(move |cx| {
if let Some((workspace, pane)) =
workspace.upgrade(cx).zip(pane.upgrade(cx))
{
workspace.update(cx, |workspace, cx| {
Self::close_item_by_id(
workspace, pane, item_id, cx,
)
.detach_and_log_err(cx);
});
}
});
}
})
.on_down(
MouseButton::Right,
move |e, _, cx| {
let item = item.clone();
cx.dispatch_action(DeployTabContextMenu {
position: e.position,
item_id: item.id(),
pane: pane.clone(),
});
move |event, pane, cx| {
pane.deploy_tab_context_menu(event.position, item.id(), cx);
},
);
@ -1622,10 +1563,17 @@ impl Pane {
.on_click(MouseButton::Left, {
let pane = pane.clone();
move |_, _, cx| {
cx.dispatch_action(CloseItemById {
item_id,
pane: pane.clone(),
})
let pane = pane.clone();
cx.window_context().defer(move |cx| {
if let Some(pane) = pane.upgrade(cx) {
if let Some(workspace) = pane.read(cx).workspace.upgrade(cx) {
workspace.update(cx, |workspace, cx| {
Self::close_item_by_id(workspace, pane, item_id, cx)
.detach_and_log_err(cx);
});
}
}
});
}
})
.into_any_named("close-tab-icon")
@ -1654,7 +1602,7 @@ impl Pane {
0,
"icons/plus_12.svg",
cx,
DeployNewMenu,
|pane, cx| pane.deploy_new_menu(cx),
self.tab_bar_context_menu
.handle_if_kind(TabBarContextMenuKind::New),
))
@ -1668,7 +1616,7 @@ impl Pane {
1,
dock_icon,
cx,
DeployDockMenu,
|pane, cx| pane.deploy_dock_menu(cx),
self.tab_bar_context_menu
.handle_if_kind(TabBarContextMenuKind::Dock),
)
@ -1679,17 +1627,22 @@ impl Pane {
2,
"icons/split_12.svg",
cx,
DeploySplitMenu,
|pane, cx| pane.deploy_split_menu(cx),
self.tab_bar_context_menu
.handle_if_kind(TabBarContextMenuKind::Split),
)
}),
)
// Add the close dock button if this pane is a dock
.with_children(
self.docked
.map(|_| render_tab_bar_button(3, "icons/x_mark_8.svg", cx, HideDock, None)),
)
.with_children(self.docked.map(|_| {
render_tab_bar_button(
3,
"icons/x_mark_8.svg",
cx,
|_, cx| cx.dispatch_action(HideDock),
None,
)
}))
.contained()
.with_style(theme.workspace.tab_bar.pane_button_container)
.flex(1., false)
@ -1863,11 +1816,11 @@ impl View for Pane {
}
}
fn render_tab_bar_button<A: Action + Clone>(
fn render_tab_bar_button<F: 'static + Fn(&mut Pane, &mut EventContext<Pane>)>(
index: usize,
icon: &'static str,
cx: &mut ViewContext<Pane>,
action: A,
on_click: F,
context_menu: Option<ViewHandle<ContextMenu>>,
) -> AnyElement<Pane> {
enum TabBarButton {}
@ -1887,9 +1840,7 @@ fn render_tab_bar_button<A: Action + Clone>(
.with_height(style.button_width)
})
.with_cursor_style(CursorStyle::PointingHand)
.on_click(MouseButton::Left, move |_, _, cx| {
cx.dispatch_action(action.clone());
}),
.on_click(MouseButton::Left, move |_, pane, cx| on_click(pane, cx)),
)
.with_children(
context_menu.map(|menu| ChildView::new(&menu, cx).aligned().bottom().right()),

View File

@ -10,10 +10,7 @@ use gpui::{
use project::ProjectEntryId;
use settings::Settings;
use crate::{
MoveItem, OpenProjectEntryInPane, Pane, SplitDirection, SplitWithItem, SplitWithProjectEntry,
Workspace,
};
use crate::{Pane, SplitDirection, Workspace};
use super::DraggedItem;
@ -72,9 +69,18 @@ where
}))
})
.on_up(MouseButton::Left, {
move |event, _, cx| {
move |event, pane, cx| {
let workspace = pane.workspace.clone();
let pane = cx.weak_handle();
handle_dropped_item(event, &pane, drop_index, allow_same_pane, split_margin, cx);
handle_dropped_item(
event,
workspace,
&pane,
drop_index,
allow_same_pane,
split_margin,
cx,
);
cx.notify();
}
})
@ -97,6 +103,7 @@ where
pub fn handle_dropped_item<V: View>(
event: MouseUp,
workspace: WeakViewHandle<Workspace>,
pane: &WeakViewHandle<Pane>,
index: usize,
allow_same_pane: bool,
@ -126,36 +133,74 @@ pub fn handle_dropped_item<V: View>(
{
let pane_to_split = pane.clone();
match action {
Action::Move(from, item_id_to_move) => cx.dispatch_action(SplitWithItem {
from,
item_id_to_move,
pane_to_split,
split_direction,
}),
Action::Open(project_entry) => cx.dispatch_action(SplitWithProjectEntry {
pane_to_split,
split_direction,
project_entry,
}),
Action::Move(from, item_id_to_move) => {
cx.window_context().defer(move |cx| {
if let Some(workspace) = workspace.upgrade(cx) {
workspace.update(cx, |workspace, cx| {
workspace.split_pane_with_item(
pane_to_split,
split_direction,
from,
item_id_to_move,
cx,
);
})
}
});
}
Action::Open(project_entry) => {
cx.window_context().defer(move |cx| {
if let Some(workspace) = workspace.upgrade(cx) {
workspace.update(cx, |workspace, cx| {
if let Some(task) = workspace.split_pane_with_project_entry(
pane_to_split,
split_direction,
project_entry,
cx,
) {
task.detach_and_log_err(cx);
}
})
}
});
}
};
} else {
match action {
Action::Move(from, item_id) => {
if pane != &from || allow_same_pane {
cx.dispatch_action(MoveItem {
item_id,
from,
to: pane.clone(),
destination_index: index,
})
let pane = pane.clone();
cx.window_context().defer(move |cx| {
if let Some(((workspace, from), to)) = workspace
.upgrade(cx)
.zip(from.upgrade(cx))
.zip(pane.upgrade(cx))
{
workspace.update(cx, |workspace, cx| {
Pane::move_item(workspace, from, to, item_id, index, cx);
})
}
});
} else {
cx.propagate_event();
}
}
Action::Open(project_entry) => cx.dispatch_action(OpenProjectEntryInPane {
pane: pane.clone(),
project_entry,
}),
Action::Open(project_entry) => {
let pane = pane.clone();
cx.window_context().defer(move |cx| {
if let Some(workspace) = workspace.upgrade(cx) {
workspace.update(cx, |workspace, cx| {
if let Some(path) =
workspace.project.read(cx).path_for_entry(project_entry, cx)
{
workspace
.open_path(path, Some(pane), true, cx)
.detach_and_log_err(cx);
}
});
}
});
}
}
}
}

View File

@ -1,4 +1,6 @@
use crate::{FollowerStatesByLeader, JoinProject, Pane, Workspace};
use std::sync::Arc;
use crate::{AppState, FollowerStatesByLeader, Pane, Workspace};
use anyhow::{anyhow, Result};
use call::{ActiveCall, ParticipantLocation};
use gpui::{
@ -70,6 +72,7 @@ impl PaneGroup {
follower_states: &FollowerStatesByLeader,
active_call: Option<&ModelHandle<ActiveCall>>,
active_pane: &ViewHandle<Pane>,
app_state: &Arc<AppState>,
cx: &mut ViewContext<Workspace>,
) -> AnyElement<Workspace> {
self.root.render(
@ -78,6 +81,7 @@ impl PaneGroup {
follower_states,
active_call,
active_pane,
app_state,
cx,
)
}
@ -131,6 +135,7 @@ impl Member {
follower_states: &FollowerStatesByLeader,
active_call: Option<&ModelHandle<ActiveCall>>,
active_pane: &ViewHandle<Pane>,
app_state: &Arc<AppState>,
cx: &mut ViewContext<Workspace>,
) -> AnyElement<Workspace> {
enum FollowIntoExternalProject {}
@ -175,6 +180,7 @@ impl Member {
} else {
let leader_user = leader.user.clone();
let leader_user_id = leader.user.id;
let app_state = Arc::downgrade(app_state);
Some(
MouseEventHandler::<FollowIntoExternalProject, _>::new(
pane.id(),
@ -199,10 +205,15 @@ impl Member {
)
.with_cursor_style(CursorStyle::PointingHand)
.on_click(MouseButton::Left, move |_, _, cx| {
cx.dispatch_action(JoinProject {
project_id: leader_project_id,
follow_user_id: leader_user_id,
})
if let Some(app_state) = app_state.upgrade() {
crate::join_remote_project(
leader_project_id,
leader_user_id,
app_state,
cx,
)
.detach_and_log_err(cx);
}
})
.aligned()
.bottom()
@ -257,6 +268,7 @@ impl Member {
follower_states,
active_call,
active_pane,
app_state,
cx,
),
}
@ -360,6 +372,7 @@ impl PaneAxis {
follower_state: &FollowerStatesByLeader,
active_call: Option<&ModelHandle<ActiveCall>>,
active_pane: &ViewHandle<Pane>,
app_state: &Arc<AppState>,
cx: &mut ViewContext<Workspace>,
) -> AnyElement<Workspace> {
let last_member_ix = self.members.len() - 1;
@ -370,8 +383,15 @@ impl PaneAxis {
flex = cx.global::<Settings>().active_pane_magnification;
}
let mut member =
member.render(project, theme, follower_state, active_call, active_pane, cx);
let mut member = member.render(
project,
theme,
follower_state,
active_call,
active_pane,
app_state,
cx,
);
if ix < last_member_ix {
let mut border = theme.workspace.pane_divider;
border.left = false;

View File

@ -25,7 +25,6 @@ use client::{
use collections::{hash_map, HashMap, HashSet};
use dock::{Dock, DockDefaultItemFactory, ToggleDockButton};
use drag_and_drop::DragAndDrop;
use fs::{self, Fs};
use futures::{
channel::{mpsc, oneshot},
future::try_join_all,
@ -38,7 +37,7 @@ use gpui::{
rect::RectF,
vector::{vec2f, Vector2F},
},
impl_actions, impl_internal_actions,
impl_actions,
keymap_matcher::KeymapContext,
platform::{
CursorStyle, MouseButton, PathPromptOptions, Platform, PromptLevel, WindowBounds,
@ -135,41 +134,6 @@ pub struct OpenPaths {
#[derive(Clone, Deserialize, PartialEq)]
pub struct ActivatePane(pub usize);
#[derive(Clone, PartialEq)]
pub struct ToggleFollow(pub PeerId);
#[derive(Clone, PartialEq)]
pub struct JoinProject {
pub project_id: u64,
pub follow_user_id: u64,
}
#[derive(Clone, PartialEq)]
pub struct OpenSharedScreen {
pub peer_id: PeerId,
}
#[derive(Clone, PartialEq)]
pub struct SplitWithItem {
pane_to_split: WeakViewHandle<Pane>,
split_direction: SplitDirection,
from: WeakViewHandle<Pane>,
item_id_to_move: usize,
}
#[derive(Clone, PartialEq)]
pub struct SplitWithProjectEntry {
pane_to_split: WeakViewHandle<Pane>,
split_direction: SplitDirection,
project_entry: ProjectEntryId,
}
#[derive(Clone, PartialEq)]
pub struct OpenProjectEntryInPane {
pane: WeakViewHandle<Pane>,
project_entry: ProjectEntryId,
}
pub struct Toast {
id: usize,
msg: Cow<'static, str>,
@ -220,34 +184,8 @@ impl Clone for Toast {
}
}
#[derive(Clone, PartialEq)]
pub struct DismissToast {
id: usize,
}
impl DismissToast {
pub fn new(id: usize) -> Self {
DismissToast { id }
}
}
pub type WorkspaceId = i64;
impl_internal_actions!(
workspace,
[
OpenPaths,
ToggleFollow,
JoinProject,
OpenSharedScreen,
RemoveWorktreeFromProject,
SplitWithItem,
SplitWithProjectEntry,
OpenProjectEntryInPane,
Toast,
DismissToast
]
);
impl_actions!(workspace, [ActivatePane]);
pub fn init(app_state: Arc<AppState>, cx: &mut AppContext) {
@ -255,81 +193,53 @@ pub fn init(app_state: Arc<AppState>, cx: &mut AppContext) {
dock::init(cx);
notifications::init(cx);
cx.add_global_action(|_: &Open, cx: &mut AppContext| {
let mut paths = cx.prompt_for_paths(PathPromptOptions {
files: true,
directories: true,
multiple: true,
});
cx.spawn(|mut cx| async move {
if let Some(paths) = paths.recv().await.flatten() {
cx.update(|cx| cx.dispatch_global_action(OpenPaths { paths }));
}
})
.detach();
});
cx.add_action(|_, _: &Open, cx: &mut ViewContext<Workspace>| {
let mut paths = cx.prompt_for_paths(PathPromptOptions {
files: true,
directories: true,
multiple: true,
});
let handle = cx.handle().downgrade();
cx.spawn(|_, mut cx| async move {
if let Some(paths) = paths.recv().await.flatten() {
cx.update(|cx| {
cx.dispatch_action_at(handle.window_id(), handle.id(), OpenPaths { paths })
})
}
})
.detach();
});
cx.add_global_action({
let app_state = Arc::downgrade(&app_state);
move |action: &OpenPaths, cx: &mut AppContext| {
move |_: &Open, cx: &mut AppContext| {
let mut paths = cx.prompt_for_paths(PathPromptOptions {
files: true,
directories: true,
multiple: true,
});
if let Some(app_state) = app_state.upgrade() {
open_paths(&action.paths, &app_state, None, cx).detach();
}
}
});
cx.add_async_action({
let app_state = Arc::downgrade(&app_state);
move |workspace, action: &OpenPaths, cx: &mut ViewContext<Workspace>| {
if !workspace.project().read(cx).is_local() {
cx.propagate_action();
return None;
}
let app_state = app_state.upgrade()?;
let window_id = cx.window_id();
let action = action.clone();
let is_remote = workspace.project.read(cx).is_remote();
let has_worktree = workspace.project.read(cx).worktrees(cx).next().is_some();
let has_dirty_items = workspace.items(cx).any(|item| item.is_dirty(cx));
let close_task = if is_remote || has_worktree || has_dirty_items {
None
} else {
Some(workspace.prepare_to_close(false, cx))
};
Some(cx.spawn(|_, mut cx| async move {
let window_id_to_replace = if let Some(close_task) = close_task {
if !close_task.await? {
return Ok(());
cx.spawn(move |mut cx| async move {
if let Some(paths) = paths.recv().await.flatten() {
cx.update(|cx| {
open_paths(&paths, &app_state, None, cx).detach_and_log_err(cx)
});
}
Some(window_id)
} else {
None
};
cx.update(|cx| open_paths(&action.paths, &app_state, window_id_to_replace, cx))
.await?;
Ok(())
}))
})
.detach();
}
}
});
cx.add_action({
let app_state = Arc::downgrade(&app_state);
move |_, _: &Open, cx: &mut ViewContext<Workspace>| {
let mut paths = cx.prompt_for_paths(PathPromptOptions {
files: true,
directories: true,
multiple: true,
});
if let Some(app_state) = app_state.upgrade() {
cx.spawn(|this, mut cx| async move {
if let Some(paths) = paths.recv().await.flatten() {
if let Some(task) = this
.update(&mut cx, |this, cx| {
this.open_workspace_for_paths(paths, app_state, cx)
})
.log_err()
{
task.await.log_err();
}
}
})
.detach();
}
}
});
cx.add_global_action({
let app_state = Arc::downgrade(&app_state);
move |_: &NewWindow, cx: &mut AppContext| {
@ -347,14 +257,11 @@ pub fn init(app_state: Arc<AppState>, cx: &mut AppContext) {
}
});
cx.add_async_action(Workspace::toggle_follow);
cx.add_async_action(Workspace::follow_next_collaborator);
cx.add_async_action(Workspace::close);
cx.add_global_action(Workspace::close_global);
cx.add_async_action(Workspace::save_all);
cx.add_action(Workspace::open_shared_screen);
cx.add_action(Workspace::add_folder_to_project);
cx.add_action(Workspace::remove_folder_from_project);
cx.add_action(
|workspace: &mut Workspace, _: &Unfollow, cx: &mut ViewContext<Workspace>| {
let pane = workspace.active_pane().clone();
@ -384,30 +291,6 @@ pub fn init(app_state: Arc<AppState>, cx: &mut AppContext) {
});
cx.add_action(Workspace::activate_pane_at_index);
cx.add_action(Workspace::split_pane_with_item);
cx.add_async_action(Workspace::split_pane_with_project_entry);
cx.add_async_action(
|workspace: &mut Workspace,
OpenProjectEntryInPane {
pane,
project_entry,
}: &_,
cx| {
workspace
.project
.read(cx)
.path_for_entry(*project_entry, cx)
.map(|path| {
let task = workspace.open_path(path, Some(pane.clone()), true, cx);
cx.foreground().spawn(async move {
task.await?;
Ok(())
})
})
},
);
cx.add_action(|_: &mut Workspace, _: &install_cli::Install, cx| {
cx.spawn(|workspace, mut cx| async move {
let err = install_cli::install_cli(&cx)
@ -431,24 +314,6 @@ pub fn init(app_state: Arc<AppState>, cx: &mut AppContext) {
.detach();
});
cx.add_action(|workspace: &mut Workspace, alert: &Toast, cx| {
workspace.dismiss_notification::<MessageNotification>(alert.id, cx);
workspace.show_notification(alert.id, cx, |cx| {
cx.add_view(|_cx| match &alert.click {
Some((click_msg, action)) => MessageNotification::new_boxed_action(
alert.msg.clone(),
action.boxed_clone(),
click_msg.clone(),
),
None => MessageNotification::new_message(alert.msg.clone()),
})
})
});
cx.add_action(|workspace: &mut Workspace, alert: &DismissToast, cx| {
workspace.dismiss_notification::<MessageNotification>(alert.id, cx);
});
let client = &app_state.client;
client.add_view_request_handler(Workspace::handle_follow);
client.add_view_message_handler(Workspace::handle_unfollow);
@ -617,10 +482,7 @@ pub enum Event {
pub struct Workspace {
weak_self: WeakViewHandle<Self>,
client: Arc<Client>,
user_store: ModelHandle<client::UserStore>,
remote_entity_subscription: Option<client::Subscription>,
fs: Arc<dyn Fs>,
modal: Option<AnyViewHandle>,
center: PaneGroup,
left_sidebar: ViewHandle<Sidebar>,
@ -641,7 +503,7 @@ pub struct Workspace {
active_call: Option<(ModelHandle<ActiveCall>, Vec<gpui::Subscription>)>,
leader_updates_tx: mpsc::UnboundedSender<(PeerId, proto::UpdateFollowers)>,
database_id: WorkspaceId,
background_actions: BackgroundActions,
app_state: Arc<AppState>,
_window_subscriptions: [Subscription; 3],
_apply_leader_updates: Task<Result<()>>,
_observe_current_user: Task<Result<()>>,
@ -671,8 +533,7 @@ impl Workspace {
serialized_workspace: Option<SerializedWorkspace>,
workspace_id: WorkspaceId,
project: ModelHandle<Project>,
dock_default_factory: DockDefaultItemFactory,
background_actions: BackgroundActions,
app_state: Arc<AppState>,
cx: &mut ViewContext<Self>,
) -> Self {
cx.observe(&project, |_, _, cx| cx.notify()).detach();
@ -709,8 +570,8 @@ impl Workspace {
let weak_handle = cx.weak_handle();
let center_pane =
cx.add_view(|cx| Pane::new(weak_handle.id(), None, background_actions, cx));
let center_pane = cx
.add_view(|cx| Pane::new(weak_handle.clone(), None, app_state.background_actions, cx));
let pane_id = center_pane.id();
cx.subscribe(&center_pane, move |this, _, event, cx| {
this.handle_pane_event(pane_id, event, cx)
@ -719,18 +580,14 @@ impl Workspace {
cx.focus(&center_pane);
cx.emit(Event::PaneAdded(center_pane.clone()));
let dock = Dock::new(
weak_handle.id(),
dock_default_factory,
background_actions,
app_state.dock_default_item_factory,
app_state.background_actions,
cx,
);
let dock_pane = dock.pane().clone();
let fs = project.read(cx).fs().clone();
let user_store = project.read(cx).user_store();
let client = project.read(cx).client();
let mut current_user = user_store.read(cx).watch_current_user();
let mut connection_status = client.status();
let mut current_user = app_state.user_store.read(cx).watch_current_user();
let mut connection_status = app_state.client.status();
let _observe_current_user = cx.spawn(|this, mut cx| async move {
current_user.recv().await;
connection_status.recv().await;
@ -823,10 +680,7 @@ impl Workspace {
status_bar,
titlebar_item: None,
notifications: Default::default(),
client,
remote_entity_subscription: None,
user_store,
fs,
left_sidebar,
right_sidebar,
project: project.clone(),
@ -836,7 +690,7 @@ impl Workspace {
window_edited: false,
active_call,
database_id: workspace_id,
background_actions,
app_state,
_observe_current_user,
_apply_leader_updates,
leader_updates_tx,
@ -925,8 +779,7 @@ impl Workspace {
serialized_workspace,
workspace_id,
project_handle.clone(),
app_state.dock_default_item_factory,
app_state.background_actions,
app_state.clone(),
cx,
);
(app_state.initialize_workspace)(&mut workspace, &app_state, cx);
@ -1036,8 +889,12 @@ impl Workspace {
&self.status_bar
}
pub fn app_state(&self) -> &Arc<AppState> {
&self.app_state
}
pub fn user_store(&self) -> &ModelHandle<UserStore> {
&self.user_store
&self.app_state.user_store
}
pub fn project(&self) -> &ModelHandle<Project> {
@ -1045,7 +902,7 @@ impl Workspace {
}
pub fn client(&self) -> &Client {
&self.client
&self.app_state.client
}
pub fn set_titlebar_item(&mut self, item: AnyViewHandle, cx: &mut ViewContext<Self>) {
@ -1243,6 +1100,37 @@ impl Workspace {
})
}
pub fn open_workspace_for_paths(
&mut self,
paths: Vec<PathBuf>,
app_state: Arc<AppState>,
cx: &mut ViewContext<Self>,
) -> Task<Result<()>> {
let window_id = cx.window_id();
let is_remote = self.project.read(cx).is_remote();
let has_worktree = self.project.read(cx).worktrees(cx).next().is_some();
let has_dirty_items = self.items(cx).any(|item| item.is_dirty(cx));
let close_task = if is_remote || has_worktree || has_dirty_items {
None
} else {
Some(self.prepare_to_close(false, cx))
};
cx.spawn(|_, mut cx| async move {
let window_id_to_replace = if let Some(close_task) = close_task {
if !close_task.await? {
return Ok(());
}
Some(window_id)
} else {
None
};
cx.update(|cx| open_paths(&paths, &app_state, window_id_to_replace, cx))
.await?;
Ok(())
})
}
#[allow(clippy::type_complexity)]
pub fn open_paths(
&mut self,
@ -1250,7 +1138,7 @@ impl Workspace {
visible: bool,
cx: &mut ViewContext<Self>,
) -> Task<Vec<Option<Result<Box<dyn ItemHandle>, anyhow::Error>>>> {
let fs = self.fs.clone();
let fs = self.app_state.fs.clone();
// Sort the paths to ensure we add worktrees for parents before their children.
abs_paths.sort_unstable();
@ -1319,15 +1207,6 @@ impl Workspace {
.detach_and_log_err(cx);
}
fn remove_folder_from_project(
&mut self,
RemoveWorktreeFromProject(worktree_id): &RemoveWorktreeFromProject,
cx: &mut ViewContext<Self>,
) {
self.project
.update(cx, |project, cx| project.remove_worktree(*worktree_id, cx));
}
fn project_path_for_path(
project: ModelHandle<Project>,
abs_path: &Path,
@ -1561,8 +1440,14 @@ impl Workspace {
}
fn add_pane(&mut self, cx: &mut ViewContext<Self>) -> ViewHandle<Pane> {
let pane =
cx.add_view(|cx| Pane::new(self.weak_handle().id(), None, self.background_actions, cx));
let pane = cx.add_view(|cx| {
Pane::new(
self.weak_handle(),
None,
self.app_state.background_actions,
cx,
)
});
let pane_id = pane.id();
cx.subscribe(&pane, move |this, _, event, cx| {
this.handle_pane_event(pane_id, event, cx)
@ -1678,10 +1563,8 @@ impl Workspace {
item
}
pub fn open_shared_screen(&mut self, action: &OpenSharedScreen, cx: &mut ViewContext<Self>) {
if let Some(shared_screen) =
self.shared_screen_for_peer(action.peer_id, &self.active_pane, cx)
{
pub fn open_shared_screen(&mut self, peer_id: PeerId, cx: &mut ViewContext<Self>) {
if let Some(shared_screen) = self.shared_screen_for_peer(peer_id, &self.active_pane, cx) {
let pane = self.active_pane.clone();
Pane::add_item(self, &pane, Box::new(shared_screen), false, true, None, cx);
}
@ -1755,7 +1638,7 @@ impl Workspace {
proto::update_followers::Variant::UpdateActiveView(proto::UpdateActiveView {
id: self.active_item(cx).and_then(|item| {
item.to_followable_item_handle(cx)?
.remote_id(&self.client, cx)
.remote_id(&self.app_state.client, cx)
.map(|id| id.to_proto())
}),
leader_id: self.leader_for_pane(&pane),
@ -1833,35 +1716,37 @@ impl Workspace {
maybe_pane_handle
}
pub fn split_pane_with_item(&mut self, action: &SplitWithItem, cx: &mut ViewContext<Self>) {
let Some(pane_to_split) = action.pane_to_split.upgrade(cx) else { return; };
let Some(from) = action.from.upgrade(cx) else { return; };
pub fn split_pane_with_item(
&mut self,
pane_to_split: WeakViewHandle<Pane>,
split_direction: SplitDirection,
from: WeakViewHandle<Pane>,
item_id_to_move: usize,
cx: &mut ViewContext<Self>,
) {
let Some(pane_to_split) = pane_to_split.upgrade(cx) else { return; };
let Some(from) = from.upgrade(cx) else { return; };
if &pane_to_split == self.dock_pane() {
warn!("Can't split dock pane.");
return;
}
let new_pane = self.add_pane(cx);
Pane::move_item(
self,
from.clone(),
new_pane.clone(),
action.item_id_to_move,
0,
cx,
);
Pane::move_item(self, from.clone(), new_pane.clone(), item_id_to_move, 0, cx);
self.center
.split(&pane_to_split, &new_pane, action.split_direction)
.split(&pane_to_split, &new_pane, split_direction)
.unwrap();
cx.notify();
}
pub fn split_pane_with_project_entry(
&mut self,
action: &SplitWithProjectEntry,
pane_to_split: WeakViewHandle<Pane>,
split_direction: SplitDirection,
project_entry: ProjectEntryId,
cx: &mut ViewContext<Self>,
) -> Option<Task<Result<()>>> {
let pane_to_split = action.pane_to_split.upgrade(cx)?;
let pane_to_split = pane_to_split.upgrade(cx)?;
if &pane_to_split == self.dock_pane() {
warn!("Can't split dock pane.");
return None;
@ -1869,13 +1754,10 @@ impl Workspace {
let new_pane = self.add_pane(cx);
self.center
.split(&pane_to_split, &new_pane, action.split_direction)
.split(&pane_to_split, &new_pane, split_direction)
.unwrap();
let path = self
.project
.read(cx)
.path_for_entry(action.project_entry, cx)?;
let path = self.project.read(cx).path_for_entry(project_entry, cx)?;
let task = self.open_path(path, Some(new_pane.downgrade()), true, cx);
Some(cx.foreground().spawn(async move {
task.await?;
@ -1924,8 +1806,11 @@ impl Workspace {
fn project_remote_id_changed(&mut self, remote_id: Option<u64>, cx: &mut ViewContext<Self>) {
if let Some(remote_id) = remote_id {
self.remote_entity_subscription =
Some(self.client.add_view_for_remote_entity(remote_id, cx));
self.remote_entity_subscription = Some(
self.app_state
.client
.add_view_for_remote_entity(remote_id, cx),
);
} else {
self.remote_entity_subscription.take();
}
@ -1945,10 +1830,9 @@ impl Workspace {
pub fn toggle_follow(
&mut self,
ToggleFollow(leader_id): &ToggleFollow,
leader_id: PeerId,
cx: &mut ViewContext<Self>,
) -> Option<Task<Result<()>>> {
let leader_id = *leader_id;
let pane = self.active_pane().clone();
if let Some(prev_leader_id) = self.unfollow(&pane, cx) {
@ -1966,7 +1850,7 @@ impl Workspace {
cx.notify();
let project_id = self.project.read(cx).remote_id()?;
let request = self.client.request(proto::Follow {
let request = self.app_state.client.request(proto::Follow {
project_id,
leader_id: Some(leader_id),
});
@ -2027,7 +1911,7 @@ impl Workspace {
next_leader_id
.or_else(|| collaborators.keys().copied().next())
.and_then(|leader_id| self.toggle_follow(&ToggleFollow(leader_id), cx))
.and_then(|leader_id| self.toggle_follow(leader_id, cx))
}
pub fn unfollow(
@ -2045,7 +1929,8 @@ impl Workspace {
if states_by_pane.is_empty() {
self.follower_states_by_leader.remove(&leader_id);
if let Some(project_id) = self.project.read(cx).remote_id() {
self.client
self.app_state
.client
.send(proto::Unfollow {
project_id,
leader_id: Some(leader_id),
@ -2226,7 +2111,7 @@ impl Workspace {
mut cx: AsyncAppContext,
) -> Result<proto::FollowResponse> {
this.update(&mut cx, |this, cx| {
let client = &this.client;
let client = &this.app_state.client;
this.leader_state
.followers
.insert(envelope.original_sender_id()?);
@ -2434,7 +2319,8 @@ impl Workspace {
) -> Option<()> {
let project_id = self.project.read(cx).remote_id()?;
if !self.leader_state.followers.is_empty() {
self.client
self.app_state
.client
.send(proto::UpdateFollowers {
project_id,
follower_ids: self.leader_state.followers.iter().copied().collect(),
@ -2766,7 +2652,18 @@ impl Workspace {
#[cfg(any(test, feature = "test-support"))]
pub fn test_new(project: ModelHandle<Project>, cx: &mut ViewContext<Self>) -> Self {
Self::new(None, 0, project, |_, _| None, || &[], cx)
let app_state = Arc::new(AppState {
languages: project.read(cx).languages().clone(),
themes: ThemeRegistry::new((), cx.font_cache().clone()),
client: project.read(cx).client(),
user_store: project.read(cx).user_store(),
fs: project.read(cx).fs().clone(),
build_window_options: |_, _, _| Default::default(),
initialize_workspace: |_, _, _| {},
dock_default_item_factory: |_, _| None,
background_actions: || &[],
});
Self::new(None, 0, project, app_state, cx)
}
}
@ -2852,6 +2749,7 @@ impl View for Workspace {
&self.follower_states_by_leader,
self.active_call(),
self.active_pane(),
&self.app_state,
cx,
))
.flex(1., true),
@ -3083,6 +2981,87 @@ pub fn open_new(
})
}
pub fn join_remote_project(
project_id: u64,
follow_user_id: u64,
app_state: Arc<AppState>,
cx: &mut AppContext,
) -> Task<Result<()>> {
cx.spawn(|mut cx| async move {
let existing_workspace = cx.update(|cx| {
cx.window_ids()
.filter_map(|window_id| cx.root_view(window_id)?.clone().downcast::<Workspace>())
.find(|workspace| {
workspace.read(cx).project().read(cx).remote_id() == Some(project_id)
})
});
let workspace = if let Some(existing_workspace) = existing_workspace {
existing_workspace.downgrade()
} else {
let active_call = cx.read(ActiveCall::global);
let room = active_call
.read_with(&cx, |call, _| call.room().cloned())
.ok_or_else(|| anyhow!("not in a call"))?;
let project = room
.update(&mut cx, |room, cx| {
room.join_project(
project_id,
app_state.languages.clone(),
app_state.fs.clone(),
cx,
)
})
.await?;
let (_, workspace) = cx.add_window(
(app_state.build_window_options)(None, None, cx.platform().as_ref()),
|cx| {
let mut workspace =
Workspace::new(Default::default(), 0, project, app_state.clone(), cx);
(app_state.initialize_workspace)(&mut workspace, &app_state, cx);
workspace
},
);
workspace.downgrade()
};
cx.activate_window(workspace.window_id());
cx.platform().activate(true);
workspace.update(&mut cx, |workspace, cx| {
if let Some(room) = ActiveCall::global(cx).read(cx).room().cloned() {
let follow_peer_id = room
.read(cx)
.remote_participants()
.iter()
.find(|(_, participant)| participant.user.id == follow_user_id)
.map(|(_, p)| p.peer_id)
.or_else(|| {
// If we couldn't follow the given user, follow the host instead.
let collaborator = workspace
.project()
.read(cx)
.collaborators()
.values()
.find(|collaborator| collaborator.replica_id == 0)?;
Some(collaborator.peer_id)
});
if let Some(follow_peer_id) = follow_peer_id {
if !workspace.is_being_followed(follow_peer_id) {
workspace
.toggle_follow(follow_peer_id, cx)
.map(|follow| follow.detach_and_log_err(cx));
}
}
}
})?;
anyhow::Ok(())
})
}
fn parse_pixel_position_env_var(value: &str) -> Option<Vector2F> {
let mut parts = value.split(',');
let width: usize = parts.next()?.parse().ok()?;
@ -3109,16 +3088,7 @@ mod tests {
let fs = FakeFs::new(cx.background());
let project = Project::test(fs, [], cx).await;
let (_, workspace) = cx.add_window(|cx| {
Workspace::new(
Default::default(),
0,
project.clone(),
|_, _| None,
|| &[],
cx,
)
});
let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
// Adding an item with no ambiguity renders the tab without detail.
let item1 = cx.add_view(&workspace, |_| {
@ -3182,16 +3152,7 @@ mod tests {
.await;
let project = Project::test(fs, ["root1".as_ref()], cx).await;
let (window_id, workspace) = cx.add_window(|cx| {
Workspace::new(
Default::default(),
0,
project.clone(),
|_, _| None,
|| &[],
cx,
)
});
let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
let worktree_id = project.read_with(cx, |project, cx| {
project.worktrees(cx).next().unwrap().read(cx).id()
});
@ -3281,16 +3242,7 @@ mod tests {
fs.insert_tree("/root", json!({ "one": "" })).await;
let project = Project::test(fs, ["root".as_ref()], cx).await;
let (window_id, workspace) = cx.add_window(|cx| {
Workspace::new(
Default::default(),
0,
project.clone(),
|_, _| None,
|| &[],
cx,
)
});
let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
// When there are no dirty items, there's nothing to do.
let item1 = cx.add_view(&workspace, |_| TestItem::new());
@ -3325,9 +3277,7 @@ mod tests {
let fs = FakeFs::new(cx.background());
let project = Project::test(fs, None, cx).await;
let (window_id, workspace) = cx.add_window(|cx| {
Workspace::new(Default::default(), 0, project, |_, _| None, || &[], cx)
});
let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
let item1 = cx.add_view(&workspace, |cx| {
TestItem::new()
@ -3434,9 +3384,7 @@ mod tests {
let fs = FakeFs::new(cx.background());
let project = Project::test(fs, [], cx).await;
let (window_id, workspace) = cx.add_window(|cx| {
Workspace::new(Default::default(), 0, project, |_, _| None, || &[], cx)
});
let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
// Create several workspace items with single project entries, and two
// workspace items with multiple project entries.
@ -3543,9 +3491,7 @@ mod tests {
let fs = FakeFs::new(cx.background());
let project = Project::test(fs, [], cx).await;
let (window_id, workspace) = cx.add_window(|cx| {
Workspace::new(Default::default(), 0, project, |_, _| None, || &[], cx)
});
let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
let item = cx.add_view(&workspace, |cx| {
TestItem::new().with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
@ -3662,9 +3608,7 @@ mod tests {
let fs = FakeFs::new(cx.background());
let project = Project::test(fs, [], cx).await;
let (_, workspace) = cx.add_window(|cx| {
Workspace::new(Default::default(), 0, project, |_, _| None, || &[], cx)
});
let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
let item = cx.add_view(&workspace, |cx| {
TestItem::new().with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])

View File

@ -44,7 +44,7 @@ use theme::ThemeRegistry;
use util::{channel::RELEASE_CHANNEL, paths, ResultExt, TryFutureExt};
use workspace::{
self, dock::FocusDock, item::ItemHandle, notifications::NotifyResultExt, AppState, NewFile,
OpenPaths, Workspace,
Workspace,
};
use zed::{self, build_window_options, initialize_workspace, languages, menus, OpenSettings};
@ -160,7 +160,6 @@ fn main() {
vim::init(cx);
terminal_view::init(cx);
theme_testbench::init(cx);
recent_projects::init(cx);
copilot::init(http.clone(), node_runtime, cx);
cx.spawn(|cx| watch_themes(fs.clone(), themes.clone(), cx))
@ -194,12 +193,13 @@ fn main() {
auto_update::init(http, client::ZED_SERVER_URL.clone(), cx);
workspace::init(app_state.clone(), cx);
recent_projects::init(cx, Arc::downgrade(&app_state));
journal::init(app_state.clone(), cx);
language_selector::init(app_state.clone(), cx);
theme_selector::init(app_state.clone(), cx);
zed::init(&app_state, cx);
collab_ui::init(app_state.clone(), cx);
collab_ui::init(&app_state, cx);
feedback::init(app_state.clone(), cx);
welcome::init(cx);
@ -212,7 +212,7 @@ fn main() {
cx.spawn(|cx| async move { restore_or_create_workspace(&app_state, cx).await })
.detach()
} else {
cx.dispatch_global_action(OpenPaths { paths });
workspace::open_paths(&paths, &app_state, None, cx).detach_and_log_err(cx);
}
} else {
if let Ok(Some(connection)) = cli_connections_rx.try_next() {
@ -267,11 +267,9 @@ fn main() {
async fn restore_or_create_workspace(app_state: &Arc<AppState>, mut cx: AsyncAppContext) {
if let Some(location) = workspace::last_opened_workspace_paths().await {
cx.update(|cx| {
cx.dispatch_global_action(OpenPaths {
paths: location.paths().as_ref().clone(),
})
});
cx.update(|cx| workspace::open_paths(location.paths().as_ref(), app_state, None, cx))
.await
.log_err();
} else if matches!(KEY_VALUE_STORE.read_kvp(FIRST_OPEN), Ok(None)) {
cx.update(|cx| show_welcome_experience(app_state, cx));
} else {

View File

@ -31,7 +31,7 @@ use serde::Deserialize;
use serde_json::to_string_pretty;
use settings::Settings;
use std::{borrow::Cow, env, path::Path, str, sync::Arc};
use terminal_view::terminal_button::{self, TerminalButton};
use terminal_view::terminal_button::TerminalButton;
use util::{channel::ReleaseChannel, paths, ResultExt};
use uuid::Uuid;
pub use workspace;
@ -73,7 +73,6 @@ actions!(
const MIN_FONT_SIZE: f32 = 6.0;
pub fn init(app_state: &Arc<AppState>, cx: &mut gpui::AppContext) {
terminal_button::init(cx);
cx.add_action(about);
cx.add_global_action(|_: &Hide, cx: &mut gpui::AppContext| {
cx.platform().hide();
@ -261,7 +260,6 @@ pub fn init(app_state: &Arc<AppState>, cx: &mut gpui::AppContext) {
},
);
activity_indicator::init(cx);
copilot_button::init(cx);
lsp_log::init(cx);
call::init(app_state.client.clone(), app_state.user_store.clone(), cx);
settings::KeymapFileContent::load_defaults(cx);
@ -302,8 +300,9 @@ pub fn initialize_workspace(
cx.emit(workspace::Event::PaneAdded(workspace.active_pane().clone()));
cx.emit(workspace::Event::PaneAdded(workspace.dock_pane().clone()));
let collab_titlebar_item =
cx.add_view(|cx| CollabTitlebarItem::new(&workspace_handle, &app_state.user_store, cx));
let collab_titlebar_item = cx.add_view(|cx| {
CollabTitlebarItem::new(&workspace_handle, app_state.user_store.clone(), cx)
});
workspace.set_titlebar_item(collab_titlebar_item.into_any(), cx);
let project_panel = ProjectPanel::new(workspace.project().clone(), cx);