diff --git a/Cargo.lock b/Cargo.lock index d1f5631f6f..e2b29c7aeb 100644 --- a/Cargo.lock +++ b/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", diff --git a/README.md b/README.md index d23744aac0..6908cebf24 100644 --- a/README.md +++ b/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 diff --git a/crates/activity_indicator/src/activity_indicator.rs b/crates/activity_indicator/src/activity_indicator.rs index 4ea031985e..d5ee1364b3 100644 --- a/crates/activity_indicator/src/activity_indicator.rs +++ b/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>, + on_click: Option)>>, } 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::::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() diff --git a/crates/auto_update/src/auto_update.rs b/crates/auto_update/src/auto_update.rs index 02cbab21d0..abf95ff45a 100644 --- a/crates/auto_update/src/auto_update.rs +++ b/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>, + 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, 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::() @@ -74,25 +82,32 @@ pub fn init(http_client: Arc, 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::() - && *cx.global::() == 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::() + && *cx.global::() == 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, 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::>().telemetry().installation_id(); + let release_channel = cx + .has_global::() + .then(|| cx.global::().display_name()); + let telemetry = cx.global::().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); diff --git a/crates/auto_update/src/update_notification.rs b/crates/auto_update/src/update_notification.rs index c0b88fdf5e..b48ac2a413 100644 --- a/crates/auto_update/src/update_notification.rs +++ b/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") } diff --git a/crates/breadcrumbs/src/breadcrumbs.rs b/crates/breadcrumbs/src/breadcrumbs.rs index c09706f378..f3be60f8de 100644 --- a/crates/breadcrumbs/src/breadcrumbs.rs +++ b/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>, project_search: Option>, subscription: Option, + workspace: WeakViewHandle, } 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::( 0, diff --git a/crates/client/src/client.rs b/crates/client/src/client.rs index 62135900a3..18a0f32ed6 100644 --- a/crates/client/src/client.rs +++ b/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::().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 { - self.telemetry.log_file_path() - } - - pub fn metrics_id(&self) -> Option> { - self.telemetry.metrics_id() - } - - pub fn is_staff(&self) -> Option { - self.telemetry.is_staff() + pub fn telemetry(&self) -> &Arc { + &self.telemetry } } diff --git a/crates/client/src/telemetry.rs b/crates/client/src/telemetry.rs index 7ee099dfab..5dfb6595d1 100644 --- a/crates/client/src/telemetry.rs +++ b/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>, - device_id: Option>, + metrics_id: Option>, // Per logged-in user + installation_id: Option>, // Per app installation app_version: Option>, release_channel: Option<&'static str>, os_version: Option>, os_name: &'static str, - queue: Vec, - next_event_id: usize, - flush_task: Option>, + mixpanel_events_queue: Vec, + clickhouse_events_queue: Vec, + next_mixpanel_event_id: usize, + flush_mixpanel_events_task: Option>, + flush_clickhouse_events_task: Option>, log_file: Option, is_staff: Option, } 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 = 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>, + app_version: Option>, + os_name: &'static str, + os_version: Option>, + release_channel: Option<&'static str>, + events: Vec, +} + +#[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, + 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>, + #[serde(rename = "distinct_id")] + installation_id: Option>, #[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, + installation_id: Arc, #[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 = device_id.into(); + let installation_id: Arc = 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()); } - if !state.queue.is_empty() { - drop(state); - this.flush(); + + 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 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> = 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, + 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, 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) -> Option> { + self.state.lock().installation_id.clone() + } + pub fn is_staff(self: &Arc) -> Option { self.state.lock().is_staff } - fn flush(self: &Arc) { + fn flush_mixpanel_events(self: &Arc) { 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) { + 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(); + } } diff --git a/crates/collab_ui/src/collab_titlebar_item.rs b/crates/collab_ui/src/collab_titlebar_item.rs index 95fe26fa11..97fb82a5d6 100644 --- a/crates/collab_ui/src/collab_titlebar_item.rs +++ b/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, + project: ModelHandle, user_store: ModelHandle, + client: Arc, + workspace: WeakViewHandle, contacts_popover: Option>, user_menu: ViewHandle, - collaborator_list_popover: Option>, _subscriptions: Vec, } @@ -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, - user_store: ModelHandle, + workspace: &Workspace, + workspace_handle: &ViewHandle, cx: &mut ViewContext, ) -> 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) { - 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) { @@ -198,71 +199,42 @@ impl CollabTitlebarItem { } fn share_project(&mut self, _: &ShareProject, cx: &mut ViewContext) { - 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) { - 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, - ) { - 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) { 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::( 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::( 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::( @@ -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::( 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(), ), diff --git a/crates/collab_ui/src/collab_ui.rs b/crates/collab_ui/src/collab_ui.rs index 3f3998fb6d..c0734388b1 100644 --- a/crates/collab_ui/src/collab_ui.rs +++ b/crates/collab_ui/src/collab_ui.rs @@ -1,5 +1,4 @@ mod collab_titlebar_item; -mod collaborator_list_popover; mod contact_finder; mod contact_list; mod contact_notification; diff --git a/crates/collab_ui/src/collaborator_list_popover.rs b/crates/collab_ui/src/collaborator_list_popover.rs deleted file mode 100644 index 6820644441..0000000000 --- a/crates/collab_ui/src/collaborator_list_popover.rs +++ /dev/null @@ -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, -} - -impl Entity for CollaboratorListPopover { - type Event = Event; -} - -impl View for CollaboratorListPopover { - fn ui_name() -> &'static str { - "CollaboratorListPopover" - } - - fn render(&mut self, cx: &mut ViewContext) -> AnyElement { - let theme = cx.global::().theme.clone(); - - MouseEventHandler::::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) { - cx.emit(Event::Dismissed); - } -} - -impl CollaboratorListPopover { - pub fn new(user_store: ModelHandle, cx: &mut ViewContext) -> 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::>(); - - //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::, - 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( - index: usize, - username: &str, - username_action: Option, - username_tooltip: Option, - icon: Svg, - icon_action: IA, - icon_tooltip: String, - cx: &mut ViewContext, -) -> AnyElement { - enum Username {} - enum UsernameTooltip {} - enum Icon {} - enum IconTooltip {} - - let theme = &cx.global::().theme; - let username_theme = theme.contact_list.contact_username.text.clone(); - let tooltip_theme = theme.tooltip.clone(); - - let username = - MouseEventHandler::::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::( - index, - username_tooltip, - None, - tooltip_theme.clone(), - cx, - ) - .into_any() - } else { - username.into_any() - }) - .with_child( - MouseEventHandler::::new(index, cx, |_, _| icon) - .on_click(MouseButton::Left, move |_, _, cx| { - cx.dispatch_action(icon_action.clone()) - }) - .with_tooltip::(index, icon_tooltip, None, tooltip_theme, cx), - ) - .into_any() -} diff --git a/crates/collab_ui/src/contact_finder.rs b/crates/collab_ui/src/contact_finder.rs index b07d6d7e2b..8530867f14 100644 --- a/crates/collab_ui/src/contact_finder.rs +++ b/crates/collab_ui/src/contact_finder.rs @@ -23,6 +23,7 @@ pub fn build_contact_finder( }, cx, ) + .with_theme(|theme| theme.contact_finder.picker.clone()) } pub struct ContactFinderDelegate { diff --git a/crates/collab_ui/src/contact_list.rs b/crates/collab_ui/src/contact_list.rs index 319df337d7..87aa41b7a4 100644 --- a/crates/collab_ui/src/contact_list.rs +++ b/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, cx: &mut ViewContext) -> Self { + pub fn new( + project: ModelHandle, + user_store: ModelHandle, + workspace: WeakViewHandle, + cx: &mut ViewContext, + ) -> 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::( 0, diff --git a/crates/collab_ui/src/contacts_popover.rs b/crates/collab_ui/src/contacts_popover.rs index 60f0bf0e73..35734d81f4 100644 --- a/crates/collab_ui/src/contacts_popover.rs +++ b/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, user_store: ModelHandle, workspace: WeakViewHandle, _subscription: Option, } impl ContactsPopover { - pub fn new(workspace: &ViewHandle, cx: &mut ViewContext) -> Self { + pub fn new( + project: ModelHandle, + user_store: ModelHandle, + workspace: WeakViewHandle, + cx: &mut ViewContext, + ) -> 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) { - 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() } diff --git a/crates/collab_ui/src/incoming_call_notification.rs b/crates/collab_ui/src/incoming_call_notification.rs index 2e3048a4f8..35484b3309 100644 --- a/crates/collab_ui/src/incoming_call_notification.rs +++ b/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(); diff --git a/crates/collab_ui/src/project_shared_notification.rs b/crates/collab_ui/src/project_shared_notification.rs index 1304688ca1..8a41368276 100644 --- a/crates/collab_ui/src/project_shared_notification.rs +++ b/crates/collab_ui/src/project_shared_notification.rs @@ -58,14 +58,14 @@ pub fn init(app_state: &Arc, 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()); } } } diff --git a/crates/collab_ui/src/sharing_status_indicator.rs b/crates/collab_ui/src/sharing_status_indicator.rs index 42c3c886ad..9fbe57af65 100644 --- a/crates/collab_ui/src/sharing_status_indicator.rs +++ b/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() } diff --git a/crates/command_palette/src/command_palette.rs b/crates/command_palette/src/command_palette.rs index 1d9ac62c2c..441fbb84a6 100644 --- a/crates/command_palette/src/command_palette.rs +++ b/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, 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"); }); diff --git a/crates/context_menu/src/context_menu.rs b/crates/context_menu/src/context_menu.rs index 6f66d710cb..e0429bd01b 100644 --- a/crates/context_menu/src/context_menu.rs +++ b/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); + }) } } diff --git a/crates/copilot/src/copilot.rs b/crates/copilot/src/copilot.rs index 6bc3622ab1..ea25355065 100644 --- a/crates/copilot/src/copilot.rs +++ b/crates/copilot/src/copilot.rs @@ -560,7 +560,7 @@ impl Copilot { } } - fn reinstall(&mut self, cx: &mut ModelContext) -> Task<()> { + pub fn reinstall(&mut self, cx: &mut ModelContext) -> Task<()> { let start_task = cx .spawn({ let http = self.http.clone(); @@ -932,7 +932,7 @@ async fn get_copilot_lsp(http: Arc) -> anyhow::Result { ///Check for the latest copilot language server and download it if we haven't already async fn fetch_latest(http: Arc) -> anyhow::Result { - 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)); diff --git a/crates/copilot/src/sign_in.rs b/crates/copilot/src/sign_in.rs index fdb4828cd0..da3c96956e 100644 --- a/crates/copilot/src/sign_in.rs +++ b/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::( + .with_child(theme::ui::cta_button::( 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::( + .with_child(theme::ui::cta_button::( "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::( + .with_child(theme::ui::cta_button::( "Subscribe on GitHub", style.auth.content_width, &style.auth.cta_button, diff --git a/crates/copilot_button/src/copilot_button.rs b/crates/copilot_button/src/copilot_button.rs index 93adc95efd..aa93af4d4c 100644 --- a/crates/copilot_button/src/copilot_button.rs +++ b/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, popup_menu: ViewHandle, editor_subscription: Option<(Subscription, usize)>, editor_enabled: Option, @@ -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, cx: &mut ViewContext) -> Self { + pub fn new(cx: &mut ViewContext) -> 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::() - .zip(app_state.upgrade()) - { + if let Some(workspace) = cx.root_view().clone().downcast::() { 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, - app_state: Arc, path_to_disable: Option>, 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() diff --git a/crates/diagnostics/src/diagnostics.rs b/crates/diagnostics/src/diagnostics.rs index 6973d83f9b..17d142ba4b 100644 --- a/crates/diagnostics/src/diagnostics.rs +++ b/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::(); 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| { diff --git a/crates/diagnostics/src/items.rs b/crates/diagnostics/src/items.rs index 19b1506509..f0ceacc619 100644 --- a/crates/diagnostics/src/items.rs +++ b/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>, + workspace: WeakViewHandle, current_diagnostic: Option, in_progress_checks: HashSet, _observe_active_editor: Option, @@ -25,7 +26,8 @@ pub fn init(cx: &mut AppContext) { } impl DiagnosticIndicator { - pub fn new(project: &ModelHandle, cx: &mut ViewContext) -> Self { + pub fn new(workspace: &Workspace, cx: &mut ViewContext) -> 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::( 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) }), ); } diff --git a/crates/editor/Cargo.toml b/crates/editor/Cargo.toml index e8cb323a27..e4767a12e2 100644 --- a/crates/editor/Cargo.toml +++ b/crates/editor/Cargo.toml @@ -23,6 +23,7 @@ test-support = [ ] [dependencies] +client = { path = "../client" } clock = { path = "../clock" } copilot = { path = "../copilot" } db = { path = "../db" } diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 69eb8170ac..be57596584 100644 --- a/crates/editor/src/editor.rs +++ b/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> { + 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> { + 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, - ) { - Self::go_to_definition_of_kind(GotoDefinitionKind::Symbol, workspace, cx); + pub fn go_to_definition(&mut self, _: &GoToDefinition, cx: &mut ViewContext) { + self.go_to_definition_of_kind(GotoDefinitionKind::Symbol, cx); } - pub fn go_to_type_definition( - workspace: &mut Workspace, - _: &GoToTypeDefinition, - cx: &mut ViewContext, - ) { - Self::go_to_definition_of_kind(GotoDefinitionKind::Type, workspace, cx); + pub fn go_to_type_definition(&mut self, _: &GoToTypeDefinition, cx: &mut ViewContext) { + self.go_to_definition_of_kind(GotoDefinitionKind::Type, cx); } - fn go_to_definition_of_kind( - kind: GotoDefinitionKind, - workspace: &mut Workspace, - cx: &mut ViewContext, - ) { - let active_item = workspace.active_item(cx); - let editor_handle = if let Some(editor) = active_item - .as_ref() - .and_then(|item| item.act_as::(cx)) - { - editor - } else { - return; - }; - - let editor = editor_handle.read(cx); - let buffer = editor.buffer.read(cx); - let head = editor.selections.newest::(cx).head(); + fn go_to_definition_of_kind(&mut self, kind: GotoDefinitionKind, cx: &mut ViewContext) { + let Some(workspace) = self.workspace(cx) else { return }; + let buffer = self.buffer.read(cx); + let head = self.selections.newest::(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, - definitions: Vec, - cx: &mut ViewContext, + &mut self, + mut definitions: Vec, + cx: &mut ViewContext, ) { - 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 = 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) { +pub fn highlight_diagnostic_message( + inital_highlights: Vec, + message: &str, +) -> (String, Vec) { 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(), "")]) diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 11d1ce8cae..7c43885763 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -211,10 +211,13 @@ impl EditorElement { enum GutterHandlers {} scene.push_mouse_region( MouseRegion::new::(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 !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); - } - }); - - 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::(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), diff --git a/crates/editor/src/hover_popover.rs b/crates/editor/src/hover_popover.rs index 2932fa547e..438c662ed1 100644 --- a/crates/editor/src/hover_popover.rs +++ b/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::( diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index 83e971358d..dcd49607fb 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -636,7 +636,7 @@ impl Item for Editor { project: ModelHandle, cx: &mut ViewContext, ) -> Task> { - 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 { diff --git a/crates/editor/src/link_go_to_definition.rs b/crates/editor/src/link_go_to_definition.rs index 69c45b9da8..b2105c1c81 100644 --- a/crates/editor/src/link_go_to_definition.rs +++ b/crates/editor/src/link_go_to_definition.rs @@ -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) { } pub fn go_to_fetched_definition( - workspace: &mut Workspace, + editor: &mut Editor, point: DisplayPoint, - cx: &mut ViewContext, + cx: &mut ViewContext, ) { - 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, + cx: &mut ViewContext, ) { - 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, + cx: &mut ViewContext, ) { - let active_item = workspace.active_item(cx); - let editor_handle = if let Some(editor) = active_item - .as_ref() - .and_then(|item| item.act_as::(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(); diff --git a/crates/feedback/Cargo.toml b/crates/feedback/Cargo.toml index e94243bd30..e74e14ff4c 100644 --- a/crates/feedback/Cargo.toml +++ b/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" } diff --git a/crates/feedback/src/deploy_feedback_button.rs b/crates/feedback/src/deploy_feedback_button.rs index 9536477c74..b464d00887 100644 --- a/crates/feedback/src/deploy_feedback_button.rs +++ b/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, } 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::( diff --git a/crates/feedback/src/feedback.rs b/crates/feedback/src/feedback.rs index a8860f7bc5..7cbb3a673b 100644 --- a/crates/feedback/src/feedback.rs +++ b/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, -} - -impl_actions!(zed, [OpenBrowser]); +use workspace::Workspace; actions!( zed, @@ -28,29 +18,20 @@ actions!( ] ); -pub fn init(app_state: Arc, 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| { + 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, cx: &mut AppContext) { cx.add_action( |_: &mut Workspace, _: &RequestFeature, cx: &mut ViewContext| { 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| { - 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| { - 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); } diff --git a/crates/feedback/src/feedback_editor.rs b/crates/feedback/src/feedback_editor.rs index 7bf5328048..d5d20b069a 100644 --- a/crates/feedback/src/feedback_editor.rs +++ b/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 = 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, cx: &mut AppContext) { +pub fn init(cx: &mut AppContext) { cx.add_action({ move |workspace: &mut Workspace, _: &GiveFeedback, cx: &mut ViewContext| { - 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>, + installation_id: Option>, system_specs: SystemSpecs, is_staff: bool, token: &'a str, @@ -94,7 +83,7 @@ impl FeedbackEditor { } } - fn handle_save(&mut self, cx: &mut ViewContext) -> Task> { + pub fn submit(&mut self, cx: &mut ViewContext) -> Task> { 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, - cx: &mut ViewContext, - ) { - let markdown = app_state.languages.language_for_name("Markdown"); + pub fn deploy(workspace: &mut Workspace, cx: &mut ViewContext) { + 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, cx: &mut ViewContext, ) -> Task> { - self.handle_save(cx) + self.submit(cx) } fn save_as( @@ -299,7 +288,7 @@ impl Item for FeedbackEditor { _: std::path::PathBuf, cx: &mut ViewContext, ) -> Task> { - 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 { diff --git a/crates/feedback/src/feedback_info_text.rs b/crates/feedback/src/feedback_info_text.rs index b557c4f7e1..9aee4e0e68 100644 --- a/crates/feedback/src/feedback_info_text.rs +++ b/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>, @@ -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( diff --git a/crates/feedback/src/submit_feedback_button.rs b/crates/feedback/src/submit_feedback_button.rs index 918c74bed8..ccd58c3dc9 100644 --- a/crates/feedback/src/submit_feedback_button.rs +++ b/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>, @@ -18,6 +22,18 @@ impl SubmitFeedbackButton { active_item: Default::default(), } } + + pub fn submit( + &mut self, + _: &SubmitFeedback, + cx: &mut ViewContext, + ) -> Option>> { + 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() diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index c201febed5..4d84f7c070 100644 --- a/crates/gpui/src/app.rs +++ b/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>( + &self, + window_id: usize, + callback: F, + ) -> Option { + self.0.borrow_mut().read_window(window_id, callback) + } + pub fn update_window 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 { + 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 { + self.read(|cx| cx.windows.get(&window_id).map(|w| w.root_view().clone())) + } + + pub fn window_ids(&self) -> Vec { + self.read(|cx| cx.windows.keys().copied().collect()) + } + pub fn add_model(&mut self, build_model: F) -> ModelHandle 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 { &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 + '_ { - 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(&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( &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, - ) { - 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::>(); 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, - 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, - }, 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) { - 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)) { 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()); } diff --git a/crates/gpui/src/app/test_app_context.rs b/crates/gpui/src/app/test_app_context.rs index 3a03a81c47..2d079a6042 100644 --- a/crates/gpui/src/app/test_app_context.rs +++ b/crates/gpui/src/app/test_app_context.rs @@ -72,14 +72,16 @@ impl TestAppContext { } pub fn dispatch_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(&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 { - 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>(&self, callback: F) -> T { diff --git a/crates/gpui/src/app/window.rs b/crates/gpui/src/app/window.rs index f54c18c755..49befafbec 100644 --- a/crates/gpui/src/app/window.rs +++ b/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 { - 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 Element 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 { diff --git a/crates/gpui/src/elements.rs b/crates/gpui/src/elements.rs index 09b9c40589..7de0bc10f5 100644 --- a/crates/gpui/src/elements.rs +++ b/crates/gpui/src/elements.rs @@ -45,7 +45,6 @@ use std::{ mem, ops::{Deref, DerefMut, Range}, }; -use util::ResultExt; pub trait Element: 'static { type LayoutState; @@ -709,7 +708,12 @@ impl AnyRootElement for RootElement { .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 AnyRootElement for RootElement { } } -impl Element for RootElement { - type LayoutState = (); - type PaintState = (); - - fn layout( - &mut self, - constraint: SizeConstraint, - _view: &mut V, - cx: &mut ViewContext, - ) -> (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, - ) { - AnyRootElement::paint(self, scene, bounds.origin(), visible_bounds, cx).log_err(); - } - - fn rect_for_text_range( - &self, - range_utf16: Range, - _bounds: RectF, - _visible_bounds: RectF, - _layout: &Self::LayoutState, - _paint: &Self::PaintState, - _view: &V, - cx: &ViewContext, - ) -> Option { - 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, - ) -> serde_json::Value { - AnyRootElement::debug(self, cx) - .log_err() - .unwrap_or_default() - } -} - pub trait ParentElement<'a, V: View>: Extend> + Sized { fn add_children>(&mut self, children: impl IntoIterator) { self.extend(children.into_iter().map(|child| child.into_any())); diff --git a/crates/gpui/src/platform/mac/window.rs b/crates/gpui/src/platform/mac/window.rs index d96f9bc4ae..bcff08d005 100644 --- a/crates/gpui/src/platform/mac/window.rs +++ b/crates/gpui/src/platform/mac/window.rs @@ -699,6 +699,31 @@ impl platform::Window for Window { msg: &str, answers: &[&str], ) -> oneshot::Receiver { + // 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() diff --git a/crates/gpui/src/test.rs b/crates/gpui/src/test.rs index 3b2a5e9960..def8ba2ce5 100644 --- a/crates/gpui/src/test.rs +++ b/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()); diff --git a/crates/gpui/src/views/select.rs b/crates/gpui/src/views/select.rs index 285f37639e..f3be9de3ec 100644 --- a/crates/gpui/src/views/select.rs +++ b/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| 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| { - cx.dispatch_action(SelectItem(ix)) - }, - ) + .on_click(MouseButton::Left, move |_, this, cx| { + this.select_item(&SelectItem(ix), cx); + }) .into_any() })) }, diff --git a/crates/gpui_macros/src/gpui_macros.rs b/crates/gpui_macros/src/gpui_macros.rs index ca15fa14a2..e976245e06 100644 --- a/crates/gpui_macros/src/gpui_macros.rs +++ b/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()); )); diff --git a/crates/language_selector/src/active_buffer_language.rs b/crates/language_selector/src/active_buffer_language.rs index 17e53b378c..425f4c8dd7 100644 --- a/crates/language_selector/src/active_buffer_language.rs +++ b/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>>, + workspace: WeakViewHandle, _observe_active_editor: Option, } -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 { diff --git a/crates/language_selector/src/language_selector.rs b/crates/language_selector/src/language_selector.rs index 29da7c926d..fd43111443 100644 --- a/crates/language_selector/src/language_selector.rs +++ b/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, cx: &mut AppContext) { +pub fn init(cx: &mut AppContext) { Picker::::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, + _: &Toggle, cx: &mut ViewContext, ) -> 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), diff --git a/crates/node_runtime/src/node_runtime.rs b/crates/node_runtime/src/node_runtime.rs index 079b6a5e45..e2a8d0d003 100644 --- a/crates/node_runtime/src/node_runtime.rs +++ b/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 { 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, directory: &Path, + packages: impl IntoIterator, ) -> 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(()) } diff --git a/crates/outline/src/outline.rs b/crates/outline/src/outline.rs index b2154e7bb2..6ecaf370e4 100644 --- a/crates/outline/src/outline.rs +++ b/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) { +pub fn toggle(workspace: &mut Workspace, _: &Toggle, cx: &mut ViewContext) { if let Some(editor) = workspace .active_item(cx) .and_then(|item| item.downcast::()) diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index 0ca187bfe5..373417b167 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/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, context_menu: ViewHandle, dragged_entry_destination: Option>, + workspace: WeakViewHandle, } #[derive(Copy, Clone)] @@ -137,7 +138,8 @@ pub enum Event { } impl ProjectPanel { - pub fn new(project: ModelHandle, cx: &mut ViewContext) -> ViewHandle { + pub fn new(workspace: &mut Workspace, cx: &mut ViewContext) -> ViewHandle { + let project = workspace.project().clone(); let project_panel = cx.add_view(|cx: &mut ViewContext| { 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); diff --git a/crates/recent_projects/src/recent_projects.rs b/crates/recent_projects/src/recent_projects.rs index 6429448f75..644e74d878 100644 --- a/crates/recent_projects/src/recent_projects.rs +++ b/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) { - cx.add_async_action( - move |_: &mut Workspace, _: &OpenRecent, cx: &mut ViewContext| { - toggle(app_state.clone(), cx) - }, - ); +pub fn init(cx: &mut AppContext) { + cx.add_async_action(toggle); RecentProjects::init(cx); } -fn toggle(app_state: Weak, cx: &mut ViewContext) -> Option>> { +fn toggle( + _: &mut Workspace, + _: &OpenRecent, + cx: &mut ViewContext, +) -> Option>> { Some(cx.spawn(|workspace, mut cx| async move { let workspace_locations: Vec<_> = cx .background() @@ -49,11 +49,7 @@ fn toggle(app_state: Weak, cx: &mut ViewContext) -> 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, cx: &mut ViewContext) -> 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; struct RecentProjectsDelegate { workspace: WeakViewHandle, workspace_locations: Vec, - app_state: Weak, selected_match_index: usize, matches: Vec, } @@ -83,12 +78,10 @@ impl RecentProjectsDelegate { fn new( workspace: WeakViewHandle, workspace_locations: Vec, - app_state: Weak, ) -> 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) { - 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); diff --git a/crates/search/src/buffer_search.rs b/crates/search/src/buffer_search.rs index 91ca99c5c3..ee5a2e8332 100644 --- a/crates/search/src/buffer_search.rs +++ b/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::( @@ -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::( @@ -405,7 +407,6 @@ impl BufferSearchBar { theme: &theme::Search, cx: &mut ViewContext, ) -> AnyElement { - let action = Box::new(Dismiss); let tooltip = "Dismiss Buffer Search"; let tooltip_style = cx.global::().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::(0, tooltip.to_string(), Some(action), tooltip_style, cx) + .with_tooltip::( + 0, + tooltip.to_string(), + Some(Box::new(Dismiss)), + tooltip_style, + cx, + ) .into_any() } diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index ac478a8a2c..ea29f9cfda 100644 --- a/crates/search/src/project_search.rs +++ b/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::( @@ -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::( diff --git a/crates/terminal_view/src/terminal_button.rs b/crates/terminal_view/src/terminal_button.rs index 6349cbbfa4..8edf03f527 100644 --- a/crates/terminal_view/src/terminal_button.rs +++ b/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, @@ -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) + }) + } } }; }) diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index d296284772..1211f53742 100644 --- a/crates/theme/src/theme.rs +++ b/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, diff --git a/crates/theme/src/ui.rs b/crates/theme/src/ui.rs index 1198e81e92..b86bfca8c4 100644 --- a/crates/theme/src/ui.rs +++ b/crates/theme/src/ui.rs @@ -156,24 +156,7 @@ pub fn keystroke_label( pub type ButtonStyle = Interactive; -pub fn cta_button( - label: L, - action: A, - max_width: f32, - style: &ButtonStyle, - cx: &mut ViewContext, -) -> MouseEventHandler -where - L: Into>, - A: 'static + Action + Clone, - V: View, -{ - cta_button_with_click::(label, max_width, style, cx, move |_, _, cx| { - cx.dispatch_action(action.clone()) - }) -} - -pub fn cta_button_with_click( +pub fn cta_button( label: L, max_width: f32, style: &ButtonStyle, diff --git a/crates/theme_selector/src/theme_selector.rs b/crates/theme_selector/src/theme_selector.rs index 1f2d73df14..21332114e2 100644 --- a/crates/theme_selector/src/theme_selector.rs +++ b/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, 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, cx: &mut ViewContext) { - workspace.toggle_modal(cx, |_, cx| { +pub fn toggle(workspace: &mut Workspace, _: &Toggle, cx: &mut ViewContext) { + workspace.toggle_modal(cx, |workspace, cx| { + let themes = workspace.app_state().themes.clone(); cx.add_view(|cx| ThemeSelector::new(ThemeSelectorDelegate::new(themes, cx), cx)) }); } diff --git a/crates/util/src/github.rs b/crates/util/src/github.rs index 3bb4baa293..b1e981ae49 100644 --- a/crates/util/src/github.rs +++ b/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, + 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, ) -> Result { 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::(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::>(body.as_slice()) { + Ok(releases) => releases, - release.context("error deserializing latest release") + Err(_) => { + log::error!( + "Error deserializing Github API response text: {:?}", + String::from_utf8_lossy(body.as_slice()) + ); + return Err(anyhow!("error deserializing latest release")); + } + }; + + releases + .into_iter() + .find(|release| release.pre_release == pre_release) + .ok_or(anyhow!("Failed to find a release")) } diff --git a/crates/util/src/http.rs b/crates/util/src/http.rs index e29768a53e..e7f39552b0 100644 --- a/crates/util/src/http.rs +++ b/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, 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") diff --git a/crates/welcome/src/base_keymap_picker.rs b/crates/welcome/src/base_keymap_picker.rs index 7347a559a9..260c279e18 100644 --- a/crates/welcome/src/base_keymap_picker.rs +++ b/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, diff --git a/crates/welcome/src/welcome.rs b/crates/welcome/src/welcome.rs index 629e6f3989..a3d91adc91 100644 --- a/crates/welcome/src/welcome.rs +++ b/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, 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, cx: &mut AppContext) { } pub struct WelcomePage { + workspace: WeakViewHandle, _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::( "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::( "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 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 { + pub fn new(workspace: &Workspace, cx: &mut ViewContext) -> Self { WelcomePage { + workspace: workspace.weak_handle(), _settings_subscription: cx.observe_global::(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, ) -> Option { - Some(WelcomePage::new(cx)) + Some(WelcomePage { + workspace: self.workspace.clone(), + _settings_subscription: cx.observe_global::(move |_, cx| cx.notify()), + }) } } diff --git a/crates/workspace/src/dock.rs b/crates/workspace/src/dock.rs index 33cd833019..8ac432dc47 100644 --- a/crates/workspace/src/dock.rs +++ b/crates/workspace/src/dock.rs @@ -271,11 +271,11 @@ impl Dock { } } - fn focus_dock(workspace: &mut Workspace, _: &FocusDock, cx: &mut ViewContext) { + pub fn focus_dock(workspace: &mut Workspace, _: &FocusDock, cx: &mut ViewContext) { Self::set_dock_position(workspace, workspace.dock.position.show(), true, cx); } - fn hide_dock(workspace: &mut Workspace, _: &HideDock, cx: &mut ViewContext) { + pub fn hide_dock(workspace: &mut Workspace, _: &HideDock, cx: &mut ViewContext) { 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), ) diff --git a/crates/workspace/src/dock/toggle_dock_button.rs b/crates/workspace/src/dock/toggle_dock_button.rs index bf85183938..1fda55b783 100644 --- a/crates/workspace/src/dock/toggle_dock_button.rs +++ b/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, } @@ -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::( 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::( 0, diff --git a/crates/workspace/src/notifications.rs b/crates/workspace/src/notifications.rs index 57749a5c2b..7881603bbc 100644 --- a/crates/workspace/src/notifications.rs +++ b/crates/workspace/src/notifications.rs @@ -114,17 +114,14 @@ impl Workspace { pub fn show_toast(&mut self, toast: Toast, cx: &mut ViewContext) { self.dismiss_notification::(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>, + on_click: Option)>>, click_message: Option>, } @@ -207,36 +202,31 @@ pub mod simple_message_notification { } impl MessageNotification { - pub fn new_message>>(message: S) -> MessageNotification { + pub fn new(message: S) -> MessageNotification + where + S: Into>, + { Self { message: message.into(), - click_action: None, + on_click: None, click_message: None, } } - pub fn new_boxed_action>, S2: Into>>( - message: S1, - click_action: Box, - click_message: S2, - ) -> Self { - Self { - message: message.into(), - click_action: Some(click_action), - click_message: Some(click_message.into()), - } + pub fn with_click_message(mut self, message: S) -> Self + where + S: Into>, + { + self.click_message = Some(message.into()); + self } - pub fn new>, A: Action, S2: Into>>( - message: S1, - click_action: A, - click_message: S2, - ) -> Self { - Self { - message: message.into(), - click_action: Some(Box::new(click_action) as Box), - click_message: Some(click_message.into()), - } + pub fn on_click(mut self, on_click: F) -> Self + where + F: 'static + Fn(&mut ViewContext), + { + self.on_click = Some(Arc::new(on_click)); + self } pub fn dismiss(&mut self, _: &CancelMessageNotification, cx: &mut ViewContext) { @@ -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::::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, )) diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs index 41f4d5d111..8bd42fed04 100644 --- a/crates/workspace/src/pane.rs +++ b/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 { + &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") diff --git a/crates/workspace/src/sidebar.rs b/crates/workspace/src/sidebar.rs index 2581c87f42..2b114d83ec 100644 --- a/crates/workspace/src/sidebar.rs +++ b/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::( ix, diff --git a/crates/workspace/src/toolbar.rs b/crates/workspace/src/toolbar.rs index e9cc90f64d..eac9963d38 100644 --- a/crates/workspace/src/toolbar.rs +++ b/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( +fn nav_button)>( svg_path: &'static str, style: theme::Interactive, tooltip_style: TooltipStyle, enabled: bool, spacing: f32, - action: A, + on_click: F, tooltip_action: A, action_name: &str, cx: &mut ViewContext, @@ -195,8 +227,8 @@ fn nav_button( } 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::( 0, diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 64fa9c7062..bb2629862d 100644 --- a/crates/workspace/src/workspace.rs +++ b/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)>, + on_click: Option<(Cow<'static, str>, Arc)>, } impl Toast { @@ -147,21 +148,17 @@ impl Toast { Toast { id, msg: msg.into(), - click: None, + on_click: None, } } - pub fn new_action>, I2: Into>>( - 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(mut self, message: M, on_click: F) -> Self + where + M: Into>, + 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, cx: &mut AppContext) { } } }); - cx.add_action({ - let app_state = Arc::downgrade(&app_state); - move |_, _: &Open, cx: &mut ViewContext| { - 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, 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, cx: &mut AppContext) { .detach(); }); - cx.add_action({ - let app_state = app_state.clone(); + cx.add_action( move |_: &mut Workspace, _: &OpenSettings, cx: &mut ViewContext| { - 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( &mut self, - app_state: &Arc, cx: &mut ViewContext, callback: F, ) -> Task> @@ -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> { let active_call = self.active_call().cloned(); let window_id = cx.window_id(); - let workspace_count = cx - .window_ids() - .collect::>() - .into_iter() - .filter_map(|window_id| { - cx.app_context() - .root_view(window_id)? - .clone() - .downcast::() - }) - .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::()) + .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) -> Option>> { + 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, - app_state: Arc, cx: &mut ViewContext, ) -> Task> { 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, 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); pub fn activate_workspace_for_project( - cx: &mut AppContext, + cx: &mut AsyncAppContext, predicate: impl Fn(&mut Project, &mut ModelContext) -> bool, ) -> Option> { - for window_id in cx.window_ids().collect::>() { + 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::() { @@ -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, cx: &mut ViewContext, default_content: impl 'static + Send + FnOnce() -> Rope, ) -> Task>> { 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> { 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::()) - .find(|workspace| { + let existing_workspace = cx + .window_ids() + .into_iter() + .filter_map(|window_id| cx.root_view(window_id)?.clone().downcast::()) + .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::().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::()? + .downgrade(), + ) + }) + .collect::>(); + + // 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 { let mut parts = value.split(','); let width: usize = parts.next()?.parse().ok()?; diff --git a/crates/zed/src/languages/c.rs b/crates/zed/src/languages/c.rs index e142028196..84c5798b07 100644 --- a/crates/zed/src/languages/c.rs +++ b/crates/zed/src/languages/c.rs @@ -23,7 +23,7 @@ impl super::LspAdapter for CLspAdapter { &self, http: Arc, ) -> Result> { - 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 diff --git a/crates/zed/src/languages/elixir.rs b/crates/zed/src/languages/elixir.rs index a2debcdb2d..2939a0fa5f 100644 --- a/crates/zed/src/languages/elixir.rs +++ b/crates/zed/src/languages/elixir.rs @@ -24,7 +24,7 @@ impl LspAdapter for ElixirLspAdapter { &self, http: Arc, ) -> Result> { - 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 diff --git a/crates/zed/src/languages/go.rs b/crates/zed/src/languages/go.rs index 760c5f353d..ed24abb45c 100644 --- a/crates/zed/src/languages/go.rs +++ b/crates/zed/src/languages/go.rs @@ -33,7 +33,7 @@ impl super::LspAdapter for GoLspAdapter { &self, http: Arc, ) -> Result> { - let release = latest_github_release("golang/tools", http).await?; + let release = latest_github_release("golang/tools", false, http).await?; let version: Option = release.name.strip_prefix("gopls/v").map(str::to_string); if version.is_none() { log::warn!( diff --git a/crates/zed/src/languages/html.rs b/crates/zed/src/languages/html.rs index be5493b4cb..68f780c3af 100644 --- a/crates/zed/src/languages/html.rs +++ b/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?; } diff --git a/crates/zed/src/languages/json.rs b/crates/zed/src/languages/json.rs index 5c3edfba25..d87d36abfe 100644 --- a/crates/zed/src/languages/json.rs +++ b/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?; } diff --git a/crates/zed/src/languages/lua.rs b/crates/zed/src/languages/lua.rs index 2a18138cb7..f204eb2555 100644 --- a/crates/zed/src/languages/lua.rs +++ b/crates/zed/src/languages/lua.rs @@ -30,7 +30,7 @@ impl super::LspAdapter for LuaLspAdapter { &self, http: Arc, ) -> Result> { - 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", diff --git a/crates/zed/src/languages/python.rs b/crates/zed/src/languages/python.rs index 08476c9c21..acd31e8205 100644 --- a/crates/zed/src/languages/python.rs +++ b/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?; } diff --git a/crates/zed/src/languages/rust.rs b/crates/zed/src/languages/rust.rs index 3808444ad9..92fb5bc3b2 100644 --- a/crates/zed/src/languages/rust.rs +++ b/crates/zed/src/languages/rust.rs @@ -24,18 +24,17 @@ impl LspAdapter for RustLspAdapter { &self, http: Arc, ) -> Result> { - 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(), diff --git a/crates/zed/src/languages/typescript.rs b/crates/zed/src/languages/typescript.rs index e4a540dcd8..429a5d9421 100644 --- a/crates/zed/src/languages/typescript.rs +++ b/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 { vec![ @@ -69,24 +71,24 @@ impl LspAdapter for TypeScriptLspAdapter { async fn fetch_server_binary( &self, - versions: Box, + version: Box, _: Arc, container_dir: PathBuf, ) -> Result { - let versions = versions.downcast::().unwrap(); + let version = version.downcast::().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) -> 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, + http: Arc, ) -> Result> { - 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, - _: Arc, + version: Box, + http: Arc, container_dir: PathBuf, ) -> Result { - let version = versions.downcast::().unwrap(); - let server_path = container_dir.join(Self::SERVER_PATH); + let version = version.downcast::().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 { (|| 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() diff --git a/crates/zed/src/languages/yaml.rs b/crates/zed/src/languages/yaml.rs index fadc74b698..fed76cd5b9 100644 --- a/crates/zed/src/languages/yaml.rs +++ b/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?; } diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index 771775de57..f498078b52 100644 --- a/crates/zed/src/main.rs +++ b/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::>() { + if let Some(app_state) = cx.global::>().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::().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, 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(); }); } } diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 4478a88837..f687237bd2 100644 --- a/crates/zed/src/zed.rs +++ b/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, 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, cx| { @@ -146,72 +149,71 @@ pub fn init(app_state: &Arc, 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| { - open_log_file(workspace, app_state.clone(), cx); - } - }); - cx.add_action({ - let app_state = app_state.clone(); - move |_: &mut Workspace, _: &OpenLicenses, cx: &mut ViewContext| { + open_log_file(workspace, cx); + }, + ); + cx.add_action( + move |workspace: &mut Workspace, _: &OpenLicenses, cx: &mut ViewContext| { 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| { - 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| { - 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| { + 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| { 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| { + }, + ); + cx.add_action( + move |workspace: &mut Workspace, + _: &OpenDefaultSettings, + cx: &mut ViewContext| { 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| { - let app_state = app_state.clone(); + move |workspace: &mut Workspace, _: &DebugElements, cx: &mut ViewContext| { + 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, 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::()? - .downgrade(), - ) - }) - .collect::>(); - - // 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::().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::()? - .downgrade(), - ) - }) - .collect::>(); - - // 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::().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::()? + .downgrade(), + ) + }) + .collect::>(); + + // 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(), @@ -464,20 +437,15 @@ fn about(_: &mut Workspace, _: &About, cx: &mut gpui::ViewContext) { cx.prompt(PromptLevel::Info, &format!("{app_name} {version}"), &["OK"]); } -fn open_log_file( - workspace: &mut Workspace, - app_state: Arc, - cx: &mut ViewContext, -) { +fn open_log_file(workspace: &mut Workspace, cx: &mut ViewContext) { 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, - cx: &mut ViewContext, -) { - workspace.with_local_workspace(&app_state.clone(), cx, move |_, cx| { +fn open_telemetry_log_file(workspace: &mut Workspace, cx: &mut ViewContext) { + 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) -> Option { - 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, + workspace: &mut Workspace, asset_path: &'static str, title: &'static str, language: &'static str, cx: &mut ViewContext, ) { - 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 diff --git a/styles/src/styleTree/editor.ts b/styles/src/styleTree/editor.ts index 84ef51406e..cd0adf6bc7 100644 --- a/styles/src/styleTree/editor.ts +++ b/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", diff --git a/styles/src/styleTree/hoverPopover.ts b/styles/src/styleTree/hoverPopover.ts index fadd62db1d..b01402e069 100644 --- a/styles/src/styleTree/hoverPopover.ts +++ b/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 } }