diff --git a/Cargo.lock b/Cargo.lock index 7b7acd54f2a6bed1f22530b4a80ce7e2c0510634..88340c716bc0fc3ec54bf4f54d2f7ebb6235b6d8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1949,6 +1949,7 @@ name = "collab_ui2" version = "0.1.0" dependencies = [ "anyhow", + "auto_update2", "call2", "channel2", "client2", @@ -1957,6 +1958,7 @@ dependencies = [ "db2", "editor2", "feature_flags2", + "feedback2", "futures 0.3.28", "fuzzy2", "gpui2", @@ -1978,6 +1980,7 @@ dependencies = [ "settings2", "smallvec", "theme2", + "theme_selector2", "time", "tree-sitter-markdown", "ui2", diff --git a/crates/auto_update2/src/auto_update.rs b/crates/auto_update2/src/auto_update.rs index 4bf2bca0633ef1d9199750c38ae2c2d4ed0b2651..79269acc27e78164f11407ea80dfae8df8631554 100644 --- a/crates/auto_update2/src/auto_update.rs +++ b/crates/auto_update2/src/auto_update.rs @@ -6,7 +6,7 @@ use db::kvp::KEY_VALUE_STORE; use db::RELEASE_CHANNEL; use gpui::{ actions, AppContext, AsyncAppContext, Context as _, Model, ModelContext, SemanticVersion, Task, - ViewContext, VisualContext, + ViewContext, VisualContext, WindowContext, }; use isahc::AsyncBody; @@ -118,7 +118,7 @@ pub fn init(http_client: Arc, server_url: String, cx: &mut AppCo } } -pub fn check(_: &Check, cx: &mut ViewContext) { +pub fn check(_: &Check, cx: &mut WindowContext) { if let Some(updater) = AutoUpdater::get(cx) { updater.update(cx, |updater, cx| updater.poll(cx)); } else { diff --git a/crates/client2/src/client2.rs b/crates/client2/src/client2.rs index bf8d5dda771c3d9ddea8a3b8b3978318f051fdc3..2c9787298944cba3e49134ccb215768707f4c803 100644 --- a/crates/client2/src/client2.rs +++ b/crates/client2/src/client2.rs @@ -11,8 +11,8 @@ use async_tungstenite::tungstenite::{ http::{Request, StatusCode}, }; use futures::{ - future::LocalBoxFuture, AsyncReadExt, FutureExt, SinkExt, StreamExt, TryFutureExt as _, - TryStreamExt, + channel::oneshot, future::LocalBoxFuture, AsyncReadExt, FutureExt, SinkExt, StreamExt, + TryFutureExt as _, TryStreamExt, }; use gpui::{ actions, serde_json, AnyModel, AnyWeakModel, AppContext, AsyncAppContext, Model, @@ -1020,91 +1020,116 @@ impl Client { ) -> Task> { let http = self.http.clone(); cx.spawn(|cx| async move { - // Generate a pair of asymmetric encryption keys. The public key will be used by the - // zed server to encrypt the user's access token, so that it can'be intercepted by - // any other app running on the user's device. - let (public_key, private_key) = - rpc::auth::keypair().expect("failed to generate keypair for auth"); - let public_key_string = - String::try_from(public_key).expect("failed to serialize public key for auth"); - - if let Some((login, token)) = IMPERSONATE_LOGIN.as_ref().zip(ADMIN_API_TOKEN.as_ref()) { - return Self::authenticate_as_admin(http, login.clone(), token.clone()).await; - } - - // Start an HTTP server to receive the redirect from Zed's sign-in page. - let server = tiny_http::Server::http("127.0.0.1:0").expect("failed to find open port"); - let port = server.server_addr().port(); + let background = cx.background_executor().clone(); - // Open the Zed sign-in page in the user's browser, with query parameters that indicate - // that the user is signing in from a Zed app running on the same device. - let mut url = format!( - "{}/native_app_signin?native_app_port={}&native_app_public_key={}", - *ZED_SERVER_URL, port, public_key_string - ); + let (open_url_tx, open_url_rx) = oneshot::channel::(); + cx.update(|cx| { + cx.spawn(move |cx| async move { + let url = open_url_rx.await?; + cx.update(|cx| cx.open_url(&url)) + }) + .detach_and_log_err(cx); + }) + .log_err(); + + let credentials = background + .clone() + .spawn(async move { + // Generate a pair of asymmetric encryption keys. The public key will be used by the + // zed server to encrypt the user's access token, so that it can'be intercepted by + // any other app running on the user's device. + let (public_key, private_key) = + rpc::auth::keypair().expect("failed to generate keypair for auth"); + let public_key_string = String::try_from(public_key) + .expect("failed to serialize public key for auth"); + + if let Some((login, token)) = + IMPERSONATE_LOGIN.as_ref().zip(ADMIN_API_TOKEN.as_ref()) + { + return Self::authenticate_as_admin(http, login.clone(), token.clone()) + .await; + } - if let Some(impersonate_login) = IMPERSONATE_LOGIN.as_ref() { - log::info!("impersonating user @{}", impersonate_login); - write!(&mut url, "&impersonate={}", impersonate_login).unwrap(); - } + // Start an HTTP server to receive the redirect from Zed's sign-in page. + let server = + tiny_http::Server::http("127.0.0.1:0").expect("failed to find open port"); + let port = server.server_addr().port(); + + // Open the Zed sign-in page in the user's browser, with query parameters that indicate + // that the user is signing in from a Zed app running on the same device. + let mut url = format!( + "{}/native_app_signin?native_app_port={}&native_app_public_key={}", + *ZED_SERVER_URL, port, public_key_string + ); + + if let Some(impersonate_login) = IMPERSONATE_LOGIN.as_ref() { + log::info!("impersonating user @{}", impersonate_login); + write!(&mut url, "&impersonate={}", impersonate_login).unwrap(); + } - cx.update(|cx| cx.open_url(&url))?; - - // Receive the HTTP request from the user's browser. Retrieve the user id and encrypted - // access token from the query params. - // - // TODO - Avoid ever starting more than one HTTP server. Maybe switch to using a - // custom URL scheme instead of this local HTTP server. - let (user_id, access_token) = cx - .spawn(|_| async move { - for _ in 0..100 { - if let Some(req) = server.recv_timeout(Duration::from_secs(1))? { - let path = req.url(); - let mut user_id = None; - let mut access_token = None; - let url = Url::parse(&format!("http://example.com{}", path)) - .context("failed to parse login notification url")?; - for (key, value) in url.query_pairs() { - if key == "access_token" { - access_token = Some(value.to_string()); - } else if key == "user_id" { - user_id = Some(value.to_string()); + open_url_tx.send(url).log_err(); + + // Receive the HTTP request from the user's browser. Retrieve the user id and encrypted + // access token from the query params. + // + // TODO - Avoid ever starting more than one HTTP server. Maybe switch to using a + // custom URL scheme instead of this local HTTP server. + let (user_id, access_token) = background + .spawn(async move { + for _ in 0..100 { + if let Some(req) = server.recv_timeout(Duration::from_secs(1))? { + let path = req.url(); + let mut user_id = None; + let mut access_token = None; + let url = Url::parse(&format!("http://example.com{}", path)) + .context("failed to parse login notification url")?; + for (key, value) in url.query_pairs() { + if key == "access_token" { + access_token = Some(value.to_string()); + } else if key == "user_id" { + user_id = Some(value.to_string()); + } + } + + let post_auth_url = + format!("{}/native_app_signin_succeeded", *ZED_SERVER_URL); + req.respond( + tiny_http::Response::empty(302).with_header( + tiny_http::Header::from_bytes( + &b"Location"[..], + post_auth_url.as_bytes(), + ) + .unwrap(), + ), + ) + .context("failed to respond to login http request")?; + return Ok(( + user_id + .ok_or_else(|| anyhow!("missing user_id parameter"))?, + access_token.ok_or_else(|| { + anyhow!("missing access_token parameter") + })?, + )); } } - let post_auth_url = - format!("{}/native_app_signin_succeeded", *ZED_SERVER_URL); - req.respond( - tiny_http::Response::empty(302).with_header( - tiny_http::Header::from_bytes( - &b"Location"[..], - post_auth_url.as_bytes(), - ) - .unwrap(), - ), - ) - .context("failed to respond to login http request")?; - return Ok(( - user_id.ok_or_else(|| anyhow!("missing user_id parameter"))?, - access_token - .ok_or_else(|| anyhow!("missing access_token parameter"))?, - )); - } - } + Err(anyhow!("didn't receive login redirect")) + }) + .await?; - Err(anyhow!("didn't receive login redirect")) + let access_token = private_key + .decrypt_string(&access_token) + .context("failed to decrypt access token")?; + + Ok(Credentials { + user_id: user_id.parse()?, + access_token, + }) }) .await?; - let access_token = private_key - .decrypt_string(&access_token) - .context("failed to decrypt access token")?; cx.update(|cx| cx.activate(true))?; - - Ok(Credentials { - user_id: user_id.parse()?, - access_token, - }) + Ok(credentials) }) } diff --git a/crates/collab_ui2/Cargo.toml b/crates/collab_ui2/Cargo.toml index 9d84d7f887c56b77df18c237d697be063f745091..da13da8306810b3bae744db12e9dcfeefbda4664 100644 --- a/crates/collab_ui2/Cargo.toml +++ b/crates/collab_ui2/Cargo.toml @@ -22,7 +22,7 @@ test-support = [ ] [dependencies] -# auto_update = { path = "../auto_update" } +auto_update = { package = "auto_update2", path = "../auto_update2" } db = { package = "db2", path = "../db2" } call = { package = "call2", path = "../call2" } client = { package = "client2", path = "../client2" } @@ -32,7 +32,7 @@ collections = { path = "../collections" } # context_menu = { path = "../context_menu" } # drag_and_drop = { path = "../drag_and_drop" } editor = { package="editor2", path = "../editor2" } -#feedback = { path = "../feedback" } +feedback = { package = "feedback2", path = "../feedback2" } fuzzy = { package = "fuzzy2", path = "../fuzzy2" } gpui = { package = "gpui2", path = "../gpui2" } language = { package = "language2", path = "../language2" } @@ -46,7 +46,7 @@ rpc = { package ="rpc2", path = "../rpc2" } settings = { package = "settings2", path = "../settings2" } feature_flags = { package = "feature_flags2", path = "../feature_flags2"} theme = { package = "theme2", path = "../theme2" } -# theme_selector = { path = "../theme_selector" } +theme_selector = { package = "theme_selector2", path = "../theme_selector2" } vcs_menu = { package = "vcs_menu2", path = "../vcs_menu2" } ui = { package = "ui2", path = "../ui2" } util = { path = "../util" } diff --git a/crates/collab_ui2/src/collab_titlebar_item.rs b/crates/collab_ui2/src/collab_titlebar_item.rs index 2c48a66a1d7be20e155c08df1179eb203cddd614..b127708fa690dfc4976c8df246c9d3afa7b3f1a4 100644 --- a/crates/collab_ui2/src/collab_titlebar_item.rs +++ b/crates/collab_ui2/src/collab_titlebar_item.rs @@ -1,9 +1,10 @@ use crate::face_pile::FacePile; +use auto_update::AutoUpdateStatus; use call::{ActiveCall, ParticipantLocation, Room}; use client::{proto::PeerId, Client, ParticipantIndex, User, UserStore}; use gpui::{ - actions, canvas, div, overlay, point, px, rems, AnyElement, AppContext, DismissEvent, Div, - Element, FocusableView, Hsla, InteractiveElement, IntoElement, Model, ParentElement, Path, + actions, canvas, div, overlay, point, px, rems, Action, AnyElement, AppContext, DismissEvent, + Div, Element, FocusableView, Hsla, InteractiveElement, IntoElement, Model, ParentElement, Path, Render, Stateful, StatefulInteractiveElement, Styled, Subscription, View, ViewContext, VisualContext, WeakView, WindowBounds, }; @@ -53,7 +54,6 @@ pub struct CollabTitlebarItem { workspace: WeakView, branch_popover: Option>, project_popover: Option, - //user_menu: ViewHandle, _subscriptions: Vec, } @@ -233,68 +233,17 @@ impl Render for CollabTitlebarItem { }), ) }) - .child( - h_stack() - .border_color(gpui::red()) - .border_1() - .px_1p5() - .map(|this| { - if let Some(user) = current_user { - // TODO: Finish implementing user menu popover - // - this.child( - popover_menu("user-menu") - .menu(|cx| { - ContextMenu::build(cx, |menu, _| { - menu.header("ADADA") - }) - }) - .trigger( - ButtonLike::new("user-menu") - .child( - h_stack() - .gap_0p5() - .child(Avatar::new( - user.avatar_uri.clone(), - )) - .child( - IconElement::new(Icon::ChevronDown) - .color(Color::Muted), - ), - ) - .style(ButtonStyle::Subtle) - .tooltip(move |cx| { - Tooltip::text("Toggle User Menu", cx) - }), - ) - .anchor(gpui::AnchorCorner::TopRight), - ) - // this.child( - // ButtonLike::new("user-menu") - // .child( - // h_stack().gap_0p5().child(Avatar::data(avatar)).child( - // IconElement::new(Icon::ChevronDown).color(Color::Muted), - // ), - // ) - // .style(ButtonStyle::Subtle) - // .tooltip(move |cx| Tooltip::text("Toggle User Menu", cx)), - // ) - } else { - this.child(Button::new("sign_in", "Sign in").on_click( - move |_, cx| { - let client = client.clone(); - cx.spawn(move |mut cx| async move { - client - .authenticate_and_connect(true, &cx) - .await - .notify_async_err(&mut cx); - }) - .detach(); - }, - )) - } - }), - ), + .map(|el| { + let status = self.client.status(); + let status = &*status.borrow(); + if matches!(status, client::Status::Connected { .. }) { + el.child(self.render_user_menu_button(cx)) + } else { + el.children(self.render_connection_status(status, cx)) + .child(self.render_sign_in_button(cx)) + .child(self.render_user_menu_button(cx)) + } + }), ) } } @@ -336,12 +285,6 @@ impl CollabTitlebarItem { project, user_store, client, - // user_menu: cx.add_view(|cx| { - // let view_id = cx.view_id(); - // let mut menu = ContextMenu::new(view_id, cx); - // menu.set_position_mode(OverlayPositionMode::Local); - // menu - // }), branch_popover: None, project_popover: None, _subscriptions: subscriptions, @@ -582,154 +525,113 @@ impl CollabTitlebarItem { cx.notify(); } - // fn render_user_menu_button( - // &self, - // theme: &Theme, - // avatar: Option>, - // cx: &mut ViewContext, - // ) -> AnyElement { - // let tooltip = theme.tooltip.clone(); - // let user_menu_button_style = if avatar.is_some() { - // &theme.titlebar.user_menu.user_menu_button_online - // } else { - // &theme.titlebar.user_menu.user_menu_button_offline - // }; - - // let avatar_style = &user_menu_button_style.avatar; - // Stack::new() - // .with_child( - // MouseEventHandler::new::(0, cx, |state, _| { - // let style = user_menu_button_style - // .user_menu - // .inactive_state() - // .style_for(state); - - // let mut dropdown = Flex::row().align_children_center(); - - // if let Some(avatar_img) = avatar { - // dropdown = dropdown.with_child(Self::render_face( - // avatar_img, - // *avatar_style, - // Color::transparent_black(), - // None, - // )); - // }; - - // dropdown - // .with_child( - // Svg::new("icons/caret_down.svg") - // .with_color(user_menu_button_style.icon.color) - // .constrained() - // .with_width(user_menu_button_style.icon.width) - // .contained() - // .into_any(), - // ) - // .aligned() - // .constrained() - // .with_height(style.width) - // .contained() - // .with_style(style.container) - // .into_any() - // }) - // .with_cursor_style(CursorStyle::PointingHand) - // .on_down(MouseButton::Left, move |_, this, cx| { - // this.user_menu.update(cx, |menu, _| menu.delay_cancel()); - // }) - // .on_click(MouseButton::Left, move |_, this, cx| { - // this.toggle_user_menu(&Default::default(), cx) - // }) - // .with_tooltip::( - // 0, - // "Toggle User Menu".to_owned(), - // Some(Box::new(ToggleUserMenu)), - // tooltip, - // cx, - // ) - // .contained(), - // ) - // .with_child( - // ChildView::new(&self.user_menu, cx) - // .aligned() - // .bottom() - // .right(), - // ) - // .into_any() - // } - - // fn render_sign_in_button(&self, theme: &Theme, cx: &mut ViewContext) -> AnyElement { - // let titlebar = &theme.titlebar; - // MouseEventHandler::new::(0, cx, |state, _| { - // let style = titlebar.sign_in_button.inactive_state().style_for(state); - // Label::new("Sign In", style.text.clone()) - // .contained() - // .with_style(style.container) - // }) - // .with_cursor_style(CursorStyle::PointingHand) - // .on_click(MouseButton::Left, move |_, this, cx| { - // let client = this.client.clone(); - // cx.app_context() - // .spawn(|cx| async move { client.authenticate_and_connect(true, &cx).await }) - // .detach_and_log_err(cx); - // }) - // .into_any() - // } - - // fn render_connection_status( - // &self, - // status: &client::Status, - // cx: &mut ViewContext, - // ) -> Option> { - // enum ConnectionStatusButton {} - - // let theme = &theme::current(cx).clone(); - // match status { - // client::Status::ConnectionError - // | client::Status::ConnectionLost - // | client::Status::Reauthenticating { .. } - // | client::Status::Reconnecting { .. } - // | client::Status::ReconnectionError { .. } => Some( - // Svg::new("icons/disconnected.svg") - // .with_color(theme.titlebar.offline_icon.color) - // .constrained() - // .with_width(theme.titlebar.offline_icon.width) - // .aligned() - // .contained() - // .with_style(theme.titlebar.offline_icon.container) - // .into_any(), - // ), - // client::Status::UpgradeRequired => { - // let auto_updater = auto_update::AutoUpdater::get(cx); - // let label = match auto_updater.map(|auto_update| auto_update.read(cx).status()) { - // Some(AutoUpdateStatus::Updated) => "Please restart Zed to Collaborate", - // Some(AutoUpdateStatus::Installing) - // | Some(AutoUpdateStatus::Downloading) - // | Some(AutoUpdateStatus::Checking) => "Updating...", - // Some(AutoUpdateStatus::Idle) | Some(AutoUpdateStatus::Errored) | None => { - // "Please update Zed to Collaborate" - // } - // }; - - // Some( - // MouseEventHandler::new::(0, cx, |_, _| { - // Label::new(label, theme.titlebar.outdated_warning.text.clone()) - // .contained() - // .with_style(theme.titlebar.outdated_warning.container) - // .aligned() - // }) - // .with_cursor_style(CursorStyle::PointingHand) - // .on_click(MouseButton::Left, |_, _, cx| { - // if let Some(auto_updater) = auto_update::AutoUpdater::get(cx) { - // if auto_updater.read(cx).status() == AutoUpdateStatus::Updated { - // workspace::restart(&Default::default(), cx); - // return; - // } - // } - // auto_update::check(&Default::default(), cx); - // }) - // .into_any(), - // ) - // } - // _ => None, - // } - // } + fn render_connection_status( + &self, + status: &client::Status, + cx: &mut ViewContext, + ) -> Option { + match status { + client::Status::ConnectionError + | client::Status::ConnectionLost + | client::Status::Reauthenticating { .. } + | client::Status::Reconnecting { .. } + | client::Status::ReconnectionError { .. } => Some( + div() + .id("disconnected") + .bg(gpui::red()) // todo!() @nate + .child(IconElement::new(Icon::Disconnected)) + .tooltip(|cx| Tooltip::text("Disconnected", cx)) + .into_any_element(), + ), + client::Status::UpgradeRequired => { + let auto_updater = auto_update::AutoUpdater::get(cx); + let label = match auto_updater.map(|auto_update| auto_update.read(cx).status()) { + Some(AutoUpdateStatus::Updated) => "Please restart Zed to Collaborate", + Some(AutoUpdateStatus::Installing) + | Some(AutoUpdateStatus::Downloading) + | Some(AutoUpdateStatus::Checking) => "Updating...", + Some(AutoUpdateStatus::Idle) | Some(AutoUpdateStatus::Errored) | None => { + "Please update Zed to Collaborate" + } + }; + + Some( + div() + .bg(gpui::red()) // todo!() @nate + .child(Button::new("connection-status", label).on_click(|_, cx| { + if let Some(auto_updater) = auto_update::AutoUpdater::get(cx) { + if auto_updater.read(cx).status() == AutoUpdateStatus::Updated { + workspace::restart(&Default::default(), cx); + return; + } + } + auto_update::check(&Default::default(), cx); + })) + .into_any_element(), + ) + } + _ => None, + } + } + + pub fn render_sign_in_button(&mut self, _: &mut ViewContext) -> Button { + let client = self.client.clone(); + Button::new("sign_in", "Sign in").on_click(move |_, cx| { + let client = client.clone(); + cx.spawn(move |mut cx| async move { + client + .authenticate_and_connect(true, &cx) + .await + .notify_async_err(&mut cx); + }) + .detach(); + }) + } + + pub fn render_user_menu_button(&mut self, cx: &mut ViewContext) -> impl Element { + if let Some(user) = self.user_store.read(cx).current_user() { + popover_menu("user-menu") + .menu(|cx| { + ContextMenu::build(cx, |menu, _| { + menu.action("Settings", zed_actions::OpenSettings.boxed_clone()) + .action("Theme", theme_selector::Toggle.boxed_clone()) + .separator() + .action("Share Feedback", feedback::GiveFeedback.boxed_clone()) + .action("Sign Out", client::SignOut.boxed_clone()) + }) + }) + .trigger( + ButtonLike::new("user-menu") + .child( + h_stack() + .gap_0p5() + .child(Avatar::new(user.avatar_uri.clone())) + .child(IconElement::new(Icon::ChevronDown).color(Color::Muted)), + ) + .style(ButtonStyle::Subtle) + .tooltip(move |cx| Tooltip::text("Toggle User Menu", cx)), + ) + .anchor(gpui::AnchorCorner::TopRight) + } else { + popover_menu("user-menu") + .menu(|cx| { + ContextMenu::build(cx, |menu, _| { + menu.action("Settings", zed_actions::OpenSettings.boxed_clone()) + .action("Theme", theme_selector::Toggle.boxed_clone()) + .separator() + .action("Share Feedback", feedback::GiveFeedback.boxed_clone()) + }) + }) + .trigger( + ButtonLike::new("user-menu") + .child( + h_stack() + .gap_0p5() + .child(IconElement::new(Icon::ChevronDown).color(Color::Muted)), + ) + .style(ButtonStyle::Subtle) + .tooltip(move |cx| Tooltip::text("Toggle User Menu", cx)), + ) + } + } } diff --git a/crates/ui2/src/components/icon.rs b/crates/ui2/src/components/icon.rs index b68aa579c026d518f33e9eabc94824270c8104e7..6876630100e95d3ec23252e80787642263783d56 100644 --- a/crates/ui2/src/components/icon.rs +++ b/crates/ui2/src/components/icon.rs @@ -50,6 +50,7 @@ pub enum Icon { CopilotError, CopilotDisabled, Dash, + Disconnected, Envelope, ExternalLink, ExclamationTriangle, @@ -129,6 +130,7 @@ impl Icon { Icon::CopilotError => "icons/copilot_error.svg", Icon::CopilotDisabled => "icons/copilot_disabled.svg", Icon::Dash => "icons/dash.svg", + Icon::Disconnected => "icons/disconnected.svg", Icon::Envelope => "icons/feedback.svg", Icon::ExclamationTriangle => "icons/warning.svg", Icon::ExternalLink => "icons/external_link.svg", diff --git a/crates/zed2/src/zed2.rs b/crates/zed2/src/zed2.rs index d5fc0e8fdc9d3badeb4f89ad6d31dd6f5ace1979..27e32fff179819fb79f87af303a4f996bc295b26 100644 --- a/crates/zed2/src/zed2.rs +++ b/crates/zed2/src/zed2.rs @@ -41,7 +41,7 @@ use workspace::{ notifications::simple_message_notification::MessageNotification, open_new, AppState, NewFile, NewWindow, Workspace, WorkspaceSettings, }; -use zed_actions::{OpenBrowser, OpenZedURL, Quit}; +use zed_actions::{OpenBrowser, OpenSettings, OpenZedURL, Quit}; actions!( zed, @@ -59,7 +59,6 @@ actions!( OpenLicenses, OpenLocalSettings, OpenLog, - OpenSettings, OpenTelemetryLog, ResetBufferFontSize, ResetDatabase, diff --git a/crates/zed_actions2/src/lib.rs b/crates/zed_actions2/src/lib.rs index fa1a4a5ea961ce77ecd5d199a182c88c17166bbe..badf76a6e7b4bc84a81184b81fca33c413b61bfa 100644 --- a/crates/zed_actions2/src/lib.rs +++ b/crates/zed_actions2/src/lib.rs @@ -22,4 +22,4 @@ pub struct OpenZedURL { impl_actions!(zed, [OpenBrowser, OpenZedURL]); -actions!(zed, [Quit]); +actions!(zed, [OpenSettings, Quit]);