Merge branch 'main' into copilot-disabled-globs

Max Brunsfeld created

Change summary

Cargo.lock                                             |   2 
README.md                                              |   5 
crates/activity_indicator/src/activity_indicator.rs    |  40 
crates/auto_update/src/auto_update.rs                  |  70 ++
crates/auto_update/src/update_notification.rs          |   6 
crates/breadcrumbs/src/breadcrumbs.rs                  |  16 
crates/client/src/client.rs                            |  35 -
crates/client/src/telemetry.rs                         | 223 +++++++-
crates/collab_ui/src/collab_titlebar_item.rs           | 161 ++---
crates/collab_ui/src/collab_ui.rs                      |   1 
crates/collab_ui/src/collaborator_list_popover.rs      | 161 ------
crates/collab_ui/src/contact_finder.rs                 |   1 
crates/collab_ui/src/contact_list.rs                   |  68 +-
crates/collab_ui/src/contacts_popover.rs               |  51 +
crates/collab_ui/src/incoming_call_notification.rs     |  38 
crates/collab_ui/src/project_shared_notification.rs    |   4 
crates/collab_ui/src/sharing_status_indicator.rs       |   9 
crates/command_palette/src/command_palette.rs          |  17 
crates/context_menu/src/context_menu.rs                |  35 
crates/copilot/src/copilot.rs                          |   4 
crates/copilot/src/sign_in.rs                          |  21 
crates/copilot_button/src/copilot_button.rs            |  34 
crates/diagnostics/src/diagnostics.rs                  |  15 
crates/diagnostics/src/items.rs                        |  25 
crates/editor/Cargo.toml                               |   1 
crates/editor/src/editor.rs                            | 212 +++++---
crates/editor/src/element.rs                           |  41 
crates/editor/src/hover_popover.rs                     |  13 
crates/editor/src/items.rs                             |   2 
crates/editor/src/link_go_to_definition.rs             |  87 +--
crates/feedback/Cargo.toml                             |  18 
crates/feedback/src/deploy_feedback_button.rs          |  19 
crates/feedback/src/feedback.rs                        |  57 -
crates/feedback/src/feedback_editor.rs                 |  75 +-
crates/feedback/src/feedback_info_text.rs              |   4 
crates/feedback/src/submit_feedback_button.rs          |  24 
crates/gpui/src/app.rs                                 | 160 ++----
crates/gpui/src/app/test_app_context.rs                |  18 
crates/gpui/src/app/window.rs                          |  19 
crates/gpui/src/elements.rs                            |  65 --
crates/gpui/src/platform/mac/window.rs                 |  39 +
crates/gpui/src/test.rs                                |   2 
crates/gpui/src/views/select.rs                        |  20 
crates/gpui_macros/src/gpui_macros.rs                  |   4 
crates/language_selector/src/active_buffer_language.rs |  22 
crates/language_selector/src/language_selector.rs      |  14 
crates/node_runtime/src/node_runtime.rs                |  46 +
crates/outline/src/outline.rs                          |   2 
crates/project_panel/src/project_panel.rs              |  23 
crates/recent_projects/src/recent_projects.rs          |  41 -
crates/search/src/buffer_search.rs                     |  24 
crates/search/src/project_search.rs                    |  11 
crates/terminal_view/src/terminal_button.rs            |  12 
crates/theme/src/theme.rs                              |   1 
crates/theme/src/ui.rs                                 |  19 
crates/theme_selector/src/theme_selector.rs            |  14 
crates/util/src/github.rs                              |  32 
crates/util/src/http.rs                                |   6 
crates/welcome/src/base_keymap_picker.rs               |   2 
crates/welcome/src/welcome.rs                          |  48 +
crates/workspace/src/dock.rs                           |   8 
crates/workspace/src/dock/toggle_dock_button.rs        |  22 
crates/workspace/src/notifications.rs                  |  88 +--
crates/workspace/src/pane.rs                           |  48 +
crates/workspace/src/sidebar.rs                        |   6 
crates/workspace/src/toolbar.rs                        |  46 +
crates/workspace/src/workspace.rs                      | 288 ++++++-----
crates/zed/src/languages/c.rs                          |   2 
crates/zed/src/languages/elixir.rs                     |   2 
crates/zed/src/languages/go.rs                         |   2 
crates/zed/src/languages/html.rs                       |   2 
crates/zed/src/languages/json.rs                       |   2 
crates/zed/src/languages/lua.rs                        |   2 
crates/zed/src/languages/python.rs                     |   2 
crates/zed/src/languages/rust.rs                       |   8 
crates/zed/src/languages/typescript.rs                 | 123 ++--
crates/zed/src/languages/yaml.rs                       |   2 
crates/zed/src/main.rs                                 |  46 +
crates/zed/src/zed.rs                                  | 249 ++++-----
styles/src/styleTree/editor.ts                         |   3 
styles/src/styleTree/hoverPopover.ts                   |   2 
81 files changed, 1,648 insertions(+), 1,544 deletions(-)

Detailed changes

Cargo.lock πŸ”—

@@ -1973,6 +1973,7 @@ version = "0.1.0"
 dependencies = [
  "aho-corasick",
  "anyhow",
+ "client",
  "clock",
  "collections",
  "context_menu",
@@ -2156,6 +2157,7 @@ dependencies = [
  "serde",
  "serde_derive",
  "settings",
+ "smallvec",
  "sysinfo",
  "theme",
  "tree-sitter-markdown",

README.md πŸ”—

@@ -2,7 +2,7 @@
 
 [![CI](https://github.com/zed-industries/zed/actions/workflows/ci.yml/badge.svg)](https://github.com/zed-industries/zed/actions/workflows/ci.yml)
 
-Welcome to Zed, a lightning-fast, collaborative code editor that makes your dreams come true. 
+Welcome to Zed, a lightning-fast, collaborative code editor that makes your dreams come true.
 
 ## Development tips
 
@@ -31,7 +31,8 @@ Welcome to Zed, a lightning-fast, collaborative code editor that makes your drea
 
 * Set up a local `zed` database and seed it with some initial users:
 
-    Create a personal GitHub token to run `script/bootstrap` once successfully. Then delete that token.
+    Create a personal GitHub token to run `script/bootstrap` once successfully: the token needs to have an access to private repositories for the script to work (`repo` OAuth scope).
+    Then delete that token.
 
     ```
     GITHUB_TOKEN=<$token> script/bootstrap

crates/activity_indicator/src/activity_indicator.rs πŸ”—

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

crates/auto_update/src/auto_update.rs πŸ”—

@@ -1,13 +1,15 @@
 mod update_notification;
 
 use anyhow::{anyhow, Context, Result};
-use client::{ZED_APP_PATH, ZED_APP_VERSION, ZED_SECRET_CLIENT_TOKEN};
+use client::{Client, ZED_APP_PATH, ZED_APP_VERSION, ZED_SECRET_CLIENT_TOKEN};
 use db::kvp::KEY_VALUE_STORE;
 use gpui::{
     actions, platform::AppVersion, AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle,
     Task, WeakViewHandle,
 };
+use isahc::AsyncBody;
 use serde::Deserialize;
+use serde_derive::Serialize;
 use settings::Settings;
 use smol::{fs::File, io::AsyncReadExt, process::Command};
 use std::{ffi::OsString, sync::Arc, time::Duration};
@@ -21,6 +23,13 @@ const POLL_INTERVAL: Duration = Duration::from_secs(60 * 60);
 
 actions!(auto_update, [Check, DismissErrorMessage, ViewReleaseNotes]);
 
+#[derive(Serialize)]
+struct UpdateRequestBody {
+    installation_id: Option<Arc<str>>,
+    release_channel: Option<&'static str>,
+    telemetry: bool,
+}
+
 #[derive(Clone, Copy, PartialEq, Eq)]
 pub enum AutoUpdateStatus {
     Idle,
@@ -51,9 +60,8 @@ impl Entity for AutoUpdater {
 
 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()) {
-        let server_url = server_url;
         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
                 .global::<Settings>()
@@ -74,25 +82,32 @@ pub fn init(http_client: Arc<dyn HttpClient>, server_url: String, cx: &mut AppCo
             updater
         });
         cx.set_global(Some(auto_updater));
-        cx.add_global_action(|_: &Check, cx| {
-            if let Some(updater) = AutoUpdater::get(cx) {
-                updater.update(cx, |updater, cx| updater.poll(cx));
-            }
-        });
-        cx.add_global_action(move |_: &ViewReleaseNotes, cx| {
-            let latest_release_url = if cx.has_global::<ReleaseChannel>()
-                && *cx.global::<ReleaseChannel>() == ReleaseChannel::Preview
-            {
-                format!("{server_url}/releases/preview/latest")
-            } else {
-                format!("{server_url}/releases/latest")
-            };
-            cx.platform().open_url(&latest_release_url);
-        });
+        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) {
+        updater.update(cx, |updater, cx| updater.poll(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>()
+            && *cx.global::<ReleaseChannel>() == ReleaseChannel::Preview
+        {
+            format!("{server_url}/releases/preview/latest")
+        } else {
+            format!("{server_url}/releases/latest")
+        };
+        cx.platform().open_url(&latest_release_url);
+    }
+}
+
 pub fn notify_of_any_new_update(
     workspace: WeakViewHandle<Workspace>,
     cx: &mut AppContext,
@@ -241,7 +256,24 @@ impl AutoUpdater {
         mounted_app_path.push("/");
 
         let mut dmg_file = File::create(&dmg_path).await?;
-        let mut response = client.get(&release.url, Default::default(), true).await?;
+
+        let (installation_id, release_channel, telemetry) = cx.read(|cx| {
+            let installation_id = cx.global::<Arc<Client>>().telemetry().installation_id();
+            let release_channel = cx
+                .has_global::<ReleaseChannel>()
+                .then(|| cx.global::<ReleaseChannel>().display_name());
+            let telemetry = cx.global::<Settings>().telemetry().metrics();
+
+            (installation_id, release_channel, telemetry)
+        });
+
+        let request_body = AsyncBody::from(serde_json::to_string(&UpdateRequestBody {
+            installation_id,
+            release_channel,
+            telemetry,
+        })?);
+
+        let mut response = client.post_json(&release.url, request_body, true).await?;
         smol::io::copy(response.body_mut(), &mut dmg_file).await?;
         log::info!("downloaded update. path:{:?}", dmg_path);
 

crates/auto_update/src/update_notification.rs πŸ”—

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

crates/breadcrumbs/src/breadcrumbs.rs πŸ”—

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

crates/client/src/client.rs πŸ”—

@@ -17,7 +17,7 @@ use futures::{
 use gpui::{
     actions,
     platform::AppVersion,
-    serde_json::{self, Value},
+    serde_json::{self},
     AnyModelHandle, AnyWeakModelHandle, AnyWeakViewHandle, AppContext, AsyncAppContext, Entity,
     ModelHandle, Task, View, ViewContext, WeakViewHandle,
 };
@@ -27,7 +27,7 @@ use postage::watch;
 use rand::prelude::*;
 use rpc::proto::{AnyTypedEnvelope, EntityMessage, EnvelopedMessage, PeerId, RequestMessage};
 use serde::Deserialize;
-use settings::{Settings, TelemetrySettings};
+use settings::Settings;
 use std::{
     any::TypeId,
     collections::HashMap,
@@ -47,6 +47,7 @@ use util::http::HttpClient;
 use util::{ResultExt, TryFutureExt};
 
 pub use rpc::*;
+pub use telemetry::ClickhouseEvent;
 pub use user::*;
 
 lazy_static! {
@@ -736,7 +737,7 @@ impl Client {
             read_from_keychain = credentials.is_some();
             if read_from_keychain {
                 cx.read(|cx| {
-                    self.report_event(
+                    self.telemetry().report_mixpanel_event(
                         "read credentials from keychain",
                         Default::default(),
                         cx.global::<Settings>().telemetry(),
@@ -1116,7 +1117,7 @@ impl Client {
                 .context("failed to decrypt access token")?;
             platform.activate(true);
 
-            telemetry.report_event(
+            telemetry.report_mixpanel_event(
                 "authenticate with browser",
                 Default::default(),
                 metrics_enabled,
@@ -1338,30 +1339,8 @@ impl Client {
         }
     }
 
-    pub fn start_telemetry(&self) {
-        self.telemetry.start();
-    }
-
-    pub fn report_event(
-        &self,
-        kind: &str,
-        properties: Value,
-        telemetry_settings: TelemetrySettings,
-    ) {
-        self.telemetry
-            .report_event(kind, properties.clone(), telemetry_settings);
-    }
-
-    pub fn telemetry_log_file_path(&self) -> Option<PathBuf> {
-        self.telemetry.log_file_path()
-    }
-
-    pub fn metrics_id(&self) -> Option<Arc<str>> {
-        self.telemetry.metrics_id()
-    }
-
-    pub fn is_staff(&self) -> Option<bool> {
-        self.telemetry.is_staff()
+    pub fn telemetry(&self) -> &Arc<Telemetry> {
+        &self.telemetry
     }
 }
 

crates/client/src/telemetry.rs πŸ”—

@@ -1,3 +1,4 @@
+use crate::{ZED_SECRET_CLIENT_TOKEN, ZED_SERVER_URL};
 use db::kvp::KEY_VALUE_STORE;
 use gpui::{
     executor::Background,
@@ -29,26 +30,62 @@ pub struct Telemetry {
 
 #[derive(Default)]
 struct TelemetryState {
-    metrics_id: Option<Arc<str>>,
-    device_id: Option<Arc<str>>,
+    metrics_id: Option<Arc<str>>,      // Per logged-in user
+    installation_id: Option<Arc<str>>, // Per app installation
     app_version: Option<Arc<str>>,
     release_channel: Option<&'static str>,
     os_version: Option<Arc<str>>,
     os_name: &'static str,
-    queue: Vec<MixpanelEvent>,
-    next_event_id: usize,
-    flush_task: Option<Task<()>>,
+    mixpanel_events_queue: Vec<MixpanelEvent>,
+    clickhouse_events_queue: Vec<ClickhouseEventWrapper>,
+    next_mixpanel_event_id: usize,
+    flush_mixpanel_events_task: Option<Task<()>>,
+    flush_clickhouse_events_task: Option<Task<()>>,
     log_file: Option<NamedTempFile>,
     is_staff: Option<bool>,
 }
 
 const MIXPANEL_EVENTS_URL: &'static str = "https://api.mixpanel.com/track";
 const MIXPANEL_ENGAGE_URL: &'static str = "https://api.mixpanel.com/engage#profile-set";
+const CLICKHOUSE_EVENTS_URL_PATH: &'static str = "/api/events";
 
 lazy_static! {
     static ref MIXPANEL_TOKEN: Option<String> = std::env::var("ZED_MIXPANEL_TOKEN")
         .ok()
         .or_else(|| option_env!("ZED_MIXPANEL_TOKEN").map(|key| key.to_string()));
+    static ref CLICKHOUSE_EVENTS_URL: String =
+        format!("{}{}", *ZED_SERVER_URL, CLICKHOUSE_EVENTS_URL_PATH);
+}
+
+#[derive(Serialize, Debug)]
+struct ClickhouseEventRequestBody {
+    token: &'static str,
+    installation_id: Option<Arc<str>>,
+    app_version: Option<Arc<str>>,
+    os_name: &'static str,
+    os_version: Option<Arc<str>>,
+    release_channel: Option<&'static str>,
+    events: Vec<ClickhouseEventWrapper>,
+}
+
+#[derive(Serialize, Debug)]
+struct ClickhouseEventWrapper {
+    time: u128,
+    signed_in: bool,
+    #[serde(flatten)]
+    event: ClickhouseEvent,
+}
+
+#[derive(Serialize, Debug)]
+#[serde(tag = "type")]
+pub enum ClickhouseEvent {
+    Editor {
+        operation: &'static str,
+        file_extension: Option<String>,
+        vim_mode: bool,
+        copilot_enabled: bool,
+        copilot_enabled_for_language: bool,
+    },
 }
 
 #[derive(Serialize, Debug)]
@@ -63,7 +100,8 @@ struct MixpanelEventProperties {
     #[serde(skip_serializing_if = "str::is_empty")]
     token: &'static str,
     time: u128,
-    distinct_id: Option<Arc<str>>,
+    #[serde(rename = "distinct_id")]
+    installation_id: Option<Arc<str>>,
     #[serde(rename = "$insert_id")]
     insert_id: usize,
     // Custom fields
@@ -86,7 +124,7 @@ struct MixpanelEngageRequest {
     #[serde(rename = "$token")]
     token: &'static str,
     #[serde(rename = "$distinct_id")]
-    distinct_id: Arc<str>,
+    installation_id: Arc<str>,
     #[serde(rename = "$set")]
     set: Value,
 }
@@ -119,11 +157,13 @@ impl Telemetry {
                 os_name: platform.os_name().into(),
                 app_version: platform.app_version().ok().map(|v| v.to_string().into()),
                 release_channel,
-                device_id: None,
+                installation_id: None,
                 metrics_id: None,
-                queue: Default::default(),
-                flush_task: Default::default(),
-                next_event_id: 0,
+                mixpanel_events_queue: Default::default(),
+                clickhouse_events_queue: Default::default(),
+                flush_mixpanel_events_task: Default::default(),
+                flush_clickhouse_events_task: Default::default(),
+                next_mixpanel_event_id: 0,
                 log_file: None,
                 is_staff: None,
             }),
@@ -154,29 +194,38 @@ impl Telemetry {
         self.executor
             .spawn(
                 async move {
-                    let device_id =
-                        if let Ok(Some(device_id)) = KEY_VALUE_STORE.read_kvp("device_id") {
-                            device_id
+                    let installation_id =
+                        if let Ok(Some(installation_id)) = KEY_VALUE_STORE.read_kvp("device_id") {
+                            installation_id
                         } else {
-                            let device_id = Uuid::new_v4().to_string();
+                            let installation_id = Uuid::new_v4().to_string();
                             KEY_VALUE_STORE
-                                .write_kvp("device_id".to_string(), device_id.clone())
+                                .write_kvp("device_id".to_string(), installation_id.clone())
                                 .await?;
-                            device_id
+                            installation_id
                         };
 
-                    let device_id: Arc<str> = device_id.into();
+                    let installation_id: Arc<str> = installation_id.into();
                     let mut state = this.state.lock();
-                    state.device_id = Some(device_id.clone());
-                    for event in &mut state.queue {
+                    state.installation_id = Some(installation_id.clone());
+
+                    for event in &mut state.mixpanel_events_queue {
                         event
                             .properties
-                            .distinct_id
-                            .get_or_insert_with(|| device_id.clone());
+                            .installation_id
+                            .get_or_insert_with(|| installation_id.clone());
+                    }
+
+                    let has_mixpanel_events = !state.mixpanel_events_queue.is_empty();
+                    let has_clickhouse_events = !state.clickhouse_events_queue.is_empty();
+                    drop(state);
+
+                    if has_mixpanel_events {
+                        this.flush_mixpanel_events();
                     }
-                    if !state.queue.is_empty() {
-                        drop(state);
-                        this.flush();
+
+                    if has_clickhouse_events {
+                        this.flush_clickhouse_events();
                     }
 
                     anyhow::Ok(())
@@ -200,19 +249,19 @@ impl Telemetry {
 
         let this = self.clone();
         let mut state = self.state.lock();
-        let device_id = state.device_id.clone();
+        let installation_id = state.installation_id.clone();
         let metrics_id: Option<Arc<str>> = metrics_id.map(|id| id.into());
         state.metrics_id = metrics_id.clone();
         state.is_staff = Some(is_staff);
         drop(state);
 
-        if let Some((token, device_id)) = MIXPANEL_TOKEN.as_ref().zip(device_id) {
+        if let Some((token, installation_id)) = MIXPANEL_TOKEN.as_ref().zip(installation_id) {
             self.executor
                 .spawn(
                     async move {
                         let json_bytes = serde_json::to_vec(&[MixpanelEngageRequest {
                             token,
-                            distinct_id: device_id,
+                            installation_id,
                             set: json!({
                                 "Staff": is_staff,
                                 "ID": metrics_id,
@@ -221,7 +270,7 @@ impl Telemetry {
                         }])?;
 
                         this.http_client
-                            .post_json(MIXPANEL_ENGAGE_URL, json_bytes.into())
+                            .post_json(MIXPANEL_ENGAGE_URL, json_bytes.into(), false)
                             .await?;
                         anyhow::Ok(())
                     }
@@ -231,7 +280,42 @@ impl Telemetry {
         }
     }
 
-    pub fn report_event(
+    pub fn report_clickhouse_event(
+        self: &Arc<Self>,
+        event: ClickhouseEvent,
+        telemetry_settings: TelemetrySettings,
+    ) {
+        if !telemetry_settings.metrics() {
+            return;
+        }
+
+        let mut state = self.state.lock();
+        let signed_in = state.metrics_id.is_some();
+        state.clickhouse_events_queue.push(ClickhouseEventWrapper {
+            time: SystemTime::now()
+                .duration_since(UNIX_EPOCH)
+                .unwrap()
+                .as_millis(),
+            signed_in,
+            event,
+        });
+
+        if state.installation_id.is_some() {
+            if state.mixpanel_events_queue.len() >= MAX_QUEUE_LEN {
+                drop(state);
+                self.flush_clickhouse_events();
+            } else {
+                let this = self.clone();
+                let executor = self.executor.clone();
+                state.flush_clickhouse_events_task = Some(self.executor.spawn(async move {
+                    executor.timer(DEBOUNCE_INTERVAL).await;
+                    this.flush_clickhouse_events();
+                }));
+            }
+        }
+    }
+
+    pub fn report_mixpanel_event(
         self: &Arc<Self>,
         kind: &str,
         properties: Value,
@@ -243,15 +327,15 @@ impl Telemetry {
 
         let mut state = self.state.lock();
         let event = MixpanelEvent {
-            event: kind.to_string(),
+            event: kind.into(),
             properties: MixpanelEventProperties {
                 token: "",
                 time: SystemTime::now()
                     .duration_since(UNIX_EPOCH)
                     .unwrap()
                     .as_millis(),
-                distinct_id: state.device_id.clone(),
-                insert_id: post_inc(&mut state.next_event_id),
+                installation_id: state.installation_id.clone(),
+                insert_id: post_inc(&mut state.next_mixpanel_event_id),
                 event_properties: if let Value::Object(properties) = properties {
                     Some(properties)
                 } else {
@@ -264,17 +348,17 @@ impl Telemetry {
                 signed_in: state.metrics_id.is_some(),
             },
         };
-        state.queue.push(event);
-        if state.device_id.is_some() {
-            if state.queue.len() >= MAX_QUEUE_LEN {
+        state.mixpanel_events_queue.push(event);
+        if state.installation_id.is_some() {
+            if state.mixpanel_events_queue.len() >= MAX_QUEUE_LEN {
                 drop(state);
-                self.flush();
+                self.flush_mixpanel_events();
             } else {
                 let this = self.clone();
                 let executor = self.executor.clone();
-                state.flush_task = Some(self.executor.spawn(async move {
+                state.flush_mixpanel_events_task = Some(self.executor.spawn(async move {
                     executor.timer(DEBOUNCE_INTERVAL).await;
-                    this.flush();
+                    this.flush_mixpanel_events();
                 }));
             }
         }
@@ -284,14 +368,18 @@ impl Telemetry {
         self.state.lock().metrics_id.clone()
     }
 
+    pub fn installation_id(self: &Arc<Self>) -> Option<Arc<str>> {
+        self.state.lock().installation_id.clone()
+    }
+
     pub fn is_staff(self: &Arc<Self>) -> Option<bool> {
         self.state.lock().is_staff
     }
 
-    fn flush(self: &Arc<Self>) {
+    fn flush_mixpanel_events(self: &Arc<Self>) {
         let mut state = self.state.lock();
-        let mut events = mem::take(&mut state.queue);
-        state.flush_task.take();
+        let mut events = mem::take(&mut state.mixpanel_events_queue);
+        state.flush_mixpanel_events_task.take();
         drop(state);
 
         if let Some(token) = MIXPANEL_TOKEN.as_ref() {
@@ -316,7 +404,7 @@ impl Telemetry {
                         json_bytes.clear();
                         serde_json::to_writer(&mut json_bytes, &events)?;
                         this.http_client
-                            .post_json(MIXPANEL_EVENTS_URL, json_bytes.into())
+                            .post_json(MIXPANEL_EVENTS_URL, json_bytes.into(), false)
                             .await?;
                         anyhow::Ok(())
                     }
@@ -325,4 +413,53 @@ impl Telemetry {
                 .detach();
         }
     }
+
+    fn flush_clickhouse_events(self: &Arc<Self>) {
+        let mut state = self.state.lock();
+        let mut events = mem::take(&mut state.clickhouse_events_queue);
+        state.flush_clickhouse_events_task.take();
+        drop(state);
+
+        let this = self.clone();
+        self.executor
+            .spawn(
+                async move {
+                    let mut json_bytes = Vec::new();
+
+                    if let Some(file) = &mut this.state.lock().log_file {
+                        let file = file.as_file_mut();
+                        for event in &mut events {
+                            json_bytes.clear();
+                            serde_json::to_writer(&mut json_bytes, event)?;
+                            file.write_all(&json_bytes)?;
+                            file.write(b"\n")?;
+                        }
+                    }
+
+                    {
+                        let state = this.state.lock();
+                        json_bytes.clear();
+                        serde_json::to_writer(
+                            &mut json_bytes,
+                            &ClickhouseEventRequestBody {
+                                token: ZED_SECRET_CLIENT_TOKEN,
+                                installation_id: state.installation_id.clone(),
+                                app_version: state.app_version.clone(),
+                                os_name: state.os_name,
+                                os_version: state.os_version.clone(),
+                                release_channel: state.release_channel,
+                                events,
+                            },
+                        )?;
+                    }
+
+                    this.http_client
+                        .post_json(CLICKHOUSE_EVENTS_URL.as_str(), json_bytes.into(), false)
+                        .await?;
+                    anyhow::Ok(())
+                }
+                .log_err(),
+            )
+            .detach();
+    }
 }

crates/collab_ui/src/collab_titlebar_item.rs πŸ”—

@@ -1,10 +1,9 @@
 use crate::{
-    collaborator_list_popover, collaborator_list_popover::CollaboratorListPopover,
     contact_notification::ContactNotification, contacts_popover, face_pile::FacePile,
-    ToggleScreenSharing,
+    toggle_screen_sharing, ToggleScreenSharing,
 };
 use call::{ActiveCall, ParticipantLocation, Room};
-use client::{proto::PeerId, ContactEventKind, SignIn, SignOut, User, UserStore};
+use client::{proto::PeerId, Client, ContactEventKind, SignIn, SignOut, User, UserStore};
 use clock::ReplicaId;
 use contacts_popover::ContactsPopover;
 use context_menu::{ContextMenu, ContextMenuItem};
@@ -18,6 +17,7 @@ use gpui::{
     AppContext, Entity, ImageData, ModelHandle, SceneBuilder, Subscription, View, ViewContext,
     ViewHandle, WeakViewHandle,
 };
+use project::Project;
 use settings::Settings;
 use std::{ops::Range, sync::Arc};
 use theme::{AvatarStyle, Theme};
@@ -27,7 +27,6 @@ use workspace::{FollowNextCollaborator, Workspace};
 actions!(
     collab,
     [
-        ToggleCollaboratorList,
         ToggleContactsMenu,
         ToggleUserMenu,
         ShareProject,
@@ -36,7 +35,6 @@ actions!(
 );
 
 pub fn init(cx: &mut AppContext) {
-    cx.add_action(CollabTitlebarItem::toggle_collaborator_list_popover);
     cx.add_action(CollabTitlebarItem::toggle_contacts_popover);
     cx.add_action(CollabTitlebarItem::share_project);
     cx.add_action(CollabTitlebarItem::unshare_project);
@@ -44,11 +42,12 @@ pub fn init(cx: &mut AppContext) {
 }
 
 pub struct CollabTitlebarItem {
-    workspace: WeakViewHandle<Workspace>,
+    project: ModelHandle<Project>,
     user_store: ModelHandle<UserStore>,
+    client: Arc<Client>,
+    workspace: WeakViewHandle<Workspace>,
     contacts_popover: Option<ViewHandle<ContactsPopover>>,
     user_menu: ViewHandle<ContextMenu>,
-    collaborator_list_popover: Option<ViewHandle<CollaboratorListPopover>>,
     _subscriptions: Vec<Subscription>,
 }
 
@@ -68,7 +67,7 @@ impl View for CollabTitlebarItem {
             return Empty::new().into_any();
         };
 
-        let project = workspace.read(cx).project().read(cx);
+        let project = self.project.read(cx);
         let mut project_title = String::new();
         for (i, name) in project.worktree_root_names(cx).enumerate() {
             if i > 0 {
@@ -93,8 +92,8 @@ impl View for CollabTitlebarItem {
                 .left(),
         );
 
-        let user = workspace.read(cx).user_store().read(cx).current_user();
-        let peer_id = workspace.read(cx).client().peer_id();
+        let user = self.user_store.read(cx).current_user();
+        let peer_id = self.client.peer_id();
         if let Some(((user, peer_id), room)) = user
             .zip(peer_id)
             .zip(ActiveCall::global(cx).read(cx).room().cloned())
@@ -128,13 +127,16 @@ impl View for CollabTitlebarItem {
 
 impl CollabTitlebarItem {
     pub fn new(
-        workspace: &ViewHandle<Workspace>,
-        user_store: ModelHandle<UserStore>,
+        workspace: &Workspace,
+        workspace_handle: &ViewHandle<Workspace>,
         cx: &mut ViewContext<Self>,
     ) -> Self {
+        let project = workspace.project().clone();
+        let user_store = workspace.app_state().user_store.clone();
+        let client = workspace.app_state().client.clone();
         let active_call = ActiveCall::global(cx);
         let mut subscriptions = Vec::new();
-        subscriptions.push(cx.observe(workspace, |_, _, cx| cx.notify()));
+        subscriptions.push(cx.observe(workspace_handle, |_, _, cx| cx.notify()));
         subscriptions.push(cx.observe(&active_call, |this, _, cx| this.active_call_changed(cx)));
         subscriptions.push(cx.observe_window_activation(|this, active, cx| {
             this.window_activation_changed(active, cx)
@@ -164,30 +166,29 @@ impl CollabTitlebarItem {
         );
 
         Self {
-            workspace: workspace.downgrade(),
-            user_store: user_store.clone(),
+            workspace: workspace.weak_handle(),
+            project,
+            user_store,
+            client,
             contacts_popover: None,
             user_menu: cx.add_view(|cx| {
                 let mut menu = ContextMenu::new(cx);
                 menu.set_position_mode(OverlayPositionMode::Local);
                 menu
             }),
-            collaborator_list_popover: None,
             _subscriptions: subscriptions,
         }
     }
 
     fn window_activation_changed(&mut self, active: bool, cx: &mut ViewContext<Self>) {
-        if let Some(workspace) = self.workspace.upgrade(cx) {
-            let project = if active {
-                Some(workspace.read(cx).project().clone())
-            } else {
-                None
-            };
-            ActiveCall::global(cx)
-                .update(cx, |call, cx| call.set_location(project.as_ref(), cx))
-                .detach_and_log_err(cx);
-        }
+        let project = if active {
+            Some(self.project.clone())
+        } else {
+            None
+        };
+        ActiveCall::global(cx)
+            .update(cx, |call, cx| call.set_location(project.as_ref(), cx))
+            .detach_and_log_err(cx);
     }
 
     fn active_call_changed(&mut self, cx: &mut ViewContext<Self>) {
@@ -198,71 +199,42 @@ impl CollabTitlebarItem {
     }
 
     fn share_project(&mut self, _: &ShareProject, cx: &mut ViewContext<Self>) {
-        if let Some(workspace) = self.workspace.upgrade(cx) {
-            let active_call = ActiveCall::global(cx);
-            let project = workspace.read(cx).project().clone();
-            active_call
-                .update(cx, |call, cx| call.share_project(project, cx))
-                .detach_and_log_err(cx);
-        }
+        let active_call = ActiveCall::global(cx);
+        let project = self.project.clone();
+        active_call
+            .update(cx, |call, cx| call.share_project(project, cx))
+            .detach_and_log_err(cx);
     }
 
     fn unshare_project(&mut self, _: &UnshareProject, cx: &mut ViewContext<Self>) {
-        if let Some(workspace) = self.workspace.upgrade(cx) {
-            let active_call = ActiveCall::global(cx);
-            let project = workspace.read(cx).project().clone();
-            active_call
-                .update(cx, |call, cx| call.unshare_project(project, cx))
-                .log_err();
-        }
-    }
-
-    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();
+        let active_call = ActiveCall::global(cx);
+        let project = self.project.clone();
+        active_call
+            .update(cx, |call, cx| call.unshare_project(project, cx))
+            .log_err();
     }
 
     pub fn toggle_contacts_popover(&mut self, _: &ToggleContactsMenu, cx: &mut ViewContext<Self>) {
         if self.contacts_popover.take().is_none() {
-            if let Some(workspace) = self.workspace.upgrade(cx) {
-                let view = cx.add_view(|cx| ContactsPopover::new(&workspace, cx));
-                cx.subscribe(&view, |this, _, event, cx| {
-                    match event {
-                        contacts_popover::Event::Dismissed => {
-                            this.contacts_popover = None;
-                        }
+            let view = cx.add_view(|cx| {
+                ContactsPopover::new(
+                    self.project.clone(),
+                    self.user_store.clone(),
+                    self.workspace.clone(),
+                    cx,
+                )
+            });
+            cx.subscribe(&view, |this, _, event, cx| {
+                match event {
+                    contacts_popover::Event::Dismissed => {
+                        this.contacts_popover = None;
                     }
+                }
 
-                    cx.notify();
-                })
-                .detach();
-                self.contacts_popover = Some(view);
-            }
+                cx.notify();
+            })
+            .detach();
+            self.contacts_popover = Some(view);
         }
 
         cx.notify();
@@ -357,8 +329,8 @@ impl CollabTitlebarItem {
                         .with_style(style.container)
                 })
                 .with_cursor_style(CursorStyle::PointingHand)
-                .on_click(MouseButton::Left, move |_, _, cx| {
-                    cx.dispatch_action(ToggleContactsMenu);
+                .on_click(MouseButton::Left, move |_, this, cx| {
+                    this.toggle_contacts_popover(&Default::default(), cx)
                 })
                 .with_tooltip::<ToggleContactsMenu>(
                     0,
@@ -405,7 +377,7 @@ impl CollabTitlebarItem {
         })
         .with_cursor_style(CursorStyle::PointingHand)
         .on_click(MouseButton::Left, move |_, _, cx| {
-            cx.dispatch_action(ToggleScreenSharing);
+            toggle_screen_sharing(&Default::default(), cx)
         })
         .with_tooltip::<ToggleScreenSharing>(
             0,
@@ -451,11 +423,11 @@ impl CollabTitlebarItem {
                             .with_style(style.container)
                     })
                     .with_cursor_style(CursorStyle::PointingHand)
-                    .on_click(MouseButton::Left, move |_, _, cx| {
+                    .on_click(MouseButton::Left, move |_, this, cx| {
                         if is_shared {
-                            cx.dispatch_action(UnshareProject);
+                            this.unshare_project(&Default::default(), cx);
                         } else {
-                            cx.dispatch_action(ShareProject);
+                            this.share_project(&Default::default(), cx);
                         }
                     })
                     .with_tooltip::<ShareUnshare>(
@@ -496,8 +468,8 @@ impl CollabTitlebarItem {
                         .with_style(style.container)
                 })
                 .with_cursor_style(CursorStyle::PointingHand)
-                .on_click(MouseButton::Left, move |_, _, cx| {
-                    cx.dispatch_action(ToggleUserMenu);
+                .on_click(MouseButton::Left, move |_, this, cx| {
+                    this.toggle_user_menu(&Default::default(), cx)
                 })
                 .with_tooltip::<ToggleUserMenu>(
                     0,
@@ -527,8 +499,11 @@ impl CollabTitlebarItem {
                 .with_style(style.container)
         })
         .with_cursor_style(CursorStyle::PointingHand)
-        .on_click(MouseButton::Left, move |_, _, cx| {
-            cx.dispatch_action(SignIn);
+        .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()
     }
@@ -862,7 +837,7 @@ impl CollabTitlebarItem {
                 })
                 .with_cursor_style(CursorStyle::PointingHand)
                 .on_click(MouseButton::Left, |_, _, cx| {
-                    cx.dispatch_action(auto_update::Check);
+                    auto_update::check(&Default::default(), cx);
                 })
                 .into_any(),
             ),

crates/collab_ui/src/collaborator_list_popover.rs πŸ”—

@@ -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()
-}

crates/collab_ui/src/contact_list.rs πŸ”—

@@ -1,4 +1,3 @@
-use crate::contacts_popover;
 use call::ActiveCall;
 use client::{proto::PeerId, Contact, User, UserStore};
 use editor::{Cancel, Editor};
@@ -140,6 +139,7 @@ pub struct RespondToContactRequest {
 }
 
 pub enum Event {
+    ToggleContactFinder,
     Dismissed,
 }
 
@@ -157,7 +157,12 @@ pub struct ContactList {
 }
 
 impl ContactList {
-    pub fn new(workspace: &ViewHandle<Workspace>, cx: &mut ViewContext<Self>) -> Self {
+    pub fn new(
+        project: ModelHandle<Project>,
+        user_store: ModelHandle<UserStore>,
+        workspace: WeakViewHandle<Workspace>,
+        cx: &mut ViewContext<Self>,
+    ) -> Self {
         let filter_editor = cx.add_view(|cx| {
             let mut editor = Editor::single_line(
                 Some(Arc::new(|theme| {
@@ -262,7 +267,6 @@ impl ContactList {
         });
 
         let active_call = ActiveCall::global(cx);
-        let user_store = workspace.read(cx).user_store().clone();
         let mut subscriptions = Vec::new();
         subscriptions.push(cx.observe(&user_store, |this, _, cx| this.update_entries(cx)));
         subscriptions.push(cx.observe(&active_call, |this, _, cx| this.update_entries(cx)));
@@ -275,8 +279,8 @@ impl ContactList {
             match_candidates: Default::default(),
             filter_editor,
             _subscriptions: subscriptions,
-            project: workspace.read(cx).project().clone(),
-            workspace: workspace.downgrade(),
+            project,
+            workspace,
             user_store,
         };
         this.update_entries(cx);
@@ -1116,11 +1120,14 @@ impl ContactList {
                         )
                         .with_padding(Padding::uniform(2.))
                         .with_cursor_style(CursorStyle::PointingHand)
-                        .on_click(MouseButton::Left, move |_, _, cx| {
-                            cx.dispatch_action(RemoveContact {
-                                user_id,
-                                github_login: github_login.clone(),
-                            })
+                        .on_click(MouseButton::Left, move |_, this, cx| {
+                            this.remove_contact(
+                                &RemoveContact {
+                                    user_id,
+                                    github_login: github_login.clone(),
+                                },
+                                cx,
+                            );
                         })
                         .flex_float(),
                     )
@@ -1203,11 +1210,14 @@ impl ContactList {
                     render_icon_button(button_style, "icons/x_mark_8.svg").aligned()
                 })
                 .with_cursor_style(CursorStyle::PointingHand)
-                .on_click(MouseButton::Left, move |_, _, cx| {
-                    cx.dispatch_action(RespondToContactRequest {
-                        user_id,
-                        accept: false,
-                    })
+                .on_click(MouseButton::Left, move |_, this, cx| {
+                    this.respond_to_contact_request(
+                        &RespondToContactRequest {
+                            user_id,
+                            accept: false,
+                        },
+                        cx,
+                    );
                 })
                 .contained()
                 .with_margin_right(button_spacing),
@@ -1225,11 +1235,14 @@ impl ContactList {
                         .flex_float()
                 })
                 .with_cursor_style(CursorStyle::PointingHand)
-                .on_click(MouseButton::Left, move |_, _, cx| {
-                    cx.dispatch_action(RespondToContactRequest {
-                        user_id,
-                        accept: true,
-                    })
+                .on_click(MouseButton::Left, move |_, this, cx| {
+                    this.respond_to_contact_request(
+                        &RespondToContactRequest {
+                            user_id,
+                            accept: true,
+                        },
+                        cx,
+                    );
                 }),
             );
         } else {
@@ -1246,11 +1259,14 @@ impl ContactList {
                 })
                 .with_padding(Padding::uniform(2.))
                 .with_cursor_style(CursorStyle::PointingHand)
-                .on_click(MouseButton::Left, move |_, _, cx| {
-                    cx.dispatch_action(RemoveContact {
-                        user_id,
-                        github_login: github_login.clone(),
-                    })
+                .on_click(MouseButton::Left, move |_, this, cx| {
+                    this.remove_contact(
+                        &RemoveContact {
+                            user_id,
+                            github_login: github_login.clone(),
+                        },
+                        cx,
+                    );
                 })
                 .flex_float(),
             );
@@ -1318,7 +1334,7 @@ impl View for ContactList {
                         })
                         .with_cursor_style(CursorStyle::PointingHand)
                         .on_click(MouseButton::Left, |_, _, cx| {
-                            cx.dispatch_action(contacts_popover::ToggleContactFinder)
+                            cx.emit(Event::ToggleContactFinder)
                         })
                         .with_tooltip::<AddContact>(
                             0,

crates/collab_ui/src/contacts_popover.rs πŸ”—

@@ -1,7 +1,6 @@
 use crate::{
     contact_finder::{build_contact_finder, ContactFinder},
     contact_list::ContactList,
-    ToggleContactsMenu,
 };
 use client::UserStore;
 use gpui::{
@@ -9,6 +8,7 @@ use gpui::{
     ViewContext, ViewHandle, WeakViewHandle,
 };
 use picker::PickerEvent;
+use project::Project;
 use settings::Settings;
 use workspace::Workspace;
 
@@ -29,17 +29,26 @@ enum Child {
 
 pub struct ContactsPopover {
     child: Child,
+    project: ModelHandle<Project>,
     user_store: ModelHandle<UserStore>,
     workspace: WeakViewHandle<Workspace>,
     _subscription: Option<gpui::Subscription>,
 }
 
 impl ContactsPopover {
-    pub fn new(workspace: &ViewHandle<Workspace>, cx: &mut ViewContext<Self>) -> Self {
+    pub fn new(
+        project: ModelHandle<Project>,
+        user_store: ModelHandle<UserStore>,
+        workspace: WeakViewHandle<Workspace>,
+        cx: &mut ViewContext<Self>,
+    ) -> Self {
         let mut this = Self {
-            child: Child::ContactList(cx.add_view(|cx| ContactList::new(workspace, cx))),
-            user_store: workspace.read(cx).user_store().clone(),
-            workspace: workspace.downgrade(),
+            child: Child::ContactList(cx.add_view(|cx| {
+                ContactList::new(project.clone(), user_store.clone(), workspace.clone(), cx)
+            })),
+            project,
+            user_store,
+            workspace,
             _subscription: None,
         };
         this.show_contact_list(String::new(), cx);
@@ -68,16 +77,24 @@ impl ContactsPopover {
     }
 
     fn show_contact_list(&mut self, editor_text: String, cx: &mut ViewContext<ContactsPopover>) {
-        if let Some(workspace) = self.workspace.upgrade(cx) {
-            let child = cx
-                .add_view(|cx| ContactList::new(&workspace, cx).with_editor_text(editor_text, cx));
-            cx.focus(&child);
-            self._subscription = Some(cx.subscribe(&child, |_, _, event, cx| match event {
-                crate::contact_list::Event::Dismissed => cx.emit(Event::Dismissed),
-            }));
-            self.child = Child::ContactList(child);
-            cx.notify();
-        }
+        let child = cx.add_view(|cx| {
+            ContactList::new(
+                self.project.clone(),
+                self.user_store.clone(),
+                self.workspace.clone(),
+                cx,
+            )
+            .with_editor_text(editor_text, cx)
+        });
+        cx.focus(&child);
+        self._subscription = Some(cx.subscribe(&child, |this, _, event, cx| match event {
+            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);
+        cx.notify();
     }
 }
 
@@ -106,9 +123,7 @@ impl View for ContactsPopover {
                 .with_width(theme.contacts_popover.width)
                 .with_height(theme.contacts_popover.height)
         })
-        .on_down_out(MouseButton::Left, move |_, _, cx| {
-            cx.dispatch_action(ToggleContactsMenu);
-        })
+        .on_down_out(MouseButton::Left, move |_, _, cx| cx.emit(Event::Dismissed))
         .into_any()
     }
 

crates/collab_ui/src/incoming_call_notification.rs πŸ”—

@@ -78,24 +78,26 @@ impl IncomingCallNotification {
             let join = active_call.update(cx, |active_call, cx| active_call.accept_incoming(cx));
             let caller_user_id = self.call.calling_user.id;
             let initial_project_id = self.call.initial_project.as_ref().map(|project| project.id);
-            cx.spawn(|this, mut cx| async move {
-                join.await?;
-                if let Some(project_id) = initial_project_id {
-                    this.update(&mut cx, |this, cx| {
-                        if let Some(app_state) = this.app_state.upgrade() {
-                            workspace::join_remote_project(
-                                project_id,
-                                caller_user_id,
-                                app_state,
-                                cx,
-                            )
-                            .detach_and_log_err(cx);
-                        }
-                    })?;
-                }
-                anyhow::Ok(())
-            })
-            .detach_and_log_err(cx);
+            let app_state = self.app_state.clone();
+            cx.app_context()
+                .spawn(|mut cx| async move {
+                    join.await?;
+                    if let Some(project_id) = initial_project_id {
+                        cx.update(|cx| {
+                            if let Some(app_state) = app_state.upgrade() {
+                                workspace::join_remote_project(
+                                    project_id,
+                                    caller_user_id,
+                                    app_state,
+                                    cx,
+                                )
+                                .detach_and_log_err(cx);
+                            }
+                        });
+                    }
+                    anyhow::Ok(())
+                })
+                .detach_and_log_err(cx);
         } else {
             active_call.update(cx, |active_call, _| {
                 active_call.decline_incoming().log_err();

crates/collab_ui/src/project_shared_notification.rs πŸ”—

@@ -58,14 +58,14 @@ pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
         room::Event::RemoteProjectUnshared { project_id } => {
             if let Some(window_ids) = notification_windows.remove(&project_id) {
                 for window_id in window_ids {
-                    cx.remove_window(window_id);
+                    cx.update_window(window_id, |cx| cx.remove_window());
                 }
             }
         }
         room::Event::Left => {
             for (_, window_ids) in notification_windows.drain() {
                 for window_id in window_ids {
-                    cx.remove_window(window_id);
+                    cx.update_window(window_id, |cx| cx.remove_window());
                 }
             }
         }

crates/collab_ui/src/sharing_status_indicator.rs πŸ”—

@@ -1,3 +1,4 @@
+use crate::toggle_screen_sharing;
 use call::ActiveCall;
 use gpui::{
     color::Color,
@@ -7,8 +8,6 @@ use gpui::{
 };
 use settings::Settings;
 
-use crate::ToggleScreenSharing;
-
 pub fn init(cx: &mut AppContext) {
     let active_call = ActiveCall::global(cx);
 
@@ -20,10 +19,10 @@ pub fn init(cx: &mut AppContext) {
                     status_indicator = Some(cx.add_status_bar_item(|_| SharingStatusIndicator));
                 }
             } else if let Some((window_id, _)) = status_indicator.take() {
-                cx.remove_status_bar_item(window_id);
+                cx.update_window(window_id, |cx| cx.remove_window());
             }
         } else if let Some((window_id, _)) = status_indicator.take() {
-            cx.remove_status_bar_item(window_id);
+            cx.update_window(window_id, |cx| cx.remove_window());
         }
     })
     .detach();
@@ -54,7 +53,7 @@ impl View for SharingStatusIndicator {
                 .aligned()
         })
         .on_click(MouseButton::Left, |_, _, cx| {
-            cx.dispatch_action(ToggleScreenSharing);
+            toggle_screen_sharing(&Default::default(), cx)
         })
         .into_any()
     }

crates/command_palette/src/command_palette.rs πŸ”—

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

crates/context_menu/src/context_menu.rs πŸ”—

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

crates/copilot/src/copilot.rs πŸ”—

@@ -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
             .spawn({
                 let http = self.http.clone();
@@ -932,7 +932,7 @@ async fn get_copilot_lsp(http: Arc<dyn HttpClient>) -> anyhow::Result<PathBuf> {
 
     ///Check for the latest copilot language server and download it if we haven't already
     async fn fetch_latest(http: Arc<dyn HttpClient>) -> anyhow::Result<PathBuf> {
-        let release = latest_github_release("zed-industries/copilot", http.clone()).await?;
+        let release = latest_github_release("zed-industries/copilot", false, http.clone()).await?;
 
         let version_dir = &*paths::COPILOT_DIR.join(format!("copilot-{}", release.name));
 

crates/copilot/src/sign_in.rs πŸ”—

@@ -27,14 +27,13 @@ pub fn init(cx: &mut AppContext) {
                 crate::Status::SigningIn { prompt } => {
                     if let Some(code_verification_handle) = code_verification.as_mut() {
                         let window_id = code_verification_handle.window_id();
-                        if cx.has_window(window_id) {
-                            cx.update_window(window_id, |cx| {
-                                code_verification_handle.update(cx, |code_verification, cx| {
-                                    code_verification.set_status(status, cx)
-                                });
-                                cx.activate_window();
+                        let updated = cx.update_window(window_id, |cx| {
+                            code_verification_handle.update(cx, |code_verification, cx| {
+                                code_verification.set_status(status.clone(), cx)
                             });
-                        } else {
+                            cx.activate_window();
+                        });
+                        if updated.is_none() {
                             code_verification = Some(create_copilot_auth_window(cx, &status));
                         }
                     } else if let Some(_prompt) = prompt {
@@ -56,7 +55,7 @@ pub fn init(cx: &mut AppContext) {
                 }
                 _ => {
                     if let Some(code_verification) = code_verification.take() {
-                        cx.remove_window(code_verification.window_id());
+                        cx.update_window(code_verification.window_id(), |cx| cx.remove_window());
                     }
                 }
             }
@@ -196,7 +195,7 @@ impl CopilotCodeVerification {
                     .contained()
                     .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 {
                     "Waiting for connection..."
                 } else {
@@ -250,7 +249,7 @@ impl CopilotCodeVerification {
                     .contained()
                     .with_style(enabled_style.hint.container),
             )
-            .with_child(theme::ui::cta_button_with_click::<DoneButton, _, _, _>(
+            .with_child(theme::ui::cta_button::<DoneButton, _, _, _>(
                 "Done",
                 style.auth.content_width,
                 &style.auth.cta_button,
@@ -304,7 +303,7 @@ impl CopilotCodeVerification {
                     .contained()
                     .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",
                 style.auth.content_width,
                 &style.auth.cta_button,

crates/copilot_button/src/copilot_button.rs πŸ”—

@@ -1,6 +1,6 @@
 use anyhow::Result;
 use context_menu::{ContextMenu, ContextMenuItem};
-use copilot::{Copilot, Reinstall, SignOut, Status};
+use copilot::{Copilot, SignOut, Status};
 use editor::{scroll::autoscroll::Autoscroll, Editor};
 use gpui::{
     elements::*,
@@ -13,7 +13,7 @@ use std::{path::Path, sync::Arc};
 use util::{paths, ResultExt};
 use workspace::{
     create_and_open_local_file, item::ItemHandle,
-    notifications::simple_message_notification::OsOpen, AppState, StatusItemView, Toast, Workspace,
+    notifications::simple_message_notification::OsOpen, StatusItemView, Toast, Workspace,
 };
 
 const COPILOT_SETTINGS_URL: &str = "https://github.com/settings/copilot";
@@ -21,7 +21,6 @@ const COPILOT_STARTING_TOAST_ID: usize = 1337;
 const COPILOT_ERROR_TOAST_ID: usize = 1338;
 
 pub struct CopilotButton {
-    app_state: Arc<AppState>,
     popup_menu: ViewHandle<ContextMenu>,
     editor_subscription: Option<(Subscription, usize)>,
     editor_enabled: Option<bool>,
@@ -106,11 +105,21 @@ impl View for CopilotButton {
                             {
                                 workspace.update(cx, |workspace, cx| {
                                     workspace.show_toast(
-                                        Toast::new_action(
+                                        Toast::new(
                                             COPILOT_ERROR_TOAST_ID,
                                             format!("Copilot can't be started: {}", e),
+                                        )
+                                        .on_click(
                                             "Reinstall Copilot",
-                                            Reinstall,
+                                            |cx| {
+                                                if let Some(copilot) = Copilot::global(cx) {
+                                                    copilot
+                                                        .update(cx, |copilot, cx| {
+                                                            copilot.reinstall(cx)
+                                                        })
+                                                        .detach();
+                                                }
+                                            },
                                         ),
                                         cx,
                                     );
@@ -134,7 +143,7 @@ impl View for CopilotButton {
 }
 
 impl CopilotButton {
-    pub fn new(app_state: Arc<AppState>, cx: &mut ViewContext<Self>) -> Self {
+    pub fn new(cx: &mut ViewContext<Self>) -> Self {
         let menu = cx.add_view(|cx| {
             let mut menu = ContextMenu::new(cx);
             menu.set_position_mode(OverlayPositionMode::Local);
@@ -149,7 +158,6 @@ impl CopilotButton {
             .detach();
 
         Self {
-            app_state,
             popup_menu: menu,
             editor_subscription: None,
             editor_enabled: None,
@@ -197,7 +205,6 @@ impl CopilotButton {
 
         if let Some(path) = self.path.as_ref() {
             let path_enabled = settings.copilot_enabled_for_path(path);
-            let app_state = Arc::downgrade(&self.app_state);
             let path = path.clone();
             menu_options.push(ContextMenuItem::handler(
                 format!(
@@ -205,17 +212,11 @@ impl CopilotButton {
                     if path_enabled { "Hide" } else { "Show" }
                 ),
                 move |cx| {
-                    if let Some((workspace, app_state)) = cx
-                        .root_view()
-                        .clone()
-                        .downcast::<Workspace>()
-                        .zip(app_state.upgrade())
-                    {
+                    if let Some(workspace) = cx.root_view().clone().downcast::<Workspace>() {
                         let workspace = workspace.downgrade();
                         cx.spawn(|_, cx| {
                             configure_disabled_globs(
                                 workspace,
-                                app_state,
                                 path_enabled.then_some(path.clone()),
                                 cx,
                             )
@@ -302,13 +303,12 @@ impl StatusItemView for CopilotButton {
 
 async fn configure_disabled_globs(
     workspace: WeakViewHandle<Workspace>,
-    app_state: Arc<AppState>,
     path_to_disable: Option<Arc<Path>>,
     mut cx: AsyncAppContext,
 ) -> Result<()> {
     let settings_editor = workspace
         .update(&mut cx, |_, cx| {
-            create_and_open_local_file(&paths::SETTINGS, app_state, cx, || {
+            create_and_open_local_file(&paths::SETTINGS, cx, || {
                 Settings::initial_user_settings_content(&assets::Assets)
                     .as_ref()
                     .into()

crates/diagnostics/src/diagnostics.rs πŸ”—

@@ -677,7 +677,7 @@ impl Item for ProjectDiagnosticsEditor {
 }
 
 fn diagnostic_header_renderer(diagnostic: Diagnostic) -> RenderBlock {
-    let (message, highlights) = highlight_diagnostic_message(&diagnostic.message);
+    let (message, highlights) = highlight_diagnostic_message(Vec::new(), &diagnostic.message);
     Arc::new(move |cx| {
         let settings = cx.global::<Settings>();
         let theme = &settings.theme.editor;
@@ -697,8 +697,18 @@ fn diagnostic_header_renderer(diagnostic: Diagnostic) -> RenderBlock {
                 icon.constrained()
                     .with_width(icon_width)
                     .aligned()
-                    .contained(),
+                    .contained()
+                    .with_margin_right(cx.gutter_padding),
             )
+            .with_children(diagnostic.source.as_ref().map(|source| {
+                Label::new(
+                    format!("{source}: "),
+                    style.source.label.clone().with_font_size(font_size),
+                )
+                .contained()
+                .with_style(style.message.container)
+                .aligned()
+            }))
             .with_child(
                 Label::new(
                     message.clone(),
@@ -707,7 +717,6 @@ fn diagnostic_header_renderer(diagnostic: Diagnostic) -> RenderBlock {
                 .with_highlights(highlights.clone())
                 .contained()
                 .with_style(style.message.container)
-                .with_margin_left(cx.gutter_padding)
                 .aligned(),
             )
             .with_children(diagnostic.code.clone().map(|code| {

crates/diagnostics/src/items.rs πŸ”—

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

crates/editor/Cargo.toml πŸ”—

@@ -23,6 +23,7 @@ test-support = [
 ]
 
 [dependencies]
+client = { path = "../client" }
 clock = { path = "../clock" }
 copilot = { path = "../copilot" }
 db = { path = "../db" }

crates/editor/src/editor.rs πŸ”—

@@ -22,6 +22,7 @@ pub mod test;
 use aho_corasick::AhoCorasick;
 use anyhow::{anyhow, Result};
 use blink_manager::BlinkManager;
+use client::ClickhouseEvent;
 use clock::ReplicaId;
 use collections::{BTreeMap, Bound, HashMap, HashSet, VecDeque};
 use copilot::Copilot;
@@ -51,8 +52,8 @@ use itertools::Itertools;
 pub use language::{char_kind, CharKind};
 use language::{
     AutoindentMode, BracketPair, Buffer, CodeAction, CodeLabel, Completion, CursorShape,
-    Diagnostic, DiagnosticSeverity, IndentKind, IndentSize, Language, OffsetRangeExt, OffsetUtf16,
-    Point, Selection, SelectionGoal, TransactionId,
+    Diagnostic, DiagnosticSeverity, File, IndentKind, IndentSize, Language, OffsetRangeExt,
+    OffsetUtf16, Point, Selection, SelectionGoal, TransactionId,
 };
 use link_go_to_definition::{
     hide_link_definition, show_link_definition, LinkDefinitionKind, LinkGoToDefinitionState,
@@ -808,10 +809,13 @@ impl CompletionsMenu {
                             },
                         )
                         .with_cursor_style(CursorStyle::PointingHand)
-                        .on_down(MouseButton::Left, move |_, _, cx| {
-                            cx.dispatch_action(ConfirmCompletion {
-                                item_ix: Some(item_ix),
-                            });
+                        .on_down(MouseButton::Left, move |_, this, cx| {
+                            this.confirm_completion(
+                                &ConfirmCompletion {
+                                    item_ix: Some(item_ix),
+                                },
+                                cx,
+                            );
                         })
                         .into_any(),
                     );
@@ -969,9 +973,23 @@ impl CodeActionsMenu {
                                 .with_style(item_style)
                         })
                         .with_cursor_style(CursorStyle::PointingHand)
-                        .on_down(MouseButton::Left, move |_, _, cx| {
-                            cx.dispatch_action(ConfirmCodeAction {
-                                item_ix: Some(item_ix),
+                        .on_down(MouseButton::Left, move |_, this, cx| {
+                            let workspace = this
+                                .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(),
@@ -1295,7 +1313,7 @@ impl Editor {
             cx.set_global(ScrollbarAutoHide(should_auto_hide_scrollbars));
         }
 
-        this.report_event("open editor", cx);
+        this.report_editor_event("open", cx);
         this
     }
 
@@ -1330,6 +1348,10 @@ impl Editor {
         &self.buffer
     }
 
+    fn workspace(&self, cx: &AppContext) -> Option<ViewHandle<Workspace>> {
+        self.workspace.as_ref()?.0.upgrade(cx)
+    }
+
     pub fn title<'a>(&self, cx: &'a AppContext) -> Cow<'a, str> {
         self.buffer().read(cx).title(cx)
     }
@@ -1356,6 +1378,10 @@ impl Editor {
         self.buffer.read(cx).language_at(point, cx)
     }
 
+    pub fn file_at<'a, T: ToOffset>(&self, point: T, cx: &'a AppContext) -> Option<Arc<dyn File>> {
+        self.buffer.read(cx).read(cx).file_at(point).cloned()
+    }
+
     pub fn active_excerpt(
         &self,
         cx: &AppContext,
@@ -3148,10 +3174,13 @@ impl Editor {
                 })
                 .with_cursor_style(CursorStyle::PointingHand)
                 .with_padding(Padding::uniform(3.))
-                .on_down(MouseButton::Left, |_, _, cx| {
-                    cx.dispatch_action(ToggleCodeActions {
-                        deployed_from_indicator: true,
-                    });
+                .on_down(MouseButton::Left, |_, this, cx| {
+                    this.toggle_code_actions(
+                        &ToggleCodeActions {
+                            deployed_from_indicator: true,
+                        },
+                        cx,
+                    );
                 })
                 .into_any(),
             )
@@ -3209,11 +3238,13 @@ impl Editor {
                             .with_cursor_style(CursorStyle::PointingHand)
                             .with_padding(Padding::uniform(3.))
                             .on_click(MouseButton::Left, {
-                                move |_, _, cx| {
-                                    cx.dispatch_any_action(match fold_status {
-                                        FoldStatus::Folded => Box::new(UnfoldAt { buffer_row }),
-                                        FoldStatus::Foldable => Box::new(FoldAt { buffer_row }),
-                                    });
+                                move |_, editor, cx| match fold_status {
+                                    FoldStatus::Folded => {
+                                        editor.unfold_at(&UnfoldAt { buffer_row }, cx);
+                                    }
+                                    FoldStatus::Foldable => {
+                                        editor.fold_at(&FoldAt { buffer_row }, cx);
+                                    }
                                 }
                             })
                             .into_any()
@@ -5572,93 +5603,77 @@ impl Editor {
         }
     }
 
-    pub fn go_to_definition(
-        workspace: &mut Workspace,
-        _: &GoToDefinition,
-        cx: &mut ViewContext<Workspace>,
-    ) {
-        Self::go_to_definition_of_kind(GotoDefinitionKind::Symbol, workspace, cx);
+    pub fn go_to_definition(&mut self, _: &GoToDefinition, cx: &mut ViewContext<Self>) {
+        self.go_to_definition_of_kind(GotoDefinitionKind::Symbol, cx);
     }
 
-    pub fn go_to_type_definition(
-        workspace: &mut Workspace,
-        _: &GoToTypeDefinition,
-        cx: &mut ViewContext<Workspace>,
-    ) {
-        Self::go_to_definition_of_kind(GotoDefinitionKind::Type, workspace, cx);
+    pub fn go_to_type_definition(&mut self, _: &GoToTypeDefinition, cx: &mut ViewContext<Self>) {
+        self.go_to_definition_of_kind(GotoDefinitionKind::Type, cx);
     }
 
-    fn go_to_definition_of_kind(
-        kind: GotoDefinitionKind,
-        workspace: &mut Workspace,
-        cx: &mut ViewContext<Workspace>,
-    ) {
-        let active_item = workspace.active_item(cx);
-        let editor_handle = if let Some(editor) = active_item
-            .as_ref()
-            .and_then(|item| item.act_as::<Self>(cx))
-        {
-            editor
-        } else {
-            return;
-        };
-
-        let editor = editor_handle.read(cx);
-        let buffer = editor.buffer.read(cx);
-        let head = editor.selections.newest::<usize>(cx).head();
+    fn go_to_definition_of_kind(&mut self, kind: GotoDefinitionKind, cx: &mut ViewContext<Self>) {
+        let Some(workspace) = self.workspace(cx) else { return };
+        let buffer = self.buffer.read(cx);
+        let head = self.selections.newest::<usize>(cx).head();
         let (buffer, head) = if let Some(text_anchor) = buffer.text_anchor_for_position(head, cx) {
             text_anchor
         } else {
             return;
         };
 
-        let project = workspace.project().clone();
+        let project = workspace.read(cx).project().clone();
         let definitions = project.update(cx, |project, cx| match kind {
             GotoDefinitionKind::Symbol => project.definition(&buffer, head, cx),
             GotoDefinitionKind::Type => project.type_definition(&buffer, head, cx),
         });
 
-        cx.spawn_labeled("Fetching Definition...", |workspace, mut cx| async move {
+        cx.spawn_labeled("Fetching Definition...", |editor, mut cx| async move {
             let definitions = definitions.await?;
-            workspace.update(&mut cx, |workspace, cx| {
-                Editor::navigate_to_definitions(workspace, editor_handle, definitions, cx);
+            editor.update(&mut cx, |editor, cx| {
+                editor.navigate_to_definitions(definitions, cx);
             })?;
-
             Ok::<(), anyhow::Error>(())
         })
         .detach_and_log_err(cx);
     }
 
     pub fn navigate_to_definitions(
-        workspace: &mut Workspace,
-        editor_handle: ViewHandle<Editor>,
-        definitions: Vec<LocationLink>,
-        cx: &mut ViewContext<Workspace>,
+        &mut self,
+        mut definitions: Vec<LocationLink>,
+        cx: &mut ViewContext<Editor>,
     ) {
-        let pane = workspace.active_pane().clone();
+        let Some(workspace) = self.workspace(cx) else { return };
+        let pane = workspace.read(cx).active_pane().clone();
         // If there is one definition, just open it directly
-        if let [definition] = definitions.as_slice() {
+        if definitions.len() == 1 {
+            let definition = definitions.pop().unwrap();
             let range = definition
                 .target
                 .range
                 .to_offset(definition.target.buffer.read(cx));
 
-            let target_editor_handle =
-                workspace.open_project_item(definition.target.buffer.clone(), cx);
-            target_editor_handle.update(cx, |target_editor, cx| {
-                // When selecting a definition in a different buffer, disable the nav history
-                // to avoid creating a history entry at the previous cursor location.
-                if editor_handle != target_editor_handle {
-                    pane.update(cx, |pane, _| pane.disable_history());
-                }
-                target_editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
+            if Some(&definition.target.buffer) == self.buffer.read(cx).as_singleton().as_ref() {
+                self.change_selections(Some(Autoscroll::fit()), cx, |s| {
                     s.select_ranges([range]);
                 });
-
-                pane.update(cx, |pane, _| pane.enable_history());
-            });
+            } else {
+                cx.window_context().defer(move |cx| {
+                    let target_editor: ViewHandle<Self> = workspace.update(cx, |workspace, cx| {
+                        workspace.open_project_item(definition.target.buffer.clone(), cx)
+                    });
+                    target_editor.update(cx, |target_editor, cx| {
+                        // When selecting a definition in a different buffer, disable the nav history
+                        // to avoid creating a history entry at the previous cursor location.
+                        pane.update(cx, |pane, _| pane.disable_history());
+                        target_editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
+                            s.select_ranges([range]);
+                        });
+                        pane.update(cx, |pane, _| pane.enable_history());
+                    });
+                });
+            }
         } else if !definitions.is_empty() {
-            let replica_id = editor_handle.read(cx).replica_id(cx);
+            let replica_id = self.replica_id(cx);
             let title = definitions
                 .iter()
                 .find(|definition| definition.origin.is_some())
@@ -5678,7 +5693,9 @@ impl Editor {
                 .into_iter()
                 .map(|definition| definition.target)
                 .collect();
-            Self::open_locations_in_multibuffer(workspace, locations, replica_id, title, cx)
+            workspace.update(cx, |workspace, cx| {
+                Self::open_locations_in_multibuffer(workspace, locations, replica_id, title, cx)
+            })
         }
     }
 
@@ -6834,7 +6851,7 @@ impl Editor {
             .collect()
     }
 
-    fn report_event(&self, name: &str, cx: &AppContext) {
+    fn report_editor_event(&self, name: &'static str, cx: &AppContext) {
         if let Some((project, file)) = self.project.as_ref().zip(
             self.buffer
                 .read(cx)
@@ -6846,11 +6863,31 @@ impl Editor {
             let extension = Path::new(file.file_name(cx))
                 .extension()
                 .and_then(|e| e.to_str());
-            project.read(cx).client().report_event(
-                name,
-                json!({ "File Extension": extension, "Vim Mode": settings.vim_mode  }),
+            let telemetry = project.read(cx).client().telemetry().clone();
+            telemetry.report_mixpanel_event(
+                match name {
+                    "open" => "open editor",
+                    "save" => "save editor",
+                    _ => name,
+                },
+                json!({ "File Extension": extension, "Vim Mode": settings.vim_mode, "In Clickhouse": true  }),
                 settings.telemetry(),
             );
+            let event = ClickhouseEvent::Editor {
+                file_extension: extension.map(ToString::to_string),
+                vim_mode: settings.vim_mode,
+                operation: name,
+                copilot_enabled: settings.features.copilot,
+                copilot_enabled_for_language: settings.show_copilot_suggestions(
+                    self.language_at(0, cx)
+                        .map(|language| language.name())
+                        .as_deref(),
+                    self.file_at(0, cx)
+                        .map(|file| file.path().clone())
+                        .as_deref(),
+                ),
+            };
+            telemetry.report_clickhouse_event(event, settings.telemetry())
         }
     }
 
@@ -7494,8 +7531,16 @@ impl Deref for EditorStyle {
 
 pub fn diagnostic_block_renderer(diagnostic: Diagnostic, is_valid: bool) -> RenderBlock {
     let mut highlighted_lines = Vec::new();
-    for line in diagnostic.message.lines() {
-        highlighted_lines.push(highlight_diagnostic_message(line));
+    for (index, line) in diagnostic.message.lines().enumerate() {
+        let line = match &diagnostic.source {
+            Some(source) if index == 0 => {
+                let source_highlight = Vec::from_iter(0..source.len());
+                highlight_diagnostic_message(source_highlight, &format!("{source}: {line}"))
+            }
+
+            _ => highlight_diagnostic_message(Vec::new(), line),
+        };
+        highlighted_lines.push(line);
     }
 
     Arc::new(move |cx: &mut BlockContext| {
@@ -7519,11 +7564,14 @@ pub fn diagnostic_block_renderer(diagnostic: Diagnostic, is_valid: bool) -> Rend
     })
 }
 
-pub fn highlight_diagnostic_message(message: &str) -> (String, Vec<usize>) {
+pub fn highlight_diagnostic_message(
+    inital_highlights: Vec<usize>,
+    message: &str,
+) -> (String, Vec<usize>) {
     let mut message_without_backticks = String::new();
     let mut prev_offset = 0;
     let mut inside_block = false;
-    let mut highlights = Vec::new();
+    let mut highlights = inital_highlights;
     for (match_ix, (offset, _)) in message
         .match_indices('`')
         .chain([(message.len(), "")])

crates/editor/src/element.rs πŸ”—

@@ -211,10 +211,13 @@ impl EditorElement {
         enum GutterHandlers {}
         scene.push_mouse_region(
             MouseRegion::new::<GutterHandlers>(cx.view_id(), cx.view_id() + 1, gutter_bounds)
-                .on_hover(|hover, _: &mut Editor, cx| {
-                    cx.dispatch_action(GutterHover {
-                        hovered: hover.started,
-                    })
+                .on_hover(|hover, editor: &mut Editor, cx| {
+                    editor.gutter_hover(
+                        &GutterHover {
+                            hovered: hover.started,
+                        },
+                        cx,
+                    );
                 }),
         )
     }
@@ -309,25 +312,17 @@ impl EditorElement {
             editor.select(SelectPhase::End, cx);
         }
 
-        if let Some(workspace) = editor
-            .workspace
-            .as_ref()
-            .and_then(|(workspace, _)| workspace.upgrade(cx))
-        {
-            if !pending_nonempty_selections && cmd && text_bounds.contains_point(position) {
-                let (point, target_point) = position_map.point_for_position(text_bounds, position);
-
-                if point == target_point {
-                    workspace.update(cx, |workspace, cx| {
-                        if shift {
-                            go_to_fetched_type_definition(workspace, point, cx);
-                        } else {
-                            go_to_fetched_definition(workspace, point, cx);
-                        }
-                    });
+        if !pending_nonempty_selections && cmd && text_bounds.contains_point(position) {
+            let (point, target_point) = position_map.point_for_position(text_bounds, position);
 
-                    return true;
+            if point == target_point {
+                if shift {
+                    go_to_fetched_type_definition(editor, point, cx);
+                } else {
+                    go_to_fetched_definition(editor, point, cx);
                 }
+
+                return true;
             }
         }
 
@@ -762,8 +757,8 @@ impl EditorElement {
 
                 scene.push_mouse_region(
                     MouseRegion::new::<FoldMarkers>(cx.view_id(), *id as usize, bound)
-                        .on_click(MouseButton::Left, move |_, _: &mut Editor, cx| {
-                            cx.dispatch_action(UnfoldAt { buffer_row })
+                        .on_click(MouseButton::Left, move |_, editor: &mut Editor, cx| {
+                            editor.unfold_at(&UnfoldAt { buffer_row }, cx)
                         })
                         .with_notify_on_hover(true)
                         .with_notify_on_click(true),

crates/editor/src/hover_popover.rs πŸ”—

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

crates/editor/src/items.rs πŸ”—

@@ -636,7 +636,7 @@ impl Item for Editor {
         project: ModelHandle<Project>,
         cx: &mut ViewContext<Self>,
     ) -> Task<Result<()>> {
-        self.report_event("save editor", cx);
+        self.report_editor_event("save", cx);
         let format = self.perform_format(project.clone(), FormatTrigger::Save, cx);
         let buffers = self.buffer().clone().read(cx).all_buffers();
         cx.spawn(|_, mut cx| async move {
@@ -1,15 +1,11 @@
 use std::ops::Range;
 
+use crate::{Anchor, DisplayPoint, Editor, EditorSnapshot, SelectPhase};
 use gpui::{Task, ViewContext};
 use language::{Bias, ToOffset};
 use project::LocationLink;
 use settings::Settings;
 use util::TryFutureExt;
-use workspace::Workspace;
-
-use crate::{
-    Anchor, DisplayPoint, Editor, EditorSnapshot, GoToDefinition, GoToTypeDefinition, SelectPhase,
-};
 
 #[derive(Debug, Default)]
 pub struct LinkGoToDefinitionState {
@@ -250,70 +246,51 @@ pub fn hide_link_definition(editor: &mut Editor, cx: &mut ViewContext<Editor>) {
 }
 
 pub fn go_to_fetched_definition(
-    workspace: &mut Workspace,
+    editor: &mut Editor,
     point: DisplayPoint,
-    cx: &mut ViewContext<Workspace>,
+    cx: &mut ViewContext<Editor>,
 ) {
-    go_to_fetched_definition_of_kind(LinkDefinitionKind::Symbol, workspace, point, cx);
+    go_to_fetched_definition_of_kind(LinkDefinitionKind::Symbol, editor, point, cx);
 }
 
 pub fn go_to_fetched_type_definition(
-    workspace: &mut Workspace,
+    editor: &mut Editor,
     point: DisplayPoint,
-    cx: &mut ViewContext<Workspace>,
+    cx: &mut ViewContext<Editor>,
 ) {
-    go_to_fetched_definition_of_kind(LinkDefinitionKind::Type, workspace, point, cx);
+    go_to_fetched_definition_of_kind(LinkDefinitionKind::Type, editor, point, cx);
 }
 
 fn go_to_fetched_definition_of_kind(
     kind: LinkDefinitionKind,
-    workspace: &mut Workspace,
+    editor: &mut Editor,
     point: DisplayPoint,
-    cx: &mut ViewContext<Workspace>,
+    cx: &mut ViewContext<Editor>,
 ) {
-    let active_item = workspace.active_item(cx);
-    let editor_handle = if let Some(editor) = active_item
-        .as_ref()
-        .and_then(|item| item.act_as::<Editor>(cx))
-    {
-        editor
-    } else {
-        return;
-    };
-
-    let (cached_definitions, cached_definitions_kind) = editor_handle.update(cx, |editor, cx| {
-        let definitions = editor.link_go_to_definition_state.definitions.clone();
-        hide_link_definition(editor, cx);
-        (definitions, editor.link_go_to_definition_state.kind)
-    });
+    let cached_definitions = editor.link_go_to_definition_state.definitions.clone();
+    hide_link_definition(editor, cx);
+    let cached_definitions_kind = editor.link_go_to_definition_state.kind;
 
     let is_correct_kind = cached_definitions_kind == Some(kind);
     if !cached_definitions.is_empty() && is_correct_kind {
-        editor_handle.update(cx, |editor, cx| {
-            if !editor.focused {
-                cx.focus_self();
-            }
-        });
+        if !editor.focused {
+            cx.focus_self();
+        }
 
-        Editor::navigate_to_definitions(workspace, editor_handle, cached_definitions, cx);
+        editor.navigate_to_definitions(cached_definitions, cx);
     } else {
-        editor_handle.update(cx, |editor, cx| {
-            editor.select(
-                SelectPhase::Begin {
-                    position: point,
-                    add: false,
-                    click_count: 1,
-                },
-                cx,
-            );
-        });
+        editor.select(
+            SelectPhase::Begin {
+                position: point,
+                add: false,
+                click_count: 1,
+            },
+            cx,
+        );
 
         match kind {
-            LinkDefinitionKind::Symbol => Editor::go_to_definition(workspace, &GoToDefinition, cx),
-
-            LinkDefinitionKind::Type => {
-                Editor::go_to_type_definition(workspace, &GoToTypeDefinition, cx)
-            }
+            LinkDefinitionKind::Symbol => editor.go_to_definition(&Default::default(), cx),
+            LinkDefinitionKind::Type => editor.go_to_type_definition(&Default::default(), cx),
         }
     }
 }
@@ -426,8 +403,8 @@ mod tests {
                 ])))
             });
 
-        cx.update_workspace(|workspace, cx| {
-            go_to_fetched_type_definition(workspace, hover_point, cx);
+        cx.update_editor(|editor, cx| {
+            go_to_fetched_type_definition(editor, hover_point, cx);
         });
         requests.next().await;
         cx.foreground().run_until_parked();
@@ -635,8 +612,8 @@ mod tests {
         "});
 
         // Cmd click with existing definition doesn't re-request and dismisses highlight
-        cx.update_workspace(|workspace, cx| {
-            go_to_fetched_definition(workspace, hover_point, cx);
+        cx.update_editor(|editor, cx| {
+            go_to_fetched_definition(editor, hover_point, cx);
         });
         // Assert selection moved to to definition
         cx.lsp
@@ -676,8 +653,8 @@ mod tests {
                 },
             ])))
         });
-        cx.update_workspace(|workspace, cx| {
-            go_to_fetched_definition(workspace, hover_point, cx);
+        cx.update_editor(|editor, cx| {
+            go_to_fetched_definition(editor, hover_point, cx);
         });
         requests.next().await;
         cx.foreground().run_until_parked();

crates/feedback/Cargo.toml πŸ”—

@@ -11,25 +11,27 @@ path = "src/feedback.rs"
 test-support = []
 
 [dependencies]
-anyhow.workspace = true
 client = { path = "../client" }
 editor = { path = "../editor" }
 language = { path = "../language" }
+gpui = { path = "../gpui" }
+project = { path = "../project" }
+search = { path = "../search" }
+settings = { path = "../settings" }
+theme = { path = "../theme" }
+util = { path = "../util" }
+workspace = { path = "../workspace" }
+
 log.workspace = true
 futures.workspace = true
-gpui = { path = "../gpui" }
+anyhow.workspace = true
+smallvec.workspace = true
 human_bytes = "0.4.1"
 isahc = "1.7"
 lazy_static.workspace = true
 postage.workspace = true
-project = { path = "../project" }
-search = { path = "../search" }
 serde.workspace = true
 serde_derive.workspace = true
-settings = { path = "../settings" }
 sysinfo = "0.27.1"
-theme = { path = "../theme" }
 tree-sitter-markdown = { git = "https://github.com/MDeiml/tree-sitter-markdown", rev = "330ecab87a3e3a7211ac69bbadc19eabecdb1cca" }
 urlencoding = "2.1.2"
-util = { path = "../util" }
-workspace = { path = "../workspace" }

crates/feedback/src/deploy_feedback_button.rs πŸ”—

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

crates/feedback/src/feedback.rs πŸ”—

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

crates/feedback/src/feedback_editor.rs πŸ”—

@@ -1,10 +1,4 @@
-use std::{
-    any::TypeId,
-    borrow::Cow,
-    ops::{Range, RangeInclusive},
-    sync::Arc,
-};
-
+use crate::system_specs::SystemSpecs;
 use anyhow::bail;
 use client::{Client, ZED_SECRET_CLIENT_TOKEN, ZED_SERVER_URL};
 use editor::{Anchor, Editor};
@@ -19,46 +13,41 @@ use gpui::{
 use isahc::Request;
 use language::Buffer;
 use postage::prelude::Stream;
-
 use project::Project;
 use serde::Serialize;
+use smallvec::SmallVec;
+use std::{
+    any::TypeId,
+    borrow::Cow,
+    ops::{Range, RangeInclusive},
+    sync::Arc,
+};
 use util::ResultExt;
 use workspace::{
-    item::{Item, ItemHandle},
+    item::{Item, ItemEvent, ItemHandle},
     searchable::{SearchableItem, SearchableItemHandle},
-    AppState, Workspace,
+    Workspace,
 };
 
-use crate::{submit_feedback_button::SubmitFeedbackButton, system_specs::SystemSpecs};
-
 const FEEDBACK_CHAR_LIMIT: RangeInclusive<usize> = 10..=5000;
 const FEEDBACK_SUBMISSION_ERROR_TEXT: &str =
     "Feedback failed to submit, see error log for details.";
 
 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({
         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)]
 struct FeedbackRequestBody<'a> {
     feedback_text: &'a str,
     metrics_id: Option<Arc<str>>,
+    installation_id: Option<Arc<str>>,
     system_specs: SystemSpecs,
     is_staff: bool,
     token: &'a str,
@@ -94,7 +83,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_char_count = feedback_text.chars().count();
         let feedback_text = feedback_text.trim().to_string();
@@ -133,10 +122,8 @@ impl FeedbackEditor {
             if answer == Some(0) {
                 match FeedbackEditor::submit_feedback(&feedback_text, client, specs).await {
                     Ok(_) => {
-                        this.update(&mut cx, |_, cx| {
-                            cx.dispatch_action(workspace::CloseActiveItem);
-                        })
-                        .log_err();
+                        this.update(&mut cx, |_, cx| cx.emit(editor::Event::Closed))
+                            .log_err();
                     }
                     Err(error) => {
                         log::error!("{}", error);
@@ -164,13 +151,16 @@ impl FeedbackEditor {
     ) -> anyhow::Result<()> {
         let feedback_endpoint = format!("{}/api/feedback", *ZED_SERVER_URL);
 
-        let metrics_id = zed_client.metrics_id();
-        let is_staff = zed_client.is_staff();
+        let telemetry = zed_client.telemetry();
+        let metrics_id = telemetry.metrics_id();
+        let installation_id = telemetry.installation_id();
+        let is_staff = telemetry.is_staff();
         let http_client = zed_client.http_client();
 
         let request = FeedbackRequestBody {
             feedback_text: &feedback_text,
             metrics_id,
+            installation_id,
             system_specs,
             is_staff: is_staff.unwrap_or(false),
             token: ZED_SECRET_CLIENT_TOKEN,
@@ -197,22 +187,21 @@ impl FeedbackEditor {
 }
 
 impl FeedbackEditor {
-    pub fn deploy(
-        system_specs: SystemSpecs,
-        _: &mut Workspace,
-        app_state: Arc<AppState>,
-        cx: &mut ViewContext<Workspace>,
-    ) {
-        let markdown = app_state.languages.language_for_name("Markdown");
+    pub fn deploy(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) {
+        let markdown = workspace
+            .app_state()
+            .languages
+            .language_for_name("Markdown");
         cx.spawn(|workspace, mut cx| async move {
             let markdown = markdown.await.log_err();
             workspace
                 .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 buffer = project
                             .update(cx, |project, cx| project.create_buffer("", markdown, cx))
                             .expect("creating buffers on a local workspace always succeeds");
+                        let system_specs = SystemSpecs::new(cx);
                         let feedback_editor = cx
                             .add_view(|cx| FeedbackEditor::new(system_specs, project, buffer, cx));
                         workspace.add_item(Box::new(feedback_editor), cx);
@@ -290,7 +279,7 @@ impl Item for FeedbackEditor {
         _: ModelHandle<Project>,
         cx: &mut ViewContext<Self>,
     ) -> Task<anyhow::Result<()>> {
-        self.handle_save(cx)
+        self.submit(cx)
     }
 
     fn save_as(
@@ -299,7 +288,7 @@ impl Item for FeedbackEditor {
         _: std::path::PathBuf,
         cx: &mut ViewContext<Self>,
     ) -> Task<anyhow::Result<()>> {
-        self.handle_save(cx)
+        self.submit(cx)
     }
 
     fn reload(
@@ -352,6 +341,10 @@ impl Item for FeedbackEditor {
             None
         }
     }
+
+    fn to_item_events(event: &Self::Event) -> SmallVec<[ItemEvent; 2]> {
+        Editor::to_item_events(event)
+    }
 }
 
 impl SearchableItem for FeedbackEditor {

crates/feedback/src/feedback_info_text.rs πŸ”—

@@ -6,7 +6,7 @@ use gpui::{
 use settings::Settings;
 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 {
     active_item: Option<ViewHandle<FeedbackEditor>>,
@@ -57,7 +57,7 @@ impl View for FeedbackInfoText {
                 })
                 .with_cursor_style(CursorStyle::PointingHand)
                 .on_click(MouseButton::Left, |_, _, cx| {
-                    cx.dispatch_action(OpenZedCommunityRepo)
+                    open_zed_community_repo(&Default::default(), cx)
                 }),
             )
             .with_child(

crates/feedback/src/submit_feedback_button.rs πŸ”—

@@ -1,12 +1,16 @@
+use crate::feedback_editor::{FeedbackEditor, SubmitFeedback};
+use anyhow::Result;
 use gpui::{
     elements::{Label, MouseEventHandler},
     platform::{CursorStyle, MouseButton},
-    AnyElement, Element, Entity, View, ViewContext, ViewHandle,
+    AnyElement, AppContext, Element, Entity, Task, View, ViewContext, ViewHandle,
 };
 use settings::Settings;
 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(crate) active_item: Option<ViewHandle<FeedbackEditor>>,
@@ -18,6 +22,18 @@ impl SubmitFeedbackButton {
             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 {
@@ -39,8 +55,8 @@ impl View for SubmitFeedbackButton {
                 .with_style(style.container)
         })
         .with_cursor_style(CursorStyle::PointingHand)
-        .on_click(MouseButton::Left, |_, _, cx| {
-            cx.dispatch_action(SubmitFeedback)
+        .on_click(MouseButton::Left, |_, this, cx| {
+            this.submit(&Default::default(), cx);
         })
         .aligned()
         .contained()

crates/gpui/src/app.rs πŸ”—

@@ -43,6 +43,7 @@ use window_input_handler::WindowInputHandler;
 use crate::{
     elements::{AnyElement, AnyRootElement, RootElement},
     executor::{self, Task},
+    json,
     keymap_matcher::{self, Binding, KeymapContext, KeymapMatcher, Keystroke, MatchResult},
     platform::{
         self, FontSystem, KeyDownEvent, KeyUpEvent, ModifiersChangedEvent, MouseButton,
@@ -301,6 +302,14 @@ impl AsyncAppContext {
         self.0.borrow_mut().update(callback)
     }
 
+    pub fn read_window<T, F: FnOnce(&WindowContext) -> T>(
+        &self,
+        window_id: usize,
+        callback: F,
+    ) -> Option<T> {
+        self.0.borrow_mut().read_window(window_id, callback)
+    }
+
     pub fn update_window<T, F: FnOnce(&mut WindowContext) -> T>(
         &mut self,
         window_id: usize,
@@ -309,6 +318,44 @@ impl AsyncAppContext {
         self.0.borrow_mut().update_window(window_id, callback)
     }
 
+    pub fn debug_elements(&self, window_id: usize) -> Option<json::Value> {
+        self.0.borrow().read_window(window_id, |cx| {
+            let root_view = cx.window.root_view();
+            let root_element = cx.window.rendered_views.get(&root_view.id())?;
+            root_element.debug(cx).log_err()
+        })?
+    }
+
+    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 has_window(&self, window_id: usize) -> bool {
+        self.read(|cx| cx.windows.contains_key(&window_id))
+    }
+
+    pub fn window_is_active(&self, window_id: usize) -> bool {
+        self.read(|cx| cx.windows.get(&window_id).map_or(false, |w| w.is_active))
+    }
+
+    pub fn root_view(&self, window_id: usize) -> Option<AnyViewHandle> {
+        self.read(|cx| cx.windows.get(&window_id).map(|w| w.root_view().clone()))
+    }
+
+    pub fn window_ids(&self) -> Vec<usize> {
+        self.read(|cx| cx.windows.keys().copied().collect())
+    }
+
     pub fn add_model<T, F>(&mut self, build_model: F) -> ModelHandle<T>
     where
         T: Entity,
@@ -330,7 +377,7 @@ impl AsyncAppContext {
     }
 
     pub fn remove_window(&mut self, window_id: usize) {
-        self.update(|cx| cx.remove_window(window_id))
+        self.update_window(window_id, |cx| cx.remove_window());
     }
 
     pub fn activate_window(&mut self, window_id: usize) {
@@ -529,7 +576,7 @@ impl AppContext {
         App(self.weak_self.as_ref().unwrap().upgrade().unwrap())
     }
 
-    pub fn quit(&mut self) {
+    fn quit(&mut self) {
         let mut futures = Vec::new();
 
         self.update(|cx| {
@@ -546,7 +593,8 @@ impl AppContext {
             }
         });
 
-        self.remove_all_windows();
+        self.windows.clear();
+        self.flush_effects();
 
         let futures = futures::future::join_all(futures);
         if self
@@ -558,11 +606,6 @@ impl AppContext {
         }
     }
 
-    pub fn remove_all_windows(&mut self) {
-        self.windows.clear();
-        self.flush_effects();
-    }
-
     pub fn foreground(&self) -> &Rc<executor::Foreground> {
         &self.foreground
     }
@@ -679,24 +722,6 @@ impl AppContext {
         }
     }
 
-    pub fn has_window(&self, window_id: usize) -> bool {
-        self.window_ids()
-            .find(|window| window == &window_id)
-            .is_some()
-    }
-
-    pub fn window_is_active(&self, window_id: usize) -> bool {
-        self.windows.get(&window_id).map_or(false, |w| w.is_active)
-    }
-
-    pub fn root_view(&self, window_id: usize) -> Option<&AnyViewHandle> {
-        self.windows.get(&window_id).map(|w| w.root_view())
-    }
-
-    pub fn window_ids(&self) -> impl Iterator<Item = usize> + '_ {
-        self.windows.keys().copied()
-    }
-
     pub fn view_ui_name(&self, window_id: usize, view_id: usize) -> Option<&'static str> {
         Some(self.views.get(&(window_id, view_id))?.ui_name())
     }
@@ -1048,10 +1073,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 {
         self.update(|this| {
             if let Some((name, mut handler)) = this.global_actions.remove_entry(&action.id()) {
@@ -1266,15 +1287,6 @@ impl AppContext {
         })
     }
 
-    pub fn remove_status_bar_item(&mut self, id: usize) {
-        self.remove_window(id);
-    }
-
-    pub fn remove_window(&mut self, window_id: usize) {
-        self.windows.remove(&window_id);
-        self.flush_effects();
-    }
-
     pub fn build_window<V, F>(
         &mut self,
         window_id: usize,
@@ -1333,7 +1345,7 @@ impl AppContext {
         {
             let mut app = self.upgrade();
             platform_window.on_close(Box::new(move || {
-                app.update(|cx| cx.remove_window(window_id));
+                app.update(|cx| cx.update_window(window_id, |cx| cx.remove_window()));
             }));
         }
 
@@ -1619,17 +1631,7 @@ impl AppContext {
                         Effect::RefreshWindows => {
                             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 } => {
                             self.handle_action_dispatch_notification_effect(action_id)
                         }
@@ -1745,23 +1747,6 @@ impl AppContext {
         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) {
         let window_ids = self.windows.keys().cloned().collect::<Vec<_>>();
         for window_id in window_ids {
@@ -1920,17 +1905,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) {
         self.action_dispatch_observations
             .clone()
@@ -2159,11 +2133,6 @@ pub enum Effect {
         result: MatchResult,
     },
     RefreshWindows,
-    DispatchActionFrom {
-        window_id: usize,
-        view_id: usize,
-        action: Box<dyn Action>,
-    },
     ActionDispatchNotification {
         action_id: TypeId,
     },
@@ -2252,13 +2221,6 @@ impl Debug for Effect {
                 .field("view_id", view_id)
                 .field("subscription_id", subscription_id)
                 .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
                 .debug_struct("Effect::ActionDispatchNotification")
                 .field("action_id", action_id)
@@ -3189,20 +3151,6 @@ impl<'a, 'b, V: View> ViewContext<'a, 'b, V> {
         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>)) {
         let handle = self.handle();
         self.window_context
@@ -4708,7 +4656,7 @@ mod tests {
         assert!(model_release_observed.get());
 
         drop(view);
-        cx.remove_window(window_id);
+        cx.update_window(window_id, |cx| cx.remove_window());
         assert!(view_released.get());
         assert!(view_release_observed.get());
     }

crates/gpui/src/app/test_app_context.rs πŸ”—

@@ -72,14 +72,16 @@ impl TestAppContext {
     }
 
     pub fn dispatch_action<A: Action>(&self, window_id: usize, action: A) {
-        let mut cx = self.cx.borrow_mut();
-        if let Some(view_id) = cx.windows.get(&window_id).and_then(|w| w.focused_view_id) {
-            cx.handle_dispatch_action_from_effect(window_id, Some(view_id), &action);
-        }
+        self.cx
+            .borrow_mut()
+            .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) {
-        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) {
@@ -180,7 +182,11 @@ impl TestAppContext {
     }
 
     pub fn window_ids(&self) -> Vec<usize> {
-        self.cx.borrow().window_ids().collect()
+        self.cx.borrow().windows.keys().copied().collect()
+    }
+
+    pub fn remove_all_windows(&mut self) {
+        self.update(|cx| cx.windows.clear());
     }
 
     pub fn read<T, F: FnOnce(&AppContext) -> T>(&self, callback: F) -> T {

crates/gpui/src/app/window.rs πŸ”—

@@ -1,7 +1,7 @@
 use crate::{
     elements::AnyRootElement,
     geometry::rect::RectF,
-    json::{self, ToJson},
+    json::ToJson,
     keymap_matcher::{Binding, Keystroke, MatchResult},
     platform::{
         self, Appearance, CursorStyle, Event, KeyDownEvent, KeyUpEvent, ModifiersChangedEvent,
@@ -975,17 +975,6 @@ impl<'a> WindowContext<'a> {
             .flatten()
     }
 
-    pub fn debug_elements(&self) -> Option<json::Value> {
-        let view = self.window.root_view();
-        Some(json!({
-            "root_view": view.debug_json(self),
-            "root_element": self.window.rendered_views.get(&view.id())
-                .and_then(|root_element| {
-                    root_element.debug(self).log_err()
-                })
-        }))
-    }
-
     pub fn set_window_title(&mut self, title: &str) {
         self.window.platform_window.set_title(title);
     }
@@ -1454,13 +1443,7 @@ impl<V: View> Element<V> for ChildView {
     ) -> serde_json::Value {
         json!({
             "type": "ChildView",
-            "view_id": self.view_id,
             "bounds": bounds.to_json(),
-            "view": if let Some(view) = cx.views.get(&(cx.window_id, self.view_id))  {
-                view.debug_json(cx)
-            } else {
-                json!(null)
-            },
             "child": if let Some(element) = cx.window.rendered_views.get(&self.view_id) {
                 element.debug(&cx.window_context).log_err().unwrap_or_else(|| json!(null))
             } else {

crates/gpui/src/elements.rs πŸ”—

@@ -45,7 +45,6 @@ use std::{
     mem,
     ops::{Deref, DerefMut, Range},
 };
-use util::ResultExt;
 
 pub trait Element<V: View>: 'static {
     type LayoutState;
@@ -709,7 +708,12 @@ impl<V: View> AnyRootElement for RootElement<V> {
             .ok_or_else(|| anyhow!("debug called on a root element for a dropped view"))?;
         let view = view.read(cx);
         let view_context = ViewContext::immutable(cx, self.view.id());
-        Ok(self.element.debug(view, &view_context))
+        Ok(serde_json::json!({
+            "view_id": self.view.id(),
+            "view_name": V::ui_name(),
+            "view": view.debug_json(cx),
+            "element": self.element.debug(view, &view_context)
+        }))
     }
 
     fn name(&self) -> Option<&str> {
@@ -717,63 +721,6 @@ impl<V: View> AnyRootElement for RootElement<V> {
     }
 }
 
-impl<V: View, R: View> Element<V> for RootElement<R> {
-    type LayoutState = ();
-    type PaintState = ();
-
-    fn layout(
-        &mut self,
-        constraint: SizeConstraint,
-        _view: &mut V,
-        cx: &mut ViewContext<V>,
-    ) -> (Vector2F, ()) {
-        let size = AnyRootElement::layout(self, constraint, cx)
-            .log_err()
-            .unwrap_or_else(|| Vector2F::zero());
-        (size, ())
-    }
-
-    fn paint(
-        &mut self,
-        scene: &mut SceneBuilder,
-        bounds: RectF,
-        visible_bounds: RectF,
-        _layout: &mut Self::LayoutState,
-        _view: &mut V,
-        cx: &mut ViewContext<V>,
-    ) {
-        AnyRootElement::paint(self, scene, bounds.origin(), visible_bounds, cx).log_err();
-    }
-
-    fn rect_for_text_range(
-        &self,
-        range_utf16: Range<usize>,
-        _bounds: RectF,
-        _visible_bounds: RectF,
-        _layout: &Self::LayoutState,
-        _paint: &Self::PaintState,
-        _view: &V,
-        cx: &ViewContext<V>,
-    ) -> Option<RectF> {
-        AnyRootElement::rect_for_text_range(self, range_utf16, cx)
-            .log_err()
-            .flatten()
-    }
-
-    fn debug(
-        &self,
-        _bounds: RectF,
-        _layout: &Self::LayoutState,
-        _paint: &Self::PaintState,
-        _view: &V,
-        cx: &ViewContext<V>,
-    ) -> serde_json::Value {
-        AnyRootElement::debug(self, cx)
-            .log_err()
-            .unwrap_or_default()
-    }
-}
-
 pub trait ParentElement<'a, V: View>: Extend<AnyElement<V>> + Sized {
     fn add_children<E: Element<V>>(&mut self, children: impl IntoIterator<Item = E>) {
         self.extend(children.into_iter().map(|child| child.into_any()));

crates/gpui/src/platform/mac/window.rs πŸ”—

@@ -699,6 +699,31 @@ impl platform::Window for Window {
         msg: &str,
         answers: &[&str],
     ) -> oneshot::Receiver<usize> {
+        // macOs applies overrides to modal window buttons after they are added.
+        // Two most important for this logic are:
+        // * Buttons with "Cancel" title will be displayed as the last buttons in the modal
+        // * Last button added to the modal via `addButtonWithTitle` stays focused
+        // * Focused buttons react on "space"/" " keypresses
+        // * Usage of `keyEquivalent`, `makeFirstResponder` or `setInitialFirstResponder` does not change the focus
+        //
+        // See also https://developer.apple.com/documentation/appkit/nsalert/1524532-addbuttonwithtitle#discussion
+        // ```
+        // By default, the first button has a key equivalent of Return,
+        // any button with a title of β€œCancel” has a key equivalent of Escape,
+        // and any button with the title β€œDon’t Save” has a key equivalent of Command-D (but only if it’s not the first button).
+        // ```
+        //
+        // To avoid situations when the last element added is "Cancel" and it gets the focus
+        // (hence stealing both ESC and Space shortcuts), we find and add one non-Cancel button
+        // last, so it gets focus and a Space shortcut.
+        // This way, "Save this file? Yes/No/Cancel"-ish modals will get all three buttons mapped with a key.
+        let latest_non_cancel_label = answers
+            .iter()
+            .enumerate()
+            .rev()
+            .find(|(_, &label)| label != "Cancel")
+            .filter(|&(label_index, _)| label_index > 0);
+
         unsafe {
             let alert: id = msg_send![class!(NSAlert), alloc];
             let alert: id = msg_send![alert, init];
@@ -709,10 +734,20 @@ impl platform::Window for Window {
             };
             let _: () = msg_send![alert, setAlertStyle: alert_style];
             let _: () = msg_send![alert, setMessageText: ns_string(msg)];
-            for (ix, answer) in answers.iter().enumerate() {
+
+            for (ix, answer) in answers
+                .iter()
+                .enumerate()
+                .filter(|&(ix, _)| Some(ix) != latest_non_cancel_label.map(|(ix, _)| ix))
+            {
+                let button: id = msg_send![alert, addButtonWithTitle: ns_string(answer)];
+                let _: () = msg_send![button, setTag: ix as NSInteger];
+            }
+            if let Some((ix, answer)) = latest_non_cancel_label {
                 let button: id = msg_send![alert, addButtonWithTitle: ns_string(answer)];
                 let _: () = msg_send![button, setTag: ix as NSInteger];
             }
+
             let (done_tx, done_rx) = oneshot::channel();
             let done_tx = Cell::new(Some(done_tx));
             let block = ConcreteBlock::new(move |answer: NSInteger| {
@@ -720,7 +755,7 @@ impl platform::Window for Window {
                     let _ = postage::sink::Sink::try_send(&mut done_tx, answer.try_into().unwrap());
                 }
             });
-            let block = block.copy();
+
             let native_window = self.0.borrow().native_window;
             self.0
                 .borrow()

crates/gpui/src/test.rs πŸ”—

@@ -100,7 +100,7 @@ pub fn run_test(
                     test_fn(cx, foreground_platform.clone(), deterministic.clone(), seed);
                 });
 
-                cx.update(|cx| cx.remove_all_windows());
+                cx.remove_all_windows();
                 deterministic.run_until_parked();
                 cx.update(|cx| cx.clear_globals());
 

crates/gpui/src/views/select.rs πŸ”—

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

crates/gpui_macros/src/gpui_macros.rs πŸ”—

@@ -137,7 +137,7 @@ pub fn test(args: TokenStream, function: TokenStream) -> TokenStream {
                                 );
                             ));
                             cx_teardowns.extend(quote!(
-                                #cx_varname.update(|cx| cx.remove_all_windows());
+                                #cx_varname.remove_all_windows();
                                 deterministic.run_until_parked();
                                 #cx_varname.update(|cx| cx.clear_globals());
                             ));
@@ -212,7 +212,7 @@ pub fn test(args: TokenStream, function: TokenStream) -> TokenStream {
                                     );
                                 ));
                                 cx_teardowns.extend(quote!(
-                                    #cx_varname.update(|cx| cx.remove_all_windows());
+                                    #cx_varname.remove_all_windows();
                                     deterministic.run_until_parked();
                                     #cx_varname.update(|cx| cx.clear_globals());
                                 ));

crates/language_selector/src/active_buffer_language.rs πŸ”—

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

crates/language_selector/src/language_selector.rs πŸ”—

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

crates/node_runtime/src/node_runtime.rs πŸ”—

@@ -5,7 +5,7 @@ use futures::{future::Shared, FutureExt};
 use gpui::{executor::Background, Task};
 use parking_lot::Mutex;
 use serde::Deserialize;
-use smol::{fs, io::BufReader};
+use smol::{fs, io::BufReader, process::Command};
 use std::{
     env::consts,
     path::{Path, PathBuf},
@@ -48,12 +48,41 @@ impl NodeRuntime {
         Ok(installation_path.join("bin/node"))
     }
 
+    pub async fn run_npm_subcommand(
+        &self,
+        directory: &Path,
+        subcommand: &str,
+        args: &[&str],
+    ) -> Result<()> {
+        let installation_path = self.install_if_needed().await?;
+        let node_binary = installation_path.join("bin/node");
+        let npm_file = installation_path.join("bin/npm");
+
+        let output = Command::new(node_binary)
+            .arg(npm_file)
+            .arg(subcommand)
+            .args(args)
+            .current_dir(directory)
+            .output()
+            .await?;
+
+        if !output.status.success() {
+            return Err(anyhow!(
+                "failed to execute npm {subcommand} subcommand:\nstdout: {:?}\nstderr: {:?}",
+                String::from_utf8_lossy(&output.stdout),
+                String::from_utf8_lossy(&output.stderr)
+            ));
+        }
+
+        Ok(())
+    }
+
     pub async fn npm_package_latest_version(&self, name: &str) -> Result<String> {
         let installation_path = self.install_if_needed().await?;
         let node_binary = installation_path.join("bin/node");
         let npm_file = installation_path.join("bin/npm");
 
-        let output = smol::process::Command::new(node_binary)
+        let output = Command::new(node_binary)
             .arg(npm_file)
             .args(["-fetch-retry-mintimeout", "2000"])
             .args(["-fetch-retry-maxtimeout", "5000"])
@@ -64,11 +93,11 @@ impl NodeRuntime {
             .context("failed to run npm info")?;
 
         if !output.status.success() {
-            Err(anyhow!(
+            return Err(anyhow!(
                 "failed to execute npm info:\nstdout: {:?}\nstderr: {:?}",
                 String::from_utf8_lossy(&output.stdout),
                 String::from_utf8_lossy(&output.stderr)
-            ))?;
+            ));
         }
 
         let mut info: NpmInfo = serde_json::from_slice(&output.stdout)?;
@@ -80,14 +109,14 @@ impl NodeRuntime {
 
     pub async fn npm_install_packages(
         &self,
-        packages: impl IntoIterator<Item = (&str, &str)>,
         directory: &Path,
+        packages: impl IntoIterator<Item = (&str, &str)>,
     ) -> Result<()> {
         let installation_path = self.install_if_needed().await?;
         let node_binary = installation_path.join("bin/node");
         let npm_file = installation_path.join("bin/npm");
 
-        let output = smol::process::Command::new(node_binary)
+        let output = Command::new(node_binary)
             .arg(npm_file)
             .args(["-fetch-retry-mintimeout", "2000"])
             .args(["-fetch-retry-maxtimeout", "5000"])
@@ -103,12 +132,13 @@ impl NodeRuntime {
             .output()
             .await
             .context("failed to run npm install")?;
+
         if !output.status.success() {
-            Err(anyhow!(
+            return Err(anyhow!(
                 "failed to execute npm install:\nstdout: {:?}\nstderr: {:?}",
                 String::from_utf8_lossy(&output.stdout),
                 String::from_utf8_lossy(&output.stderr)
-            ))?;
+            ));
         }
         Ok(())
     }

crates/outline/src/outline.rs πŸ”—

@@ -24,7 +24,7 @@ pub fn init(cx: &mut AppContext) {
     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
         .active_item(cx)
         .and_then(|item| item.downcast::<Editor>())

crates/project_panel/src/project_panel.rs πŸ”—

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

crates/recent_projects/src/recent_projects.rs πŸ”—

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

crates/search/src/buffer_search.rs πŸ”—

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

crates/search/src/project_search.rs πŸ”—

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

crates/terminal_view/src/terminal_button.rs πŸ”—

@@ -7,7 +7,11 @@ use gpui::{
 };
 use settings::Settings;
 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 {
     workspace: WeakViewHandle<Workspace>,
@@ -80,7 +84,11 @@ impl View for TerminalButton {
                         this.deploy_terminal_menu(cx);
                     } else {
                         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)
+                                })
+                            }
                         }
                     };
                 })

crates/theme/src/theme.rs πŸ”—

@@ -659,6 +659,7 @@ pub struct DiagnosticPathHeader {
 pub struct DiagnosticHeader {
     #[serde(flatten)]
     pub container: ContainerStyle,
+    pub source: ContainedLabel,
     pub message: ContainedLabel,
     pub code: ContainedText,
     pub text_scale_factor: f32,

crates/theme/src/ui.rs πŸ”—

@@ -156,24 +156,7 @@ pub fn keystroke_label<V: View>(
 
 pub type ButtonStyle = Interactive<ContainedText>;
 
-pub fn cta_button<L, A, V>(
-    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>(
+pub fn cta_button<Tag, L, V, F>(
     label: L,
     max_width: f32,
     style: &ButtonStyle,

crates/theme_selector/src/theme_selector.rs πŸ”—

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

crates/util/src/github.rs πŸ”—

@@ -1,5 +1,5 @@
 use crate::http::HttpClient;
-use anyhow::{Context, Result};
+use anyhow::{anyhow, Context, Result};
 use futures::AsyncReadExt;
 use serde::Deserialize;
 use std::sync::Arc;
@@ -12,7 +12,10 @@ pub struct GitHubLspBinaryVersion {
 #[derive(Deserialize, Debug)]
 pub struct GithubRelease {
     pub name: String,
+    #[serde(rename = "prerelease")]
+    pub pre_release: bool,
     pub assets: Vec<GithubReleaseAsset>,
+    pub tarball_url: String,
 }
 
 #[derive(Deserialize, Debug)]
@@ -23,16 +26,18 @@ pub struct GithubReleaseAsset {
 
 pub async fn latest_github_release(
     repo_name_with_owner: &str,
+    pre_release: bool,
     http: Arc<dyn HttpClient>,
 ) -> Result<GithubRelease, anyhow::Error> {
     let mut response = http
         .get(
-            &format!("https://api.github.com/repos/{repo_name_with_owner}/releases/latest"),
+            &format!("https://api.github.com/repos/{repo_name_with_owner}/releases"),
             Default::default(),
             true,
         )
         .await
         .context("error fetching latest release")?;
+
     let mut body = Vec::new();
     response
         .body_mut()
@@ -40,13 +45,20 @@ pub async fn latest_github_release(
         .await
         .context("error reading latest release")?;
 
-    let release = serde_json::from_slice::<GithubRelease>(body.as_slice());
-    if release.is_err() {
-        log::error!(
-            "Github API response text: {:?}",
-            String::from_utf8_lossy(body.as_slice())
-        );
-    }
+    let releases = match serde_json::from_slice::<Vec<GithubRelease>>(body.as_slice()) {
+        Ok(releases) => releases,
+
+        Err(_) => {
+            log::error!(
+                "Error deserializing Github API response text: {:?}",
+                String::from_utf8_lossy(body.as_slice())
+            );
+            return Err(anyhow!("error deserializing latest release"));
+        }
+    };
 
-    release.context("error deserializing latest release")
+    releases
+        .into_iter()
+        .find(|release| release.pre_release == pre_release)
+        .ok_or(anyhow!("Failed to find a release"))
 }

crates/util/src/http.rs πŸ”—

@@ -40,8 +40,14 @@ pub trait HttpClient: Send + Sync {
         &'a self,
         uri: &str,
         body: AsyncBody,
+        follow_redirects: bool,
     ) -> BoxFuture<'a, Result<Response<AsyncBody>, Error>> {
         let request = isahc::Request::builder()
+            .redirect_policy(if follow_redirects {
+                RedirectPolicy::Follow
+            } else {
+                RedirectPolicy::None
+            })
             .method(Method::POST)
             .uri(uri)
             .header("Content-Type", "application/json")

crates/welcome/src/base_keymap_picker.rs πŸ”—

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

crates/welcome/src/welcome.rs πŸ”—

@@ -5,7 +5,7 @@ use std::{borrow::Cow, sync::Arc};
 use db::kvp::KEY_VALUE_STORE;
 use gpui::{
     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};
 
@@ -20,7 +20,7 @@ pub const FIRST_OPEN: &str = "first_open";
 
 pub fn init(cx: &mut AppContext) {
     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)
     });
 
@@ -30,7 +30,7 @@ pub fn init(cx: &mut AppContext) {
 pub fn show_welcome_experience(app_state: &Arc<AppState>, cx: &mut AppContext) {
     open_new(&app_state, cx, |workspace, 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);
         cx.focus(&welcome_page);
         cx.notify();
@@ -43,6 +43,7 @@ pub fn show_welcome_experience(app_state: &Arc<AppState>, cx: &mut AppContext) {
 }
 
 pub struct WelcomePage {
+    workspace: WeakViewHandle<Workspace>,
     _settings_subscription: Subscription,
 }
 
@@ -97,26 +98,46 @@ impl View for WelcomePage {
                 )
                 .with_child(
                     Flex::column()
-                        .with_child(theme::ui::cta_button(
+                        .with_child(theme::ui::cta_button::<theme_selector::Toggle, _, _, _>(
                             "Choose a theme",
-                            theme_selector::Toggle,
                             width,
                             &theme.welcome.button,
                             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",
-                            ToggleBaseKeymapSelector,
                             width,
                             &theme.welcome.button,
                             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_cli::Install,
                             width,
                             &theme.welcome.button,
                             cx,
+                            |_, _, cx| {
+                                cx.app_context()
+                                    .spawn(|cx| async move { install_cli::install_cli(&cx).await })
+                                    .detach_and_log_err(cx);
+                            },
                         ))
                         .contained()
                         .with_style(theme.welcome.button_group)
@@ -190,8 +211,9 @@ impl View for WelcomePage {
 }
 
 impl WelcomePage {
-    pub fn new(cx: &mut ViewContext<Self>) -> Self {
+    pub fn new(workspace: &Workspace, cx: &mut ViewContext<Self>) -> Self {
         WelcomePage {
+            workspace: workspace.weak_handle(),
             _settings_subscription: cx.observe_global::<Settings, _>(move |_, cx| cx.notify()),
         }
     }
@@ -220,11 +242,15 @@ impl Item for WelcomePage {
     fn show_toolbar(&self) -> bool {
         false
     }
+
     fn clone_on_split(
         &self,
         _workspace_id: WorkspaceId,
         cx: &mut ViewContext<Self>,
     ) -> Option<Self> {
-        Some(WelcomePage::new(cx))
+        Some(WelcomePage {
+            workspace: self.workspace.clone(),
+            _settings_subscription: cx.observe_global::<Settings, _>(move |_, cx| cx.notify()),
+        })
     }
 }

crates/workspace/src/dock.rs πŸ”—

@@ -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);
     }
 
-    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);
     }
 
@@ -374,8 +374,8 @@ impl Dock {
                                     .with_background_color(style.wash_color)
                             })
                             .capture_all()
-                            .on_down(MouseButton::Left, |_, _, cx| {
-                                cx.dispatch_action(HideDock);
+                            .on_down(MouseButton::Left, |_, workspace, cx| {
+                                Dock::hide_dock(workspace, &Default::default(), cx)
                             })
                             .with_cursor_style(CursorStyle::Arrow),
                         )

crates/workspace/src/dock/toggle_dock_button.rs πŸ”—

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

crates/workspace/src/notifications.rs πŸ”—

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

crates/workspace/src/pane.rs πŸ”—

@@ -2,7 +2,7 @@ mod dragged_item_receiver;
 
 use super::{ItemHandle, SplitDirection};
 use crate::{
-    dock::{icon_for_dock_anchor, AnchorDockBottom, AnchorDockRight, ExpandDock, HideDock},
+    dock::{icon_for_dock_anchor, AnchorDockBottom, AnchorDockRight, Dock, ExpandDock},
     item::WeakItemHandle,
     toolbar::Toolbar,
     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 {
         self.is_active
     }
@@ -1340,8 +1344,8 @@ impl Pane {
                                         cx,
                                     )
                                 })
-                                .on_down(MouseButton::Left, move |_, _, cx| {
-                                    cx.dispatch_action(ActivateItem(ix));
+                                .on_down(MouseButton::Left, move |_, this, cx| {
+                                    this.activate_item(ix, true, true, cx);
                                 })
                                 .on_click(MouseButton::Middle, {
                                     let item_id = item.id();
@@ -1639,7 +1643,15 @@ impl Pane {
                     3,
                     "icons/x_mark_8.svg",
                     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,
                 )
             }))
@@ -1693,8 +1705,8 @@ impl View for Pane {
                             })
                             .on_down(
                                 MouseButton::Left,
-                                move |_, _, cx| {
-                                    cx.dispatch_action(ActivateItem(active_item_index));
+                                move |_, this, cx| {
+                                    this.activate_item(active_item_index, true, true, cx);
                                 },
                             ),
                         );
@@ -1759,15 +1771,27 @@ impl View for Pane {
         })
         .on_down(
             MouseButton::Navigate(NavigationDirection::Back),
-            move |_, _, cx| {
-                let pane = cx.weak_handle();
-                cx.dispatch_action(GoBack { pane: Some(pane) });
+            move |_, pane, cx| {
+                if let Some(workspace) = pane.workspace.upgrade(cx) {
+                    let pane = cx.weak_handle();
+                    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), {
-            move |_, _, cx| {
-                let pane = cx.weak_handle();
-                cx.dispatch_action(GoForward { pane: Some(pane) })
+            move |_, pane, cx| {
+                if let Some(workspace) = pane.workspace.upgrade(cx) {
+                    let pane = cx.weak_handle();
+                    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")

crates/workspace/src/sidebar.rs πŸ”—

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

crates/workspace/src/toolbar.rs πŸ”—

@@ -130,8 +130,23 @@ impl View for Toolbar {
                         tooltip_style.clone(),
                         enable_go_backward,
                         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 },
                         "Go Back",
@@ -143,7 +158,24 @@ impl View for Toolbar {
                         tooltip_style,
                         enable_go_forward,
                         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 },
                         "Go Forward",
                         cx,
@@ -161,13 +193,13 @@ impl View for Toolbar {
 }
 
 #[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,
     style: theme::Interactive<theme::IconButton>,
     tooltip_style: TooltipStyle,
     enabled: bool,
     spacing: f32,
-    action: A,
+    on_click: F,
     tooltip_action: A,
     action_name: &str,
     cx: &mut ViewContext<Toolbar>,
@@ -195,8 +227,8 @@ fn nav_button<A: Action + Clone>(
     } else {
         CursorStyle::default()
     })
-    .on_click(MouseButton::Left, move |_, _, cx| {
-        cx.dispatch_action(action.clone())
+    .on_click(MouseButton::Left, move |_, toolbar, cx| {
+        on_click(toolbar, cx)
     })
     .with_tooltip::<A>(
         0,

crates/workspace/src/workspace.rs πŸ”—

@@ -42,8 +42,9 @@ use gpui::{
         CursorStyle, MouseButton, PathPromptOptions, Platform, PromptLevel, WindowBounds,
         WindowOptions,
     },
-    Action, AnyModelHandle, AnyViewHandle, AppContext, AsyncAppContext, Entity, ModelContext,
-    ModelHandle, SizeConstraint, Subscription, Task, View, ViewContext, ViewHandle, WeakViewHandle,
+    AnyModelHandle, AnyViewHandle, AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle,
+    SizeConstraint, Subscription, Task, View, ViewContext, ViewHandle, WeakViewHandle,
+    WindowContext,
 };
 use item::{FollowableItem, FollowableItemHandle, Item, ItemHandle, ProjectItem};
 use language::{LanguageRegistry, Rope};
@@ -59,7 +60,7 @@ use std::{
 };
 
 use crate::{
-    notifications::simple_message_notification::{MessageNotification, OsOpen},
+    notifications::simple_message_notification::MessageNotification,
     persistence::model::{SerializedPane, SerializedPaneGroup, SerializedWorkspace},
 };
 use lazy_static::lazy_static;
@@ -139,7 +140,7 @@ pub struct ActivatePane(pub usize);
 pub struct Toast {
     id: usize,
     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 {
@@ -147,21 +148,17 @@ impl Toast {
         Toast {
             id,
             msg: msg.into(),
-            click: None,
+            on_click: None,
         }
     }
 
-    pub fn new_action<I1: Into<Cow<'static, str>>, I2: Into<Cow<'static, str>>>(
-        id: usize,
-        msg: I1,
-        click_msg: I2,
-        action: impl Action,
-    ) -> Self {
-        Toast {
-            id,
-            msg: msg.into(),
-            click: Some((click_msg.into(), Box::new(action))),
-        }
+    pub fn on_click<F, M>(mut self, message: M, on_click: F) -> Self
+    where
+        M: Into<Cow<'static, str>>,
+        F: Fn(&mut WindowContext) + 'static,
+    {
+        self.on_click = Some((message.into(), Arc::new(on_click)));
+        self
     }
 }
 
@@ -169,7 +166,7 @@ impl PartialEq for Toast {
     fn eq(&self, other: &Self) -> bool {
         self.id == other.id
             && self.msg == other.msg
-            && self.click.is_some() == other.click.is_some()
+            && self.on_click.is_some() == other.on_click.is_some()
     }
 }
 
@@ -178,10 +175,7 @@ impl Clone for Toast {
         Toast {
             id: self.id,
             msg: self.msg.to_owned(),
-            click: self
-                .click
-                .as_ref()
-                .map(|(msg, click)| (msg.to_owned(), click.boxed_clone())),
+            on_click: self.on_click.clone(),
         }
     }
 }
@@ -216,52 +210,12 @@ pub fn init(app_state: Arc<AppState>, cx: &mut AppContext) {
             }
         }
     });
-    cx.add_action({
-        let app_state = Arc::downgrade(&app_state);
-        move |_, _: &Open, cx: &mut ViewContext<Workspace>| {
-            let mut paths = cx.prompt_for_paths(PathPromptOptions {
-                files: true,
-                directories: true,
-                multiple: true,
-            });
-
-            if let Some(app_state) = app_state.upgrade() {
-                cx.spawn(|this, mut cx| async move {
-                    if let Some(paths) = paths.recv().await.flatten() {
-                        if let Some(task) = this
-                            .update(&mut cx, |this, cx| {
-                                this.open_workspace_for_paths(paths, app_state, cx)
-                            })
-                            .log_err()
-                        {
-                            task.await.log_err();
-                        }
-                    }
-                })
-                .detach();
-            }
-        }
-    });
-    cx.add_global_action({
-        let app_state = Arc::downgrade(&app_state);
-        move |_: &NewWindow, cx: &mut AppContext| {
-            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::open);
 
     cx.add_async_action(Workspace::follow_next_collaborator);
     cx.add_async_action(Workspace::close);
     cx.add_global_action(Workspace::close_global);
+    cx.add_global_action(restart);
     cx.add_async_action(Workspace::save_all);
     cx.add_action(Workspace::add_folder_to_project);
     cx.add_action(
@@ -305,9 +259,7 @@ pub fn init(app_state: Arc<AppState>, cx: &mut AppContext) {
                 } else {
                     workspace.show_notification(1, cx, |cx| {
                         cx.add_view(|_| {
-                            MessageNotification::new_message(
-                                "Successfully installed the `zed` binary",
-                            )
+                            MessageNotification::new("Successfully installed the `zed` binary")
                         })
                     });
                 }
@@ -316,17 +268,16 @@ pub fn init(app_state: Arc<AppState>, cx: &mut AppContext) {
         .detach();
     });
 
-    cx.add_action({
-        let app_state = app_state.clone();
+    cx.add_action(
         move |_: &mut Workspace, _: &OpenSettings, cx: &mut ViewContext<Workspace>| {
-            create_and_open_local_file(&paths::SETTINGS, app_state.clone(), cx, || {
+            create_and_open_local_file(&paths::SETTINGS, cx, || {
                 Settings::initial_user_settings_content(&Assets)
                     .as_ref()
                     .into()
             })
             .detach_and_log_err(cx);
-        }
-    });
+        },
+    );
 
     let client = &app_state.client;
     client.add_view_request_handler(Workspace::handle_follow);
@@ -934,7 +885,6 @@ impl Workspace {
     /// to the callback. Otherwise, a new empty window will be created.
     pub fn with_local_workspace<T, F>(
         &mut self,
-        app_state: &Arc<AppState>,
         cx: &mut ViewContext<Self>,
         callback: F,
     ) -> Task<Result<T>>
@@ -945,7 +895,7 @@ impl Workspace {
         if self.project.read(cx).is_local() {
             Task::Ready(Some(Ok(callback(self, cx))))
         } 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 {
                 let (workspace, _) = task.await;
                 workspace.update(&mut cx, callback)
@@ -981,12 +931,18 @@ impl Workspace {
     }
 
     pub fn close_global(_: &CloseWindow, cx: &mut AppContext) {
-        let id = cx.window_ids().find(|&id| cx.window_is_active(id));
-        if let Some(id) = id {
-            //This can only get called when the window's project connection has been lost
-            //so we don't need to prompt the user for anything and instead just close the window
-            cx.remove_window(id);
-        }
+        cx.spawn(|mut cx| async move {
+            let id = cx
+                .window_ids()
+                .into_iter()
+                .find(|&id| cx.window_is_active(id));
+            if let Some(id) = id {
+                //This can only get called when the window's project connection has been lost
+                //so we don't need to prompt the user for anything and instead just close the window
+                cx.remove_window(id);
+            }
+        })
+        .detach();
     }
 
     pub fn close(
@@ -1011,19 +967,14 @@ impl Workspace {
     ) -> Task<Result<bool>> {
         let active_call = self.active_call().cloned();
         let window_id = cx.window_id();
-        let workspace_count = cx
-            .window_ids()
-            .collect::<Vec<_>>()
-            .into_iter()
-            .filter_map(|window_id| {
-                cx.app_context()
-                    .root_view(window_id)?
-                    .clone()
-                    .downcast::<Workspace>()
-            })
-            .count();
 
         cx.spawn(|this, mut cx| async move {
+            let workspace_count = cx
+                .window_ids()
+                .into_iter()
+                .filter_map(|window_id| cx.root_view(window_id)?.clone().downcast::<Workspace>())
+                .count();
+
             if let Some(active_call) = active_call {
                 if !quitting
                     && workspace_count == 1
@@ -1114,10 +1065,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(
         &mut self,
         paths: Vec<PathBuf>,
-        app_state: Arc<AppState>,
         cx: &mut ViewContext<Self>,
     ) -> Task<Result<()>> {
         let window_id = cx.window_id();
@@ -1129,6 +1099,7 @@ impl Workspace {
         } else {
             Some(self.prepare_to_close(false, cx))
         };
+        let app_state = self.app_state.clone();
 
         cx.spawn(|_, mut cx| async move {
             let window_id_to_replace = if let Some(close_task) = close_task {
@@ -2682,36 +2653,37 @@ impl Workspace {
 }
 
 fn notify_if_database_failed(workspace: &WeakViewHandle<Workspace>, cx: &mut AsyncAppContext) {
-    workspace.update(cx, |workspace, cx| {
-        if (*db::ALL_FILE_DB_FAILED).load(std::sync::atomic::Ordering::Acquire) {
-            workspace.show_notification_once(0, cx, |cx| {
-                cx.add_view(|_| {
-                    MessageNotification::new(
-                        "Failed to load any database file.",
-                        OsOpen::new("https://github.com/zed-industries/community/issues/new?assignees=&labels=defect%2Ctriage&template=2_bug_report.yml".to_string()),
-                        "Click to let us know about this error"
-                    )
-                })
-            });
-        } else {
-            let backup_path = (*db::BACKUP_DB_PATH).read();
-            if let Some(backup_path) = &*backup_path {
+    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) {
                 workspace.show_notification_once(0, cx, |cx| {
                     cx.add_view(|_| {
-                        let backup_path = backup_path.to_string_lossy();
-                        MessageNotification::new(
-                            format!(
-                                "Database file was corrupted. Old database backed up to {}",
-                                backup_path
-                            ),
-                            OsOpen::new(backup_path.to_string()),
-                            "Click to show old database in finder",
-                        )
+                        MessageNotification::new("Failed to load any database file.")
+                            .with_click_message("Click to let us know about this error")
+                            .on_click(|cx| cx.platform().open_url(REPORT_ISSUE_URL))
                     })
                 });
+            } else {
+                let backup_path = (*db::BACKUP_DB_PATH).read();
+                if let Some(backup_path) = backup_path.clone() {
+                    workspace.show_notification_once(0, cx, move |cx| {
+                        cx.add_view(move |_| {
+                            MessageNotification::new(format!(
+                                "Database file was corrupted. Old database backed up to {}",
+                                backup_path.display()
+                            ))
+                            .with_click_message("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 {
@@ -2891,10 +2863,10 @@ impl std::fmt::Debug for OpenPaths {
 pub struct WorkspaceCreated(WeakViewHandle<Workspace>);
 
 pub fn activate_workspace_for_project(
-    cx: &mut AppContext,
+    cx: &mut AsyncAppContext,
     predicate: impl Fn(&mut Project, &mut ModelContext<Project>) -> bool,
 ) -> Option<WeakViewHandle<Workspace>> {
-    for window_id in cx.window_ids().collect::<Vec<_>>() {
+    for window_id in cx.window_ids() {
         let handle = cx
             .update_window(window_id, |cx| {
                 if let Some(workspace_handle) = cx.root_view().clone().downcast::<Workspace>() {
@@ -2933,13 +2905,14 @@ pub fn open_paths(
 > {
     log::info!("open paths {:?}", abs_paths);
 
-    // Open paths in existing workspace if possible
-    let existing =
-        activate_workspace_for_project(cx, |project, cx| project.contains_paths(abs_paths, cx));
-
     let app_state = app_state.clone();
     let abs_paths = abs_paths.to_vec();
     cx.spawn(|mut cx| async move {
+        // Open paths in existing workspace if possible
+        let existing = activate_workspace_for_project(&mut cx, |project, cx| {
+            project.contains_paths(&abs_paths, cx)
+        });
+
         if let Some(existing) = existing {
             Ok((
                 existing.clone(),
@@ -2997,12 +2970,11 @@ pub fn open_new(
 
 pub fn create_and_open_local_file(
     path: &'static Path,
-    app_state: Arc<AppState>,
     cx: &mut ViewContext<Workspace>,
     default_content: impl 'static + Send + FnOnce() -> Rope,
 ) -> Task<Result<Box<dyn ItemHandle>>> {
     cx.spawn(|workspace, mut cx| async move {
-        let fs = &app_state.fs;
+        let fs = workspace.read_with(&cx, |workspace, _| workspace.app_state().fs.clone())?;
         if !fs.is_file(path).await {
             fs.create_file(path, Default::default()).await?;
             fs.save(path, &default_content(), Default::default())
@@ -3011,7 +2983,7 @@ pub fn create_and_open_local_file(
 
         let mut items = workspace
             .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)
                 })
             })?
@@ -3030,13 +3002,16 @@ pub fn join_remote_project(
     cx: &mut AppContext,
 ) -> Task<Result<()>> {
     cx.spawn(|mut cx| async move {
-        let existing_workspace = cx.update(|cx| {
-            cx.window_ids()
-                .filter_map(|window_id| cx.root_view(window_id)?.clone().downcast::<Workspace>())
-                .find(|workspace| {
+        let existing_workspace = cx
+            .window_ids()
+            .into_iter()
+            .filter_map(|window_id| cx.root_view(window_id)?.clone().downcast::<Workspace>())
+            .find(|workspace| {
+                cx.read_window(workspace.window_id(), |cx| {
                     workspace.read(cx).project().read(cx).remote_id() == Some(project_id)
                 })
-        });
+                .unwrap_or(false)
+            });
 
         let workspace = if let Some(existing_workspace) = existing_workspace {
             existing_workspace.downgrade()
@@ -3104,6 +3079,59 @@ pub fn join_remote_project(
     })
 }
 
+pub fn restart(_: &Restart, cx: &mut AppContext) {
+    let should_confirm = cx.global::<Settings>().confirm_quit;
+    cx.spawn(|mut cx| async move {
+        let mut workspaces = cx
+            .window_ids()
+            .into_iter()
+            .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()));
+
+        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> {
     let mut parts = value.split(',');
     let width: usize = parts.next()?.parse().ok()?;

crates/zed/src/languages/c.rs πŸ”—

@@ -23,7 +23,7 @@ impl super::LspAdapter for CLspAdapter {
         &self,
         http: Arc<dyn HttpClient>,
     ) -> Result<Box<dyn 'static + Send + Any>> {
-        let release = latest_github_release("clangd/clangd", http).await?;
+        let release = latest_github_release("clangd/clangd", false, http).await?;
         let asset_name = format!("clangd-mac-{}.zip", release.name);
         let asset = release
             .assets

crates/zed/src/languages/elixir.rs πŸ”—

@@ -24,7 +24,7 @@ impl LspAdapter for ElixirLspAdapter {
         &self,
         http: Arc<dyn HttpClient>,
     ) -> Result<Box<dyn 'static + Send + Any>> {
-        let release = latest_github_release("elixir-lsp/elixir-ls", http).await?;
+        let release = latest_github_release("elixir-lsp/elixir-ls", false, http).await?;
         let asset_name = "elixir-ls.zip";
         let asset = release
             .assets

crates/zed/src/languages/go.rs πŸ”—

@@ -33,7 +33,7 @@ impl super::LspAdapter for GoLspAdapter {
         &self,
         http: Arc<dyn HttpClient>,
     ) -> Result<Box<dyn 'static + Send + Any>> {
-        let release = latest_github_release("golang/tools", http).await?;
+        let release = latest_github_release("golang/tools", false, http).await?;
         let version: Option<String> = release.name.strip_prefix("gopls/v").map(str::to_string);
         if version.is_none() {
             log::warn!(

crates/zed/src/languages/html.rs πŸ”—

@@ -57,8 +57,8 @@ impl LspAdapter for HtmlLspAdapter {
         if fs::metadata(&server_path).await.is_err() {
             self.node
                 .npm_install_packages(
-                    [("vscode-langservers-extracted", version.as_str())],
                     &container_dir,
+                    [("vscode-langservers-extracted", version.as_str())],
                 )
                 .await?;
         }

crates/zed/src/languages/json.rs πŸ”—

@@ -76,8 +76,8 @@ impl LspAdapter for JsonLspAdapter {
         if fs::metadata(&server_path).await.is_err() {
             self.node
                 .npm_install_packages(
-                    [("vscode-json-languageserver", version.as_str())],
                     &container_dir,
+                    [("vscode-json-languageserver", version.as_str())],
                 )
                 .await?;
         }

crates/zed/src/languages/lua.rs πŸ”—

@@ -30,7 +30,7 @@ impl super::LspAdapter for LuaLspAdapter {
         &self,
         http: Arc<dyn HttpClient>,
     ) -> Result<Box<dyn 'static + Send + Any>> {
-        let release = latest_github_release("LuaLS/lua-language-server", http).await?;
+        let release = latest_github_release("LuaLS/lua-language-server", false, http).await?;
         let version = release.name.clone();
         let platform = match consts::ARCH {
             "x86_64" => "x64",

crates/zed/src/languages/python.rs πŸ”—

@@ -53,7 +53,7 @@ impl LspAdapter for PythonLspAdapter {
 
         if fs::metadata(&server_path).await.is_err() {
             self.node
-                .npm_install_packages([("pyright", version.as_str())], &container_dir)
+                .npm_install_packages(&container_dir, [("pyright", version.as_str())])
                 .await?;
         }
 

crates/zed/src/languages/rust.rs πŸ”—

@@ -24,18 +24,17 @@ impl LspAdapter for RustLspAdapter {
         &self,
         http: Arc<dyn HttpClient>,
     ) -> Result<Box<dyn 'static + Send + Any>> {
-        let release = latest_github_release("rust-analyzer/rust-analyzer", http).await?;
+        let release = latest_github_release("rust-analyzer/rust-analyzer", false, http).await?;
         let asset_name = format!("rust-analyzer-{}-apple-darwin.gz", consts::ARCH);
         let asset = release
             .assets
             .iter()
             .find(|asset| asset.name == asset_name)
             .ok_or_else(|| anyhow!("no asset found matching {:?}", asset_name))?;
-        let version = GitHubLspBinaryVersion {
+        Ok(Box::new(GitHubLspBinaryVersion {
             name: release.name,
             url: asset.browser_download_url.clone(),
-        };
-        Ok(Box::new(version) as Box<_>)
+        }))
     }
 
     async fn fetch_server_binary(
@@ -77,6 +76,7 @@ impl LspAdapter for RustLspAdapter {
             while let Some(entry) = entries.next().await {
                 last = Some(entry?.path());
             }
+
             anyhow::Ok(LanguageServerBinary {
                 path: last.ok_or_else(|| anyhow!("no cached binary"))?,
                 arguments: Default::default(),

crates/zed/src/languages/typescript.rs πŸ”—

@@ -1,4 +1,6 @@
 use anyhow::{anyhow, Result};
+use async_compression::futures::bufread::GzipDecoder;
+use async_tar::Archive;
 use async_trait::async_trait;
 use futures::{future::BoxFuture, FutureExt};
 use gpui::AppContext;
@@ -6,7 +8,7 @@ use language::{LanguageServerBinary, LanguageServerName, LspAdapter};
 use lsp::CodeActionKind;
 use node_runtime::NodeRuntime;
 use serde_json::{json, Value};
-use smol::fs;
+use smol::{fs, io::BufReader, stream::StreamExt};
 use std::{
     any::Any,
     ffi::OsString,
@@ -14,8 +16,8 @@ use std::{
     path::{Path, PathBuf},
     sync::Arc,
 };
-use util::http::HttpClient;
-use util::ResultExt;
+use util::{fs::remove_matching, github::latest_github_release, http::HttpClient};
+use util::{github::GitHubLspBinaryVersion, ResultExt};
 
 fn typescript_server_binary_arguments(server_path: &Path) -> Vec<OsString> {
     vec![
@@ -69,24 +71,24 @@ impl LspAdapter for TypeScriptLspAdapter {
 
     async fn fetch_server_binary(
         &self,
-        versions: Box<dyn 'static + Send + Any>,
+        version: Box<dyn 'static + Send + Any>,
         _: Arc<dyn HttpClient>,
         container_dir: PathBuf,
     ) -> Result<LanguageServerBinary> {
-        let versions = versions.downcast::<TypeScriptVersions>().unwrap();
+        let version = version.downcast::<TypeScriptVersions>().unwrap();
         let server_path = container_dir.join(Self::NEW_SERVER_PATH);
 
         if fs::metadata(&server_path).await.is_err() {
             self.node
                 .npm_install_packages(
+                    &container_dir,
                     [
-                        ("typescript", versions.typescript_version.as_str()),
+                        ("typescript", version.typescript_version.as_str()),
                         (
                             "typescript-language-server",
-                            versions.server_version.as_str(),
+                            version.server_version.as_str(),
                         ),
                     ],
-                    &container_dir,
                 )
                 .await?;
         }
@@ -172,8 +174,7 @@ pub struct EsLintLspAdapter {
 }
 
 impl EsLintLspAdapter {
-    const SERVER_PATH: &'static str =
-        "node_modules/vscode-langservers-extracted/lib/eslint-language-server/eslintServer.js";
+    const SERVER_PATH: &'static str = "vscode-eslint/server/out/eslintServer.js";
 
     #[allow(unused)]
     pub fn new(node: Arc<NodeRuntime>) -> Self {
@@ -187,35 +188,10 @@ impl LspAdapter for EsLintLspAdapter {
         Some(
             future::ready(json!({
                 "": {
-                      "validate": "on",
-                      "packageManager": "npm",
-                      "useESLintClass": false,
-                      "experimental": {
-                        "useFlatConfig": false
-                      },
-                      "codeActionOnSave": {
-                        "mode": "all"
-                      },
-                      "format": false,
-                      "quiet": false,
-                      "onIgnoredFiles": "off",
-                      "options": {},
-                      "rulesCustomizations": [],
-                      "run": "onType",
-                      "problems": {
-                        "shortenToSingleLine": false
-                      },
-                      "nodePath": null,
-                      "codeAction": {
-                        "disableRuleComment": {
-                          "enable": true,
-                          "location": "separateLine",
-                          "commentStyle": "line"
-                        },
-                        "showDocumentation": {
-                          "enable": true
-                        }
-                      }
+                    "validate": "on",
+                    "rulesCustomizations": [],
+                    "run": "onType",
+                    "nodePath": null,
                 }
             }))
             .boxed(),
@@ -228,30 +204,50 @@ impl LspAdapter for EsLintLspAdapter {
 
     async fn fetch_latest_server_version(
         &self,
-        _: Arc<dyn HttpClient>,
+        http: Arc<dyn HttpClient>,
     ) -> Result<Box<dyn 'static + Send + Any>> {
-        Ok(Box::new(
-            self.node
-                .npm_package_latest_version("vscode-langservers-extracted")
-                .await?,
-        ))
+        // At the time of writing the latest vscode-eslint release was released in 2020 and requires
+        // special custom LSP protocol extensions be handled to fully initalize. Download the latest
+        // prerelease instead to sidestep this issue
+        let release = latest_github_release("microsoft/vscode-eslint", true, http).await?;
+        Ok(Box::new(GitHubLspBinaryVersion {
+            name: release.name,
+            url: release.tarball_url,
+        }))
     }
 
     async fn fetch_server_binary(
         &self,
-        versions: Box<dyn 'static + Send + Any>,
-        _: Arc<dyn HttpClient>,
+        version: Box<dyn 'static + Send + Any>,
+        http: Arc<dyn HttpClient>,
         container_dir: PathBuf,
     ) -> Result<LanguageServerBinary> {
-        let version = versions.downcast::<String>().unwrap();
-        let server_path = container_dir.join(Self::SERVER_PATH);
+        let version = version.downcast::<GitHubLspBinaryVersion>().unwrap();
+        let destination_path = container_dir.join(format!("vscode-eslint-{}", version.name));
+        let server_path = destination_path.join(Self::SERVER_PATH);
 
         if fs::metadata(&server_path).await.is_err() {
+            remove_matching(&container_dir, |entry| entry != destination_path).await;
+
+            let mut response = http
+                .get(&version.url, Default::default(), true)
+                .await
+                .map_err(|err| anyhow!("error downloading release: {}", err))?;
+            let decompressed_bytes = GzipDecoder::new(BufReader::new(response.body_mut()));
+            let archive = Archive::new(decompressed_bytes);
+            archive.unpack(&destination_path).await?;
+
+            let mut dir = fs::read_dir(&destination_path).await?;
+            let first = dir.next().await.ok_or(anyhow!("missing first file"))??;
+            let repo_root = destination_path.join("vscode-eslint");
+            fs::rename(first.path(), &repo_root).await?;
+
             self.node
-                .npm_install_packages(
-                    [("vscode-langservers-extracted", version.as_str())],
-                    &container_dir,
-                )
+                .run_npm_subcommand(&repo_root, "install", &[])
+                .await?;
+
+            self.node
+                .run_npm_subcommand(&repo_root, "run-script", &["compile"])
                 .await?;
         }
 
@@ -263,18 +259,17 @@ impl LspAdapter for EsLintLspAdapter {
 
     async fn cached_server_binary(&self, container_dir: PathBuf) -> Option<LanguageServerBinary> {
         (|| async move {
-            let server_path = container_dir.join(Self::SERVER_PATH);
-            if server_path.exists() {
-                Ok(LanguageServerBinary {
-                    path: self.node.binary_path().await?,
-                    arguments: eslint_server_binary_arguments(&server_path),
-                })
-            } else {
-                Err(anyhow!(
-                    "missing executable in directory {:?}",
-                    container_dir
-                ))
+            // This is unfortunate but we don't know what the version is to build a path directly
+            let mut dir = fs::read_dir(&container_dir).await?;
+            let first = dir.next().await.ok_or(anyhow!("missing first file"))??;
+            if !first.file_type().await?.is_dir() {
+                return Err(anyhow!("First entry is not a directory"));
             }
+
+            Ok(LanguageServerBinary {
+                path: first.path().join(Self::SERVER_PATH),
+                arguments: Default::default(),
+            })
         })()
         .await
         .log_err()

crates/zed/src/languages/yaml.rs πŸ”—

@@ -61,7 +61,7 @@ impl LspAdapter for YamlLspAdapter {
 
         if fs::metadata(&server_path).await.is_err() {
             self.node
-                .npm_install_packages([("yaml-language-server", version.as_str())], &container_dir)
+                .npm_install_packages(&container_dir, [("yaml-language-server", version.as_str())])
                 .await?;
         }
 

crates/zed/src/main.rs πŸ”—

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

crates/zed/src/zed.rs πŸ”—

@@ -11,6 +11,7 @@ use collections::VecDeque;
 pub use editor;
 use editor::{Editor, MultiBuffer};
 
+use anyhow::anyhow;
 use feedback::{
     feedback_info_text::FeedbackInfoText, submit_feedback_button::SubmitFeedbackButton,
 };
@@ -20,7 +21,7 @@ use gpui::{
     geometry::vector::vec2f,
     impl_actions,
     platform::{Platform, PromptLevel, TitlebarOptions, WindowBounds, WindowKind, WindowOptions},
-    ViewContext,
+    AppContext, ViewContext,
 };
 pub use lsp;
 pub use project;
@@ -34,7 +35,10 @@ use terminal_view::terminal_button::TerminalButton;
 use util::{channel::ReleaseChannel, paths, ResultExt};
 use uuid::Uuid;
 pub use workspace;
-use workspace::{create_and_open_local_file, sidebar::SidebarSide, AppState, Restart, Workspace};
+use workspace::{
+    create_and_open_local_file, open_new, sidebar::SidebarSide, AppState, NewFile, NewWindow,
+    Workspace,
+};
 
 #[derive(Deserialize, Clone, PartialEq)]
 pub struct OpenBrowser {
@@ -111,7 +115,6 @@ pub fn init(app_state: &Arc<AppState>, cx: &mut gpui::AppContext) {
         },
     );
     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 |_: &IncreaseBufferFontSize, cx| {
         cx.update_global::<Settings, _, _>(|settings, cx| {
@@ -146,72 +149,71 @@ pub fn init(app_state: &Arc<AppState>, cx: &mut gpui::AppContext) {
         })
         .detach_and_log_err(cx);
     });
-    cx.add_action({
-        let app_state = app_state.clone();
+    cx.add_action(
         move |workspace: &mut Workspace, _: &OpenLog, cx: &mut ViewContext<Workspace>| {
-            open_log_file(workspace, app_state.clone(), cx);
-        }
-    });
-    cx.add_action({
-        let app_state = app_state.clone();
-        move |_: &mut Workspace, _: &OpenLicenses, cx: &mut ViewContext<Workspace>| {
+            open_log_file(workspace, cx);
+        },
+    );
+    cx.add_action(
+        move |workspace: &mut Workspace, _: &OpenLicenses, cx: &mut ViewContext<Workspace>| {
             open_bundled_file(
-                app_state.clone(),
+                workspace,
                 "licenses.md",
                 "Open Source License Attribution",
                 "Markdown",
                 cx,
             );
-        }
-    });
-    cx.add_action({
-        let app_state = app_state.clone();
+        },
+    );
+    cx.add_action(
         move |workspace: &mut Workspace, _: &OpenTelemetryLog, cx: &mut ViewContext<Workspace>| {
-            open_telemetry_log_file(workspace, app_state.clone(), cx);
-        }
-    });
-    cx.add_action({
-        let app_state = app_state.clone();
+            open_telemetry_log_file(workspace, cx);
+        },
+    );
+    cx.add_action(
         move |_: &mut Workspace, _: &OpenKeymap, cx: &mut ViewContext<Workspace>| {
-            create_and_open_local_file(&paths::KEYMAP, app_state.clone(), cx, Default::default)
-                .detach_and_log_err(cx);
-        }
-    });
-    cx.add_action({
-        let app_state = app_state.clone();
-        move |_: &mut Workspace, _: &OpenDefaultKeymap, cx: &mut ViewContext<Workspace>| {
+            create_and_open_local_file(&paths::KEYMAP, cx, Default::default).detach_and_log_err(cx);
+        },
+    );
+    cx.add_action(
+        move |workspace: &mut Workspace, _: &OpenDefaultKeymap, cx: &mut ViewContext<Workspace>| {
             open_bundled_file(
-                app_state.clone(),
+                workspace,
                 "keymaps/default.json",
                 "Default Key Bindings",
                 "JSON",
                 cx,
             );
-        }
-    });
-    cx.add_action({
-        let app_state = app_state.clone();
-        move |_: &mut Workspace, _: &OpenDefaultSettings, cx: &mut ViewContext<Workspace>| {
+        },
+    );
+    cx.add_action(
+        move |workspace: &mut Workspace,
+              _: &OpenDefaultSettings,
+              cx: &mut ViewContext<Workspace>| {
             open_bundled_file(
-                app_state.clone(),
+                workspace,
                 DEFAULT_SETTINGS_ASSET_PATH,
                 "Default Settings",
                 "JSON",
                 cx,
             );
-        }
-    });
+        },
+    );
     cx.add_action({
-        let app_state = app_state.clone();
-        move |_: &mut Workspace, _: &DebugElements, cx: &mut ViewContext<Workspace>| {
-            let app_state = app_state.clone();
+        move |workspace: &mut Workspace, _: &DebugElements, cx: &mut ViewContext<Workspace>| {
+            let app_state = workspace.app_state().clone();
             let markdown = app_state.languages.language_for_name("JSON");
-            let content = to_string_pretty(&cx.debug_elements()).unwrap();
+            let window_id = cx.window_id();
             cx.spawn(|workspace, mut cx| async move {
                 let markdown = markdown.await.log_err();
+                let content = to_string_pretty(
+                    &cx.debug_elements(window_id)
+                        .ok_or_else(|| anyhow!("could not debug elements for {window_id}"))?,
+                )
+                .unwrap();
                 workspace
                     .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 buffer = project
@@ -243,6 +245,28 @@ pub fn init(app_state: &Arc<AppState>, cx: &mut gpui::AppContext) {
             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);
     lsp_log::init(cx);
     call::init(app_state.client.clone(), app_state.user_store.clone(), cx);
@@ -260,7 +284,7 @@ pub fn initialize_workspace(
             if let workspace::Event::PaneAdded(pane) = event {
                 pane.update(cx, |pane, 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);
                         let buffer_search_bar = cx.add_view(BufferSearchBar::new);
                         toolbar.add_item(buffer_search_bar, cx);
@@ -284,12 +308,11 @@ pub fn initialize_workspace(
     cx.emit(workspace::Event::PaneAdded(workspace.active_pane().clone()));
     cx.emit(workspace::Event::PaneAdded(workspace.dock_pane().clone()));
 
-    let collab_titlebar_item = cx.add_view(|cx| {
-        CollabTitlebarItem::new(&workspace_handle, app_state.user_store.clone(), cx)
-    });
+    let collab_titlebar_item =
+        cx.add_view(|cx| CollabTitlebarItem::new(workspace, &workspace_handle, 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| {
         sidebar.add_item(
             "icons/folder_tree_16.svg",
@@ -300,14 +323,15 @@ pub fn initialize_workspace(
     });
 
     let toggle_terminal = cx.add_view(|cx| TerminalButton::new(workspace_handle.clone(), cx));
-    let copilot = cx.add_view(|cx| copilot_button::CopilotButton::new(app_state.clone(), cx));
+    let copilot = cx.add_view(|cx| copilot_button::CopilotButton::new(cx));
     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 =
         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 =
-        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());
     workspace.status_bar().update(cx, |status_bar, cx| {
         status_bar.add_left_item(diagnostic_summary, cx);
@@ -354,77 +378,26 @@ 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()));
-
+fn quit(_: &Quit, cx: &mut gpui::AppContext) {
     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) {
-    let mut workspaces = cx
-        .window_ids()
-        .filter_map(|window_id| {
-            Some(
-                cx.root_view(window_id)?
-                    .clone()
-                    .downcast::<Workspace>()?
-                    .downgrade(),
-            )
-        })
-        .collect::<Vec<_>>();
+        let mut workspaces = cx
+            .window_ids()
+            .into_iter()
+            .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()));
+        // 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(),
@@ -464,20 +437,15 @@ fn about(_: &mut Workspace, _: &About, cx: &mut gpui::ViewContext<Workspace>) {
     cx.prompt(PromptLevel::Info, &format!("{app_name} {version}"), &["OK"]);
 }
 
-fn open_log_file(
-    workspace: &mut Workspace,
-    app_state: Arc<AppState>,
-    cx: &mut ViewContext<Workspace>,
-) {
+fn open_log_file(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) {
     const MAX_LINES: usize = 1000;
 
     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 {
-                let (old_log, new_log) = futures::join!(
-                    app_state.fs.load(&paths::OLD_LOG),
-                    app_state.fs.load(&paths::LOG)
-                );
+                let (old_log, new_log) =
+                    futures::join!(fs.load(&paths::OLD_LOG), fs.load(&paths::LOG));
 
                 let mut lines = VecDeque::with_capacity(MAX_LINES);
                 for line in old_log
@@ -522,15 +490,12 @@ fn open_log_file(
         .detach();
 }
 
-fn open_telemetry_log_file(
-    workspace: &mut Workspace,
-    app_state: Arc<AppState>,
-    cx: &mut ViewContext<Workspace>,
-) {
-    workspace.with_local_workspace(&app_state.clone(), cx, move |_, cx| {
+fn open_telemetry_log_file(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) {
+    workspace.with_local_workspace(cx, move |workspace, cx| {
+        let app_state = workspace.app_state().clone();
         cx.spawn(|workspace, mut cx| async move {
             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()?;
                 app_state.fs.load(&path).await.log_err()
             }
 
@@ -583,18 +548,18 @@ fn open_telemetry_log_file(
 }
 
 fn open_bundled_file(
-    app_state: Arc<AppState>,
+    workspace: &mut Workspace,
     asset_path: &'static str,
     title: &'static str,
     language: &'static str,
     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 {
         let language = language.await.log_err();
         workspace
             .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 buffer = project.update(cx, |project, cx| {
                         let text = Assets::get(asset_path)
@@ -825,8 +790,12 @@ mod tests {
     #[gpui::test]
     async fn test_new_empty_workspace(cx: &mut TestAppContext) {
         let app_state = init(cx);
-        cx.update(|cx| open_new(&app_state, cx, |_, cx| cx.dispatch_action(NewFile)))
-            .await;
+        cx.update(|cx| {
+            open_new(&app_state, cx, |workspace, cx| {
+                Editor::new_file(workspace, &Default::default(), cx)
+            })
+        })
+        .await;
 
         let window_id = *cx.window_ids().first().unwrap();
         let workspace = cx

styles/src/styleTree/editor.ts πŸ”—

@@ -176,6 +176,9 @@ export default function editor(colorScheme: ColorScheme) {
                     left: 10,
                 },
             },
+            source: {
+                text: text(colorScheme.middle, "sans", { size: "sm", weight: "bold", }),
+            },
             message: {
                 highlightText: text(colorScheme.middle, "sans", {
                     size: "sm",

styles/src/styleTree/hoverPopover.ts πŸ”—

@@ -40,7 +40,7 @@ export default function HoverPopover(colorScheme: ColorScheme) {
             padding: { top: 4 },
         },
         prose: text(layer, "sans", { size: "sm" }),
-        diagnosticSourceHighlight: { underline: true, color: foreground(layer, "accent") },
+        diagnosticSourceHighlight: { color: foreground(layer, "accent") },
         highlight: colorScheme.ramps.neutral(0.5).alpha(0.2).hex(), // TODO: blend was used here. Replace with something better
     }
 }