Merge pull request #2424 from zed-industries/constrain-programmatic-dispatch

Allow programmatic action dispatch only via `AsyncAppContext`
This commit is contained in:
Antonio Scandurra 2023-05-01 17:10:57 +02:00 committed by GitHub
commit a8084ad3f4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
48 changed files with 844 additions and 944 deletions

View file

@ -5,7 +5,7 @@ use gpui::{
actions, anyhow, actions, anyhow,
elements::*, elements::*,
platform::{CursorStyle, MouseButton}, platform::{CursorStyle, MouseButton},
Action, AppContext, Entity, ModelHandle, View, ViewContext, ViewHandle, AppContext, Entity, ModelHandle, View, ViewContext, ViewHandle,
}; };
use language::{LanguageRegistry, LanguageServerBinaryStatus}; use language::{LanguageRegistry, LanguageServerBinaryStatus};
use project::{LanguageServerProgress, Project}; use project::{LanguageServerProgress, Project};
@ -45,7 +45,7 @@ struct PendingWork<'a> {
struct Content { struct Content {
icon: Option<&'static str>, icon: Option<&'static str>,
message: String, message: String,
action: Option<Box<dyn Action>>, on_click: Option<Arc<dyn Fn(&mut ActivityIndicator, &mut ViewContext<ActivityIndicator>)>>,
} }
pub fn init(cx: &mut AppContext) { pub fn init(cx: &mut AppContext) {
@ -199,7 +199,7 @@ impl ActivityIndicator {
return Content { return Content {
icon: None, icon: None,
message, message,
action: None, on_click: None,
}; };
} }
@ -230,7 +230,7 @@ impl ActivityIndicator {
downloading.join(", "), downloading.join(", "),
if downloading.len() > 1 { "s" } else { "" } if downloading.len() > 1 { "s" } else { "" }
), ),
action: None, on_click: None,
}; };
} else if !checking_for_update.is_empty() { } else if !checking_for_update.is_empty() {
return Content { return Content {
@ -244,7 +244,7 @@ impl ActivityIndicator {
"" ""
} }
), ),
action: None, on_click: None,
}; };
} else if !failed.is_empty() { } else if !failed.is_empty() {
return Content { return Content {
@ -254,7 +254,9 @@ impl ActivityIndicator {
failed.join(", "), failed.join(", "),
if failed.len() > 1 { "s" } else { "" } if failed.len() > 1 { "s" } else { "" }
), ),
action: Some(Box::new(ShowErrorMessage)), on_click: Some(Arc::new(|this, cx| {
this.show_error_message(&Default::default(), cx)
})),
}; };
} }
@ -264,27 +266,31 @@ impl ActivityIndicator {
AutoUpdateStatus::Checking => Content { AutoUpdateStatus::Checking => Content {
icon: Some(DOWNLOAD_ICON), icon: Some(DOWNLOAD_ICON),
message: "Checking for Zed updates…".to_string(), message: "Checking for Zed updates…".to_string(),
action: None, on_click: None,
}, },
AutoUpdateStatus::Downloading => Content { AutoUpdateStatus::Downloading => Content {
icon: Some(DOWNLOAD_ICON), icon: Some(DOWNLOAD_ICON),
message: "Downloading Zed update…".to_string(), message: "Downloading Zed update…".to_string(),
action: None, on_click: None,
}, },
AutoUpdateStatus::Installing => Content { AutoUpdateStatus::Installing => Content {
icon: Some(DOWNLOAD_ICON), icon: Some(DOWNLOAD_ICON),
message: "Installing Zed update…".to_string(), message: "Installing Zed update…".to_string(),
action: None, on_click: None,
}, },
AutoUpdateStatus::Updated => Content { AutoUpdateStatus::Updated => Content {
icon: None, icon: None,
message: "Click to restart and update Zed".to_string(), message: "Click to restart and update Zed".to_string(),
action: Some(Box::new(workspace::Restart)), on_click: Some(Arc::new(|_, cx| {
workspace::restart(&Default::default(), cx)
})),
}, },
AutoUpdateStatus::Errored => Content { AutoUpdateStatus::Errored => Content {
icon: Some(WARNING_ICON), icon: Some(WARNING_ICON),
message: "Auto update failed".to_string(), message: "Auto update failed".to_string(),
action: Some(Box::new(DismissErrorMessage)), on_click: Some(Arc::new(|this, cx| {
this.dismiss_error_message(&Default::default(), cx)
})),
}, },
AutoUpdateStatus::Idle => Default::default(), AutoUpdateStatus::Idle => Default::default(),
}; };
@ -294,7 +300,7 @@ impl ActivityIndicator {
return Content { return Content {
icon: None, icon: None,
message: most_recent_active_task.to_string(), message: most_recent_active_task.to_string(),
action: None, on_click: None,
}; };
} }
@ -315,7 +321,7 @@ impl View for ActivityIndicator {
let Content { let Content {
icon, icon,
message, message,
action, on_click,
} = self.content_to_render(cx); } = self.content_to_render(cx);
let mut element = MouseEventHandler::<Self, _>::new(0, cx, |state, cx| { let mut element = MouseEventHandler::<Self, _>::new(0, cx, |state, cx| {
@ -325,7 +331,7 @@ impl View for ActivityIndicator {
.workspace .workspace
.status_bar .status_bar
.lsp_status; .lsp_status;
let style = if state.hovered() && action.is_some() { let style = if state.hovered() && on_click.is_some() {
theme.hover.as_ref().unwrap_or(&theme.default) theme.hover.as_ref().unwrap_or(&theme.default)
} else { } else {
&theme.default &theme.default
@ -353,12 +359,10 @@ impl View for ActivityIndicator {
.aligned() .aligned()
}); });
if let Some(action) = action { if let Some(on_click) = on_click.clone() {
element = element element = element
.with_cursor_style(CursorStyle::PointingHand) .with_cursor_style(CursorStyle::PointingHand)
.on_click(MouseButton::Left, move |_, _, cx| { .on_click(MouseButton::Left, move |_, this, cx| on_click(this, cx));
cx.dispatch_any_action(action.boxed_clone())
});
} }
element.into_any() element.into_any()

View file

@ -51,9 +51,8 @@ impl Entity for AutoUpdater {
pub fn init(http_client: Arc<dyn HttpClient>, server_url: String, cx: &mut AppContext) { pub fn init(http_client: Arc<dyn HttpClient>, server_url: String, cx: &mut AppContext) {
if let Some(version) = (*ZED_APP_VERSION).or_else(|| cx.platform().app_version().ok()) { if let Some(version) = (*ZED_APP_VERSION).or_else(|| cx.platform().app_version().ok()) {
let server_url = server_url;
let auto_updater = cx.add_model(|cx| { let auto_updater = cx.add_model(|cx| {
let updater = AutoUpdater::new(version, http_client, server_url.clone()); let updater = AutoUpdater::new(version, http_client, server_url);
let mut update_subscription = cx let mut update_subscription = cx
.global::<Settings>() .global::<Settings>()
@ -74,12 +73,21 @@ pub fn init(http_client: Arc<dyn HttpClient>, server_url: String, cx: &mut AppCo
updater updater
}); });
cx.set_global(Some(auto_updater)); cx.set_global(Some(auto_updater));
cx.add_global_action(|_: &Check, cx| { cx.add_global_action(check);
cx.add_global_action(view_release_notes);
cx.add_action(UpdateNotification::dismiss);
}
}
pub fn check(_: &Check, cx: &mut AppContext) {
if let Some(updater) = AutoUpdater::get(cx) { if let Some(updater) = AutoUpdater::get(cx) {
updater.update(cx, |updater, cx| updater.poll(cx)); updater.update(cx, |updater, cx| updater.poll(cx));
} }
}); }
cx.add_global_action(move |_: &ViewReleaseNotes, cx| {
fn view_release_notes(_: &ViewReleaseNotes, cx: &mut AppContext) {
if let Some(auto_updater) = AutoUpdater::get(cx) {
let server_url = &auto_updater.read(cx).server_url;
let latest_release_url = if cx.has_global::<ReleaseChannel>() let latest_release_url = if cx.has_global::<ReleaseChannel>()
&& *cx.global::<ReleaseChannel>() == ReleaseChannel::Preview && *cx.global::<ReleaseChannel>() == ReleaseChannel::Preview
{ {
@ -88,8 +96,6 @@ pub fn init(http_client: Arc<dyn HttpClient>, server_url: String, cx: &mut AppCo
format!("{server_url}/releases/latest") format!("{server_url}/releases/latest")
}; };
cx.platform().open_url(&latest_release_url); cx.platform().open_url(&latest_release_url);
});
cx.add_action(UpdateNotification::dismiss);
} }
} }

View file

@ -63,8 +63,8 @@ impl View for UpdateNotification {
.with_height(style.button_width) .with_height(style.button_width)
}) })
.with_padding(Padding::uniform(5.)) .with_padding(Padding::uniform(5.))
.on_click(MouseButton::Left, move |_, _, cx| { .on_click(MouseButton::Left, move |_, this, cx| {
cx.dispatch_action(Cancel) this.dismiss(&Default::default(), cx)
}) })
.aligned() .aligned()
.constrained() .constrained()
@ -84,7 +84,7 @@ impl View for UpdateNotification {
}) })
.with_cursor_style(CursorStyle::PointingHand) .with_cursor_style(CursorStyle::PointingHand)
.on_click(MouseButton::Left, |_, _, cx| { .on_click(MouseButton::Left, |_, _, cx| {
cx.dispatch_action(ViewReleaseNotes) crate::view_release_notes(&Default::default(), cx)
}) })
.into_any_named("update notification") .into_any_named("update notification")
} }

View file

@ -1,13 +1,13 @@
use gpui::{ use gpui::{
elements::*, platform::MouseButton, AppContext, Entity, Subscription, View, ViewContext, elements::*, platform::MouseButton, AppContext, Entity, Subscription, View, ViewContext,
ViewHandle, ViewHandle, WeakViewHandle,
}; };
use itertools::Itertools; use itertools::Itertools;
use search::ProjectSearchView; use search::ProjectSearchView;
use settings::Settings; use settings::Settings;
use workspace::{ use workspace::{
item::{ItemEvent, ItemHandle}, item::{ItemEvent, ItemHandle},
ToolbarItemLocation, ToolbarItemView, ToolbarItemLocation, ToolbarItemView, Workspace,
}; };
pub enum Event { pub enum Event {
@ -19,15 +19,17 @@ pub struct Breadcrumbs {
active_item: Option<Box<dyn ItemHandle>>, active_item: Option<Box<dyn ItemHandle>>,
project_search: Option<ViewHandle<ProjectSearchView>>, project_search: Option<ViewHandle<ProjectSearchView>>,
subscription: Option<Subscription>, subscription: Option<Subscription>,
workspace: WeakViewHandle<Workspace>,
} }
impl Breadcrumbs { impl Breadcrumbs {
pub fn new() -> Self { pub fn new(workspace: &Workspace) -> Self {
Self { Self {
pane_focused: false, pane_focused: false,
active_item: Default::default(), active_item: Default::default(),
subscription: Default::default(), subscription: Default::default(),
project_search: Default::default(), project_search: Default::default(),
workspace: workspace.weak_handle(),
} }
} }
} }
@ -85,8 +87,12 @@ impl View for Breadcrumbs {
let style = style.style_for(state, false); let style = style.style_for(state, false);
crumbs.with_style(style.container) crumbs.with_style(style.container)
}) })
.on_click(MouseButton::Left, |_, _, cx| { .on_click(MouseButton::Left, |_, this, cx| {
cx.dispatch_action(outline::Toggle); if let Some(workspace) = this.workspace.upgrade(cx) {
workspace.update(cx, |workspace, cx| {
outline::toggle(workspace, &Default::default(), cx)
})
}
}) })
.with_tooltip::<Breadcrumbs>( .with_tooltip::<Breadcrumbs>(
0, 0,

View file

@ -1,7 +1,6 @@
use crate::{ use crate::{
collaborator_list_popover, collaborator_list_popover::CollaboratorListPopover,
contact_notification::ContactNotification, contacts_popover, face_pile::FacePile, contact_notification::ContactNotification, contacts_popover, face_pile::FacePile,
ToggleScreenSharing, toggle_screen_sharing, ToggleScreenSharing,
}; };
use call::{ActiveCall, ParticipantLocation, Room}; use call::{ActiveCall, ParticipantLocation, Room};
use client::{proto::PeerId, ContactEventKind, SignIn, SignOut, User, UserStore}; use client::{proto::PeerId, ContactEventKind, SignIn, SignOut, User, UserStore};
@ -27,7 +26,6 @@ use workspace::{FollowNextCollaborator, Workspace};
actions!( actions!(
collab, collab,
[ [
ToggleCollaboratorList,
ToggleContactsMenu, ToggleContactsMenu,
ToggleUserMenu, ToggleUserMenu,
ShareProject, ShareProject,
@ -36,7 +34,6 @@ actions!(
); );
pub fn init(cx: &mut AppContext) { 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::toggle_contacts_popover);
cx.add_action(CollabTitlebarItem::share_project); cx.add_action(CollabTitlebarItem::share_project);
cx.add_action(CollabTitlebarItem::unshare_project); cx.add_action(CollabTitlebarItem::unshare_project);
@ -48,7 +45,6 @@ pub struct CollabTitlebarItem {
user_store: ModelHandle<UserStore>, user_store: ModelHandle<UserStore>,
contacts_popover: Option<ViewHandle<ContactsPopover>>, contacts_popover: Option<ViewHandle<ContactsPopover>>,
user_menu: ViewHandle<ContextMenu>, user_menu: ViewHandle<ContextMenu>,
collaborator_list_popover: Option<ViewHandle<CollaboratorListPopover>>,
_subscriptions: Vec<Subscription>, _subscriptions: Vec<Subscription>,
} }
@ -172,7 +168,6 @@ impl CollabTitlebarItem {
menu.set_position_mode(OverlayPositionMode::Local); menu.set_position_mode(OverlayPositionMode::Local);
menu menu
}), }),
collaborator_list_popover: None,
_subscriptions: subscriptions, _subscriptions: subscriptions,
} }
} }
@ -217,36 +212,6 @@ impl CollabTitlebarItem {
} }
} }
pub fn toggle_collaborator_list_popover(
&mut self,
_: &ToggleCollaboratorList,
cx: &mut ViewContext<Self>,
) {
match self.collaborator_list_popover.take() {
Some(_) => {}
None => {
if let Some(workspace) = self.workspace.upgrade(cx) {
let user_store = workspace.read(cx).user_store().clone();
let view = cx.add_view(|cx| CollaboratorListPopover::new(user_store, cx));
cx.subscribe(&view, |this, _, event, cx| {
match event {
collaborator_list_popover::Event::Dismissed => {
this.collaborator_list_popover = None;
}
}
cx.notify();
})
.detach();
self.collaborator_list_popover = Some(view);
}
}
}
cx.notify();
}
pub fn toggle_contacts_popover(&mut self, _: &ToggleContactsMenu, cx: &mut ViewContext<Self>) { pub fn toggle_contacts_popover(&mut self, _: &ToggleContactsMenu, cx: &mut ViewContext<Self>) {
if self.contacts_popover.take().is_none() { if self.contacts_popover.take().is_none() {
if let Some(workspace) = self.workspace.upgrade(cx) { if let Some(workspace) = self.workspace.upgrade(cx) {
@ -357,8 +322,8 @@ impl CollabTitlebarItem {
.with_style(style.container) .with_style(style.container)
}) })
.with_cursor_style(CursorStyle::PointingHand) .with_cursor_style(CursorStyle::PointingHand)
.on_click(MouseButton::Left, move |_, _, cx| { .on_click(MouseButton::Left, move |_, this, cx| {
cx.dispatch_action(ToggleContactsMenu); this.toggle_contacts_popover(&Default::default(), cx)
}) })
.with_tooltip::<ToggleContactsMenu>( .with_tooltip::<ToggleContactsMenu>(
0, 0,
@ -405,7 +370,7 @@ impl CollabTitlebarItem {
}) })
.with_cursor_style(CursorStyle::PointingHand) .with_cursor_style(CursorStyle::PointingHand)
.on_click(MouseButton::Left, move |_, _, cx| { .on_click(MouseButton::Left, move |_, _, cx| {
cx.dispatch_action(ToggleScreenSharing); toggle_screen_sharing(&Default::default(), cx)
}) })
.with_tooltip::<ToggleScreenSharing>( .with_tooltip::<ToggleScreenSharing>(
0, 0,
@ -451,11 +416,11 @@ impl CollabTitlebarItem {
.with_style(style.container) .with_style(style.container)
}) })
.with_cursor_style(CursorStyle::PointingHand) .with_cursor_style(CursorStyle::PointingHand)
.on_click(MouseButton::Left, move |_, _, cx| { .on_click(MouseButton::Left, move |_, this, cx| {
if is_shared { if is_shared {
cx.dispatch_action(UnshareProject); this.unshare_project(&Default::default(), cx);
} else { } else {
cx.dispatch_action(ShareProject); this.share_project(&Default::default(), cx);
} }
}) })
.with_tooltip::<ShareUnshare>( .with_tooltip::<ShareUnshare>(
@ -496,8 +461,8 @@ impl CollabTitlebarItem {
.with_style(style.container) .with_style(style.container)
}) })
.with_cursor_style(CursorStyle::PointingHand) .with_cursor_style(CursorStyle::PointingHand)
.on_click(MouseButton::Left, move |_, _, cx| { .on_click(MouseButton::Left, move |_, this, cx| {
cx.dispatch_action(ToggleUserMenu); this.toggle_user_menu(&Default::default(), cx)
}) })
.with_tooltip::<ToggleUserMenu>( .with_tooltip::<ToggleUserMenu>(
0, 0,
@ -527,8 +492,13 @@ impl CollabTitlebarItem {
.with_style(style.container) .with_style(style.container)
}) })
.with_cursor_style(CursorStyle::PointingHand) .with_cursor_style(CursorStyle::PointingHand)
.on_click(MouseButton::Left, move |_, _, cx| { .on_click(MouseButton::Left, move |_, this, cx| {
cx.dispatch_action(SignIn); if let Some(workspace) = this.workspace.upgrade(cx) {
let client = workspace.read(cx).app_state().client.clone();
cx.app_context()
.spawn(|cx| async move { client.authenticate_and_connect(true, &cx).await })
.detach_and_log_err(cx);
}
}) })
.into_any() .into_any()
} }
@ -862,7 +832,7 @@ impl CollabTitlebarItem {
}) })
.with_cursor_style(CursorStyle::PointingHand) .with_cursor_style(CursorStyle::PointingHand)
.on_click(MouseButton::Left, |_, _, cx| { .on_click(MouseButton::Left, |_, _, cx| {
cx.dispatch_action(auto_update::Check); auto_update::check(&Default::default(), cx);
}) })
.into_any(), .into_any(),
), ),

View file

@ -1,5 +1,4 @@
mod collab_titlebar_item; mod collab_titlebar_item;
mod collaborator_list_popover;
mod contact_finder; mod contact_finder;
mod contact_list; mod contact_list;
mod contact_notification; mod contact_notification;

View file

@ -1,161 +0,0 @@
use call::ActiveCall;
use client::UserStore;
use gpui::Action;
use gpui::{actions, elements::*, platform::MouseButton, Entity, ModelHandle, View, ViewContext};
use settings::Settings;
use crate::collab_titlebar_item::ToggleCollaboratorList;
pub(crate) enum Event {
Dismissed,
}
enum Collaborator {
SelfUser { username: String },
RemoteUser { username: String },
}
actions!(collaborator_list_popover, [NoOp]);
pub(crate) struct CollaboratorListPopover {
list_state: ListState<Self>,
}
impl Entity for CollaboratorListPopover {
type Event = Event;
}
impl View for CollaboratorListPopover {
fn ui_name() -> &'static str {
"CollaboratorListPopover"
}
fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
let theme = cx.global::<Settings>().theme.clone();
MouseEventHandler::<Self, Self>::new(0, cx, |_, _| {
List::new(self.list_state.clone())
.contained()
.with_style(theme.contacts_popover.container) //TODO: Change the name of this theme key
.constrained()
.with_width(theme.contacts_popover.width)
.with_height(theme.contacts_popover.height)
})
.on_down_out(MouseButton::Left, move |_, _, cx| {
cx.dispatch_action(ToggleCollaboratorList);
})
.into_any()
}
fn focus_out(&mut self, _: gpui::AnyViewHandle, cx: &mut ViewContext<Self>) {
cx.emit(Event::Dismissed);
}
}
impl CollaboratorListPopover {
pub fn new(user_store: ModelHandle<UserStore>, cx: &mut ViewContext<Self>) -> Self {
let active_call = ActiveCall::global(cx);
let mut collaborators = user_store
.read(cx)
.current_user()
.map(|u| Collaborator::SelfUser {
username: u.github_login.clone(),
})
.into_iter()
.collect::<Vec<_>>();
//TODO: What should the canonical sort here look like, consult contacts list implementation
if let Some(room) = active_call.read(cx).room() {
for participant in room.read(cx).remote_participants() {
collaborators.push(Collaborator::RemoteUser {
username: participant.1.user.github_login.clone(),
});
}
}
Self {
list_state: ListState::new(
collaborators.len(),
Orientation::Top,
0.,
move |_, index, cx| match &collaborators[index] {
Collaborator::SelfUser { username } => render_collaborator_list_entry(
index,
username,
None::<NoOp>,
None,
Svg::new("icons/chevron_right_12.svg"),
NoOp,
"Leave call".to_owned(),
cx,
),
Collaborator::RemoteUser { username } => render_collaborator_list_entry(
index,
username,
Some(NoOp),
Some(format!("Follow {username}")),
Svg::new("icons/x_mark_12.svg"),
NoOp,
format!("Remove {username} from call"),
cx,
),
},
),
}
}
}
fn render_collaborator_list_entry<UA: Action + Clone, IA: Action + Clone>(
index: usize,
username: &str,
username_action: Option<UA>,
username_tooltip: Option<String>,
icon: Svg,
icon_action: IA,
icon_tooltip: String,
cx: &mut ViewContext<CollaboratorListPopover>,
) -> AnyElement<CollaboratorListPopover> {
enum Username {}
enum UsernameTooltip {}
enum Icon {}
enum IconTooltip {}
let theme = &cx.global::<Settings>().theme;
let username_theme = theme.contact_list.contact_username.text.clone();
let tooltip_theme = theme.tooltip.clone();
let username =
MouseEventHandler::<Username, CollaboratorListPopover>::new(index, cx, |_, _| {
Label::new(username.to_owned(), username_theme.clone())
})
.on_click(MouseButton::Left, move |_, _, cx| {
if let Some(username_action) = username_action.clone() {
cx.dispatch_action(username_action);
}
});
Flex::row()
.with_child(if let Some(username_tooltip) = username_tooltip {
username
.with_tooltip::<UsernameTooltip>(
index,
username_tooltip,
None,
tooltip_theme.clone(),
cx,
)
.into_any()
} else {
username.into_any()
})
.with_child(
MouseEventHandler::<Icon, CollaboratorListPopover>::new(index, cx, |_, _| icon)
.on_click(MouseButton::Left, move |_, _, cx| {
cx.dispatch_action(icon_action.clone())
})
.with_tooltip::<IconTooltip>(index, icon_tooltip, None, tooltip_theme, cx),
)
.into_any()
}

View file

@ -1,4 +1,3 @@
use crate::contacts_popover;
use call::ActiveCall; use call::ActiveCall;
use client::{proto::PeerId, Contact, User, UserStore}; use client::{proto::PeerId, Contact, User, UserStore};
use editor::{Cancel, Editor}; use editor::{Cancel, Editor};
@ -140,6 +139,7 @@ pub struct RespondToContactRequest {
} }
pub enum Event { pub enum Event {
ToggleContactFinder,
Dismissed, Dismissed,
} }
@ -1116,11 +1116,14 @@ impl ContactList {
) )
.with_padding(Padding::uniform(2.)) .with_padding(Padding::uniform(2.))
.with_cursor_style(CursorStyle::PointingHand) .with_cursor_style(CursorStyle::PointingHand)
.on_click(MouseButton::Left, move |_, _, cx| { .on_click(MouseButton::Left, move |_, this, cx| {
cx.dispatch_action(RemoveContact { this.remove_contact(
&RemoveContact {
user_id, user_id,
github_login: github_login.clone(), github_login: github_login.clone(),
}) },
cx,
);
}) })
.flex_float(), .flex_float(),
) )
@ -1203,11 +1206,14 @@ impl ContactList {
render_icon_button(button_style, "icons/x_mark_8.svg").aligned() render_icon_button(button_style, "icons/x_mark_8.svg").aligned()
}) })
.with_cursor_style(CursorStyle::PointingHand) .with_cursor_style(CursorStyle::PointingHand)
.on_click(MouseButton::Left, move |_, _, cx| { .on_click(MouseButton::Left, move |_, this, cx| {
cx.dispatch_action(RespondToContactRequest { this.respond_to_contact_request(
&RespondToContactRequest {
user_id, user_id,
accept: false, accept: false,
}) },
cx,
);
}) })
.contained() .contained()
.with_margin_right(button_spacing), .with_margin_right(button_spacing),
@ -1225,11 +1231,14 @@ impl ContactList {
.flex_float() .flex_float()
}) })
.with_cursor_style(CursorStyle::PointingHand) .with_cursor_style(CursorStyle::PointingHand)
.on_click(MouseButton::Left, move |_, _, cx| { .on_click(MouseButton::Left, move |_, this, cx| {
cx.dispatch_action(RespondToContactRequest { this.respond_to_contact_request(
&RespondToContactRequest {
user_id, user_id,
accept: true, accept: true,
}) },
cx,
);
}), }),
); );
} else { } else {
@ -1246,11 +1255,14 @@ impl ContactList {
}) })
.with_padding(Padding::uniform(2.)) .with_padding(Padding::uniform(2.))
.with_cursor_style(CursorStyle::PointingHand) .with_cursor_style(CursorStyle::PointingHand)
.on_click(MouseButton::Left, move |_, _, cx| { .on_click(MouseButton::Left, move |_, this, cx| {
cx.dispatch_action(RemoveContact { this.remove_contact(
&RemoveContact {
user_id, user_id,
github_login: github_login.clone(), github_login: github_login.clone(),
}) },
cx,
);
}) })
.flex_float(), .flex_float(),
); );
@ -1318,7 +1330,7 @@ impl View for ContactList {
}) })
.with_cursor_style(CursorStyle::PointingHand) .with_cursor_style(CursorStyle::PointingHand)
.on_click(MouseButton::Left, |_, _, cx| { .on_click(MouseButton::Left, |_, _, cx| {
cx.dispatch_action(contacts_popover::ToggleContactFinder) cx.emit(Event::ToggleContactFinder)
}) })
.with_tooltip::<AddContact>( .with_tooltip::<AddContact>(
0, 0,

View file

@ -1,7 +1,6 @@
use crate::{ use crate::{
contact_finder::{build_contact_finder, ContactFinder}, contact_finder::{build_contact_finder, ContactFinder},
contact_list::ContactList, contact_list::ContactList,
ToggleContactsMenu,
}; };
use client::UserStore; use client::UserStore;
use gpui::{ use gpui::{
@ -72,8 +71,11 @@ impl ContactsPopover {
let child = cx let child = cx
.add_view(|cx| ContactList::new(&workspace, cx).with_editor_text(editor_text, cx)); .add_view(|cx| ContactList::new(&workspace, cx).with_editor_text(editor_text, cx));
cx.focus(&child); cx.focus(&child);
self._subscription = Some(cx.subscribe(&child, |_, _, event, cx| match event { self._subscription = Some(cx.subscribe(&child, |this, _, event, cx| match event {
crate::contact_list::Event::Dismissed => cx.emit(Event::Dismissed), crate::contact_list::Event::Dismissed => cx.emit(Event::Dismissed),
crate::contact_list::Event::ToggleContactFinder => {
this.toggle_contact_finder(&Default::default(), cx)
}
})); }));
self.child = Child::ContactList(child); self.child = Child::ContactList(child);
cx.notify(); cx.notify();
@ -106,9 +108,7 @@ impl View for ContactsPopover {
.with_width(theme.contacts_popover.width) .with_width(theme.contacts_popover.width)
.with_height(theme.contacts_popover.height) .with_height(theme.contacts_popover.height)
}) })
.on_down_out(MouseButton::Left, move |_, _, cx| { .on_down_out(MouseButton::Left, move |_, _, cx| cx.emit(Event::Dismissed))
cx.dispatch_action(ToggleContactsMenu);
})
.into_any() .into_any()
} }

View file

@ -1,3 +1,4 @@
use crate::toggle_screen_sharing;
use call::ActiveCall; use call::ActiveCall;
use gpui::{ use gpui::{
color::Color, color::Color,
@ -7,8 +8,6 @@ use gpui::{
}; };
use settings::Settings; use settings::Settings;
use crate::ToggleScreenSharing;
pub fn init(cx: &mut AppContext) { pub fn init(cx: &mut AppContext) {
let active_call = ActiveCall::global(cx); let active_call = ActiveCall::global(cx);
@ -54,7 +53,7 @@ impl View for SharingStatusIndicator {
.aligned() .aligned()
}) })
.on_click(MouseButton::Left, |_, _, cx| { .on_click(MouseButton::Left, |_, _, cx| {
cx.dispatch_action(ToggleScreenSharing); toggle_screen_sharing(&Default::default(), cx)
}) })
.into_any() .into_any()
} }

View file

@ -167,9 +167,11 @@ impl PickerDelegate for CommandPaletteDelegate {
let focused_view_id = self.focused_view_id; let focused_view_id = self.focused_view_id;
let action_ix = self.matches[self.selected_ix].candidate_id; let action_ix = self.matches[self.selected_ix].candidate_id;
let action = self.actions.remove(action_ix).action; let action = self.actions.remove(action_ix).action;
cx.defer(move |_, cx| { cx.app_context()
cx.dispatch_any_action_at(window_id, focused_view_id, action); .spawn(move |mut cx| async move {
}); cx.dispatch_action(window_id, focused_view_id, action.as_ref())
})
.detach_and_log_err(cx);
} }
cx.emit(PickerEvent::Dismiss); cx.emit(PickerEvent::Dismiss);
} }
@ -266,9 +268,11 @@ impl std::fmt::Debug for Command {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use std::sync::Arc;
use super::*; use super::*;
use editor::Editor; use editor::Editor;
use gpui::TestAppContext; use gpui::{executor::Deterministic, TestAppContext};
use project::Project; use project::Project;
use workspace::{AppState, Workspace}; use workspace::{AppState, Workspace};
@ -289,7 +293,8 @@ mod tests {
} }
#[gpui::test] #[gpui::test]
async fn test_command_palette(cx: &mut TestAppContext) { async fn test_command_palette(deterministic: Arc<Deterministic>, cx: &mut TestAppContext) {
deterministic.forbid_parking();
let app_state = cx.update(AppState::test); let app_state = cx.update(AppState::test);
cx.update(|cx| { cx.update(|cx| {
@ -331,7 +336,7 @@ mod tests {
assert_eq!(palette.delegate().matches[0].string, "editor: backspace"); assert_eq!(palette.delegate().matches[0].string, "editor: backspace");
palette.confirm(&Default::default(), cx); palette.confirm(&Default::default(), cx);
}); });
deterministic.run_until_parked();
editor.read_with(cx, |editor, cx| { editor.read_with(cx, |editor, cx| {
assert_eq!(editor.text(cx), "ab"); assert_eq!(editor.text(cx), "ab");
}); });

View file

@ -227,11 +227,13 @@ impl ContextMenu {
match action { match action {
ContextMenuItemAction::Action(action) => { ContextMenuItemAction::Action(action) => {
let window_id = cx.window_id(); let window_id = cx.window_id();
cx.dispatch_any_action_at( let view_id = self.parent_view_id;
window_id, let action = action.boxed_clone();
self.parent_view_id, cx.app_context()
action.boxed_clone(), .spawn(|mut cx| async move {
); cx.dispatch_action(window_id, view_id, action.as_ref())
})
.detach_and_log_err(cx);
} }
ContextMenuItemAction::Handler(handler) => handler(cx), ContextMenuItemAction::Handler(handler) => handler(cx),
} }
@ -459,11 +461,16 @@ impl ContextMenu {
let window_id = cx.window_id(); let window_id = cx.window_id();
match &action { match &action {
ContextMenuItemAction::Action(action) => { ContextMenuItemAction::Action(action) => {
cx.dispatch_any_action_at( let action = action.boxed_clone();
cx.app_context()
.spawn(|mut cx| async move {
cx.dispatch_action(
window_id, window_id,
view_id, view_id,
action.boxed_clone(), action.as_ref(),
); )
})
.detach_and_log_err(cx);
} }
ContextMenuItemAction::Handler(handler) => handler(cx), ContextMenuItemAction::Handler(handler) => handler(cx),
} }
@ -485,7 +492,11 @@ impl ContextMenu {
.contained() .contained()
.with_style(style.container) .with_style(style.container)
}) })
.on_down_out(MouseButton::Left, |_, _, cx| cx.dispatch_action(Cancel)) .on_down_out(MouseButton::Left, |_, this, cx| {
.on_down_out(MouseButton::Right, |_, _, cx| cx.dispatch_action(Cancel)) this.cancel(&Default::default(), cx);
})
.on_down_out(MouseButton::Right, |_, this, cx| {
this.cancel(&Default::default(), cx);
})
} }
} }

View file

@ -560,7 +560,7 @@ impl Copilot {
} }
} }
fn reinstall(&mut self, cx: &mut ModelContext<Self>) -> Task<()> { pub fn reinstall(&mut self, cx: &mut ModelContext<Self>) -> Task<()> {
let start_task = cx let start_task = cx
.spawn({ .spawn({
let http = self.http.clone(); let http = self.http.clone();

View file

@ -196,7 +196,7 @@ impl CopilotCodeVerification {
.contained() .contained()
.with_style(style.auth.prompting.hint.container.clone()), .with_style(style.auth.prompting.hint.container.clone()),
) )
.with_child(theme::ui::cta_button_with_click::<ConnectButton, _, _, _>( .with_child(theme::ui::cta_button::<ConnectButton, _, _, _>(
if connect_clicked { if connect_clicked {
"Waiting for connection..." "Waiting for connection..."
} else { } else {
@ -250,7 +250,7 @@ impl CopilotCodeVerification {
.contained() .contained()
.with_style(enabled_style.hint.container), .with_style(enabled_style.hint.container),
) )
.with_child(theme::ui::cta_button_with_click::<DoneButton, _, _, _>( .with_child(theme::ui::cta_button::<DoneButton, _, _, _>(
"Done", "Done",
style.auth.content_width, style.auth.content_width,
&style.auth.cta_button, &style.auth.cta_button,
@ -304,7 +304,7 @@ impl CopilotCodeVerification {
.contained() .contained()
.with_style(unauthorized_style.warning.container), .with_style(unauthorized_style.warning.container),
) )
.with_child(theme::ui::cta_button_with_click::<Self, _, _, _>( .with_child(theme::ui::cta_button::<Self, _, _, _>(
"Subscribe on GitHub", "Subscribe on GitHub",
style.auth.content_width, style.auth.content_width,
&style.auth.cta_button, &style.auth.cta_button,

View file

@ -1,5 +1,5 @@
use context_menu::{ContextMenu, ContextMenuItem}; use context_menu::{ContextMenu, ContextMenuItem};
use copilot::{Copilot, Reinstall, SignOut, Status}; use copilot::{Copilot, SignOut, Status};
use editor::Editor; use editor::Editor;
use gpui::{ use gpui::{
elements::*, elements::*,
@ -103,11 +103,21 @@ impl View for CopilotButton {
{ {
workspace.update(cx, |workspace, cx| { workspace.update(cx, |workspace, cx| {
workspace.show_toast( workspace.show_toast(
Toast::new_action( Toast::new(
COPILOT_ERROR_TOAST_ID, COPILOT_ERROR_TOAST_ID,
format!("Copilot can't be started: {}", e), format!("Copilot can't be started: {}", e),
)
.on_click(
"Reinstall Copilot", "Reinstall Copilot",
Reinstall, |cx| {
if let Some(copilot) = Copilot::global(cx) {
copilot
.update(cx, |copilot, cx| {
copilot.reinstall(cx)
})
.detach();
}
},
), ),
cx, cx,
); );

View file

@ -3,18 +3,19 @@ use editor::{Editor, GoToDiagnostic};
use gpui::{ use gpui::{
elements::*, elements::*,
platform::{CursorStyle, MouseButton}, platform::{CursorStyle, MouseButton},
serde_json, AppContext, Entity, ModelHandle, Subscription, View, ViewContext, ViewHandle, serde_json, AppContext, Entity, Subscription, View, ViewContext, ViewHandle, WeakViewHandle,
WeakViewHandle,
}; };
use language::Diagnostic; use language::Diagnostic;
use lsp::LanguageServerId; use lsp::LanguageServerId;
use project::Project;
use settings::Settings; use settings::Settings;
use workspace::{item::ItemHandle, StatusItemView}; use workspace::{item::ItemHandle, StatusItemView, Workspace};
use crate::ProjectDiagnosticsEditor;
pub struct DiagnosticIndicator { pub struct DiagnosticIndicator {
summary: project::DiagnosticSummary, summary: project::DiagnosticSummary,
active_editor: Option<WeakViewHandle<Editor>>, active_editor: Option<WeakViewHandle<Editor>>,
workspace: WeakViewHandle<Workspace>,
current_diagnostic: Option<Diagnostic>, current_diagnostic: Option<Diagnostic>,
in_progress_checks: HashSet<LanguageServerId>, in_progress_checks: HashSet<LanguageServerId>,
_observe_active_editor: Option<Subscription>, _observe_active_editor: Option<Subscription>,
@ -25,7 +26,8 @@ pub fn init(cx: &mut AppContext) {
} }
impl DiagnosticIndicator { impl DiagnosticIndicator {
pub fn new(project: &ModelHandle<Project>, cx: &mut ViewContext<Self>) -> Self { pub fn new(workspace: &Workspace, cx: &mut ViewContext<Self>) -> Self {
let project = workspace.project();
cx.subscribe(project, |this, project, event, cx| match event { cx.subscribe(project, |this, project, event, cx| match event {
project::Event::DiskBasedDiagnosticsStarted { language_server_id } => { project::Event::DiskBasedDiagnosticsStarted { language_server_id } => {
this.in_progress_checks.insert(*language_server_id); this.in_progress_checks.insert(*language_server_id);
@ -46,6 +48,7 @@ impl DiagnosticIndicator {
.language_servers_running_disk_based_diagnostics() .language_servers_running_disk_based_diagnostics()
.collect(), .collect(),
active_editor: None, active_editor: None,
workspace: workspace.weak_handle(),
current_diagnostic: None, current_diagnostic: None,
_observe_active_editor: None, _observe_active_editor: None,
} }
@ -163,8 +166,12 @@ impl View for DiagnosticIndicator {
}) })
}) })
.with_cursor_style(CursorStyle::PointingHand) .with_cursor_style(CursorStyle::PointingHand)
.on_click(MouseButton::Left, |_, _, cx| { .on_click(MouseButton::Left, |_, this, cx| {
cx.dispatch_action(crate::Deploy) if let Some(workspace) = this.workspace.upgrade(cx) {
workspace.update(cx, |workspace, cx| {
ProjectDiagnosticsEditor::deploy(workspace, &Default::default(), cx)
})
}
}) })
.with_tooltip::<Summary>( .with_tooltip::<Summary>(
0, 0,
@ -200,8 +207,8 @@ impl View for DiagnosticIndicator {
.with_margin_left(item_spacing) .with_margin_left(item_spacing)
}) })
.with_cursor_style(CursorStyle::PointingHand) .with_cursor_style(CursorStyle::PointingHand)
.on_click(MouseButton::Left, |_, _, cx| { .on_click(MouseButton::Left, |_, this, cx| {
cx.dispatch_action(GoToDiagnostic) this.go_to_next_diagnostic(&Default::default(), cx)
}), }),
); );
} }

View file

@ -809,10 +809,13 @@ impl CompletionsMenu {
}, },
) )
.with_cursor_style(CursorStyle::PointingHand) .with_cursor_style(CursorStyle::PointingHand)
.on_down(MouseButton::Left, move |_, _, cx| { .on_down(MouseButton::Left, move |_, this, cx| {
cx.dispatch_action(ConfirmCompletion { this.confirm_completion(
&ConfirmCompletion {
item_ix: Some(item_ix), item_ix: Some(item_ix),
}); },
cx,
);
}) })
.into_any(), .into_any(),
); );
@ -970,9 +973,23 @@ impl CodeActionsMenu {
.with_style(item_style) .with_style(item_style)
}) })
.with_cursor_style(CursorStyle::PointingHand) .with_cursor_style(CursorStyle::PointingHand)
.on_down(MouseButton::Left, move |_, _, cx| { .on_down(MouseButton::Left, move |_, this, cx| {
cx.dispatch_action(ConfirmCodeAction { let workspace = this
item_ix: Some(item_ix), .workspace
.as_ref()
.and_then(|(workspace, _)| workspace.upgrade(cx));
cx.window_context().defer(move |cx| {
if let Some(workspace) = workspace {
workspace.update(cx, |workspace, cx| {
if let Some(task) = Editor::confirm_code_action(
workspace,
&Default::default(),
cx,
) {
task.detach_and_log_err(cx);
}
});
}
}); });
}) })
.into_any(), .into_any(),
@ -3138,10 +3155,13 @@ impl Editor {
}) })
.with_cursor_style(CursorStyle::PointingHand) .with_cursor_style(CursorStyle::PointingHand)
.with_padding(Padding::uniform(3.)) .with_padding(Padding::uniform(3.))
.on_down(MouseButton::Left, |_, _, cx| { .on_down(MouseButton::Left, |_, this, cx| {
cx.dispatch_action(ToggleCodeActions { this.toggle_code_actions(
&ToggleCodeActions {
deployed_from_indicator: true, deployed_from_indicator: true,
}); },
cx,
);
}) })
.into_any(), .into_any(),
) )
@ -3199,11 +3219,13 @@ impl Editor {
.with_cursor_style(CursorStyle::PointingHand) .with_cursor_style(CursorStyle::PointingHand)
.with_padding(Padding::uniform(3.)) .with_padding(Padding::uniform(3.))
.on_click(MouseButton::Left, { .on_click(MouseButton::Left, {
move |_, _, cx| { move |_, editor, cx| match fold_status {
cx.dispatch_any_action(match fold_status { FoldStatus::Folded => {
FoldStatus::Folded => Box::new(UnfoldAt { buffer_row }), editor.unfold_at(&UnfoldAt { buffer_row }, cx);
FoldStatus::Foldable => Box::new(FoldAt { buffer_row }), }
}); FoldStatus::Foldable => {
editor.fold_at(&FoldAt { buffer_row }, cx);
}
} }
}) })
.into_any() .into_any()

View file

@ -211,10 +211,13 @@ impl EditorElement {
enum GutterHandlers {} enum GutterHandlers {}
scene.push_mouse_region( scene.push_mouse_region(
MouseRegion::new::<GutterHandlers>(cx.view_id(), cx.view_id() + 1, gutter_bounds) MouseRegion::new::<GutterHandlers>(cx.view_id(), cx.view_id() + 1, gutter_bounds)
.on_hover(|hover, _: &mut Editor, cx| { .on_hover(|hover, editor: &mut Editor, cx| {
cx.dispatch_action(GutterHover { editor.gutter_hover(
&GutterHover {
hovered: hover.started, hovered: hover.started,
}) },
cx,
);
}), }),
) )
} }
@ -754,8 +757,8 @@ impl EditorElement {
scene.push_mouse_region( scene.push_mouse_region(
MouseRegion::new::<FoldMarkers>(cx.view_id(), *id as usize, bound) MouseRegion::new::<FoldMarkers>(cx.view_id(), *id as usize, bound)
.on_click(MouseButton::Left, move |_, _: &mut Editor, cx| { .on_click(MouseButton::Left, move |_, editor: &mut Editor, cx| {
cx.dispatch_action(UnfoldAt { buffer_row }) editor.unfold_at(&UnfoldAt { buffer_row }, cx)
}) })
.with_notify_on_hover(true) .with_notify_on_hover(true)
.with_notify_on_click(true), .with_notify_on_click(true),

View file

@ -1,3 +1,7 @@
use crate::{
display_map::ToDisplayPoint, Anchor, AnchorRangeExt, DisplayPoint, Editor, EditorSnapshot,
EditorStyle, RangeToAnchorExt,
};
use futures::FutureExt; use futures::FutureExt;
use gpui::{ use gpui::{
actions, actions,
@ -12,11 +16,6 @@ use settings::Settings;
use std::{ops::Range, sync::Arc, time::Duration}; use std::{ops::Range, sync::Arc, time::Duration};
use util::TryFutureExt; use util::TryFutureExt;
use crate::{
display_map::ToDisplayPoint, Anchor, AnchorRangeExt, DisplayPoint, Editor, EditorSnapshot,
EditorStyle, GoToDiagnostic, RangeToAnchorExt,
};
pub const HOVER_DELAY_MILLIS: u64 = 350; pub const HOVER_DELAY_MILLIS: u64 = 350;
pub const HOVER_REQUEST_DELAY_MILLIS: u64 = 200; pub const HOVER_REQUEST_DELAY_MILLIS: u64 = 200;
@ -668,8 +667,8 @@ impl DiagnosticPopover {
..Default::default() ..Default::default()
}) })
.on_move(|_, _, _| {}) // Consume move events so they don't reach regions underneath. .on_move(|_, _, _| {}) // Consume move events so they don't reach regions underneath.
.on_click(MouseButton::Left, |_, _, cx| { .on_click(MouseButton::Left, |_, this, cx| {
cx.dispatch_action(GoToDiagnostic) this.go_to_diagnostic(&Default::default(), cx)
}) })
.with_cursor_style(CursorStyle::PointingHand) .with_cursor_style(CursorStyle::PointingHand)
.with_tooltip::<PrimaryDiagnostic>( .with_tooltip::<PrimaryDiagnostic>(

View file

@ -1,15 +1,16 @@
use gpui::{ use gpui::{
elements::*, elements::*,
platform::{CursorStyle, MouseButton}, platform::{CursorStyle, MouseButton},
Entity, View, ViewContext, Entity, View, ViewContext, WeakViewHandle,
}; };
use settings::Settings; use settings::Settings;
use workspace::{item::ItemHandle, StatusItemView}; use workspace::{item::ItemHandle, StatusItemView, Workspace};
use crate::feedback_editor::{FeedbackEditor, GiveFeedback}; use crate::feedback_editor::{FeedbackEditor, GiveFeedback};
pub struct DeployFeedbackButton { pub struct DeployFeedbackButton {
active: bool, active: bool,
workspace: WeakViewHandle<Workspace>,
} }
impl Entity for DeployFeedbackButton { impl Entity for DeployFeedbackButton {
@ -17,8 +18,11 @@ impl Entity for DeployFeedbackButton {
} }
impl DeployFeedbackButton { impl DeployFeedbackButton {
pub fn new() -> Self { pub fn new(workspace: &Workspace) -> Self {
DeployFeedbackButton { active: false } DeployFeedbackButton {
active: false,
workspace: workspace.weak_handle(),
}
} }
} }
@ -52,9 +56,12 @@ impl View for DeployFeedbackButton {
.with_style(style.container) .with_style(style.container)
}) })
.with_cursor_style(CursorStyle::PointingHand) .with_cursor_style(CursorStyle::PointingHand)
.on_click(MouseButton::Left, move |_, _, cx| { .on_click(MouseButton::Left, move |_, this, cx| {
if !active { if !active {
cx.dispatch_action(GiveFeedback) if let Some(workspace) = this.workspace.upgrade(cx) {
workspace
.update(cx, |workspace, cx| FeedbackEditor::deploy(workspace, cx))
}
} }
}) })
.with_tooltip::<Self>( .with_tooltip::<Self>(

View file

@ -3,20 +3,10 @@ pub mod feedback_editor;
pub mod feedback_info_text; pub mod feedback_info_text;
pub mod submit_feedback_button; pub mod submit_feedback_button;
use std::sync::Arc;
mod system_specs; mod system_specs;
use gpui::{actions, impl_actions, platform::PromptLevel, AppContext, ClipboardItem, ViewContext}; use gpui::{actions, platform::PromptLevel, AppContext, ClipboardItem, ViewContext};
use serde::Deserialize;
use system_specs::SystemSpecs; use system_specs::SystemSpecs;
use workspace::{AppState, Workspace}; use workspace::Workspace;
#[derive(Deserialize, Clone, PartialEq)]
pub struct OpenBrowser {
pub url: Arc<str>,
}
impl_actions!(zed, [OpenBrowser]);
actions!( actions!(
zed, zed,
@ -28,29 +18,20 @@ actions!(
] ]
); );
pub fn init(app_state: Arc<AppState>, cx: &mut AppContext) { pub fn init(cx: &mut AppContext) {
let system_specs = SystemSpecs::new(&cx); feedback_editor::init(cx);
let system_specs_text = system_specs.to_string();
feedback_editor::init(system_specs, app_state, cx);
cx.add_global_action(move |action: &OpenBrowser, cx| cx.platform().open_url(&action.url));
let url = format!(
"https://github.com/zed-industries/community/issues/new?assignees=&labels=defect%2Ctriage&template=2_bug_report.yml&environment={}",
urlencoding::encode(&system_specs_text)
);
cx.add_action( cx.add_action(
move |_: &mut Workspace, move |_: &mut Workspace,
_: &CopySystemSpecsIntoClipboard, _: &CopySystemSpecsIntoClipboard,
cx: &mut ViewContext<Workspace>| { cx: &mut ViewContext<Workspace>| {
let specs = SystemSpecs::new(&cx).to_string();
cx.prompt( cx.prompt(
PromptLevel::Info, PromptLevel::Info,
&format!("Copied into clipboard:\n\n{system_specs_text}"), &format!("Copied into clipboard:\n\n{specs}"),
&["OK"], &["OK"],
); );
let item = ClipboardItem::new(system_specs_text.clone()); let item = ClipboardItem::new(specs.clone());
cx.write_to_clipboard(item); cx.write_to_clipboard(item);
}, },
); );
@ -58,24 +39,24 @@ pub fn init(app_state: Arc<AppState>, cx: &mut AppContext) {
cx.add_action( cx.add_action(
|_: &mut Workspace, _: &RequestFeature, cx: &mut ViewContext<Workspace>| { |_: &mut Workspace, _: &RequestFeature, cx: &mut ViewContext<Workspace>| {
let url = "https://github.com/zed-industries/community/issues/new?assignees=&labels=enhancement%2Ctriage&template=0_feature_request.yml"; let url = "https://github.com/zed-industries/community/issues/new?assignees=&labels=enhancement%2Ctriage&template=0_feature_request.yml";
cx.dispatch_action(OpenBrowser { cx.platform().open_url(url);
url: url.into(),
});
}, },
); );
cx.add_action( cx.add_action(
move |_: &mut Workspace, _: &FileBugReport, cx: &mut ViewContext<Workspace>| { move |_: &mut Workspace, _: &FileBugReport, cx: &mut ViewContext<Workspace>| {
cx.dispatch_action(OpenBrowser { let url = format!(
url: url.clone().into(), "https://github.com/zed-industries/community/issues/new?assignees=&labels=defect%2Ctriage&template=2_bug_report.yml&environment={}",
}); urlencoding::encode(&SystemSpecs::new(&cx).to_string())
);
cx.platform().open_url(&url);
}, },
); );
cx.add_action( cx.add_global_action(open_zed_community_repo);
|_: &mut Workspace, _: &OpenZedCommunityRepo, cx: &mut ViewContext<Workspace>| { }
let url = "https://github.com/zed-industries/community";
cx.dispatch_action(OpenBrowser { url: url.into() }); pub fn open_zed_community_repo(_: &OpenZedCommunityRepo, cx: &mut AppContext) {
}, let url = "https://github.com/zed-industries/community";
); cx.platform().open_url(&url);
} }

View file

@ -1,10 +1,4 @@
use std::{ use crate::system_specs::SystemSpecs;
any::TypeId,
borrow::Cow,
ops::{Range, RangeInclusive},
sync::Arc,
};
use anyhow::bail; use anyhow::bail;
use client::{Client, ZED_SECRET_CLIENT_TOKEN, ZED_SERVER_URL}; use client::{Client, ZED_SECRET_CLIENT_TOKEN, ZED_SERVER_URL};
use editor::{Anchor, Editor}; use editor::{Anchor, Editor};
@ -19,40 +13,34 @@ use gpui::{
use isahc::Request; use isahc::Request;
use language::Buffer; use language::Buffer;
use postage::prelude::Stream; use postage::prelude::Stream;
use project::Project; use project::Project;
use serde::Serialize; use serde::Serialize;
use std::{
any::TypeId,
borrow::Cow,
ops::{Range, RangeInclusive},
sync::Arc,
};
use util::ResultExt; use util::ResultExt;
use workspace::{ use workspace::{
item::{Item, ItemHandle}, item::{Item, ItemEvent, ItemHandle},
searchable::{SearchableItem, SearchableItemHandle}, searchable::{SearchableItem, SearchableItemHandle},
AppState, Workspace, smallvec::SmallVec,
Workspace,
}; };
use crate::{submit_feedback_button::SubmitFeedbackButton, system_specs::SystemSpecs};
const FEEDBACK_CHAR_LIMIT: RangeInclusive<usize> = 10..=5000; const FEEDBACK_CHAR_LIMIT: RangeInclusive<usize> = 10..=5000;
const FEEDBACK_SUBMISSION_ERROR_TEXT: &str = const FEEDBACK_SUBMISSION_ERROR_TEXT: &str =
"Feedback failed to submit, see error log for details."; "Feedback failed to submit, see error log for details.";
actions!(feedback, [GiveFeedback, SubmitFeedback]); actions!(feedback, [GiveFeedback, SubmitFeedback]);
pub fn init(system_specs: SystemSpecs, app_state: Arc<AppState>, cx: &mut AppContext) { pub fn init(cx: &mut AppContext) {
cx.add_action({ cx.add_action({
move |workspace: &mut Workspace, _: &GiveFeedback, cx: &mut ViewContext<Workspace>| { move |workspace: &mut Workspace, _: &GiveFeedback, cx: &mut ViewContext<Workspace>| {
FeedbackEditor::deploy(system_specs.clone(), workspace, app_state.clone(), cx); FeedbackEditor::deploy(workspace, cx);
} }
}); });
cx.add_async_action(
|submit_feedback_button: &mut SubmitFeedbackButton, _: &SubmitFeedback, cx| {
if let Some(active_item) = submit_feedback_button.active_item.as_ref() {
Some(active_item.update(cx, |feedback_editor, cx| feedback_editor.handle_save(cx)))
} else {
None
}
},
);
} }
#[derive(Serialize)] #[derive(Serialize)]
@ -94,7 +82,7 @@ impl FeedbackEditor {
} }
} }
fn handle_save(&mut self, cx: &mut ViewContext<Self>) -> Task<anyhow::Result<()>> { pub fn submit(&mut self, cx: &mut ViewContext<Self>) -> Task<anyhow::Result<()>> {
let feedback_text = self.editor.read(cx).text(cx); let feedback_text = self.editor.read(cx).text(cx);
let feedback_char_count = feedback_text.chars().count(); let feedback_char_count = feedback_text.chars().count();
let feedback_text = feedback_text.trim().to_string(); let feedback_text = feedback_text.trim().to_string();
@ -133,9 +121,7 @@ impl FeedbackEditor {
if answer == Some(0) { if answer == Some(0) {
match FeedbackEditor::submit_feedback(&feedback_text, client, specs).await { match FeedbackEditor::submit_feedback(&feedback_text, client, specs).await {
Ok(_) => { Ok(_) => {
this.update(&mut cx, |_, cx| { this.update(&mut cx, |_, cx| cx.emit(editor::Event::Closed))
cx.dispatch_action(workspace::CloseActiveItem);
})
.log_err(); .log_err();
} }
Err(error) => { Err(error) => {
@ -198,22 +184,21 @@ impl FeedbackEditor {
} }
impl FeedbackEditor { impl FeedbackEditor {
pub fn deploy( pub fn deploy(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) {
system_specs: SystemSpecs, let markdown = workspace
_: &mut Workspace, .app_state()
app_state: Arc<AppState>, .languages
cx: &mut ViewContext<Workspace>, .language_for_name("Markdown");
) {
let markdown = app_state.languages.language_for_name("Markdown");
cx.spawn(|workspace, mut cx| async move { cx.spawn(|workspace, mut cx| async move {
let markdown = markdown.await.log_err(); let markdown = markdown.await.log_err();
workspace workspace
.update(&mut cx, |workspace, cx| { .update(&mut cx, |workspace, cx| {
workspace.with_local_workspace(&app_state, cx, |workspace, cx| { workspace.with_local_workspace(cx, |workspace, cx| {
let project = workspace.project().clone(); let project = workspace.project().clone();
let buffer = project let buffer = project
.update(cx, |project, cx| project.create_buffer("", markdown, cx)) .update(cx, |project, cx| project.create_buffer("", markdown, cx))
.expect("creating buffers on a local workspace always succeeds"); .expect("creating buffers on a local workspace always succeeds");
let system_specs = SystemSpecs::new(cx);
let feedback_editor = cx let feedback_editor = cx
.add_view(|cx| FeedbackEditor::new(system_specs, project, buffer, cx)); .add_view(|cx| FeedbackEditor::new(system_specs, project, buffer, cx));
workspace.add_item(Box::new(feedback_editor), cx); workspace.add_item(Box::new(feedback_editor), cx);
@ -291,7 +276,7 @@ impl Item for FeedbackEditor {
_: ModelHandle<Project>, _: ModelHandle<Project>,
cx: &mut ViewContext<Self>, cx: &mut ViewContext<Self>,
) -> Task<anyhow::Result<()>> { ) -> Task<anyhow::Result<()>> {
self.handle_save(cx) self.submit(cx)
} }
fn save_as( fn save_as(
@ -300,7 +285,7 @@ impl Item for FeedbackEditor {
_: std::path::PathBuf, _: std::path::PathBuf,
cx: &mut ViewContext<Self>, cx: &mut ViewContext<Self>,
) -> Task<anyhow::Result<()>> { ) -> Task<anyhow::Result<()>> {
self.handle_save(cx) self.submit(cx)
} }
fn reload( fn reload(
@ -353,6 +338,10 @@ impl Item for FeedbackEditor {
None None
} }
} }
fn to_item_events(event: &Self::Event) -> SmallVec<[ItemEvent; 2]> {
Editor::to_item_events(event)
}
} }
impl SearchableItem for FeedbackEditor { impl SearchableItem for FeedbackEditor {

View file

@ -6,7 +6,7 @@ use gpui::{
use settings::Settings; use settings::Settings;
use workspace::{item::ItemHandle, ToolbarItemLocation, ToolbarItemView}; use workspace::{item::ItemHandle, ToolbarItemLocation, ToolbarItemView};
use crate::{feedback_editor::FeedbackEditor, OpenZedCommunityRepo}; use crate::{feedback_editor::FeedbackEditor, open_zed_community_repo, OpenZedCommunityRepo};
pub struct FeedbackInfoText { pub struct FeedbackInfoText {
active_item: Option<ViewHandle<FeedbackEditor>>, active_item: Option<ViewHandle<FeedbackEditor>>,
@ -57,7 +57,7 @@ impl View for FeedbackInfoText {
}) })
.with_cursor_style(CursorStyle::PointingHand) .with_cursor_style(CursorStyle::PointingHand)
.on_click(MouseButton::Left, |_, _, cx| { .on_click(MouseButton::Left, |_, _, cx| {
cx.dispatch_action(OpenZedCommunityRepo) open_zed_community_repo(&Default::default(), cx)
}), }),
) )
.with_child( .with_child(

View file

@ -1,12 +1,16 @@
use crate::feedback_editor::{FeedbackEditor, SubmitFeedback};
use anyhow::Result;
use gpui::{ use gpui::{
elements::{Label, MouseEventHandler}, elements::{Label, MouseEventHandler},
platform::{CursorStyle, MouseButton}, platform::{CursorStyle, MouseButton},
AnyElement, Element, Entity, View, ViewContext, ViewHandle, AnyElement, AppContext, Element, Entity, Task, View, ViewContext, ViewHandle,
}; };
use settings::Settings; use settings::Settings;
use workspace::{item::ItemHandle, ToolbarItemLocation, ToolbarItemView}; use workspace::{item::ItemHandle, ToolbarItemLocation, ToolbarItemView};
use crate::feedback_editor::{FeedbackEditor, SubmitFeedback}; pub fn init(cx: &mut AppContext) {
cx.add_async_action(SubmitFeedbackButton::submit);
}
pub struct SubmitFeedbackButton { pub struct SubmitFeedbackButton {
pub(crate) active_item: Option<ViewHandle<FeedbackEditor>>, pub(crate) active_item: Option<ViewHandle<FeedbackEditor>>,
@ -18,6 +22,18 @@ impl SubmitFeedbackButton {
active_item: Default::default(), active_item: Default::default(),
} }
} }
pub fn submit(
&mut self,
_: &SubmitFeedback,
cx: &mut ViewContext<Self>,
) -> Option<Task<Result<()>>> {
if let Some(active_item) = self.active_item.as_ref() {
Some(active_item.update(cx, |feedback_editor, cx| feedback_editor.submit(cx)))
} else {
None
}
}
} }
impl Entity for SubmitFeedbackButton { impl Entity for SubmitFeedbackButton {
@ -39,8 +55,8 @@ impl View for SubmitFeedbackButton {
.with_style(style.container) .with_style(style.container)
}) })
.with_cursor_style(CursorStyle::PointingHand) .with_cursor_style(CursorStyle::PointingHand)
.on_click(MouseButton::Left, |_, _, cx| { .on_click(MouseButton::Left, |_, this, cx| {
cx.dispatch_action(SubmitFeedback) this.submit(&Default::default(), cx);
}) })
.aligned() .aligned()
.contained() .contained()

View file

@ -309,6 +309,20 @@ impl AsyncAppContext {
self.0.borrow_mut().update_window(window_id, callback) self.0.borrow_mut().update_window(window_id, callback)
} }
pub fn dispatch_action(
&mut self,
window_id: usize,
view_id: usize,
action: &dyn Action,
) -> Result<()> {
self.0
.borrow_mut()
.update_window(window_id, |window| {
window.handle_dispatch_action_from_effect(Some(view_id), action);
})
.ok_or_else(|| anyhow!("window not found"))
}
pub fn add_model<T, F>(&mut self, build_model: F) -> ModelHandle<T> pub fn add_model<T, F>(&mut self, build_model: F) -> ModelHandle<T>
where where
T: Entity, T: Entity,
@ -1048,10 +1062,6 @@ impl AppContext {
} }
} }
pub fn dispatch_global_action<A: Action>(&mut self, action: A) {
self.dispatch_global_action_any(&action);
}
fn dispatch_global_action_any(&mut self, action: &dyn Action) -> bool { fn dispatch_global_action_any(&mut self, action: &dyn Action) -> bool {
self.update(|this| { self.update(|this| {
if let Some((name, mut handler)) = this.global_actions.remove_entry(&action.id()) { if let Some((name, mut handler)) = this.global_actions.remove_entry(&action.id()) {
@ -1619,17 +1629,7 @@ impl AppContext {
Effect::RefreshWindows => { Effect::RefreshWindows => {
refreshing = true; refreshing = true;
} }
Effect::DispatchActionFrom {
window_id,
view_id,
action,
} => {
self.handle_dispatch_action_from_effect(
window_id,
Some(view_id),
action.as_ref(),
);
}
Effect::ActionDispatchNotification { action_id } => { Effect::ActionDispatchNotification { action_id } => {
self.handle_action_dispatch_notification_effect(action_id) self.handle_action_dispatch_notification_effect(action_id)
} }
@ -1745,23 +1745,6 @@ impl AppContext {
self.pending_effects.push_back(Effect::RefreshWindows); self.pending_effects.push_back(Effect::RefreshWindows);
} }
pub fn dispatch_action_at(&mut self, window_id: usize, view_id: usize, action: impl Action) {
self.dispatch_any_action_at(window_id, view_id, Box::new(action));
}
pub fn dispatch_any_action_at(
&mut self,
window_id: usize,
view_id: usize,
action: Box<dyn Action>,
) {
self.pending_effects.push_back(Effect::DispatchActionFrom {
window_id,
view_id,
action,
});
}
fn perform_window_refresh(&mut self) { fn perform_window_refresh(&mut self) {
let window_ids = self.windows.keys().cloned().collect::<Vec<_>>(); let window_ids = self.windows.keys().cloned().collect::<Vec<_>>();
for window_id in window_ids { for window_id in window_ids {
@ -1920,17 +1903,6 @@ impl AppContext {
}); });
} }
fn handle_dispatch_action_from_effect(
&mut self,
window_id: usize,
view_id: Option<usize>,
action: &dyn Action,
) {
self.update_window(window_id, |cx| {
cx.handle_dispatch_action_from_effect(view_id, action)
});
}
fn handle_action_dispatch_notification_effect(&mut self, action_id: TypeId) { fn handle_action_dispatch_notification_effect(&mut self, action_id: TypeId) {
self.action_dispatch_observations self.action_dispatch_observations
.clone() .clone()
@ -2159,11 +2131,6 @@ pub enum Effect {
result: MatchResult, result: MatchResult,
}, },
RefreshWindows, RefreshWindows,
DispatchActionFrom {
window_id: usize,
view_id: usize,
action: Box<dyn Action>,
},
ActionDispatchNotification { ActionDispatchNotification {
action_id: TypeId, action_id: TypeId,
}, },
@ -2252,13 +2219,6 @@ impl Debug for Effect {
.field("view_id", view_id) .field("view_id", view_id)
.field("subscription_id", subscription_id) .field("subscription_id", subscription_id)
.finish(), .finish(),
Effect::DispatchActionFrom {
window_id, view_id, ..
} => f
.debug_struct("Effect::DispatchActionFrom")
.field("window_id", window_id)
.field("view_id", view_id)
.finish(),
Effect::ActionDispatchNotification { action_id, .. } => f Effect::ActionDispatchNotification { action_id, .. } => f
.debug_struct("Effect::ActionDispatchNotification") .debug_struct("Effect::ActionDispatchNotification")
.field("action_id", action_id) .field("action_id", action_id)
@ -3189,20 +3149,6 @@ impl<'a, 'b, V: View> ViewContext<'a, 'b, V> {
self.window_context.notify_view(window_id, view_id); self.window_context.notify_view(window_id, view_id);
} }
pub fn dispatch_action(&mut self, action: impl Action) {
let window_id = self.window_id;
let view_id = self.view_id;
self.window_context
.dispatch_action_at(window_id, view_id, action)
}
pub fn dispatch_any_action(&mut self, action: Box<dyn Action>) {
let window_id = self.window_id;
let view_id = self.view_id;
self.window_context
.dispatch_any_action_at(window_id, view_id, action)
}
pub fn defer(&mut self, callback: impl 'static + FnOnce(&mut V, &mut ViewContext<V>)) { pub fn defer(&mut self, callback: impl 'static + FnOnce(&mut V, &mut ViewContext<V>)) {
let handle = self.handle(); let handle = self.handle();
self.window_context self.window_context

View file

@ -72,14 +72,16 @@ impl TestAppContext {
} }
pub fn dispatch_action<A: Action>(&self, window_id: usize, action: A) { pub fn dispatch_action<A: Action>(&self, window_id: usize, action: A) {
let mut cx = self.cx.borrow_mut(); self.cx
if let Some(view_id) = cx.windows.get(&window_id).and_then(|w| w.focused_view_id) { .borrow_mut()
cx.handle_dispatch_action_from_effect(window_id, Some(view_id), &action); .update_window(window_id, |window| {
} window.handle_dispatch_action_from_effect(window.focused_view_id(), &action);
})
.expect("window not found");
} }
pub fn dispatch_global_action<A: Action>(&self, action: A) { pub fn dispatch_global_action<A: Action>(&self, action: A) {
self.cx.borrow_mut().dispatch_global_action(action); self.cx.borrow_mut().dispatch_global_action_any(&action);
} }
pub fn dispatch_keystroke(&mut self, window_id: usize, keystroke: Keystroke, is_held: bool) { pub fn dispatch_keystroke(&mut self, window_id: usize, keystroke: Keystroke, is_held: bool) {

View file

@ -1,8 +1,8 @@
use serde::Deserialize; use serde::Deserialize;
use crate::{ use crate::{
actions, elements::*, impl_actions, platform::MouseButton, AppContext, Entity, EventContext, actions, elements::*, impl_actions, platform::MouseButton, AppContext, Entity, View,
View, ViewContext, WeakViewHandle, ViewContext, WeakViewHandle,
}; };
pub struct Select { pub struct Select {
@ -116,10 +116,9 @@ impl View for Select {
.contained() .contained()
.with_style(style.header) .with_style(style.header)
}) })
.on_click( .on_click(MouseButton::Left, move |_, this, cx| {
MouseButton::Left, this.toggle(&Default::default(), cx);
move |_, _, cx: &mut EventContext<Self>| cx.dispatch_action(ToggleSelect), }),
),
); );
if self.is_open { if self.is_open {
result.add_child(Overlay::new( result.add_child(Overlay::new(
@ -143,12 +142,9 @@ impl View for Select {
cx, cx,
) )
}) })
.on_click( .on_click(MouseButton::Left, move |_, this, cx| {
MouseButton::Left, this.select_item(&SelectItem(ix), cx);
move |_, _, cx: &mut EventContext<Self>| { })
cx.dispatch_action(SelectItem(ix))
},
)
.into_any() .into_any()
})) }))
}, },

View file

@ -2,27 +2,23 @@ use editor::Editor;
use gpui::{ use gpui::{
elements::*, elements::*,
platform::{CursorStyle, MouseButton}, platform::{CursorStyle, MouseButton},
Entity, Subscription, View, ViewContext, ViewHandle, Entity, Subscription, View, ViewContext, ViewHandle, WeakViewHandle,
}; };
use settings::Settings; use settings::Settings;
use std::sync::Arc; use std::sync::Arc;
use workspace::{item::ItemHandle, StatusItemView}; use workspace::{item::ItemHandle, StatusItemView, Workspace};
pub struct ActiveBufferLanguage { pub struct ActiveBufferLanguage {
active_language: Option<Option<Arc<str>>>, active_language: Option<Option<Arc<str>>>,
workspace: WeakViewHandle<Workspace>,
_observe_active_editor: Option<Subscription>, _observe_active_editor: Option<Subscription>,
} }
impl Default for ActiveBufferLanguage {
fn default() -> Self {
Self::new()
}
}
impl ActiveBufferLanguage { impl ActiveBufferLanguage {
pub fn new() -> Self { pub fn new(workspace: &Workspace) -> Self {
Self { Self {
active_language: None, active_language: None,
workspace: workspace.weak_handle(),
_observe_active_editor: None, _observe_active_editor: None,
} }
} }
@ -66,8 +62,12 @@ impl View for ActiveBufferLanguage {
.with_style(style.container) .with_style(style.container)
}) })
.with_cursor_style(CursorStyle::PointingHand) .with_cursor_style(CursorStyle::PointingHand)
.on_click(MouseButton::Left, |_, _, cx| { .on_click(MouseButton::Left, |_, this, cx| {
cx.dispatch_action(crate::Toggle) if let Some(workspace) = this.workspace.upgrade(cx) {
workspace.update(cx, |workspace, cx| {
crate::toggle(workspace, &Default::default(), cx)
});
}
}) })
.into_any() .into_any()
} else { } else {

View file

@ -11,21 +11,18 @@ use project::Project;
use settings::Settings; use settings::Settings;
use std::sync::Arc; use std::sync::Arc;
use util::ResultExt; use util::ResultExt;
use workspace::{AppState, Workspace}; use workspace::Workspace;
actions!(language_selector, [Toggle]); actions!(language_selector, [Toggle]);
pub fn init(app_state: Arc<AppState>, cx: &mut AppContext) { pub fn init(cx: &mut AppContext) {
Picker::<LanguageSelectorDelegate>::init(cx); Picker::<LanguageSelectorDelegate>::init(cx);
cx.add_action({ cx.add_action(toggle);
let language_registry = app_state.languages.clone();
move |workspace, _: &Toggle, cx| toggle(workspace, language_registry.clone(), cx)
});
} }
fn toggle( pub fn toggle(
workspace: &mut Workspace, workspace: &mut Workspace,
registry: Arc<LanguageRegistry>, _: &Toggle,
cx: &mut ViewContext<Workspace>, cx: &mut ViewContext<Workspace>,
) -> Option<()> { ) -> Option<()> {
let (_, buffer, _) = workspace let (_, buffer, _) = workspace
@ -34,6 +31,7 @@ fn toggle(
.read(cx) .read(cx)
.active_excerpt(cx)?; .active_excerpt(cx)?;
workspace.toggle_modal(cx, |workspace, cx| { workspace.toggle_modal(cx, |workspace, cx| {
let registry = workspace.app_state().languages.clone();
cx.add_view(|cx| { cx.add_view(|cx| {
Picker::new( Picker::new(
LanguageSelectorDelegate::new(buffer, workspace.project().clone(), registry), LanguageSelectorDelegate::new(buffer, workspace.project().clone(), registry),

View file

@ -24,7 +24,7 @@ pub fn init(cx: &mut AppContext) {
OutlineView::init(cx); OutlineView::init(cx);
} }
fn toggle(workspace: &mut Workspace, _: &Toggle, cx: &mut ViewContext<Workspace>) { pub fn toggle(workspace: &mut Workspace, _: &Toggle, cx: &mut ViewContext<Workspace>) {
if let Some(editor) = workspace if let Some(editor) = workspace
.active_item(cx) .active_item(cx)
.and_then(|item| item.downcast::<Editor>()) .and_then(|item| item.downcast::<Editor>())

View file

@ -13,7 +13,7 @@ use gpui::{
keymap_matcher::KeymapContext, keymap_matcher::KeymapContext,
platform::{CursorStyle, MouseButton, PromptLevel}, platform::{CursorStyle, MouseButton, PromptLevel},
AnyElement, AppContext, ClipboardItem, Element, Entity, ModelHandle, Task, View, ViewContext, AnyElement, AppContext, ClipboardItem, Element, Entity, ModelHandle, Task, View, ViewContext,
ViewHandle, ViewHandle, WeakViewHandle,
}; };
use menu::{Confirm, SelectNext, SelectPrev}; use menu::{Confirm, SelectNext, SelectPrev};
use project::{Entry, EntryKind, Project, ProjectEntryId, ProjectPath, Worktree, WorktreeId}; use project::{Entry, EntryKind, Project, ProjectEntryId, ProjectPath, Worktree, WorktreeId};
@ -44,6 +44,7 @@ pub struct ProjectPanel {
clipboard_entry: Option<ClipboardEntry>, clipboard_entry: Option<ClipboardEntry>,
context_menu: ViewHandle<ContextMenu>, context_menu: ViewHandle<ContextMenu>,
dragged_entry_destination: Option<Arc<Path>>, dragged_entry_destination: Option<Arc<Path>>,
workspace: WeakViewHandle<Workspace>,
} }
#[derive(Copy, Clone)] #[derive(Copy, Clone)]
@ -137,7 +138,8 @@ pub enum Event {
} }
impl ProjectPanel { impl ProjectPanel {
pub fn new(project: ModelHandle<Project>, cx: &mut ViewContext<Workspace>) -> ViewHandle<Self> { pub fn new(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) -> ViewHandle<Self> {
let project = workspace.project().clone();
let project_panel = cx.add_view(|cx: &mut ViewContext<Self>| { let project_panel = cx.add_view(|cx: &mut ViewContext<Self>| {
cx.observe(&project, |this, _, cx| { cx.observe(&project, |this, _, cx| {
this.update_visible_entries(None, cx); this.update_visible_entries(None, cx);
@ -206,6 +208,7 @@ impl ProjectPanel {
clipboard_entry: None, clipboard_entry: None,
context_menu: cx.add_view(ContextMenu::new), context_menu: cx.add_view(ContextMenu::new),
dragged_entry_destination: None, dragged_entry_destination: None,
workspace: workspace.weak_handle(),
}; };
this.update_visible_entries(None, cx); this.update_visible_entries(None, cx);
this this
@ -1296,8 +1299,14 @@ impl View for ProjectPanel {
) )
} }
}) })
.on_click(MouseButton::Left, move |_, _, cx| { .on_click(MouseButton::Left, move |_, this, cx| {
cx.dispatch_action(workspace::Open) if let Some(workspace) = this.workspace.upgrade(cx) {
workspace.update(cx, |workspace, cx| {
if let Some(task) = workspace.open(&Default::default(), cx) {
task.detach_and_log_err(cx);
}
})
}
}) })
.with_cursor_style(CursorStyle::PointingHand), .with_cursor_style(CursorStyle::PointingHand),
) )
@ -1400,7 +1409,7 @@ mod tests {
let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await; let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await;
let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
let panel = workspace.update(cx, |_, cx| ProjectPanel::new(project, cx)); let panel = workspace.update(cx, |workspace, cx| ProjectPanel::new(workspace, cx));
assert_eq!( assert_eq!(
visible_entries_as_strings(&panel, 0..50, cx), visible_entries_as_strings(&panel, 0..50, cx),
&[ &[
@ -1492,7 +1501,7 @@ mod tests {
let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await; let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await;
let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
let panel = workspace.update(cx, |_, cx| ProjectPanel::new(project, cx)); let panel = workspace.update(cx, |workspace, cx| ProjectPanel::new(workspace, cx));
select_path(&panel, "root1", cx); select_path(&panel, "root1", cx);
assert_eq!( assert_eq!(
@ -1785,7 +1794,7 @@ mod tests {
let project = Project::test(fs.clone(), ["/root1".as_ref()], cx).await; let project = Project::test(fs.clone(), ["/root1".as_ref()], cx).await;
let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
let panel = workspace.update(cx, |_, cx| ProjectPanel::new(project, cx)); let panel = workspace.update(cx, |workspace, cx| ProjectPanel::new(workspace, cx));
panel.update(cx, |panel, cx| { panel.update(cx, |panel, cx| {
panel.select_next(&Default::default(), cx); panel.select_next(&Default::default(), cx);

View file

@ -11,24 +11,24 @@ use highlighted_workspace_location::HighlightedWorkspaceLocation;
use ordered_float::OrderedFloat; use ordered_float::OrderedFloat;
use picker::{Picker, PickerDelegate, PickerEvent}; use picker::{Picker, PickerDelegate, PickerEvent};
use settings::Settings; use settings::Settings;
use std::sync::{Arc, Weak}; use std::sync::Arc;
use workspace::{ use workspace::{
notifications::simple_message_notification::MessageNotification, AppState, Workspace, notifications::simple_message_notification::MessageNotification, Workspace, WorkspaceLocation,
WorkspaceLocation, WORKSPACE_DB, WORKSPACE_DB,
}; };
actions!(projects, [OpenRecent]); actions!(projects, [OpenRecent]);
pub fn init(cx: &mut AppContext, app_state: Weak<AppState>) { pub fn init(cx: &mut AppContext) {
cx.add_async_action( cx.add_async_action(toggle);
move |_: &mut Workspace, _: &OpenRecent, cx: &mut ViewContext<Workspace>| {
toggle(app_state.clone(), cx)
},
);
RecentProjects::init(cx); RecentProjects::init(cx);
} }
fn toggle(app_state: Weak<AppState>, cx: &mut ViewContext<Workspace>) -> Option<Task<Result<()>>> { fn toggle(
_: &mut Workspace,
_: &OpenRecent,
cx: &mut ViewContext<Workspace>,
) -> Option<Task<Result<()>>> {
Some(cx.spawn(|workspace, mut cx| async move { Some(cx.spawn(|workspace, mut cx| async move {
let workspace_locations: Vec<_> = cx let workspace_locations: Vec<_> = cx
.background() .background()
@ -49,11 +49,7 @@ fn toggle(app_state: Weak<AppState>, cx: &mut ViewContext<Workspace>) -> Option<
let workspace = cx.weak_handle(); let workspace = cx.weak_handle();
cx.add_view(|cx| { cx.add_view(|cx| {
RecentProjects::new( RecentProjects::new(
RecentProjectsDelegate::new( RecentProjectsDelegate::new(workspace, workspace_locations),
workspace,
workspace_locations,
app_state.clone(),
),
cx, cx,
) )
.with_max_size(800., 1200.) .with_max_size(800., 1200.)
@ -61,7 +57,7 @@ fn toggle(app_state: Weak<AppState>, cx: &mut ViewContext<Workspace>) -> Option<
}); });
} else { } else {
workspace.show_notification(0, cx, |cx| { workspace.show_notification(0, cx, |cx| {
cx.add_view(|_| MessageNotification::new_message("No recent projects to open.")) cx.add_view(|_| MessageNotification::new("No recent projects to open."))
}) })
} }
})?; })?;
@ -74,7 +70,6 @@ type RecentProjects = Picker<RecentProjectsDelegate>;
struct RecentProjectsDelegate { struct RecentProjectsDelegate {
workspace: WeakViewHandle<Workspace>, workspace: WeakViewHandle<Workspace>,
workspace_locations: Vec<WorkspaceLocation>, workspace_locations: Vec<WorkspaceLocation>,
app_state: Weak<AppState>,
selected_match_index: usize, selected_match_index: usize,
matches: Vec<StringMatch>, matches: Vec<StringMatch>,
} }
@ -83,12 +78,10 @@ impl RecentProjectsDelegate {
fn new( fn new(
workspace: WeakViewHandle<Workspace>, workspace: WeakViewHandle<Workspace>,
workspace_locations: Vec<WorkspaceLocation>, workspace_locations: Vec<WorkspaceLocation>,
app_state: Weak<AppState>,
) -> Self { ) -> Self {
Self { Self {
workspace, workspace,
workspace_locations, workspace_locations,
app_state,
selected_match_index: 0, selected_match_index: 0,
matches: Default::default(), matches: Default::default(),
} }
@ -155,20 +148,16 @@ impl PickerDelegate for RecentProjectsDelegate {
} }
fn confirm(&mut self, cx: &mut ViewContext<RecentProjects>) { fn confirm(&mut self, cx: &mut ViewContext<RecentProjects>) {
if let Some(((selected_match, workspace), app_state)) = self if let Some((selected_match, workspace)) = self
.matches .matches
.get(self.selected_index()) .get(self.selected_index())
.zip(self.workspace.upgrade(cx)) .zip(self.workspace.upgrade(cx))
.zip(self.app_state.upgrade())
{ {
let workspace_location = &self.workspace_locations[selected_match.candidate_id]; let workspace_location = &self.workspace_locations[selected_match.candidate_id];
workspace workspace
.update(cx, |workspace, cx| { .update(cx, |workspace, cx| {
workspace.open_workspace_for_paths( workspace
workspace_location.paths().as_ref().clone(), .open_workspace_for_paths(workspace_location.paths().as_ref().clone(), cx)
app_state,
cx,
)
}) })
.detach_and_log_err(cx); .detach_and_log_err(cx);
cx.emit(PickerEvent::Dismiss); cx.emit(PickerEvent::Dismiss);

View file

@ -338,8 +338,8 @@ impl BufferSearchBar {
.contained() .contained()
.with_style(style.container) .with_style(style.container)
}) })
.on_click(MouseButton::Left, move |_, _, cx| { .on_click(MouseButton::Left, move |_, this, cx| {
cx.dispatch_any_action(option.to_toggle_action()) this.toggle_search_option(option, cx);
}) })
.with_cursor_style(CursorStyle::PointingHand) .with_cursor_style(CursorStyle::PointingHand)
.with_tooltip::<Self>( .with_tooltip::<Self>(
@ -386,8 +386,10 @@ impl BufferSearchBar {
.with_style(style.container) .with_style(style.container)
}) })
.on_click(MouseButton::Left, { .on_click(MouseButton::Left, {
let action = action.boxed_clone(); move |_, this, cx| match direction {
move |_, _, cx| cx.dispatch_any_action(action.boxed_clone()) Direction::Prev => this.select_prev_match(&Default::default(), cx),
Direction::Next => this.select_next_match(&Default::default(), cx),
}
}) })
.with_cursor_style(CursorStyle::PointingHand) .with_cursor_style(CursorStyle::PointingHand)
.with_tooltip::<NavButton>( .with_tooltip::<NavButton>(
@ -405,7 +407,6 @@ impl BufferSearchBar {
theme: &theme::Search, theme: &theme::Search,
cx: &mut ViewContext<Self>, cx: &mut ViewContext<Self>,
) -> AnyElement<Self> { ) -> AnyElement<Self> {
let action = Box::new(Dismiss);
let tooltip = "Dismiss Buffer Search"; let tooltip = "Dismiss Buffer Search";
let tooltip_style = cx.global::<Settings>().theme.tooltip.clone(); let tooltip_style = cx.global::<Settings>().theme.tooltip.clone();
@ -422,12 +423,17 @@ impl BufferSearchBar {
.contained() .contained()
.with_style(style.container) .with_style(style.container)
}) })
.on_click(MouseButton::Left, { .on_click(MouseButton::Left, move |_, this, cx| {
let action = action.boxed_clone(); this.dismiss(&Default::default(), cx)
move |_, _, cx| cx.dispatch_any_action(action.boxed_clone())
}) })
.with_cursor_style(CursorStyle::PointingHand) .with_cursor_style(CursorStyle::PointingHand)
.with_tooltip::<CloseButton>(0, tooltip.to_string(), Some(action), tooltip_style, cx) .with_tooltip::<CloseButton>(
0,
tooltip.to_string(),
Some(Box::new(Dismiss)),
tooltip_style,
cx,
)
.into_any() .into_any()
} }

View file

@ -788,9 +788,10 @@ impl ProjectSearchBar {
.contained() .contained()
.with_style(style.container) .with_style(style.container)
}) })
.on_click(MouseButton::Left, { .on_click(MouseButton::Left, move |_, this, cx| {
let action = action.boxed_clone(); if let Some(search) = this.active_project_search.as_ref() {
move |_, _, cx| cx.dispatch_any_action(action.boxed_clone()) search.update(cx, |search, cx| search.select_match(direction, cx));
}
}) })
.with_cursor_style(CursorStyle::PointingHand) .with_cursor_style(CursorStyle::PointingHand)
.with_tooltip::<NavButton>( .with_tooltip::<NavButton>(
@ -822,8 +823,8 @@ impl ProjectSearchBar {
.contained() .contained()
.with_style(style.container) .with_style(style.container)
}) })
.on_click(MouseButton::Left, move |_, _, cx| { .on_click(MouseButton::Left, move |_, this, cx| {
cx.dispatch_any_action(option.to_toggle_action()) this.toggle_search_option(option, cx);
}) })
.with_cursor_style(CursorStyle::PointingHand) .with_cursor_style(CursorStyle::PointingHand)
.with_tooltip::<Self>( .with_tooltip::<Self>(

View file

@ -7,7 +7,11 @@ use gpui::{
}; };
use settings::Settings; use settings::Settings;
use std::any::TypeId; use std::any::TypeId;
use workspace::{dock::FocusDock, item::ItemHandle, NewTerminal, StatusItemView, Workspace}; use workspace::{
dock::{Dock, FocusDock},
item::ItemHandle,
NewTerminal, StatusItemView, Workspace,
};
pub struct TerminalButton { pub struct TerminalButton {
workspace: WeakViewHandle<Workspace>, workspace: WeakViewHandle<Workspace>,
@ -80,7 +84,11 @@ impl View for TerminalButton {
this.deploy_terminal_menu(cx); this.deploy_terminal_menu(cx);
} else { } else {
if !active { if !active {
cx.dispatch_action(FocusDock); if let Some(workspace) = this.workspace.upgrade(cx) {
workspace.update(cx, |workspace, cx| {
Dock::focus_dock(workspace, &Default::default(), cx)
})
}
} }
}; };
}) })

View file

@ -156,24 +156,7 @@ pub fn keystroke_label<V: View>(
pub type ButtonStyle = Interactive<ContainedText>; pub type ButtonStyle = Interactive<ContainedText>;
pub fn cta_button<L, A, V>( pub fn cta_button<Tag, L, V, F>(
label: L,
action: A,
max_width: f32,
style: &ButtonStyle,
cx: &mut ViewContext<V>,
) -> MouseEventHandler<A, V>
where
L: Into<Cow<'static, str>>,
A: 'static + Action + Clone,
V: View,
{
cta_button_with_click::<A, _, _, _>(label, max_width, style, cx, move |_, _, cx| {
cx.dispatch_action(action.clone())
})
}
pub fn cta_button_with_click<Tag, L, V, F>(
label: L, label: L,
max_width: f32, max_width: f32,
style: &ButtonStyle, style: &ButtonStyle,

View file

@ -6,20 +6,18 @@ use staff_mode::StaffMode;
use std::sync::Arc; use std::sync::Arc;
use theme::{Theme, ThemeMeta, ThemeRegistry}; use theme::{Theme, ThemeMeta, ThemeRegistry};
use util::ResultExt; use util::ResultExt;
use workspace::{AppState, Workspace}; use workspace::Workspace;
actions!(theme_selector, [Toggle, Reload]); actions!(theme_selector, [Toggle, Reload]);
pub fn init(app_state: Arc<AppState>, cx: &mut AppContext) { pub fn init(cx: &mut AppContext) {
cx.add_action({ cx.add_action(toggle);
let theme_registry = app_state.themes.clone();
move |workspace, _: &Toggle, cx| toggle(workspace, theme_registry.clone(), cx)
});
ThemeSelector::init(cx); ThemeSelector::init(cx);
} }
fn toggle(workspace: &mut Workspace, themes: Arc<ThemeRegistry>, cx: &mut ViewContext<Workspace>) { pub fn toggle(workspace: &mut Workspace, _: &Toggle, cx: &mut ViewContext<Workspace>) {
workspace.toggle_modal(cx, |_, cx| { workspace.toggle_modal(cx, |workspace, cx| {
let themes = workspace.app_state().themes.clone();
cx.add_view(|cx| ThemeSelector::new(ThemeSelectorDelegate::new(themes, cx), cx)) cx.add_view(|cx| ThemeSelector::new(ThemeSelectorDelegate::new(themes, cx), cx))
}); });
} }

View file

@ -18,7 +18,7 @@ pub fn init(cx: &mut AppContext) {
BaseKeymapSelector::init(cx); BaseKeymapSelector::init(cx);
} }
fn toggle( pub fn toggle(
workspace: &mut Workspace, workspace: &mut Workspace,
_: &ToggleBaseKeymapSelector, _: &ToggleBaseKeymapSelector,
cx: &mut ViewContext<Workspace>, cx: &mut ViewContext<Workspace>,

View file

@ -5,7 +5,7 @@ use std::{borrow::Cow, sync::Arc};
use db::kvp::KEY_VALUE_STORE; use db::kvp::KEY_VALUE_STORE;
use gpui::{ use gpui::{
elements::{Flex, Label, ParentElement}, elements::{Flex, Label, ParentElement},
AnyElement, AppContext, Element, Entity, Subscription, View, ViewContext, AnyElement, AppContext, Element, Entity, Subscription, View, ViewContext, WeakViewHandle,
}; };
use settings::{settings_file::SettingsFile, Settings}; use settings::{settings_file::SettingsFile, Settings};
@ -20,7 +20,7 @@ pub const FIRST_OPEN: &str = "first_open";
pub fn init(cx: &mut AppContext) { pub fn init(cx: &mut AppContext) {
cx.add_action(|workspace: &mut Workspace, _: &Welcome, cx| { cx.add_action(|workspace: &mut Workspace, _: &Welcome, cx| {
let welcome_page = cx.add_view(WelcomePage::new); let welcome_page = cx.add_view(|cx| WelcomePage::new(workspace, cx));
workspace.add_item(Box::new(welcome_page), cx) workspace.add_item(Box::new(welcome_page), cx)
}); });
@ -30,7 +30,7 @@ pub fn init(cx: &mut AppContext) {
pub fn show_welcome_experience(app_state: &Arc<AppState>, cx: &mut AppContext) { pub fn show_welcome_experience(app_state: &Arc<AppState>, cx: &mut AppContext) {
open_new(&app_state, cx, |workspace, cx| { open_new(&app_state, cx, |workspace, cx| {
workspace.toggle_sidebar(SidebarSide::Left, cx); workspace.toggle_sidebar(SidebarSide::Left, cx);
let welcome_page = cx.add_view(|cx| WelcomePage::new(cx)); let welcome_page = cx.add_view(|cx| WelcomePage::new(workspace, cx));
workspace.add_item_to_center(Box::new(welcome_page.clone()), cx); workspace.add_item_to_center(Box::new(welcome_page.clone()), cx);
cx.focus(&welcome_page); cx.focus(&welcome_page);
cx.notify(); cx.notify();
@ -43,6 +43,7 @@ pub fn show_welcome_experience(app_state: &Arc<AppState>, cx: &mut AppContext) {
} }
pub struct WelcomePage { pub struct WelcomePage {
workspace: WeakViewHandle<Workspace>,
_settings_subscription: Subscription, _settings_subscription: Subscription,
} }
@ -97,26 +98,46 @@ impl View for WelcomePage {
) )
.with_child( .with_child(
Flex::column() Flex::column()
.with_child(theme::ui::cta_button( .with_child(theme::ui::cta_button::<theme_selector::Toggle, _, _, _>(
"Choose a theme", "Choose a theme",
theme_selector::Toggle,
width, width,
&theme.welcome.button, &theme.welcome.button,
cx, cx,
|_, this, cx| {
if let Some(workspace) = this.workspace.upgrade(cx) {
workspace.update(cx, |workspace, cx| {
theme_selector::toggle(workspace, &Default::default(), cx)
})
}
},
)) ))
.with_child(theme::ui::cta_button( .with_child(theme::ui::cta_button::<ToggleBaseKeymapSelector, _, _, _>(
"Choose a keymap", "Choose a keymap",
ToggleBaseKeymapSelector,
width, width,
&theme.welcome.button, &theme.welcome.button,
cx, cx,
|_, this, cx| {
if let Some(workspace) = this.workspace.upgrade(cx) {
workspace.update(cx, |workspace, cx| {
base_keymap_picker::toggle(
workspace,
&Default::default(),
cx,
)
})
}
},
)) ))
.with_child(theme::ui::cta_button( .with_child(theme::ui::cta_button::<install_cli::Install, _, _, _>(
"Install the CLI", "Install the CLI",
install_cli::Install,
width, width,
&theme.welcome.button, &theme.welcome.button,
cx, cx,
|_, _, cx| {
cx.app_context()
.spawn(|cx| async move { install_cli::install_cli(&cx).await })
.detach_and_log_err(cx);
},
)) ))
.contained() .contained()
.with_style(theme.welcome.button_group) .with_style(theme.welcome.button_group)
@ -190,8 +211,9 @@ impl View for WelcomePage {
} }
impl WelcomePage { impl WelcomePage {
pub fn new(cx: &mut ViewContext<Self>) -> Self { pub fn new(workspace: &Workspace, cx: &mut ViewContext<Self>) -> Self {
WelcomePage { WelcomePage {
workspace: workspace.weak_handle(),
_settings_subscription: cx.observe_global::<Settings, _>(move |_, cx| cx.notify()), _settings_subscription: cx.observe_global::<Settings, _>(move |_, cx| cx.notify()),
} }
} }
@ -220,11 +242,15 @@ impl Item for WelcomePage {
fn show_toolbar(&self) -> bool { fn show_toolbar(&self) -> bool {
false false
} }
fn clone_on_split( fn clone_on_split(
&self, &self,
_workspace_id: WorkspaceId, _workspace_id: WorkspaceId,
cx: &mut ViewContext<Self>, cx: &mut ViewContext<Self>,
) -> Option<Self> { ) -> Option<Self> {
Some(WelcomePage::new(cx)) Some(WelcomePage {
workspace: self.workspace.clone(),
_settings_subscription: cx.observe_global::<Settings, _>(move |_, cx| cx.notify()),
})
} }
} }

View file

@ -271,11 +271,11 @@ impl Dock {
} }
} }
fn focus_dock(workspace: &mut Workspace, _: &FocusDock, cx: &mut ViewContext<Workspace>) { pub fn focus_dock(workspace: &mut Workspace, _: &FocusDock, cx: &mut ViewContext<Workspace>) {
Self::set_dock_position(workspace, workspace.dock.position.show(), true, cx); Self::set_dock_position(workspace, workspace.dock.position.show(), true, cx);
} }
fn hide_dock(workspace: &mut Workspace, _: &HideDock, cx: &mut ViewContext<Workspace>) { pub fn hide_dock(workspace: &mut Workspace, _: &HideDock, cx: &mut ViewContext<Workspace>) {
Self::set_dock_position(workspace, workspace.dock.position.hide(), true, cx); Self::set_dock_position(workspace, workspace.dock.position.hide(), true, cx);
} }
@ -374,8 +374,8 @@ impl Dock {
.with_background_color(style.wash_color) .with_background_color(style.wash_color)
}) })
.capture_all() .capture_all()
.on_down(MouseButton::Left, |_, _, cx| { .on_down(MouseButton::Left, |_, workspace, cx| {
cx.dispatch_action(HideDock); Dock::hide_dock(workspace, &Default::default(), cx)
}) })
.with_cursor_style(CursorStyle::Arrow), .with_cursor_style(CursorStyle::Arrow),
) )

View file

@ -1,3 +1,5 @@
use super::{icon_for_dock_anchor, Dock, FocusDock, HideDock};
use crate::{handle_dropped_item, StatusItemView, Workspace};
use gpui::{ use gpui::{
elements::{Empty, MouseEventHandler, Svg}, elements::{Empty, MouseEventHandler, Svg},
platform::CursorStyle, platform::CursorStyle,
@ -6,10 +8,6 @@ use gpui::{
}; };
use settings::Settings; use settings::Settings;
use crate::{handle_dropped_item, StatusItemView, Workspace};
use super::{icon_for_dock_anchor, FocusDock, HideDock};
pub struct ToggleDockButton { pub struct ToggleDockButton {
workspace: WeakViewHandle<Workspace>, workspace: WeakViewHandle<Workspace>,
} }
@ -82,8 +80,12 @@ impl View for ToggleDockButton {
if dock_position.is_visible() { if dock_position.is_visible() {
button button
.on_click(MouseButton::Left, |_, _, cx| { .on_click(MouseButton::Left, |_, this, cx| {
cx.dispatch_action(HideDock); if let Some(workspace) = this.workspace.upgrade(cx) {
workspace.update(cx, |workspace, cx| {
Dock::hide_dock(workspace, &Default::default(), cx)
})
}
}) })
.with_tooltip::<Self>( .with_tooltip::<Self>(
0, 0,
@ -94,8 +96,12 @@ impl View for ToggleDockButton {
) )
} else { } else {
button button
.on_click(MouseButton::Left, |_, _, cx| { .on_click(MouseButton::Left, |_, this, cx| {
cx.dispatch_action(FocusDock); if let Some(workspace) = this.workspace.upgrade(cx) {
workspace.update(cx, |workspace, cx| {
Dock::focus_dock(workspace, &Default::default(), cx)
})
}
}) })
.with_tooltip::<Self>( .with_tooltip::<Self>(
0, 0,

View file

@ -114,17 +114,14 @@ impl Workspace {
pub fn show_toast(&mut self, toast: Toast, cx: &mut ViewContext<Self>) { pub fn show_toast(&mut self, toast: Toast, cx: &mut ViewContext<Self>) {
self.dismiss_notification::<simple_message_notification::MessageNotification>(toast.id, cx); self.dismiss_notification::<simple_message_notification::MessageNotification>(toast.id, cx);
self.show_notification(toast.id, cx, |cx| { self.show_notification(toast.id, cx, |cx| {
cx.add_view(|_cx| match &toast.click { cx.add_view(|_cx| match toast.on_click.as_ref() {
Some((click_msg, action)) => { Some((click_msg, on_click)) => {
simple_message_notification::MessageNotification::new_boxed_action( let on_click = on_click.clone();
toast.msg.clone(), simple_message_notification::MessageNotification::new(toast.msg.clone())
action.boxed_clone(), .with_click_message(click_msg.clone())
click_msg.clone(), .on_click(move |cx| on_click(cx))
)
}
None => {
simple_message_notification::MessageNotification::new_message(toast.msg.clone())
} }
None => simple_message_notification::MessageNotification::new(toast.msg.clone()),
}) })
}) })
} }
@ -152,19 +149,17 @@ impl Workspace {
} }
pub mod simple_message_notification { pub mod simple_message_notification {
use std::borrow::Cow;
use gpui::{ use gpui::{
actions, actions,
elements::{Flex, MouseEventHandler, Padding, ParentElement, Svg, Text}, elements::{Flex, MouseEventHandler, Padding, ParentElement, Svg, Text},
impl_actions, impl_actions,
platform::{CursorStyle, MouseButton}, platform::{CursorStyle, MouseButton},
Action, AppContext, Element, Entity, View, ViewContext, AppContext, Element, Entity, View, ViewContext,
}; };
use menu::Cancel; use menu::Cancel;
use serde::Deserialize; use serde::Deserialize;
use settings::Settings; use settings::Settings;
use std::{borrow::Cow, sync::Arc};
use crate::Workspace; use crate::Workspace;
@ -194,7 +189,7 @@ pub mod simple_message_notification {
pub struct MessageNotification { pub struct MessageNotification {
message: Cow<'static, str>, message: Cow<'static, str>,
click_action: Option<Box<dyn Action>>, on_click: Option<Arc<dyn Fn(&mut ViewContext<Self>)>>,
click_message: Option<Cow<'static, str>>, click_message: Option<Cow<'static, str>>,
} }
@ -207,36 +202,31 @@ pub mod simple_message_notification {
} }
impl MessageNotification { impl MessageNotification {
pub fn new_message<S: Into<Cow<'static, str>>>(message: S) -> MessageNotification { pub fn new<S>(message: S) -> MessageNotification
where
S: Into<Cow<'static, str>>,
{
Self { Self {
message: message.into(), message: message.into(),
click_action: None, on_click: None,
click_message: None, click_message: None,
} }
} }
pub fn new_boxed_action<S1: Into<Cow<'static, str>>, S2: Into<Cow<'static, str>>>( pub fn with_click_message<S>(mut self, message: S) -> Self
message: S1, where
click_action: Box<dyn Action>, S: Into<Cow<'static, str>>,
click_message: S2, {
) -> Self { self.click_message = Some(message.into());
Self { self
message: message.into(),
click_action: Some(click_action),
click_message: Some(click_message.into()),
}
} }
pub fn new<S1: Into<Cow<'static, str>>, A: Action, S2: Into<Cow<'static, str>>>( pub fn on_click<F>(mut self, on_click: F) -> Self
message: S1, where
click_action: A, F: 'static + Fn(&mut ViewContext<Self>),
click_message: S2, {
) -> Self { self.on_click = Some(Arc::new(on_click));
Self { self
message: message.into(),
click_action: Some(Box::new(click_action) as Box<dyn Action>),
click_message: Some(click_message.into()),
}
} }
pub fn dismiss(&mut self, _: &CancelMessageNotification, cx: &mut ViewContext<Self>) { pub fn dismiss(&mut self, _: &CancelMessageNotification, cx: &mut ViewContext<Self>) {
@ -255,14 +245,10 @@ pub mod simple_message_notification {
enum MessageNotificationTag {} enum MessageNotificationTag {}
let click_action = self let click_message = self.click_message.clone();
.click_action
.as_ref()
.map(|action| action.boxed_clone());
let click_message = self.click_message.as_ref().map(|message| message.clone());
let message = self.message.clone(); let message = self.message.clone();
let on_click = self.on_click.clone();
let has_click_action = click_action.is_some(); let has_click_action = on_click.is_some();
MouseEventHandler::<MessageNotificationTag, _>::new(0, cx, |state, cx| { MouseEventHandler::<MessageNotificationTag, _>::new(0, cx, |state, cx| {
Flex::column() Flex::column()
@ -292,8 +278,8 @@ pub mod simple_message_notification {
.with_height(style.button_width) .with_height(style.button_width)
}) })
.with_padding(Padding::uniform(5.)) .with_padding(Padding::uniform(5.))
.on_click(MouseButton::Left, move |_, _, cx| { .on_click(MouseButton::Left, move |_, this, cx| {
cx.dispatch_action(CancelMessageNotification) this.dismiss(&Default::default(), cx);
}) })
.with_cursor_style(CursorStyle::PointingHand) .with_cursor_style(CursorStyle::PointingHand)
.aligned() .aligned()
@ -326,10 +312,10 @@ pub mod simple_message_notification {
// Since we're not using a proper overlay, we have to capture these extra events // Since we're not using a proper overlay, we have to capture these extra events
.on_down(MouseButton::Left, |_, _, _| {}) .on_down(MouseButton::Left, |_, _, _| {})
.on_up(MouseButton::Left, |_, _, _| {}) .on_up(MouseButton::Left, |_, _, _| {})
.on_click(MouseButton::Left, move |_, _, cx| { .on_click(MouseButton::Left, move |_, this, cx| {
if let Some(click_action) = click_action.as_ref() { if let Some(on_click) = on_click.as_ref() {
cx.dispatch_any_action(click_action.boxed_clone()); on_click(cx);
cx.dispatch_action(CancelMessageNotification) this.dismiss(&Default::default(), cx);
} }
}) })
.with_cursor_style(if has_click_action { .with_cursor_style(if has_click_action {
@ -372,7 +358,7 @@ where
Err(err) => { Err(err) => {
workspace.show_notification(0, cx, |cx| { workspace.show_notification(0, cx, |cx| {
cx.add_view(|_cx| { cx.add_view(|_cx| {
simple_message_notification::MessageNotification::new_message(format!( simple_message_notification::MessageNotification::new(format!(
"Error: {:?}", "Error: {:?}",
err, err,
)) ))

View file

@ -2,7 +2,7 @@ mod dragged_item_receiver;
use super::{ItemHandle, SplitDirection}; use super::{ItemHandle, SplitDirection};
use crate::{ use crate::{
dock::{icon_for_dock_anchor, AnchorDockBottom, AnchorDockRight, ExpandDock, HideDock}, dock::{icon_for_dock_anchor, AnchorDockBottom, AnchorDockRight, Dock, ExpandDock},
item::WeakItemHandle, item::WeakItemHandle,
toolbar::Toolbar, toolbar::Toolbar,
Item, NewFile, NewSearch, NewTerminal, Workspace, Item, NewFile, NewSearch, NewTerminal, Workspace,
@ -259,6 +259,10 @@ impl Pane {
} }
} }
pub(crate) fn workspace(&self) -> &WeakViewHandle<Workspace> {
&self.workspace
}
pub fn is_active(&self) -> bool { pub fn is_active(&self) -> bool {
self.is_active self.is_active
} }
@ -1340,8 +1344,8 @@ impl Pane {
cx, cx,
) )
}) })
.on_down(MouseButton::Left, move |_, _, cx| { .on_down(MouseButton::Left, move |_, this, cx| {
cx.dispatch_action(ActivateItem(ix)); this.activate_item(ix, true, true, cx);
}) })
.on_click(MouseButton::Middle, { .on_click(MouseButton::Middle, {
let item_id = item.id(); let item_id = item.id();
@ -1639,7 +1643,15 @@ impl Pane {
3, 3,
"icons/x_mark_8.svg", "icons/x_mark_8.svg",
cx, cx,
|_, cx| cx.dispatch_action(HideDock), |this, cx| {
if let Some(workspace) = this.workspace.upgrade(cx) {
cx.window_context().defer(move |cx| {
workspace.update(cx, |workspace, cx| {
Dock::hide_dock(workspace, &Default::default(), cx)
})
});
}
},
None, None,
) )
})) }))
@ -1693,8 +1705,8 @@ impl View for Pane {
}) })
.on_down( .on_down(
MouseButton::Left, MouseButton::Left,
move |_, _, cx| { move |_, this, cx| {
cx.dispatch_action(ActivateItem(active_item_index)); this.activate_item(active_item_index, true, true, cx);
}, },
), ),
); );
@ -1759,15 +1771,27 @@ impl View for Pane {
}) })
.on_down( .on_down(
MouseButton::Navigate(NavigationDirection::Back), MouseButton::Navigate(NavigationDirection::Back),
move |_, _, cx| { move |_, pane, cx| {
if let Some(workspace) = pane.workspace.upgrade(cx) {
let pane = cx.weak_handle(); let pane = cx.weak_handle();
cx.dispatch_action(GoBack { pane: Some(pane) }); cx.window_context().defer(move |cx| {
workspace.update(cx, |workspace, cx| {
Pane::go_back(workspace, Some(pane), cx).detach_and_log_err(cx)
})
})
}
}, },
) )
.on_down(MouseButton::Navigate(NavigationDirection::Forward), { .on_down(MouseButton::Navigate(NavigationDirection::Forward), {
move |_, _, cx| { move |_, pane, cx| {
if let Some(workspace) = pane.workspace.upgrade(cx) {
let pane = cx.weak_handle(); let pane = cx.weak_handle();
cx.dispatch_action(GoForward { pane: Some(pane) }) cx.window_context().defer(move |cx| {
workspace.update(cx, |workspace, cx| {
Pane::go_forward(workspace, Some(pane), cx).detach_and_log_err(cx)
})
})
}
} }
}) })
.into_any_named("pane") .into_any_named("pane")

View file

@ -279,9 +279,9 @@ impl View for SidebarButtons {
.with_style(style.container) .with_style(style.container)
}) })
.with_cursor_style(CursorStyle::PointingHand) .with_cursor_style(CursorStyle::PointingHand)
.on_click(MouseButton::Left, { .on_click(MouseButton::Left, move |_, this, cx| {
let action = action.clone(); this.sidebar
move |_, _, cx| cx.dispatch_action(action.clone()) .update(cx, |sidebar, cx| sidebar.toggle_item(ix, cx));
}) })
.with_tooltip::<Self>( .with_tooltip::<Self>(
ix, ix,

View file

@ -130,8 +130,23 @@ impl View for Toolbar {
tooltip_style.clone(), tooltip_style.clone(),
enable_go_backward, enable_go_backward,
spacing, spacing,
super::GoBack { {
pane: Some(pane.clone()), let pane = pane.clone();
move |toolbar, cx| {
if let Some(workspace) = toolbar
.pane
.upgrade(cx)
.and_then(|pane| pane.read(cx).workspace().upgrade(cx))
{
let pane = pane.clone();
cx.window_context().defer(move |cx| {
workspace.update(cx, |workspace, cx| {
Pane::go_back(workspace, Some(pane.clone()), cx)
.detach_and_log_err(cx);
});
})
}
}
}, },
super::GoBack { pane: None }, super::GoBack { pane: None },
"Go Back", "Go Back",
@ -143,7 +158,24 @@ impl View for Toolbar {
tooltip_style, tooltip_style,
enable_go_forward, enable_go_forward,
spacing, spacing,
super::GoForward { pane: Some(pane) }, {
let pane = pane.clone();
move |toolbar, cx| {
if let Some(workspace) = toolbar
.pane
.upgrade(cx)
.and_then(|pane| pane.read(cx).workspace().upgrade(cx))
{
let pane = pane.clone();
cx.window_context().defer(move |cx| {
workspace.update(cx, |workspace, cx| {
Pane::go_forward(workspace, Some(pane.clone()), cx)
.detach_and_log_err(cx);
});
});
}
}
},
super::GoForward { pane: None }, super::GoForward { pane: None },
"Go Forward", "Go Forward",
cx, cx,
@ -161,13 +193,13 @@ impl View for Toolbar {
} }
#[allow(clippy::too_many_arguments)] #[allow(clippy::too_many_arguments)]
fn nav_button<A: Action + Clone>( fn nav_button<A: Action, F: 'static + Fn(&mut Toolbar, &mut ViewContext<Toolbar>)>(
svg_path: &'static str, svg_path: &'static str,
style: theme::Interactive<theme::IconButton>, style: theme::Interactive<theme::IconButton>,
tooltip_style: TooltipStyle, tooltip_style: TooltipStyle,
enabled: bool, enabled: bool,
spacing: f32, spacing: f32,
action: A, on_click: F,
tooltip_action: A, tooltip_action: A,
action_name: &str, action_name: &str,
cx: &mut ViewContext<Toolbar>, cx: &mut ViewContext<Toolbar>,
@ -195,8 +227,8 @@ fn nav_button<A: Action + Clone>(
} else { } else {
CursorStyle::default() CursorStyle::default()
}) })
.on_click(MouseButton::Left, move |_, _, cx| { .on_click(MouseButton::Left, move |_, toolbar, cx| {
cx.dispatch_action(action.clone()) on_click(toolbar, cx)
}) })
.with_tooltip::<A>( .with_tooltip::<A>(
0, 0,

View file

@ -43,8 +43,9 @@ use gpui::{
CursorStyle, MouseButton, PathPromptOptions, Platform, PromptLevel, WindowBounds, CursorStyle, MouseButton, PathPromptOptions, Platform, PromptLevel, WindowBounds,
WindowOptions, WindowOptions,
}, },
Action, AnyModelHandle, AnyViewHandle, AppContext, AsyncAppContext, Entity, ModelContext, AnyModelHandle, AnyViewHandle, AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle,
ModelHandle, SizeConstraint, Subscription, Task, View, ViewContext, ViewHandle, WeakViewHandle, SizeConstraint, Subscription, Task, View, ViewContext, ViewHandle, WeakViewHandle,
WindowContext,
}; };
use item::{FollowableItem, FollowableItemHandle, Item, ItemHandle, ProjectItem}; use item::{FollowableItem, FollowableItemHandle, Item, ItemHandle, ProjectItem};
use language::LanguageRegistry; use language::LanguageRegistry;
@ -59,7 +60,7 @@ use std::{
}; };
use crate::{ use crate::{
notifications::simple_message_notification::{MessageNotification, OsOpen}, notifications::simple_message_notification::MessageNotification,
persistence::model::{SerializedPane, SerializedPaneGroup, SerializedWorkspace}, persistence::model::{SerializedPane, SerializedPaneGroup, SerializedWorkspace},
}; };
use lazy_static::lazy_static; use lazy_static::lazy_static;
@ -137,7 +138,7 @@ pub struct ActivatePane(pub usize);
pub struct Toast { pub struct Toast {
id: usize, id: usize,
msg: Cow<'static, str>, msg: Cow<'static, str>,
click: Option<(Cow<'static, str>, Box<dyn Action>)>, on_click: Option<(Cow<'static, str>, Arc<dyn Fn(&mut WindowContext)>)>,
} }
impl Toast { impl Toast {
@ -145,21 +146,17 @@ impl Toast {
Toast { Toast {
id, id,
msg: msg.into(), msg: msg.into(),
click: None, on_click: None,
} }
} }
pub fn new_action<I1: Into<Cow<'static, str>>, I2: Into<Cow<'static, str>>>( pub fn on_click<F, M>(mut self, message: M, on_click: F) -> Self
id: usize, where
msg: I1, M: Into<Cow<'static, str>>,
click_msg: I2, F: Fn(&mut WindowContext) + 'static,
action: impl Action, {
) -> Self { self.on_click = Some((message.into(), Arc::new(on_click)));
Toast { self
id,
msg: msg.into(),
click: Some((click_msg.into(), Box::new(action))),
}
} }
} }
@ -167,7 +164,7 @@ impl PartialEq for Toast {
fn eq(&self, other: &Self) -> bool { fn eq(&self, other: &Self) -> bool {
self.id == other.id self.id == other.id
&& self.msg == other.msg && self.msg == other.msg
&& self.click.is_some() == other.click.is_some() && self.on_click.is_some() == other.on_click.is_some()
} }
} }
@ -176,10 +173,7 @@ impl Clone for Toast {
Toast { Toast {
id: self.id, id: self.id,
msg: self.msg.to_owned(), msg: self.msg.to_owned(),
click: self on_click: self.on_click.clone(),
.click
.as_ref()
.map(|(msg, click)| (msg.to_owned(), click.boxed_clone())),
} }
} }
} }
@ -214,52 +208,12 @@ pub fn init(app_state: Arc<AppState>, cx: &mut AppContext) {
} }
} }
}); });
cx.add_action({ cx.add_async_action(Workspace::open);
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| {
if let Some(app_state) = app_state.upgrade() {
open_new(&app_state, cx, |_, cx| cx.dispatch_action(NewFile)).detach();
}
}
});
cx.add_global_action({
let app_state = Arc::downgrade(&app_state);
move |_: &NewFile, cx: &mut AppContext| {
if let Some(app_state) = app_state.upgrade() {
open_new(&app_state, cx, |_, cx| cx.dispatch_action(NewFile)).detach();
}
}
});
cx.add_async_action(Workspace::follow_next_collaborator); cx.add_async_action(Workspace::follow_next_collaborator);
cx.add_async_action(Workspace::close); cx.add_async_action(Workspace::close);
cx.add_global_action(Workspace::close_global); cx.add_global_action(Workspace::close_global);
cx.add_global_action(restart);
cx.add_async_action(Workspace::save_all); cx.add_async_action(Workspace::save_all);
cx.add_action(Workspace::add_folder_to_project); cx.add_action(Workspace::add_folder_to_project);
cx.add_action( cx.add_action(
@ -303,9 +257,7 @@ pub fn init(app_state: Arc<AppState>, cx: &mut AppContext) {
} else { } else {
workspace.show_notification(1, cx, |cx| { workspace.show_notification(1, cx, |cx| {
cx.add_view(|_| { cx.add_view(|_| {
MessageNotification::new_message( MessageNotification::new("Successfully installed the `zed` binary")
"Successfully installed the `zed` binary",
)
}) })
}); });
} }
@ -920,7 +872,6 @@ impl Workspace {
/// to the callback. Otherwise, a new empty window will be created. /// to the callback. Otherwise, a new empty window will be created.
pub fn with_local_workspace<T, F>( pub fn with_local_workspace<T, F>(
&mut self, &mut self,
app_state: &Arc<AppState>,
cx: &mut ViewContext<Self>, cx: &mut ViewContext<Self>,
callback: F, callback: F,
) -> Task<Result<T>> ) -> Task<Result<T>>
@ -931,7 +882,7 @@ impl Workspace {
if self.project.read(cx).is_local() { if self.project.read(cx).is_local() {
Task::Ready(Some(Ok(callback(self, cx)))) Task::Ready(Some(Ok(callback(self, cx))))
} else { } else {
let task = Self::new_local(Vec::new(), app_state.clone(), None, cx); let task = Self::new_local(Vec::new(), self.app_state.clone(), None, cx);
cx.spawn(|_vh, mut cx| async move { cx.spawn(|_vh, mut cx| async move {
let (workspace, _) = task.await; let (workspace, _) = task.await;
workspace.update(&mut cx, callback) workspace.update(&mut cx, callback)
@ -1100,10 +1051,29 @@ impl Workspace {
}) })
} }
pub fn open(&mut self, _: &Open, cx: &mut ViewContext<Self>) -> Option<Task<Result<()>>> {
let mut paths = cx.prompt_for_paths(PathPromptOptions {
files: true,
directories: true,
multiple: true,
});
Some(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, cx))
.log_err()
{
task.await?
}
}
Ok(())
}))
}
pub fn open_workspace_for_paths( pub fn open_workspace_for_paths(
&mut self, &mut self,
paths: Vec<PathBuf>, paths: Vec<PathBuf>,
app_state: Arc<AppState>,
cx: &mut ViewContext<Self>, cx: &mut ViewContext<Self>,
) -> Task<Result<()>> { ) -> Task<Result<()>> {
let window_id = cx.window_id(); let window_id = cx.window_id();
@ -1115,6 +1085,7 @@ impl Workspace {
} else { } else {
Some(self.prepare_to_close(false, cx)) Some(self.prepare_to_close(false, cx))
}; };
let app_state = self.app_state.clone();
cx.spawn(|_, mut cx| async move { cx.spawn(|_, mut cx| async move {
let window_id_to_replace = if let Some(close_task) = close_task { let window_id_to_replace = if let Some(close_task) = close_task {
@ -2668,36 +2639,37 @@ impl Workspace {
} }
fn notify_if_database_failed(workspace: &WeakViewHandle<Workspace>, cx: &mut AsyncAppContext) { fn notify_if_database_failed(workspace: &WeakViewHandle<Workspace>, cx: &mut AsyncAppContext) {
workspace.update(cx, |workspace, cx| { const REPORT_ISSUE_URL: &str ="https://github.com/zed-industries/community/issues/new?assignees=&labels=defect%2Ctriage&template=2_bug_report.yml";
workspace
.update(cx, |workspace, cx| {
if (*db::ALL_FILE_DB_FAILED).load(std::sync::atomic::Ordering::Acquire) { if (*db::ALL_FILE_DB_FAILED).load(std::sync::atomic::Ordering::Acquire) {
workspace.show_notification_once(0, cx, |cx| { workspace.show_notification_once(0, cx, |cx| {
cx.add_view(|_| { cx.add_view(|_| {
MessageNotification::new( MessageNotification::new("Failed to load any database file.")
"Failed to load any database file.", .with_click_message("Click to let us know about this error")
OsOpen::new("https://github.com/zed-industries/community/issues/new?assignees=&labels=defect%2Ctriage&template=2_bug_report.yml".to_string()), .on_click(|cx| cx.platform().open_url(REPORT_ISSUE_URL))
"Click to let us know about this error"
)
}) })
}); });
} else { } else {
let backup_path = (*db::BACKUP_DB_PATH).read(); let backup_path = (*db::BACKUP_DB_PATH).read();
if let Some(backup_path) = &*backup_path { if let Some(backup_path) = backup_path.clone() {
workspace.show_notification_once(0, cx, |cx| { workspace.show_notification_once(0, cx, move |cx| {
cx.add_view(|_| { cx.add_view(move |_| {
let backup_path = backup_path.to_string_lossy(); MessageNotification::new(format!(
MessageNotification::new(
format!(
"Database file was corrupted. Old database backed up to {}", "Database file was corrupted. Old database backed up to {}",
backup_path backup_path.display()
), ))
OsOpen::new(backup_path.to_string()), .with_click_message("Click to show old database in finder")
"Click to show old database in finder", .on_click(move |cx| {
) cx.platform().open_url(&backup_path.to_string_lossy())
})
}) })
}); });
} }
} }
}).log_err(); })
.log_err();
} }
impl Entity for Workspace { impl Entity for Workspace {
@ -3062,6 +3034,58 @@ pub fn join_remote_project(
}) })
} }
pub fn restart(_: &Restart, cx: &mut AppContext) {
let mut workspaces = cx
.window_ids()
.filter_map(|window_id| {
Some(
cx.root_view(window_id)?
.clone()
.downcast::<Workspace>()?
.downgrade(),
)
})
.collect::<Vec<_>>();
// If multiple windows have unsaved changes, and need a save prompt,
// prompt in the active window before switching to a different window.
workspaces.sort_by_key(|workspace| !cx.window_is_active(workspace.window_id()));
let should_confirm = cx.global::<Settings>().confirm_quit;
cx.spawn(|mut cx| async move {
if let (true, Some(workspace)) = (should_confirm, workspaces.first()) {
let answer = cx.prompt(
workspace.window_id(),
PromptLevel::Info,
"Are you sure you want to restart?",
&["Restart", "Cancel"],
);
if let Some(mut answer) = answer {
let answer = answer.next().await;
if answer != Some(0) {
return Ok(());
}
}
}
// If the user cancels any save prompt, then keep the app open.
for workspace in workspaces {
if !workspace
.update(&mut cx, |workspace, cx| {
workspace.prepare_to_close(true, cx)
})?
.await?
{
return Ok(());
}
}
cx.platform().restart();
anyhow::Ok(())
})
.detach_and_log_err(cx);
}
fn parse_pixel_position_env_var(value: &str) -> Option<Vector2F> { fn parse_pixel_position_env_var(value: &str) -> Option<Vector2F> {
let mut parts = value.split(','); let mut parts = value.split(',');
let width: usize = parts.next()?.parse().ok()?; let width: usize = parts.next()?.parse().ok()?;

View file

@ -10,6 +10,7 @@ use cli::{
}; };
use client::{self, UserStore, ZED_APP_VERSION, ZED_SECRET_CLIENT_TOKEN}; use client::{self, UserStore, ZED_APP_VERSION, ZED_SECRET_CLIENT_TOKEN};
use db::kvp::KEY_VALUE_STORE; use db::kvp::KEY_VALUE_STORE;
use editor::Editor;
use futures::{ use futures::{
channel::{mpsc, oneshot}, channel::{mpsc, oneshot},
FutureExt, SinkExt, StreamExt, FutureExt, SinkExt, StreamExt,
@ -29,8 +30,16 @@ use settings::{
use simplelog::ConfigBuilder; use simplelog::ConfigBuilder;
use smol::process::Command; use smol::process::Command;
use std::{ use std::{
env, ffi::OsStr, fs::OpenOptions, io::Write as _, os::unix::prelude::OsStrExt, panic, env,
path::PathBuf, sync::Arc, thread, time::Duration, ffi::OsStr,
fs::OpenOptions,
io::Write as _,
os::unix::prelude::OsStrExt,
panic,
path::PathBuf,
sync::{Arc, Weak},
thread,
time::Duration,
}; };
use terminal_view::{get_working_directory, TerminalView}; use terminal_view::{get_working_directory, TerminalView};
use util::http::{self, HttpClient}; use util::http::{self, HttpClient};
@ -43,8 +52,7 @@ use staff_mode::StaffMode;
use theme::ThemeRegistry; use theme::ThemeRegistry;
use util::{channel::RELEASE_CHANNEL, paths, ResultExt, TryFutureExt}; use util::{channel::RELEASE_CHANNEL, paths, ResultExt, TryFutureExt};
use workspace::{ use workspace::{
self, dock::FocusDock, item::ItemHandle, notifications::NotifyResultExt, AppState, NewFile, self, dock::FocusDock, item::ItemHandle, notifications::NotifyResultExt, AppState, Workspace,
Workspace,
}; };
use zed::{self, build_window_options, initialize_workspace, languages, menus, OpenSettings}; use zed::{self, build_window_options, initialize_workspace, languages, menus, OpenSettings};
@ -104,7 +112,16 @@ fn main() {
.log_err(); .log_err();
} }
}) })
.on_reopen(move |cx| cx.dispatch_global_action(NewFile)); .on_reopen(move |cx| {
if cx.has_global::<Weak<AppState>>() {
if let Some(app_state) = cx.global::<Weak<AppState>>().upgrade() {
workspace::open_new(&app_state, cx, |workspace, cx| {
Editor::new_file(workspace, &Default::default(), cx)
})
.detach();
}
}
});
app.run(move |cx| { app.run(move |cx| {
cx.set_global(*RELEASE_CHANNEL); cx.set_global(*RELEASE_CHANNEL);
@ -190,17 +207,18 @@ fn main() {
dock_default_item_factory, dock_default_item_factory,
background_actions, background_actions,
}); });
cx.set_global(Arc::downgrade(&app_state));
auto_update::init(http, client::ZED_SERVER_URL.clone(), cx); auto_update::init(http, client::ZED_SERVER_URL.clone(), cx);
workspace::init(app_state.clone(), cx); workspace::init(app_state.clone(), cx);
recent_projects::init(cx, Arc::downgrade(&app_state)); recent_projects::init(cx);
journal::init(app_state.clone(), cx); journal::init(app_state.clone(), cx);
language_selector::init(app_state.clone(), cx); language_selector::init(cx);
theme_selector::init(app_state.clone(), cx); theme_selector::init(cx);
zed::init(&app_state, cx); zed::init(&app_state, cx);
collab_ui::init(&app_state, cx); collab_ui::init(&app_state, cx);
feedback::init(app_state.clone(), cx); feedback::init(cx);
welcome::init(cx); welcome::init(cx);
cx.set_menus(menus::menus()); cx.set_menus(menus::menus());
@ -274,7 +292,10 @@ async fn restore_or_create_workspace(app_state: &Arc<AppState>, mut cx: AsyncApp
cx.update(|cx| show_welcome_experience(app_state, cx)); cx.update(|cx| show_welcome_experience(app_state, cx));
} else { } else {
cx.update(|cx| { cx.update(|cx| {
cx.dispatch_global_action(NewFile); workspace::open_new(app_state, cx, |workspace, cx| {
Editor::new_file(workspace, &Default::default(), cx)
})
.detach();
}); });
} }
} }

View file

@ -20,7 +20,7 @@ use gpui::{
geometry::vector::vec2f, geometry::vector::vec2f,
impl_actions, impl_actions,
platform::{Platform, PromptLevel, TitlebarOptions, WindowBounds, WindowKind, WindowOptions}, platform::{Platform, PromptLevel, TitlebarOptions, WindowBounds, WindowKind, WindowOptions},
AssetSource, ViewContext, AppContext, AssetSource, ViewContext,
}; };
use language::Rope; use language::Rope;
pub use lsp; pub use lsp;
@ -35,7 +35,7 @@ use terminal_view::terminal_button::TerminalButton;
use util::{channel::ReleaseChannel, paths, ResultExt}; use util::{channel::ReleaseChannel, paths, ResultExt};
use uuid::Uuid; use uuid::Uuid;
pub use workspace; pub use workspace;
use workspace::{sidebar::SidebarSide, AppState, Restart, Workspace}; use workspace::{open_new, sidebar::SidebarSide, AppState, NewFile, NewWindow, Workspace};
#[derive(Deserialize, Clone, PartialEq)] #[derive(Deserialize, Clone, PartialEq)]
pub struct OpenBrowser { pub struct OpenBrowser {
@ -113,7 +113,6 @@ pub fn init(app_state: &Arc<AppState>, cx: &mut gpui::AppContext) {
}, },
); );
cx.add_global_action(quit); cx.add_global_action(quit);
cx.add_global_action(restart);
cx.add_global_action(move |action: &OpenBrowser, cx| cx.platform().open_url(&action.url)); cx.add_global_action(move |action: &OpenBrowser, cx| cx.platform().open_url(&action.url));
cx.add_global_action(move |_: &IncreaseBufferFontSize, cx| { cx.add_global_action(move |_: &IncreaseBufferFontSize, cx| {
cx.update_global::<Settings, _, _>(|settings, cx| { cx.update_global::<Settings, _, _>(|settings, cx| {
@ -148,10 +147,9 @@ pub fn init(app_state: &Arc<AppState>, cx: &mut gpui::AppContext) {
}) })
.detach_and_log_err(cx); .detach_and_log_err(cx);
}); });
cx.add_action({ cx.add_action(
let app_state = app_state.clone(); move |workspace: &mut Workspace, _: &OpenSettings, cx: &mut ViewContext<Workspace>| {
move |_: &mut Workspace, _: &OpenSettings, cx: &mut ViewContext<Workspace>| { open_config_file(workspace, &paths::SETTINGS, cx, || {
open_config_file(&paths::SETTINGS, app_state.clone(), cx, || {
str::from_utf8( str::from_utf8(
Assets Assets
.load("settings/initial_user_settings.json") .load("settings/initial_user_settings.json")
@ -161,73 +159,68 @@ pub fn init(app_state: &Arc<AppState>, cx: &mut gpui::AppContext) {
.unwrap() .unwrap()
.into() .into()
}); });
} },
}); );
cx.add_action({ cx.add_action(
let app_state = app_state.clone();
move |workspace: &mut Workspace, _: &OpenLog, cx: &mut ViewContext<Workspace>| { move |workspace: &mut Workspace, _: &OpenLog, cx: &mut ViewContext<Workspace>| {
open_log_file(workspace, app_state.clone(), cx); open_log_file(workspace, cx);
} },
}); );
cx.add_action({ cx.add_action(
let app_state = app_state.clone(); move |workspace: &mut Workspace, _: &OpenLicenses, cx: &mut ViewContext<Workspace>| {
move |_: &mut Workspace, _: &OpenLicenses, cx: &mut ViewContext<Workspace>| {
open_bundled_file( open_bundled_file(
app_state.clone(), workspace,
"licenses.md", "licenses.md",
"Open Source License Attribution", "Open Source License Attribution",
"Markdown", "Markdown",
cx, cx,
); );
} },
}); );
cx.add_action({ cx.add_action(
let app_state = app_state.clone();
move |workspace: &mut Workspace, _: &OpenTelemetryLog, cx: &mut ViewContext<Workspace>| { move |workspace: &mut Workspace, _: &OpenTelemetryLog, cx: &mut ViewContext<Workspace>| {
open_telemetry_log_file(workspace, app_state.clone(), cx); open_telemetry_log_file(workspace, cx);
} },
}); );
cx.add_action({ cx.add_action(
let app_state = app_state.clone(); move |workspace: &mut Workspace, _: &OpenKeymap, cx: &mut ViewContext<Workspace>| {
move |_: &mut Workspace, _: &OpenKeymap, cx: &mut ViewContext<Workspace>| { open_config_file(workspace, &paths::KEYMAP, cx, Default::default);
open_config_file(&paths::KEYMAP, app_state.clone(), cx, Default::default); },
} );
}); cx.add_action(
cx.add_action({ move |workspace: &mut Workspace, _: &OpenDefaultKeymap, cx: &mut ViewContext<Workspace>| {
let app_state = app_state.clone();
move |_: &mut Workspace, _: &OpenDefaultKeymap, cx: &mut ViewContext<Workspace>| {
open_bundled_file( open_bundled_file(
app_state.clone(), workspace,
"keymaps/default.json", "keymaps/default.json",
"Default Key Bindings", "Default Key Bindings",
"JSON", "JSON",
cx, cx,
); );
} },
}); );
cx.add_action({ cx.add_action(
let app_state = app_state.clone(); move |workspace: &mut Workspace,
move |_: &mut Workspace, _: &OpenDefaultSettings, cx: &mut ViewContext<Workspace>| { _: &OpenDefaultSettings,
cx: &mut ViewContext<Workspace>| {
open_bundled_file( open_bundled_file(
app_state.clone(), workspace,
"settings/default.json", "settings/default.json",
"Default Settings", "Default Settings",
"JSON", "JSON",
cx, cx,
); );
} },
}); );
cx.add_action({ cx.add_action({
let app_state = app_state.clone(); move |workspace: &mut Workspace, _: &DebugElements, cx: &mut ViewContext<Workspace>| {
move |_: &mut Workspace, _: &DebugElements, cx: &mut ViewContext<Workspace>| { let app_state = workspace.app_state().clone();
let app_state = app_state.clone();
let markdown = app_state.languages.language_for_name("JSON"); let markdown = app_state.languages.language_for_name("JSON");
let content = to_string_pretty(&cx.debug_elements()).unwrap(); let content = to_string_pretty(&cx.debug_elements()).unwrap();
cx.spawn(|workspace, mut cx| async move { cx.spawn(|workspace, mut cx| async move {
let markdown = markdown.await.log_err(); let markdown = markdown.await.log_err();
workspace workspace
.update(&mut cx, |workspace, cx| { .update(&mut cx, |workspace, cx| {
workspace.with_local_workspace(&app_state, cx, move |workspace, cx| { workspace.with_local_workspace(cx, move |workspace, cx| {
let project = workspace.project().clone(); let project = workspace.project().clone();
let buffer = project let buffer = project
@ -259,6 +252,28 @@ pub fn init(app_state: &Arc<AppState>, cx: &mut gpui::AppContext) {
workspace.toggle_sidebar_item_focus(SidebarSide::Left, 0, cx); workspace.toggle_sidebar_item_focus(SidebarSide::Left, 0, cx);
}, },
); );
cx.add_global_action({
let app_state = Arc::downgrade(&app_state);
move |_: &NewWindow, cx: &mut AppContext| {
if let Some(app_state) = app_state.upgrade() {
open_new(&app_state, cx, |workspace, cx| {
Editor::new_file(workspace, &Default::default(), cx)
})
.detach();
}
}
});
cx.add_global_action({
let app_state = Arc::downgrade(&app_state);
move |_: &NewFile, cx: &mut AppContext| {
if let Some(app_state) = app_state.upgrade() {
open_new(&app_state, cx, |workspace, cx| {
Editor::new_file(workspace, &Default::default(), cx)
})
.detach();
}
}
});
activity_indicator::init(cx); activity_indicator::init(cx);
lsp_log::init(cx); lsp_log::init(cx);
call::init(app_state.client.clone(), app_state.user_store.clone(), cx); call::init(app_state.client.clone(), app_state.user_store.clone(), cx);
@ -276,7 +291,7 @@ pub fn initialize_workspace(
if let workspace::Event::PaneAdded(pane) = event { if let workspace::Event::PaneAdded(pane) = event {
pane.update(cx, |pane, cx| { pane.update(cx, |pane, cx| {
pane.toolbar().update(cx, |toolbar, cx| { pane.toolbar().update(cx, |toolbar, cx| {
let breadcrumbs = cx.add_view(|_| Breadcrumbs::new()); let breadcrumbs = cx.add_view(|_| Breadcrumbs::new(workspace));
toolbar.add_item(breadcrumbs, cx); toolbar.add_item(breadcrumbs, cx);
let buffer_search_bar = cx.add_view(BufferSearchBar::new); let buffer_search_bar = cx.add_view(BufferSearchBar::new);
toolbar.add_item(buffer_search_bar, cx); toolbar.add_item(buffer_search_bar, cx);
@ -305,7 +320,7 @@ pub fn initialize_workspace(
}); });
workspace.set_titlebar_item(collab_titlebar_item.into_any(), cx); workspace.set_titlebar_item(collab_titlebar_item.into_any(), cx);
let project_panel = ProjectPanel::new(workspace.project().clone(), cx); let project_panel = ProjectPanel::new(workspace, cx);
workspace.left_sidebar().update(cx, |sidebar, cx| { workspace.left_sidebar().update(cx, |sidebar, cx| {
sidebar.add_item( sidebar.add_item(
"icons/folder_tree_16.svg", "icons/folder_tree_16.svg",
@ -318,12 +333,13 @@ pub fn initialize_workspace(
let toggle_terminal = cx.add_view(|cx| TerminalButton::new(workspace_handle.clone(), cx)); let toggle_terminal = cx.add_view(|cx| TerminalButton::new(workspace_handle.clone(), cx));
let copilot = cx.add_view(|cx| copilot_button::CopilotButton::new(cx)); let copilot = cx.add_view(|cx| copilot_button::CopilotButton::new(cx));
let diagnostic_summary = let diagnostic_summary =
cx.add_view(|cx| diagnostics::items::DiagnosticIndicator::new(workspace.project(), cx)); cx.add_view(|cx| diagnostics::items::DiagnosticIndicator::new(workspace, cx));
let activity_indicator = let activity_indicator =
activity_indicator::ActivityIndicator::new(workspace, app_state.languages.clone(), cx); activity_indicator::ActivityIndicator::new(workspace, app_state.languages.clone(), cx);
let active_buffer_language = cx.add_view(|_| language_selector::ActiveBufferLanguage::new()); let active_buffer_language =
cx.add_view(|_| language_selector::ActiveBufferLanguage::new(workspace));
let feedback_button = let feedback_button =
cx.add_view(|_| feedback::deploy_feedback_button::DeployFeedbackButton::new()); cx.add_view(|_| feedback::deploy_feedback_button::DeployFeedbackButton::new(workspace));
let cursor_position = cx.add_view(|_| editor::items::CursorPosition::new()); let cursor_position = cx.add_view(|_| editor::items::CursorPosition::new());
workspace.status_bar().update(cx, |status_bar, cx| { workspace.status_bar().update(cx, |status_bar, cx| {
status_bar.add_left_item(diagnostic_summary, cx); status_bar.add_left_item(diagnostic_summary, cx);
@ -370,58 +386,6 @@ pub fn build_window_options(
} }
} }
fn restart(_: &Restart, cx: &mut gpui::AppContext) {
let mut workspaces = cx
.window_ids()
.filter_map(|window_id| {
Some(
cx.root_view(window_id)?
.clone()
.downcast::<Workspace>()?
.downgrade(),
)
})
.collect::<Vec<_>>();
// If multiple windows have unsaved changes, and need a save prompt,
// prompt in the active window before switching to a different window.
workspaces.sort_by_key(|workspace| !cx.window_is_active(workspace.window_id()));
let should_confirm = cx.global::<Settings>().confirm_quit;
cx.spawn(|mut cx| async move {
if let (true, Some(workspace)) = (should_confirm, workspaces.first()) {
let answer = cx.prompt(
workspace.window_id(),
PromptLevel::Info,
"Are you sure you want to restart?",
&["Restart", "Cancel"],
);
if let Some(mut answer) = answer {
let answer = answer.next().await;
if answer != Some(0) {
return Ok(());
}
}
}
// If the user cancels any save prompt, then keep the app open.
for workspace in workspaces {
if !workspace
.update(&mut cx, |workspace, cx| {
workspace.prepare_to_close(true, cx)
})?
.await?
{
return Ok(());
}
}
cx.platform().restart();
anyhow::Ok(())
})
.detach_and_log_err(cx);
}
fn quit(_: &Quit, cx: &mut gpui::AppContext) { fn quit(_: &Quit, cx: &mut gpui::AppContext) {
let mut workspaces = cx let mut workspaces = cx
.window_ids() .window_ids()
@ -481,13 +445,13 @@ fn about(_: &mut Workspace, _: &About, cx: &mut gpui::ViewContext<Workspace>) {
} }
fn open_config_file( fn open_config_file(
workspace: &mut Workspace,
path: &'static Path, path: &'static Path,
app_state: Arc<AppState>,
cx: &mut ViewContext<Workspace>, cx: &mut ViewContext<Workspace>,
default_content: impl 'static + Send + FnOnce() -> Rope, default_content: impl 'static + Send + FnOnce() -> Rope,
) { ) {
let fs = workspace.app_state().fs.clone();
cx.spawn(|workspace, mut cx| async move { cx.spawn(|workspace, mut cx| async move {
let fs = &app_state.fs;
if !fs.is_file(path).await { if !fs.is_file(path).await {
fs.create_file(path, Default::default()).await?; fs.create_file(path, Default::default()).await?;
fs.save(path, &default_content(), Default::default()) fs.save(path, &default_content(), Default::default())
@ -496,7 +460,7 @@ fn open_config_file(
workspace workspace
.update(&mut cx, |workspace, cx| { .update(&mut cx, |workspace, cx| {
workspace.with_local_workspace(&app_state, cx, |workspace, cx| { workspace.with_local_workspace(cx, |workspace, cx| {
workspace.open_paths(vec![path.to_path_buf()], false, cx) workspace.open_paths(vec![path.to_path_buf()], false, cx)
}) })
})? })?
@ -507,20 +471,15 @@ fn open_config_file(
.detach_and_log_err(cx) .detach_and_log_err(cx)
} }
fn open_log_file( fn open_log_file(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) {
workspace: &mut Workspace,
app_state: Arc<AppState>,
cx: &mut ViewContext<Workspace>,
) {
const MAX_LINES: usize = 1000; const MAX_LINES: usize = 1000;
workspace workspace
.with_local_workspace(&app_state.clone(), cx, move |_, cx| { .with_local_workspace(cx, move |workspace, cx| {
let fs = workspace.app_state().fs.clone();
cx.spawn(|workspace, mut cx| async move { cx.spawn(|workspace, mut cx| async move {
let (old_log, new_log) = futures::join!( let (old_log, new_log) =
app_state.fs.load(&paths::OLD_LOG), futures::join!(fs.load(&paths::OLD_LOG), fs.load(&paths::LOG));
app_state.fs.load(&paths::LOG)
);
let mut lines = VecDeque::with_capacity(MAX_LINES); let mut lines = VecDeque::with_capacity(MAX_LINES);
for line in old_log for line in old_log
@ -565,12 +524,9 @@ fn open_log_file(
.detach(); .detach();
} }
fn open_telemetry_log_file( fn open_telemetry_log_file(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) {
workspace: &mut Workspace, workspace.with_local_workspace(cx, move |workspace, cx| {
app_state: Arc<AppState>, let app_state = workspace.app_state().clone();
cx: &mut ViewContext<Workspace>,
) {
workspace.with_local_workspace(&app_state.clone(), cx, move |_, cx| {
cx.spawn(|workspace, mut cx| async move { cx.spawn(|workspace, mut cx| async move {
async fn fetch_log_string(app_state: &Arc<AppState>) -> Option<String> { async fn fetch_log_string(app_state: &Arc<AppState>) -> Option<String> {
let path = app_state.client.telemetry().log_file_path()?; let path = app_state.client.telemetry().log_file_path()?;
@ -626,18 +582,18 @@ fn open_telemetry_log_file(
} }
fn open_bundled_file( fn open_bundled_file(
app_state: Arc<AppState>, workspace: &mut Workspace,
asset_path: &'static str, asset_path: &'static str,
title: &'static str, title: &'static str,
language: &'static str, language: &'static str,
cx: &mut ViewContext<Workspace>, cx: &mut ViewContext<Workspace>,
) { ) {
let language = app_state.languages.language_for_name(language); let language = workspace.app_state().languages.language_for_name(language);
cx.spawn(|workspace, mut cx| async move { cx.spawn(|workspace, mut cx| async move {
let language = language.await.log_err(); let language = language.await.log_err();
workspace workspace
.update(&mut cx, |workspace, cx| { .update(&mut cx, |workspace, cx| {
workspace.with_local_workspace(&app_state, cx, |workspace, cx| { workspace.with_local_workspace(cx, |workspace, cx| {
let project = workspace.project(); let project = workspace.project();
let buffer = project.update(cx, |project, cx| { let buffer = project.update(cx, |project, cx| {
let text = Assets::get(asset_path) let text = Assets::get(asset_path)
@ -868,7 +824,11 @@ mod tests {
#[gpui::test] #[gpui::test]
async fn test_new_empty_workspace(cx: &mut TestAppContext) { async fn test_new_empty_workspace(cx: &mut TestAppContext) {
let app_state = init(cx); let app_state = init(cx);
cx.update(|cx| open_new(&app_state, cx, |_, cx| cx.dispatch_action(NewFile))) cx.update(|cx| {
open_new(&app_state, cx, |workspace, cx| {
Editor::new_file(workspace, &Default::default(), cx)
})
})
.await; .await;
let window_id = *cx.window_ids().first().unwrap(); let window_id = *cx.window_ids().first().unwrap();