From 6b0faa2d9cf78dd7ba657cfe56e55a0e18800576 Mon Sep 17 00:00:00 2001 From: Joseph Lyons Date: Fri, 28 Apr 2023 16:06:55 -0400 Subject: [PATCH 01/26] Rework telemetry code to support sending events to Clickhouse Co-Authored-By: Max Brunsfeld --- Cargo.lock | 1 + crates/client/src/client.rs | 35 +---- crates/client/src/telemetry.rs | 174 ++++++++++++++++++++++--- crates/editor/Cargo.toml | 1 + crates/editor/src/editor.rs | 28 +++- crates/editor/src/items.rs | 2 +- crates/feedback/src/feedback_editor.rs | 5 +- crates/zed/src/main.rs | 4 +- crates/zed/src/zed.rs | 2 +- 9 files changed, 192 insertions(+), 60 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 4b6ebbe5ed..54b2868acf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1972,6 +1972,7 @@ version = "0.1.0" dependencies = [ "aho-corasick", "anyhow", + "client", "clock", "collections", "context_menu", 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..c8f6deada8 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, @@ -35,20 +36,56 @@ struct TelemetryState { 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, // Mixpanel mixed events - will hopefully die soon + 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)] @@ -121,9 +158,11 @@ impl Telemetry { release_channel, device_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, }), @@ -168,15 +207,24 @@ impl Telemetry { let device_id: Arc = device_id.into(); let mut state = this.state.lock(); state.device_id = Some(device_id.clone()); - for event in &mut state.queue { + + for event in &mut state.mixpanel_events_queue { event .properties .distinct_id .get_or_insert_with(|| device_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(()) @@ -231,7 +279,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.device_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,7 +326,7 @@ impl Telemetry { let mut state = self.state.lock(); let event = MixpanelEvent { - event: kind.to_string(), + event: kind.into(), properties: MixpanelEventProperties { token: "", time: SystemTime::now() @@ -251,7 +334,7 @@ impl Telemetry { .unwrap() .as_millis(), distinct_id: state.device_id.clone(), - insert_id: post_inc(&mut state.next_event_id), + insert_id: post_inc(&mut state.next_mixpanel_event_id), event_properties: if let Value::Object(properties) = properties { Some(properties) } else { @@ -264,17 +347,17 @@ impl Telemetry { signed_in: state.metrics_id.is_some(), }, }; - state.queue.push(event); + state.mixpanel_events_queue.push(event); if state.device_id.is_some() { - if state.queue.len() >= MAX_QUEUE_LEN { + 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(); })); } } @@ -288,10 +371,10 @@ impl Telemetry { 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() { @@ -325,4 +408,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.device_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()) + .await?; + anyhow::Ok(()) + } + .log_err(), + ) + .detach(); + } } diff --git a/crates/editor/Cargo.toml b/crates/editor/Cargo.toml index feb55e1b2f..70b6273744 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 eea418b211..7a6a9a8de8 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; @@ -1295,7 +1296,7 @@ impl Editor { cx.set_global(ScrollbarAutoHide(should_auto_hide_scrollbars)); } - this.report_event("open editor", cx); + this.report_editor_event("open", cx); this } @@ -6819,7 +6820,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) @@ -6831,11 +6832,28 @@ 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(), + ), + }; + telemetry.report_clickhouse_event(event, settings.telemetry()) } } 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/feedback/src/feedback_editor.rs b/crates/feedback/src/feedback_editor.rs index 7bf5328048..8f41762eed 100644 --- a/crates/feedback/src/feedback_editor.rs +++ b/crates/feedback/src/feedback_editor.rs @@ -164,8 +164,9 @@ 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 is_staff = telemetry.is_staff(); let http_client = zed_client.http_client(); let request = FeedbackRequestBody { diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index 679fd39e2f..2724e8587f 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -172,8 +172,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(), diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 494739a967..01a5decfd0 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -573,7 +573,7 @@ fn open_telemetry_log_file( workspace.with_local_workspace(&app_state.clone(), cx, move |_, cx| { 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() } From 1bf85214a4579342dca1cdfb002a76053525f764 Mon Sep 17 00:00:00 2001 From: Julia Date: Fri, 28 Apr 2023 16:42:36 -0400 Subject: [PATCH 02/26] Source ESLint server from Github rather than 3rd party NPM package --- crates/copilot/src/copilot.rs | 2 +- crates/node_runtime/src/node_runtime.rs | 46 ++++++++++--- crates/util/src/github.rs | 32 ++++++--- crates/zed/src/languages/c.rs | 2 +- crates/zed/src/languages/elixir.rs | 2 +- crates/zed/src/languages/go.rs | 2 +- crates/zed/src/languages/html.rs | 2 +- crates/zed/src/languages/json.rs | 2 +- crates/zed/src/languages/lua.rs | 2 +- crates/zed/src/languages/python.rs | 2 +- crates/zed/src/languages/rust.rs | 8 +-- crates/zed/src/languages/typescript.rs | 90 +++++++++++++++---------- crates/zed/src/languages/yaml.rs | 2 +- 13 files changed, 128 insertions(+), 66 deletions(-) diff --git a/crates/copilot/src/copilot.rs b/crates/copilot/src/copilot.rs index 6bc3622ab1..f7ff163424 100644 --- a/crates/copilot/src/copilot.rs +++ b/crates/copilot/src/copilot.rs @@ -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/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/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/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..54d61e91ca 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 { @@ -228,30 +229,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 +284,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?; } From e566929d9eed8d7897396ab65d1a2989357fe19e Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Sat, 29 Apr 2023 14:53:17 +0200 Subject: [PATCH 03/26] Fix panic when clicking on a definition This was introduced with #2420 and was caused by re-entrantly updating the workspace. Instead of passing the workspace reference from the outside, we now define the definition navigation as a method on the editor which solves the issue. Note that we also needed to introduce a `defer` call when navigating to a definition to prevent the workspace from reading the editor during `open_project_item`. --- crates/editor/src/editor.rs | 100 ++++++++++----------- crates/editor/src/element.rs | 26 ++---- crates/editor/src/link_go_to_definition.rs | 87 +++++++----------- 3 files changed, 86 insertions(+), 127 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 7a6a9a8de8..b88e38850e 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -1331,6 +1331,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) } @@ -5558,93 +5562,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()) @@ -5664,7 +5652,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) + }) } } diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 11d1ce8cae..75bd572d95 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -309,25 +309,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; } } 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(); From 029538fe21576f9a9a466714dec844d12e44dd16 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 1 May 2023 11:45:35 +0200 Subject: [PATCH 04/26] Make `dispatch_global_action` private --- crates/gpui/src/app.rs | 2 +- crates/zed/src/main.rs | 23 +++++++++++++++++++---- 2 files changed, 20 insertions(+), 5 deletions(-) diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index c201febed5..a949b64c04 100644 --- a/crates/gpui/src/app.rs +++ b/crates/gpui/src/app.rs @@ -1048,7 +1048,7 @@ impl AppContext { } } - pub fn dispatch_global_action(&mut self, action: A) { + fn dispatch_global_action(&mut self, action: A) { self.dispatch_global_action_any(&action); } diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index 2724e8587f..58a53b9e40 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -29,8 +29,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}; @@ -104,7 +112,13 @@ 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, |_, cx| cx.dispatch_action(NewFile)).detach(); + } + } + }); app.run(move |cx| { cx.set_global(*RELEASE_CHANNEL); @@ -190,6 +204,7 @@ 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); @@ -274,7 +289,7 @@ 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, |_, cx| cx.dispatch_action(NewFile)).detach(); }); } } From d815fc88ae917d2180b17966ad815b06531ac87e Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 1 May 2023 14:09:39 +0200 Subject: [PATCH 05/26] Remove `ViewContext::dispatch_any_action` --- .../src/activity_indicator.rs | 40 ++--- crates/copilot/src/copilot.rs | 2 +- crates/copilot_button/src/copilot_button.rs | 16 +- crates/editor/src/editor.rs | 12 +- crates/gpui/src/app.rs | 9 +- crates/recent_projects/src/recent_projects.rs | 2 +- crates/search/src/buffer_search.rs | 24 +-- crates/search/src/project_search.rs | 11 +- crates/workspace/src/notifications.rs | 84 +++++----- crates/workspace/src/workspace.rs | 144 ++++++++++++------ crates/zed/src/zed.rs | 55 +------ 11 files changed, 197 insertions(+), 202 deletions(-) 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/copilot/src/copilot.rs b/crates/copilot/src/copilot.rs index f7ff163424..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(); diff --git a/crates/copilot_button/src/copilot_button.rs b/crates/copilot_button/src/copilot_button.rs index a597bb7e47..832bdaf3da 100644 --- a/crates/copilot_button/src/copilot_button.rs +++ b/crates/copilot_button/src/copilot_button.rs @@ -1,5 +1,5 @@ use context_menu::{ContextMenu, ContextMenuItem}; -use copilot::{Copilot, Reinstall, SignOut, Status}; +use copilot::{Copilot, SignOut, Status}; use editor::Editor; use gpui::{ elements::*, @@ -103,11 +103,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, ); diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index b88e38850e..d8c2e81ce1 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -3199,11 +3199,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() diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index a949b64c04..7343a7245d 100644 --- a/crates/gpui/src/app.rs +++ b/crates/gpui/src/app.rs @@ -1745,7 +1745,7 @@ 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) { + 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)); } @@ -3196,13 +3196,6 @@ impl<'a, 'b, V: View> ViewContext<'a, 'b, V> { .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 diff --git a/crates/recent_projects/src/recent_projects.rs b/crates/recent_projects/src/recent_projects.rs index 6429448f75..414b3e9323 100644 --- a/crates/recent_projects/src/recent_projects.rs +++ b/crates/recent_projects/src/recent_projects.rs @@ -61,7 +61,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.")) }) } })?; 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/workspace/src/notifications.rs b/crates/workspace/src/notifications.rs index 57749a5c2b..455ffb2bb0 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() @@ -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/workspace.rs b/crates/workspace/src/workspace.rs index 1a622babb3..6a1f7aa8bb 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -43,8 +43,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; @@ -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; @@ -137,7 +138,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 { @@ -145,21 +146,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 } } @@ -167,7 +164,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() } } @@ -176,10 +173,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(), } } } @@ -260,6 +254,7 @@ pub fn init(app_state: Arc, cx: &mut AppContext) { 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( @@ -303,9 +298,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") }) }); } @@ -2668,36 +2661,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 { @@ -3062,6 +3056,58 @@ pub fn join_remote_project( }) } +pub fn restart(_: &Restart, cx: &mut AppContext) { + let mut workspaces = cx + .window_ids() + .filter_map(|window_id| { + Some( + cx.root_view(window_id)? + .clone() + .downcast::()? + .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 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/zed.rs b/crates/zed/src/zed.rs index 01a5decfd0..28b17c297d 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -35,7 +35,7 @@ use terminal_view::terminal_button::TerminalButton; use util::{channel::ReleaseChannel, paths, ResultExt}; use uuid::Uuid; pub use workspace; -use workspace::{sidebar::SidebarSide, AppState, Restart, Workspace}; +use workspace::{sidebar::SidebarSide, AppState, Workspace}; #[derive(Deserialize, Clone, PartialEq)] pub struct OpenBrowser { @@ -113,7 +113,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| { @@ -370,58 +369,6 @@ pub fn build_window_options( } } -fn restart(_: &Restart, cx: &mut gpui::AppContext) { - let mut workspaces = cx - .window_ids() - .filter_map(|window_id| { - Some( - cx.root_view(window_id)? - .clone() - .downcast::()? - .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() From c4472b0786af91237b22c5aed82c8b257165b1b1 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 1 May 2023 15:48:41 +0200 Subject: [PATCH 06/26] Remove `ViewContext::dispatch_action` --- crates/auto_update/src/auto_update.rs | 40 +++-- crates/auto_update/src/update_notification.rs | 6 +- crates/breadcrumbs/src/breadcrumbs.rs | 16 +- crates/collab_ui/src/collab_titlebar_item.rs | 64 ++----- crates/collab_ui/src/collab_ui.rs | 1 - .../src/collaborator_list_popover.rs | 161 ----------------- crates/collab_ui/src/contact_list.rs | 56 +++--- crates/collab_ui/src/contacts_popover.rs | 10 +- .../collab_ui/src/sharing_status_indicator.rs | 5 +- crates/context_menu/src/context_menu.rs | 8 +- crates/copilot/src/sign_in.rs | 6 +- crates/diagnostics/src/items.rs | 25 ++- crates/editor/src/editor.rs | 42 +++-- crates/editor/src/element.rs | 15 +- crates/editor/src/hover_popover.rs | 13 +- crates/feedback/src/deploy_feedback_button.rs | 19 +- crates/feedback/src/feedback.rs | 57 ++---- crates/feedback/src/feedback_editor.rs | 67 +++---- crates/feedback/src/feedback_info_text.rs | 4 +- crates/feedback/src/submit_feedback_button.rs | 24 ++- crates/gpui/src/app.rs | 11 -- crates/gpui/src/views/select.rs | 20 +-- .../src/active_buffer_language.rs | 22 +-- .../src/language_selector.rs | 14 +- crates/outline/src/outline.rs | 2 +- crates/project_panel/src/project_panel.rs | 23 ++- crates/recent_projects/src/recent_projects.rs | 39 ++--- crates/terminal_view/src/terminal_button.rs | 12 +- crates/theme/src/ui.rs | 19 +- crates/theme_selector/src/theme_selector.rs | 14 +- crates/welcome/src/base_keymap_picker.rs | 2 +- crates/welcome/src/welcome.rs | 48 ++++-- crates/workspace/src/dock.rs | 8 +- .../workspace/src/dock/toggle_dock_button.rs | 22 ++- crates/workspace/src/notifications.rs | 4 +- crates/workspace/src/pane.rs | 46 +++-- crates/workspace/src/sidebar.rs | 6 +- crates/workspace/src/toolbar.rs | 40 ++++- crates/workspace/src/workspace.rs | 68 +++----- crates/zed/src/main.rs | 22 ++- crates/zed/src/zed.rs | 163 ++++++++++-------- 41 files changed, 574 insertions(+), 670 deletions(-) delete mode 100644 crates/collab_ui/src/collaborator_list_popover.rs diff --git a/crates/auto_update/src/auto_update.rs b/crates/auto_update/src/auto_update.rs index 02cbab21d0..2b3c7e1c63 100644 --- a/crates/auto_update/src/auto_update.rs +++ b/crates/auto_update/src/auto_update.rs @@ -51,9 +51,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 +73,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, 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/collab_ui/src/collab_titlebar_item.rs b/crates/collab_ui/src/collab_titlebar_item.rs index 95fe26fa11..a9894dade1 100644 --- a/crates/collab_ui/src/collab_titlebar_item.rs +++ b/crates/collab_ui/src/collab_titlebar_item.rs @@ -1,7 +1,6 @@ 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}; @@ -27,7 +26,6 @@ use workspace::{FollowNextCollaborator, Workspace}; actions!( collab, [ - ToggleCollaboratorList, ToggleContactsMenu, ToggleUserMenu, ShareProject, @@ -36,7 +34,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); @@ -48,7 +45,6 @@ pub struct CollabTitlebarItem { user_store: ModelHandle, contacts_popover: Option>, user_menu: ViewHandle, - collaborator_list_popover: Option>, _subscriptions: Vec, } @@ -172,7 +168,6 @@ impl CollabTitlebarItem { menu.set_position_mode(OverlayPositionMode::Local); menu }), - collaborator_list_popover: None, _subscriptions: subscriptions, } } @@ -217,36 +212,6 @@ impl CollabTitlebarItem { } } - 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(); - } - 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) { @@ -357,8 +322,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 +370,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 +416,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 +461,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 +492,13 @@ 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| { + if let Some(workspace) = this.workspace.upgrade(cx) { + let client = workspace.read(cx).app_state().client.clone(); + cx.app_context() + .spawn(|cx| async move { client.authenticate_and_connect(true, &cx).await }) + .detach_and_log_err(cx); + } }) .into_any() } @@ -862,7 +832,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_list.rs b/crates/collab_ui/src/contact_list.rs index 319df337d7..0429182bf3 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, } @@ -1116,11 +1116,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 +1206,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 +1231,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 +1255,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 +1330,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..b35eb09b31 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::{ @@ -72,8 +71,11 @@ impl ContactsPopover { 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 { + 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 +108,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/sharing_status_indicator.rs b/crates/collab_ui/src/sharing_status_indicator.rs index 42c3c886ad..447b561b95 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); @@ -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/context_menu/src/context_menu.rs b/crates/context_menu/src/context_menu.rs index 6f66d710cb..3a3ca9b66b 100644 --- a/crates/context_menu/src/context_menu.rs +++ b/crates/context_menu/src/context_menu.rs @@ -485,7 +485,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/sign_in.rs b/crates/copilot/src/sign_in.rs index fdb4828cd0..02a5b347d4 100644 --- a/crates/copilot/src/sign_in.rs +++ b/crates/copilot/src/sign_in.rs @@ -196,7 +196,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 +250,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 +304,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/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/src/editor.rs b/crates/editor/src/editor.rs index d8c2e81ce1..0849c0ef93 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -809,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(), ); @@ -970,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(), @@ -3138,10 +3155,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(), ) diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 75bd572d95..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, + ); }), ) } @@ -754,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/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 8f41762eed..253cc511ee 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,40 +13,34 @@ use gpui::{ use isahc::Request; use language::Buffer; use postage::prelude::Stream; - use project::Project; use serde::Serialize; +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, + smallvec::SmallVec, + 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)] @@ -94,7 +82,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 +121,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); @@ -198,22 +184,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); @@ -291,7 +276,7 @@ impl Item for FeedbackEditor { _: ModelHandle, cx: &mut ViewContext, ) -> Task> { - self.handle_save(cx) + self.submit(cx) } fn save_as( @@ -300,7 +285,7 @@ impl Item for FeedbackEditor { _: std::path::PathBuf, cx: &mut ViewContext, ) -> Task> { - self.handle_save(cx) + self.submit(cx) } fn reload( @@ -353,6 +338,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 7343a7245d..02d6c1a2ac 100644 --- a/crates/gpui/src/app.rs +++ b/crates/gpui/src/app.rs @@ -1745,10 +1745,6 @@ impl AppContext { self.pending_effects.push_back(Effect::RefreshWindows); } - 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, @@ -3189,13 +3185,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 defer(&mut self, callback: impl 'static + FnOnce(&mut V, &mut ViewContext)) { let handle = self.handle(); self.window_context 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/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/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 414b3e9323..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.) @@ -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/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/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/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 455ffb2bb0..7881603bbc 100644 --- a/crates/workspace/src/notifications.rs +++ b/crates/workspace/src/notifications.rs @@ -278,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() diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs index 41f4d5d111..2631f72fd4 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,13 @@ impl Pane { 3, "icons/x_mark_8.svg", cx, - |_, cx| cx.dispatch_action(HideDock), + |this, cx| { + if let Some(workspace) = this.workspace.upgrade(cx) { + workspace.update(cx, |workspace, cx| { + Dock::hide_dock(workspace, &Default::default(), cx) + }) + } + }, None, ) })) @@ -1693,8 +1703,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 +1769,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..a940bd09b2 100644 --- a/crates/workspace/src/toolbar.rs +++ b/crates/workspace/src/toolbar.rs @@ -130,8 +130,20 @@ 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)) + { + 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 +155,21 @@ 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)) + { + 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 +187,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 +221,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 6a1f7aa8bb..2a7748af0f 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -208,48 +208,7 @@ 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); @@ -913,7 +872,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> @@ -924,7 +882,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) @@ -1093,10 +1051,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(); @@ -1108,6 +1085,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 { diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index 58a53b9e40..7a66953cff 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, @@ -51,8 +52,7 @@ 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, - Workspace, + self, dock::FocusDock, item::ItemHandle, notifications::NotifyResultExt, AppState, Workspace, }; use zed::{self, build_window_options, initialize_workspace, languages, menus, OpenSettings}; @@ -115,7 +115,10 @@ fn main() { .on_reopen(move |cx| { if cx.has_global::>() { if let Some(app_state) = cx.global::>().upgrade() { - workspace::open_new(&app_state, cx, |_, cx| cx.dispatch_action(NewFile)).detach(); + workspace::open_new(&app_state, cx, |workspace, cx| { + Editor::new_file(workspace, &Default::default(), cx) + }) + .detach(); } } }); @@ -208,14 +211,14 @@ fn main() { auto_update::init(http, client::ZED_SERVER_URL.clone(), cx); workspace::init(app_state.clone(), cx); - recent_projects::init(cx, Arc::downgrade(&app_state)); + 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()); @@ -289,7 +292,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| { - workspace::open_new(app_state, cx, |_, cx| cx.dispatch_action(NewFile)).detach(); + 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 28b17c297d..9e0b55d423 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -20,7 +20,7 @@ use gpui::{ geometry::vector::vec2f, impl_actions, platform::{Platform, PromptLevel, TitlebarOptions, WindowBounds, WindowKind, WindowOptions}, - AssetSource, ViewContext, + AppContext, AssetSource, ViewContext, }; use language::Rope; pub use lsp; @@ -35,7 +35,7 @@ use terminal_view::terminal_button::TerminalButton; use util::{channel::ReleaseChannel, paths, ResultExt}; use uuid::Uuid; pub use workspace; -use workspace::{sidebar::SidebarSide, AppState, Workspace}; +use workspace::{open_new, sidebar::SidebarSide, AppState, NewFile, NewWindow, Workspace}; #[derive(Deserialize, Clone, PartialEq)] pub struct OpenBrowser { @@ -147,10 +147,9 @@ pub fn init(app_state: &Arc, cx: &mut gpui::AppContext) { }) .detach_and_log_err(cx); }); - cx.add_action({ - let app_state = app_state.clone(); - move |_: &mut Workspace, _: &OpenSettings, cx: &mut ViewContext| { - open_config_file(&paths::SETTINGS, app_state.clone(), cx, || { + cx.add_action( + move |workspace: &mut Workspace, _: &OpenSettings, cx: &mut ViewContext| { + open_config_file(workspace, &paths::SETTINGS, cx, || { str::from_utf8( Assets .load("settings/initial_user_settings.json") @@ -160,73 +159,68 @@ pub fn init(app_state: &Arc, cx: &mut gpui::AppContext) { .unwrap() .into() }); - } - }); - 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(); - move |_: &mut Workspace, _: &OpenKeymap, cx: &mut ViewContext| { - open_config_file(&paths::KEYMAP, app_state.clone(), cx, Default::default); - } - }); - cx.add_action({ - let app_state = app_state.clone(); - move |_: &mut Workspace, _: &OpenDefaultKeymap, cx: &mut ViewContext| { + open_telemetry_log_file(workspace, cx); + }, + ); + cx.add_action( + move |workspace: &mut Workspace, _: &OpenKeymap, cx: &mut ViewContext| { + open_config_file(workspace, &paths::KEYMAP, cx, Default::default); + }, + ); + 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, "settings/default.json", "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(); 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, move |workspace, cx| { + workspace.with_local_workspace(cx, move |workspace, cx| { let project = workspace.project().clone(); let buffer = project @@ -258,6 +252,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); @@ -275,7 +291,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); @@ -304,7 +320,7 @@ pub fn initialize_workspace( }); 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", @@ -317,12 +333,13 @@ 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(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); @@ -428,13 +445,13 @@ fn about(_: &mut Workspace, _: &About, cx: &mut gpui::ViewContext) { } fn open_config_file( + workspace: &mut Workspace, path: &'static Path, - app_state: Arc, cx: &mut ViewContext, default_content: impl 'static + Send + FnOnce() -> Rope, ) { + let fs = workspace.app_state().fs.clone(); cx.spawn(|workspace, mut cx| async move { - let fs = &app_state.fs; if !fs.is_file(path).await { fs.create_file(path, Default::default()).await?; fs.save(path, &default_content(), Default::default()) @@ -443,7 +460,7 @@ fn open_config_file( 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) }) })? @@ -454,20 +471,15 @@ fn open_config_file( .detach_and_log_err(cx) } -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 @@ -512,12 +524,9 @@ 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()?; @@ -573,18 +582,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) @@ -815,8 +824,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 From eb2cce98a715ae078579efdf90a9776604649da9 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 1 May 2023 16:40:57 +0200 Subject: [PATCH 07/26] Move `dispatch_action_any_action_at` to `AsyncAppContext` --- crates/command_palette/src/command_palette.rs | 8 +-- crates/context_menu/src/context_menu.rs | 27 ++++++---- crates/gpui/src/app.rs | 51 ++++++------------- 3 files changed, 37 insertions(+), 49 deletions(-) diff --git a/crates/command_palette/src/command_palette.rs b/crates/command_palette/src/command_palette.rs index 1d9ac62c2c..5b1559bd29 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); } diff --git a/crates/context_menu/src/context_menu.rs b/crates/context_menu/src/context_menu.rs index 3a3ca9b66b..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), } diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index 02d6c1a2ac..c757e7f383 100644 --- a/crates/gpui/src/app.rs +++ b/crates/gpui/src/app.rs @@ -309,6 +309,20 @@ impl AsyncAppContext { self.0.borrow_mut().update_window(window_id, callback) } + pub fn dispatch_action( + &mut self, + window_id: usize, + view_id: usize, + action: &dyn Action, + ) -> Result<()> { + self.0 + .borrow_mut() + .update_window(window_id, |window| { + window.handle_dispatch_action_from_effect(Some(view_id), action); + }) + .ok_or_else(|| anyhow!("window not found")) + } + pub fn add_model(&mut self, build_model: F) -> ModelHandle where T: Entity, @@ -1619,17 +1633,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,19 +1749,6 @@ impl AppContext { self.pending_effects.push_back(Effect::RefreshWindows); } - 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 { @@ -2155,11 +2146,6 @@ pub enum Effect { result: MatchResult, }, RefreshWindows, - DispatchActionFrom { - window_id: usize, - view_id: usize, - action: Box, - }, ActionDispatchNotification { action_id: TypeId, }, @@ -2248,13 +2234,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) From 6c931ab9da7f73344fd2698b237a1d31483cdeb1 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 1 May 2023 16:49:17 +0200 Subject: [PATCH 08/26] Inline test-only `AppContext` methods --- crates/gpui/src/app.rs | 15 --------------- crates/gpui/src/app/test_app_context.rs | 12 +++++++----- 2 files changed, 7 insertions(+), 20 deletions(-) diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index c757e7f383..589ec8f1af 100644 --- a/crates/gpui/src/app.rs +++ b/crates/gpui/src/app.rs @@ -1062,10 +1062,6 @@ impl AppContext { } } - 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()) { @@ -1907,17 +1903,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() diff --git a/crates/gpui/src/app/test_app_context.rs b/crates/gpui/src/app/test_app_context.rs index 3a03a81c47..79f35cd923 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) { From e3b2407ebfb18a9dc2f59892a9e9966e72338d40 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 1 May 2023 16:58:07 +0200 Subject: [PATCH 09/26] Run until parked now that the command palette spawns to dispatch action --- crates/command_palette/src/command_palette.rs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/crates/command_palette/src/command_palette.rs b/crates/command_palette/src/command_palette.rs index 5b1559bd29..441fbb84a6 100644 --- a/crates/command_palette/src/command_palette.rs +++ b/crates/command_palette/src/command_palette.rs @@ -268,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}; @@ -291,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| { @@ -333,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"); }); From 780ece551e58fcd6cebc4fb7af727bdf54826945 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 1 May 2023 17:06:05 +0200 Subject: [PATCH 10/26] Defer hiding the dock and going back/forward when Pane is on the stack --- crates/workspace/src/pane.rs | 8 +++++--- crates/workspace/src/toolbar.rs | 20 +++++++++++++------- 2 files changed, 18 insertions(+), 10 deletions(-) diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs index 2631f72fd4..8bd42fed04 100644 --- a/crates/workspace/src/pane.rs +++ b/crates/workspace/src/pane.rs @@ -1645,9 +1645,11 @@ impl Pane { cx, |this, cx| { if let Some(workspace) = this.workspace.upgrade(cx) { - workspace.update(cx, |workspace, cx| { - Dock::hide_dock(workspace, &Default::default(), cx) - }) + cx.window_context().defer(move |cx| { + workspace.update(cx, |workspace, cx| { + Dock::hide_dock(workspace, &Default::default(), cx) + }) + }); } }, None, diff --git a/crates/workspace/src/toolbar.rs b/crates/workspace/src/toolbar.rs index a940bd09b2..eac9963d38 100644 --- a/crates/workspace/src/toolbar.rs +++ b/crates/workspace/src/toolbar.rs @@ -138,10 +138,13 @@ impl View for Toolbar { .upgrade(cx) .and_then(|pane| pane.read(cx).workspace().upgrade(cx)) { - workspace.update(cx, |workspace, cx| { - Pane::go_back(workspace, Some(pane.clone()), cx) - .detach_and_log_err(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); + }); + }) } } }, @@ -163,9 +166,12 @@ impl View for Toolbar { .upgrade(cx) .and_then(|pane| pane.read(cx).workspace().upgrade(cx)) { - workspace.update(cx, |workspace, cx| { - Pane::go_forward(workspace, Some(pane.clone()), cx) - .detach_and_log_err(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); + }); }); } } From 4966a4a681d4a79a8222eca1434f45e6f33fe925 Mon Sep 17 00:00:00 2001 From: Julia Date: Mon, 1 May 2023 13:14:35 -0400 Subject: [PATCH 11/26] Reduce hardcoded ESLint workspace configuration --- crates/zed/src/languages/typescript.rs | 33 ++++---------------------- 1 file changed, 4 insertions(+), 29 deletions(-) diff --git a/crates/zed/src/languages/typescript.rs b/crates/zed/src/languages/typescript.rs index 54d61e91ca..429a5d9421 100644 --- a/crates/zed/src/languages/typescript.rs +++ b/crates/zed/src/languages/typescript.rs @@ -188,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(), From 40ab5c1fb9ef08101f0e7fcd5eb656b1973aabf8 Mon Sep 17 00:00:00 2001 From: Julia Date: Mon, 1 May 2023 13:15:41 -0400 Subject: [PATCH 12/26] Remove underline from diagnostic source --- styles/src/styleTree/hoverPopover.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 } } From f62ba2eec7ba0bfbd616ae41e8c196dc32b4533a Mon Sep 17 00:00:00 2001 From: "Joseph T. Lyons" Date: Mon, 1 May 2023 16:29:51 -0400 Subject: [PATCH 13/26] use installation_id over device_id --- crates/client/src/telemetry.rs | 51 ++++++++++++++------------ crates/feedback/src/feedback_editor.rs | 3 ++ 2 files changed, 31 insertions(+), 23 deletions(-) diff --git a/crates/client/src/telemetry.rs b/crates/client/src/telemetry.rs index c8f6deada8..7151dcd7bb 100644 --- a/crates/client/src/telemetry.rs +++ b/crates/client/src/telemetry.rs @@ -30,13 +30,13 @@ 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, - mixpanel_events_queue: Vec, // Mixpanel mixed events - will hopefully die soon + mixpanel_events_queue: Vec, clickhouse_events_queue: Vec, next_mixpanel_event_id: usize, flush_mixpanel_events_task: Option>, @@ -100,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 @@ -123,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, } @@ -156,7 +157,7 @@ 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, mixpanel_events_queue: Default::default(), clickhouse_events_queue: Default::default(), @@ -193,26 +194,26 @@ 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()); + state.installation_id = Some(installation_id.clone()); for event in &mut state.mixpanel_events_queue { event .properties - .distinct_id - .get_or_insert_with(|| device_id.clone()); + .installation_id + .get_or_insert_with(|| installation_id.clone()); } let has_mixpanel_events = !state.mixpanel_events_queue.is_empty(); @@ -248,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, @@ -299,7 +300,7 @@ impl Telemetry { event, }); - if state.device_id.is_some() { + if state.installation_id.is_some() { if state.mixpanel_events_queue.len() >= MAX_QUEUE_LEN { drop(state); self.flush_clickhouse_events(); @@ -333,7 +334,7 @@ impl Telemetry { .duration_since(UNIX_EPOCH) .unwrap() .as_millis(), - distinct_id: state.device_id.clone(), + 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) @@ -348,7 +349,7 @@ impl Telemetry { }, }; state.mixpanel_events_queue.push(event); - if state.device_id.is_some() { + if state.installation_id.is_some() { if state.mixpanel_events_queue.len() >= MAX_QUEUE_LEN { drop(state); self.flush_mixpanel_events(); @@ -367,6 +368,10 @@ 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 } @@ -438,7 +443,7 @@ impl Telemetry { &mut json_bytes, &ClickhouseEventRequestBody { token: ZED_SECRET_CLIENT_TOKEN, - installation_id: state.device_id.clone(), + installation_id: state.installation_id.clone(), app_version: state.app_version.clone(), os_name: state.os_name, os_version: state.os_version.clone(), diff --git a/crates/feedback/src/feedback_editor.rs b/crates/feedback/src/feedback_editor.rs index 253cc511ee..2e513f6f06 100644 --- a/crates/feedback/src/feedback_editor.rs +++ b/crates/feedback/src/feedback_editor.rs @@ -47,6 +47,7 @@ pub fn init(cx: &mut AppContext) { struct FeedbackRequestBody<'a> { feedback_text: &'a str, metrics_id: Option>, + installation_id: Option>, system_specs: SystemSpecs, is_staff: bool, token: &'a str, @@ -152,12 +153,14 @@ impl FeedbackEditor { 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, From f7de0ad8ae3b17a148f7c460491edbcfe67c9dcf Mon Sep 17 00:00:00 2001 From: Julia Date: Mon, 1 May 2023 16:48:27 -0400 Subject: [PATCH 14/26] Show diagnostic source in diagnostic multibuffer headers --- crates/diagnostics/src/diagnostics.rs | 13 +++++++++++-- crates/theme/src/theme.rs | 1 + styles/src/styleTree/editor.ts | 3 +++ 3 files changed, 15 insertions(+), 2 deletions(-) diff --git a/crates/diagnostics/src/diagnostics.rs b/crates/diagnostics/src/diagnostics.rs index 6973d83f9b..22f67265ec 100644 --- a/crates/diagnostics/src/diagnostics.rs +++ b/crates/diagnostics/src/diagnostics.rs @@ -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/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/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", From 4c1cba6def2d599c6df0b4aab7ce0b307393e0ec Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 2 May 2023 10:09:57 +0200 Subject: [PATCH 15/26] Remove unnecessary `Element` impl for `RootElement` --- crates/gpui/src/elements.rs | 58 ------------------------------------- 1 file changed, 58 deletions(-) diff --git a/crates/gpui/src/elements.rs b/crates/gpui/src/elements.rs index 09b9c40589..dbeb9c218a 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; @@ -717,63 +716,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())); From 794446bf8bd43afee0a2fb9269910c9d0c36b5ab Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 2 May 2023 11:03:01 +0200 Subject: [PATCH 16/26] Move `debug_elements` to `AsyncAppContext` Previously, `debug_elements` was available on `WindowContext`. If that method was called while having a borrow out to a view, it would panic because the view would already have been borrowed. By moving it to an `AsyncAppContext` we ensure the method can't be called while a view is being used. --- crates/gpui/src/app.rs | 9 +++++++++ crates/gpui/src/app/window.rs | 19 +------------------ crates/gpui/src/elements.rs | 7 ++++++- crates/zed/src/zed.rs | 8 +++++++- 4 files changed, 23 insertions(+), 20 deletions(-) diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index 589ec8f1af..3cc90dd4ce 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, @@ -309,6 +310,14 @@ 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, 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 dbeb9c218a..7de0bc10f5 100644 --- a/crates/gpui/src/elements.rs +++ b/crates/gpui/src/elements.rs @@ -708,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> { diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 9e0b55d423..5160426bdd 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, }; @@ -215,9 +216,14 @@ pub fn init(app_state: &Arc, cx: &mut gpui::AppContext) { 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(cx, move |workspace, cx| { From f5278c49b0e9f1d78e9d2ff7d7888f9931d8695b Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Tue, 2 May 2023 12:12:57 +0300 Subject: [PATCH 17/26] Clarify GH Token scope requirements --- README.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) 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 From f985fac6f9274f89663c2224e37dff961df3df7d Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 2 May 2023 11:47:05 +0200 Subject: [PATCH 18/26] Fix panic when showing contacts popover via keybinding --- crates/collab_ui/src/collab_titlebar_item.rs | 109 ++++++++++--------- crates/collab_ui/src/contact_list.rs | 12 +- crates/collab_ui/src/contacts_popover.rs | 49 ++++++--- crates/zed/src/zed.rs | 5 +- 4 files changed, 99 insertions(+), 76 deletions(-) diff --git a/crates/collab_ui/src/collab_titlebar_item.rs b/crates/collab_ui/src/collab_titlebar_item.rs index a9894dade1..97fb82a5d6 100644 --- a/crates/collab_ui/src/collab_titlebar_item.rs +++ b/crates/collab_ui/src/collab_titlebar_item.rs @@ -3,7 +3,7 @@ use crate::{ 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}; @@ -17,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}; @@ -41,8 +42,10 @@ 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, _subscriptions: Vec, @@ -64,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 { @@ -89,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()) @@ -124,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) @@ -160,8 +166,10 @@ 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); @@ -173,16 +181,14 @@ impl CollabTitlebarItem { } 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) { @@ -193,41 +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(); - } + 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(); @@ -493,12 +500,10 @@ impl CollabTitlebarItem { }) .with_cursor_style(CursorStyle::PointingHand) .on_click(MouseButton::Left, move |_, this, cx| { - if let Some(workspace) = this.workspace.upgrade(cx) { - let client = workspace.read(cx).app_state().client.clone(); - cx.app_context() - .spawn(|cx| async move { client.authenticate_and_connect(true, &cx).await }) - .detach_and_log_err(cx); - } + 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() } diff --git a/crates/collab_ui/src/contact_list.rs b/crates/collab_ui/src/contact_list.rs index 0429182bf3..87aa41b7a4 100644 --- a/crates/collab_ui/src/contact_list.rs +++ b/crates/collab_ui/src/contact_list.rs @@ -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); diff --git a/crates/collab_ui/src/contacts_popover.rs b/crates/collab_ui/src/contacts_popover.rs index b35eb09b31..35734d81f4 100644 --- a/crates/collab_ui/src/contacts_popover.rs +++ b/crates/collab_ui/src/contacts_popover.rs @@ -8,6 +8,7 @@ use gpui::{ ViewContext, ViewHandle, WeakViewHandle, }; use picker::PickerEvent; +use project::Project; use settings::Settings; use workspace::Workspace; @@ -28,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); @@ -67,19 +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, |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(); - } + 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(); } } diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 5160426bdd..ec4904daeb 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -321,9 +321,8 @@ 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, cx); From 94f17755333412fd24d89bf798cc707d7164b33e Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 2 May 2023 13:46:49 +0200 Subject: [PATCH 19/26] Fix broken styling in contact finder This regressed as part of #2372, where we forgot to theme the contact finder picker differently from the rest of the app. --- crates/collab_ui/src/contact_finder.rs | 1 + 1 file changed, 1 insertion(+) 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 { From 185c1650df6fef9cdc33da9d5b8791c3abf2c32b Mon Sep 17 00:00:00 2001 From: Julia Date: Tue, 2 May 2023 09:08:07 -0400 Subject: [PATCH 20/26] Show diagnostic source in inline diagnostic --- crates/diagnostics/src/diagnostics.rs | 2 +- crates/editor/src/editor.rs | 19 +++++++++++++++---- 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/crates/diagnostics/src/diagnostics.rs b/crates/diagnostics/src/diagnostics.rs index 22f67265ec..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; diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 0849c0ef93..df2ca6f43d 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -7509,8 +7509,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| { @@ -7534,11 +7542,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(), "")]) From 70f8cf4cf6e0fa9d0a8765e6353604ae0be1296c Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 2 May 2023 19:28:58 +0200 Subject: [PATCH 21/26] Move methods querying window state into `AsyncAppContext` --- .../src/project_shared_notification.rs | 4 +- .../collab_ui/src/sharing_status_indicator.rs | 4 +- crates/copilot/src/sign_in.rs | 15 ++- crates/gpui/src/app.rs | 67 ++++++------- crates/gpui/src/app/test_app_context.rs | 6 +- crates/gpui/src/test.rs | 2 +- crates/gpui_macros/src/gpui_macros.rs | 4 +- crates/workspace/src/workspace.rs | 94 ++++++++++--------- crates/zed/src/zed.rs | 33 +++---- 9 files changed, 116 insertions(+), 113 deletions(-) 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 447b561b95..9fbe57af65 100644 --- a/crates/collab_ui/src/sharing_status_indicator.rs +++ b/crates/collab_ui/src/sharing_status_indicator.rs @@ -19,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(); diff --git a/crates/copilot/src/sign_in.rs b/crates/copilot/src/sign_in.rs index 02a5b347d4..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()); } } } diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index 3cc90dd4ce..4d84f7c070 100644 --- a/crates/gpui/src/app.rs +++ b/crates/gpui/src/app.rs @@ -302,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, @@ -332,6 +340,22 @@ impl AsyncAppContext { .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, @@ -353,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) { @@ -552,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| { @@ -569,7 +593,8 @@ impl AppContext { } }); - self.remove_all_windows(); + self.windows.clear(); + self.flush_effects(); let futures = futures::future::join_all(futures); if self @@ -581,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 } @@ -702,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()) } @@ -1285,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, @@ -1352,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())); })); } @@ -4663,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 79f35cd923..2d079a6042 100644 --- a/crates/gpui/src/app/test_app_context.rs +++ b/crates/gpui/src/app/test_app_context.rs @@ -182,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/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_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/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 2a7748af0f..5a00853737 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -918,12 +918,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( @@ -948,19 +954,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 @@ -2849,10 +2850,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::() { @@ -2891,13 +2892,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(), @@ -2960,13 +2962,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() @@ -3035,24 +3040,25 @@ pub fn join_remote_project( } pub fn restart(_: &Restart, cx: &mut AppContext) { - let mut workspaces = cx - .window_ids() - .filter_map(|window_id| { - Some( - cx.root_view(window_id)? - .clone() - .downcast::()? - .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(), diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index ec4904daeb..a28f1a9fb3 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -392,24 +392,25 @@ pub fn build_window_options( } 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(), From 1398a1206299cbaf5e14b9de30d2fbfe83f04334 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Tue, 2 May 2023 17:35:23 +0300 Subject: [PATCH 22/26] More keybindings in macOs modals with buttons Closes https://github.com/zed-industries/community/issues/1095 by forcing the non-Cancel button to get a focus. Due to the way macOs handles buttons on modals, the focus gain had to be achieved via certain button addition order, rather than conventional "setFocus"-ish API, see the related comment for details. Co-authored-by: Antonio Scandurra --- crates/gpui/src/platform/mac/window.rs | 39 ++++++++++++++++++++++++-- 1 file changed, 37 insertions(+), 2 deletions(-) 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() From 2b95aba99c763e400b15b6896dd7200730f794fb Mon Sep 17 00:00:00 2001 From: Joseph Lyons Date: Tue, 2 May 2023 17:02:41 -0400 Subject: [PATCH 23/26] Add download and upload metadata to update request --- crates/auto_update/src/auto_update.rs | 30 +++++++++++++++++++++++++-- 1 file changed, 28 insertions(+), 2 deletions(-) diff --git a/crates/auto_update/src/auto_update.rs b/crates/auto_update/src/auto_update.rs index 2b3c7e1c63..68d3776e1c 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, @@ -247,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.get(&release.url, request_body, true).await?; smol::io::copy(response.body_mut(), &mut dmg_file).await?; log::info!("downloaded update. path:{:?}", dmg_path); From 69a4fffae24346f79e606a85f8153906f076b79f Mon Sep 17 00:00:00 2001 From: Joseph Lyons Date: Tue, 2 May 2023 23:22:55 -0400 Subject: [PATCH 24/26] Update `post_json` to take in a bool for allowing for redirects --- crates/client/src/telemetry.rs | 6 +++--- crates/util/src/http.rs | 6 ++++++ 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/crates/client/src/telemetry.rs b/crates/client/src/telemetry.rs index 7151dcd7bb..5dfb6595d1 100644 --- a/crates/client/src/telemetry.rs +++ b/crates/client/src/telemetry.rs @@ -270,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(()) } @@ -404,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(()) } @@ -454,7 +454,7 @@ impl Telemetry { } this.http_client - .post_json(CLICKHOUSE_EVENTS_URL.as_str(), json_bytes.into()) + .post_json(CLICKHOUSE_EVENTS_URL.as_str(), json_bytes.into(), false) .await?; anyhow::Ok(()) } 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") From 41d4454f4581639e324db86e36924adcddf24d6b Mon Sep 17 00:00:00 2001 From: Joseph Lyons Date: Tue, 2 May 2023 23:23:43 -0400 Subject: [PATCH 25/26] Use post_json so that the Content-Type is set to application/json --- crates/auto_update/src/auto_update.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/auto_update/src/auto_update.rs b/crates/auto_update/src/auto_update.rs index 68d3776e1c..abf95ff45a 100644 --- a/crates/auto_update/src/auto_update.rs +++ b/crates/auto_update/src/auto_update.rs @@ -273,7 +273,7 @@ impl AutoUpdater { telemetry, })?); - let mut response = client.get(&release.url, request_body, true).await?; + 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); From 376aa1235f55b6fa185ccea906a98fbc212204aa Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 3 May 2023 10:37:57 +0200 Subject: [PATCH 26/26] Fix "IncomingCallNotification was dropped" error when accepting a call This was caused by accepting the call, which caused the notification to be removed. When `active_call.accept_incoming()` finally completed, we would try to get the app state from it in order to join the project, but couldn't becuase the view would have already been dropped. This commit fixes the bug by capturing a weak handle to the app state when accepting the call as opposed to trying to read it from the view when the accept completes. --- .../src/incoming_call_notification.rs | 38 ++++++++++--------- 1 file changed, 20 insertions(+), 18 deletions(-) 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();