diff --git a/.cargo/ci-config.toml b/.cargo/ci-config.toml new file mode 100644 index 0000000000..6dbaf4b446 --- /dev/null +++ b/.cargo/ci-config.toml @@ -0,0 +1,12 @@ +# This config is different from config.toml in this directory, as the latter is recognized by Cargo. +# This file is placed in $HOME/.cargo/config.toml on CI runs. Cargo then merges Zeds .cargo/config.toml with $HOME/.cargo/config.toml +# with preference for settings from Zeds config.toml. +# TL;DR: If a value is set in both ci-config.toml and config.toml, config.toml value takes precedence. +# Arrays are merged together though. See: https://doc.rust-lang.org/cargo/reference/config.html#hierarchical-structure +# The intent for this file is to configure CI build process with a divergance from Zed developers experience; for example, in this config file +# we use `-D warnings` for rustflags (which makes compilation fail in presence of warnings during build process). Placing that in developers `config.toml` +# would be incovenient. +# We *could* override things like RUSTFLAGS manually by setting them as environment variables, but that is less DRY; worse yet, if you forget to set proper environment variables +# in one spot, that's going to trigger a rebuild of all of the artifacts. Using ci-config.toml we can define these overrides for CI in one spot and not worry about it. +[build] +rustflags = ["-D", "warnings"] diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 208d538976..ef718a4fa9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -23,15 +23,15 @@ jobs: - self-hosted - test steps: - - name: Set up default .cargo/config.toml - run: printf "[build]\nrustflags = [\"-D\", \"warnings\"]" > $HOME/.cargo/config.toml - - name: Checkout repo uses: actions/checkout@v3 with: clean: false submodules: "recursive" + - name: Set up default .cargo/config.toml + run: cp ./.cargo/ci-config.toml ~/.cargo/config.toml + - name: Run rustfmt uses: ./.github/actions/check_formatting diff --git a/Cargo.lock b/Cargo.lock index 8f36f2445d..99d4827966 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1186,6 +1186,7 @@ version = "0.1.0" dependencies = [ "anyhow", "async-broadcast", + "async-trait", "audio2", "client2", "collections", @@ -1204,6 +1205,7 @@ dependencies = [ "serde_json", "settings2", "util", + "workspace2", ] [[package]] @@ -1664,7 +1666,7 @@ dependencies = [ [[package]] name = "collab" -version = "0.28.0" +version = "0.29.0" dependencies = [ "anyhow", "async-trait", @@ -11381,6 +11383,7 @@ version = "0.1.0" dependencies = [ "anyhow", "async-recursion 1.0.5", + "async-trait", "bincode", "call2", "client2", @@ -11493,7 +11496,7 @@ dependencies = [ [[package]] name = "zed" -version = "0.114.0" +version = "0.115.0" dependencies = [ "activity_indicator", "ai", diff --git a/crates/call2/Cargo.toml b/crates/call2/Cargo.toml index 9e13463680..43e19b4ccb 100644 --- a/crates/call2/Cargo.toml +++ b/crates/call2/Cargo.toml @@ -31,7 +31,8 @@ media = { path = "../media" } project = { package = "project2", path = "../project2" } settings = { package = "settings2", path = "../settings2" } util = { path = "../util" } - +workspace = {package = "workspace2", path = "../workspace2"} +async-trait.workspace = true anyhow.workspace = true async-broadcast = "0.4" futures.workspace = true diff --git a/crates/call2/src/call2.rs b/crates/call2/src/call2.rs index 1f11e0650d..8e553a9b16 100644 --- a/crates/call2/src/call2.rs +++ b/crates/call2/src/call2.rs @@ -2,24 +2,29 @@ pub mod call_settings; pub mod participant; pub mod room; -use anyhow::{anyhow, Result}; +use anyhow::{anyhow, bail, Result}; +use async_trait::async_trait; use audio::Audio; use call_settings::CallSettings; -use client::{proto, Client, TelemetrySettings, TypedEnvelope, User, UserStore, ZED_ALWAYS_ACTIVE}; +use client::{ + proto::{self, PeerId}, + Client, TelemetrySettings, TypedEnvelope, User, UserStore, ZED_ALWAYS_ACTIVE, +}; use collections::HashSet; use futures::{channel::oneshot, future::Shared, Future, FutureExt}; use gpui::{ - AppContext, AsyncAppContext, Context, EventEmitter, Model, ModelContext, Subscription, Task, - WeakModel, + AppContext, AsyncAppContext, AsyncWindowContext, Context, EventEmitter, Model, ModelContext, + Subscription, Task, View, ViewContext, WeakModel, WeakView, }; +pub use participant::ParticipantLocation; use postage::watch; use project::Project; use room::Event; +pub use room::Room; use settings::Settings; use std::sync::Arc; - -pub use participant::ParticipantLocation; -pub use room::Room; +use util::ResultExt; +use workspace::{item::ItemHandle, CallHandler, Pane, Workspace}; pub fn init(client: Arc, user_store: Model, cx: &mut AppContext) { CallSettings::register(cx); @@ -464,7 +469,7 @@ impl ActiveCall { &self.pending_invites } - pub fn report_call_event(&self, operation: &'static str, cx: &AppContext) { + pub fn report_call_event(&self, operation: &'static str, cx: &mut AppContext) { if let Some(room) = self.room() { let room = room.read(cx); report_call_event_for_room(operation, room.id(), room.channel_id(), &self.client, cx); @@ -477,7 +482,7 @@ pub fn report_call_event_for_room( room_id: u64, channel_id: Option, client: &Arc, - cx: &AppContext, + cx: &mut AppContext, ) { let telemetry = client.telemetry(); let telemetry_settings = *TelemetrySettings::get_global(cx); @@ -505,6 +510,116 @@ pub fn report_call_event_for_channel( ) } +pub struct Call { + active_call: Option<(Model, Vec)>, + parent_workspace: WeakView, +} + +impl Call { + pub fn new( + parent_workspace: WeakView, + cx: &mut ViewContext<'_, Workspace>, + ) -> Box { + let mut active_call = None; + if cx.has_global::>() { + let call = cx.global::>().clone(); + let subscriptions = vec![cx.subscribe(&call, Self::on_active_call_event)]; + active_call = Some((call, subscriptions)); + } + Box::new(Self { + active_call, + parent_workspace, + }) + } + fn on_active_call_event( + workspace: &mut Workspace, + _: Model, + event: &room::Event, + cx: &mut ViewContext, + ) { + match event { + room::Event::ParticipantLocationChanged { participant_id } + | room::Event::RemoteVideoTracksChanged { participant_id } => { + workspace.leader_updated(*participant_id, cx); + } + _ => {} + } + } +} + +#[async_trait(?Send)] +impl CallHandler for Call { + fn shared_screen_for_peer( + &self, + peer_id: PeerId, + _pane: &View, + cx: &mut ViewContext, + ) -> Option> { + let (call, _) = self.active_call.as_ref()?; + let room = call.read(cx).room()?.read(cx); + let participant = room.remote_participant_for_peer_id(peer_id)?; + let _track = participant.video_tracks.values().next()?.clone(); + let _user = participant.user.clone(); + todo!(); + // for item in pane.read(cx).items_of_type::() { + // if item.read(cx).peer_id == peer_id { + // return Box::new(Some(item)); + // } + // } + + // Some(Box::new(cx.build_view(|cx| { + // SharedScreen::new(&track, peer_id, user.clone(), cx) + // }))) + } + + fn room_id(&self, cx: &AppContext) -> Option { + Some(self.active_call.as_ref()?.0.read(cx).room()?.read(cx).id()) + } + fn hang_up(&self, mut cx: AsyncWindowContext) -> Result>> { + let Some((call, _)) = self.active_call.as_ref() else { + bail!("Cannot exit a call; not in a call"); + }; + + call.update(&mut cx, |this, cx| this.hang_up(cx)) + } + fn active_project(&self, cx: &AppContext) -> Option> { + ActiveCall::global(cx).read(cx).location().cloned() + } + fn peer_state( + &mut self, + leader_id: PeerId, + cx: &mut ViewContext, + ) -> Option<(bool, bool)> { + let (call, _) = self.active_call.as_ref()?; + let room = call.read(cx).room()?.read(cx); + let participant = room.remote_participant_for_peer_id(leader_id)?; + + let leader_in_this_app; + let leader_in_this_project; + match participant.location { + ParticipantLocation::SharedProject { project_id } => { + leader_in_this_app = true; + leader_in_this_project = Some(project_id) + == self + .parent_workspace + .update(cx, |this, cx| this.project().read(cx).remote_id()) + .log_err() + .flatten(); + } + ParticipantLocation::UnsharedProject => { + leader_in_this_app = true; + leader_in_this_project = false; + } + ParticipantLocation::External => { + leader_in_this_app = false; + leader_in_this_project = false; + } + }; + + Some((leader_in_this_project, leader_in_this_app)) + } +} + #[cfg(test)] mod test { use gpui::TestAppContext; diff --git a/crates/client/src/telemetry.rs b/crates/client/src/telemetry.rs index 8f7fbeb83d..a3e7449cf8 100644 --- a/crates/client/src/telemetry.rs +++ b/crates/client/src/telemetry.rs @@ -109,6 +109,10 @@ pub enum ClickhouseEvent { virtual_memory_in_bytes: u64, milliseconds_since_first_event: i64, }, + App { + operation: &'static str, + milliseconds_since_first_event: i64, + }, } #[cfg(debug_assertions)] @@ -168,13 +172,8 @@ impl Telemetry { let mut state = self.state.lock(); state.installation_id = installation_id.map(|id| id.into()); state.session_id = Some(session_id.into()); - let has_clickhouse_events = !state.clickhouse_events_queue.is_empty(); drop(state); - if has_clickhouse_events { - self.flush_clickhouse_events(); - } - let this = self.clone(); cx.spawn(|mut cx| async move { // Avoiding calling `System::new_all()`, as there have been crashes related to it @@ -256,7 +255,7 @@ impl Telemetry { milliseconds_since_first_event: self.milliseconds_since_first_event(), }; - self.report_clickhouse_event(event, telemetry_settings) + self.report_clickhouse_event(event, telemetry_settings, false) } pub fn report_copilot_event( @@ -273,7 +272,7 @@ impl Telemetry { milliseconds_since_first_event: self.milliseconds_since_first_event(), }; - self.report_clickhouse_event(event, telemetry_settings) + self.report_clickhouse_event(event, telemetry_settings, false) } pub fn report_assistant_event( @@ -290,7 +289,7 @@ impl Telemetry { milliseconds_since_first_event: self.milliseconds_since_first_event(), }; - self.report_clickhouse_event(event, telemetry_settings) + self.report_clickhouse_event(event, telemetry_settings, false) } pub fn report_call_event( @@ -307,7 +306,7 @@ impl Telemetry { milliseconds_since_first_event: self.milliseconds_since_first_event(), }; - self.report_clickhouse_event(event, telemetry_settings) + self.report_clickhouse_event(event, telemetry_settings, false) } pub fn report_cpu_event( @@ -322,7 +321,7 @@ impl Telemetry { milliseconds_since_first_event: self.milliseconds_since_first_event(), }; - self.report_clickhouse_event(event, telemetry_settings) + self.report_clickhouse_event(event, telemetry_settings, false) } pub fn report_memory_event( @@ -337,7 +336,21 @@ impl Telemetry { milliseconds_since_first_event: self.milliseconds_since_first_event(), }; - self.report_clickhouse_event(event, telemetry_settings) + self.report_clickhouse_event(event, telemetry_settings, false) + } + + // app_events are called at app open and app close, so flush is set to immediately send + pub fn report_app_event( + self: &Arc, + telemetry_settings: TelemetrySettings, + operation: &'static str, + ) { + let event = ClickhouseEvent::App { + operation, + milliseconds_since_first_event: self.milliseconds_since_first_event(), + }; + + self.report_clickhouse_event(event, telemetry_settings, true) } fn milliseconds_since_first_event(&self) -> i64 { @@ -358,6 +371,7 @@ impl Telemetry { self: &Arc, event: ClickhouseEvent, telemetry_settings: TelemetrySettings, + immediate_flush: bool, ) { if !telemetry_settings.metrics { return; @@ -370,7 +384,7 @@ impl Telemetry { .push(ClickhouseEventWrapper { signed_in, event }); if state.installation_id.is_some() { - if state.clickhouse_events_queue.len() >= MAX_QUEUE_LEN { + if immediate_flush || state.clickhouse_events_queue.len() >= MAX_QUEUE_LEN { drop(state); self.flush_clickhouse_events(); } else { diff --git a/crates/client2/src/client2.rs b/crates/client2/src/client2.rs index b4279b023e..4ad354f2f9 100644 --- a/crates/client2/src/client2.rs +++ b/crates/client2/src/client2.rs @@ -382,7 +382,7 @@ impl settings::Settings for TelemetrySettings { } impl Client { - pub fn new(http: Arc, cx: &AppContext) -> Arc { + pub fn new(http: Arc, cx: &mut AppContext) -> Arc { Arc::new(Self { id: AtomicU64::new(0), peer: Peer::new(0), diff --git a/crates/client2/src/telemetry.rs b/crates/client2/src/telemetry.rs index 9c88d1102c..37651ebcfb 100644 --- a/crates/client2/src/telemetry.rs +++ b/crates/client2/src/telemetry.rs @@ -107,6 +107,10 @@ pub enum ClickhouseEvent { virtual_memory_in_bytes: u64, milliseconds_since_first_event: i64, }, + App { + operation: &'static str, + milliseconds_since_first_event: i64, + }, } #[cfg(debug_assertions)] @@ -122,12 +126,13 @@ const DEBOUNCE_INTERVAL: Duration = Duration::from_secs(1); const DEBOUNCE_INTERVAL: Duration = Duration::from_secs(30); impl Telemetry { - pub fn new(client: Arc, cx: &AppContext) -> Arc { + pub fn new(client: Arc, cx: &mut AppContext) -> Arc { let release_channel = if cx.has_global::() { Some(cx.global::().display_name()) } else { None }; + // TODO: Replace all hardware stuff with nested SystemSpecs json let this = Arc::new(Self { http_client: client, @@ -147,9 +152,22 @@ impl Telemetry { }), }); + // We should only ever have one instance of Telemetry, leak the subscription to keep it alive + // rather than store in TelemetryState, complicating spawn as subscriptions are not Send + // std::mem::forget(cx.on_app_quit({ + // let this = this.clone(); + // move |cx| this.shutdown_telemetry(cx) + // })); + this } + // fn shutdown_telemetry(self: &Arc, cx: &mut AppContext) -> impl Future { + // let telemetry_settings = TelemetrySettings::get_global(cx).clone(); + // self.report_app_event(telemetry_settings, "close"); + // Task::ready(()) + // } + pub fn log_file_path(&self) -> Option { Some(self.state.lock().log_file.as_ref()?.path().to_path_buf()) } @@ -163,13 +181,8 @@ impl Telemetry { let mut state = self.state.lock(); state.installation_id = installation_id.map(|id| id.into()); state.session_id = Some(session_id.into()); - let has_clickhouse_events = !state.clickhouse_events_queue.is_empty(); drop(state); - if has_clickhouse_events { - self.flush_clickhouse_events(); - } - let this = self.clone(); cx.spawn(|cx| async move { // Avoiding calling `System::new_all()`, as there have been crashes related to it @@ -257,7 +270,7 @@ impl Telemetry { milliseconds_since_first_event: self.milliseconds_since_first_event(), }; - self.report_clickhouse_event(event, telemetry_settings) + self.report_clickhouse_event(event, telemetry_settings, false) } pub fn report_copilot_event( @@ -274,7 +287,7 @@ impl Telemetry { milliseconds_since_first_event: self.milliseconds_since_first_event(), }; - self.report_clickhouse_event(event, telemetry_settings) + self.report_clickhouse_event(event, telemetry_settings, false) } pub fn report_assistant_event( @@ -291,7 +304,7 @@ impl Telemetry { milliseconds_since_first_event: self.milliseconds_since_first_event(), }; - self.report_clickhouse_event(event, telemetry_settings) + self.report_clickhouse_event(event, telemetry_settings, false) } pub fn report_call_event( @@ -308,7 +321,7 @@ impl Telemetry { milliseconds_since_first_event: self.milliseconds_since_first_event(), }; - self.report_clickhouse_event(event, telemetry_settings) + self.report_clickhouse_event(event, telemetry_settings, false) } pub fn report_cpu_event( @@ -323,7 +336,7 @@ impl Telemetry { milliseconds_since_first_event: self.milliseconds_since_first_event(), }; - self.report_clickhouse_event(event, telemetry_settings) + self.report_clickhouse_event(event, telemetry_settings, false) } pub fn report_memory_event( @@ -338,7 +351,21 @@ impl Telemetry { milliseconds_since_first_event: self.milliseconds_since_first_event(), }; - self.report_clickhouse_event(event, telemetry_settings) + self.report_clickhouse_event(event, telemetry_settings, false) + } + + // app_events are called at app open and app close, so flush is set to immediately send + pub fn report_app_event( + self: &Arc, + telemetry_settings: TelemetrySettings, + operation: &'static str, + ) { + let event = ClickhouseEvent::App { + operation, + milliseconds_since_first_event: self.milliseconds_since_first_event(), + }; + + self.report_clickhouse_event(event, telemetry_settings, true) } fn milliseconds_since_first_event(&self) -> i64 { @@ -359,6 +386,7 @@ impl Telemetry { self: &Arc, event: ClickhouseEvent, telemetry_settings: TelemetrySettings, + immediate_flush: bool, ) { if !telemetry_settings.metrics { return; @@ -371,7 +399,7 @@ impl Telemetry { .push(ClickhouseEventWrapper { signed_in, event }); if state.installation_id.is_some() { - if state.clickhouse_events_queue.len() >= MAX_QUEUE_LEN { + if immediate_flush || state.clickhouse_events_queue.len() >= MAX_QUEUE_LEN { drop(state); self.flush_clickhouse_events(); } else { diff --git a/crates/collab/Cargo.toml b/crates/collab/Cargo.toml index dea6e09245..bbaf521e15 100644 --- a/crates/collab/Cargo.toml +++ b/crates/collab/Cargo.toml @@ -3,7 +3,7 @@ authors = ["Nathan Sobo "] default-run = "collab" edition = "2021" name = "collab" -version = "0.28.0" +version = "0.29.0" publish = false [[bin]] diff --git a/crates/collab2/src/tests/test_server.rs b/crates/collab2/src/tests/test_server.rs index 090a32d4ca..969869599b 100644 --- a/crates/collab2/src/tests/test_server.rs +++ b/crates/collab2/src/tests/test_server.rs @@ -149,7 +149,7 @@ impl TestServer { .user_id }; let client_name = name.to_string(); - let mut client = cx.read(|cx| Client::new(http.clone(), cx)); + let mut client = cx.update(|cx| Client::new(http.clone(), cx)); let server = self.server.clone(); let db = self.app_state.db.clone(); let connection_killers = self.connection_killers.clone(); @@ -221,6 +221,7 @@ impl TestServer { fs: fs.clone(), build_window_options: |_, _, _| Default::default(), node_runtime: FakeNodeRuntime::new(), + call_factory: |_, _| Box::new(workspace::TestCallHandler), }); cx.update(|cx| { diff --git a/crates/diagnostics2/src/diagnostics.rs b/crates/diagnostics2/src/diagnostics.rs index 9e97bff968..dc7f0a1f3f 100644 --- a/crates/diagnostics2/src/diagnostics.rs +++ b/crates/diagnostics2/src/diagnostics.rs @@ -1550,7 +1550,7 @@ mod tests { block_id: ix, editor_style: &editor::EditorStyle::default(), }) - .element_id()? + .inner_id()? .try_into() .ok()?, diff --git a/crates/diagnostics2/src/items.rs b/crates/diagnostics2/src/items.rs index bbcfa748d4..ac24b7ad50 100644 --- a/crates/diagnostics2/src/items.rs +++ b/crates/diagnostics2/src/items.rs @@ -44,7 +44,7 @@ impl Render for DiagnosticIndicator { }; h_stack() - .id(cx.entity_id()) + .id("diagnostic-indicator") .on_action(cx.listener(Self::go_to_next_diagnostic)) .rounded_md() .flex_none() diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 2558aec121..17712b7e78 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -1001,17 +1001,18 @@ impl CompletionsMenu { fn pre_resolve_completion_documentation( &self, - project: Option>, + editor: &Editor, cx: &mut ViewContext, - ) { + ) -> Option> { let settings = settings::get::(cx); if !settings.show_completion_documentation { - return; + return None; } - let Some(project) = project else { - return; + let Some(project) = editor.project.clone() else { + return None; }; + let client = project.read(cx).client(); let language_registry = project.read(cx).languages().clone(); @@ -1021,7 +1022,7 @@ impl CompletionsMenu { let completions = self.completions.clone(); let completion_indices: Vec<_> = self.matches.iter().map(|m| m.candidate_id).collect(); - cx.spawn(move |this, mut cx| async move { + Some(cx.spawn(move |this, mut cx| async move { if is_remote { let Some(project_id) = project_id else { log::error!("Remote project without remote_id"); @@ -1083,8 +1084,7 @@ impl CompletionsMenu { _ = this.update(&mut cx, |_, cx| cx.notify()); } } - }) - .detach(); + })) } fn attempt_resolve_selected_completion_documentation( @@ -3580,7 +3580,8 @@ impl Editor { let id = post_inc(&mut self.next_completion_id); let task = cx.spawn(|this, mut cx| { async move { - let menu = if let Some(completions) = completions.await.log_err() { + let completions = completions.await.log_err(); + let (menu, pre_resolve_task) = if let Some(completions) = completions { let mut menu = CompletionsMenu { id, initial_position: position, @@ -3601,21 +3602,26 @@ impl Editor { selected_item: 0, list: Default::default(), }; + menu.filter(query.as_deref(), cx.background()).await; + if menu.matches.is_empty() { - None + (None, None) } else { - _ = this.update(&mut cx, |editor, cx| { - menu.pre_resolve_completion_documentation(editor.project.clone(), cx); - }); - Some(menu) + let pre_resolve_task = this + .update(&mut cx, |editor, cx| { + menu.pre_resolve_completion_documentation(editor, cx) + }) + .ok() + .flatten(); + (Some(menu), pre_resolve_task) } } else { - None + (None, None) }; this.update(&mut cx, |this, cx| { - this.completion_tasks.retain(|(task_id, _)| *task_id > id); + this.completion_tasks.retain(|(task_id, _)| *task_id >= id); let mut context_menu = this.context_menu.write(); match context_menu.as_ref() { @@ -3636,10 +3642,10 @@ impl Editor { drop(context_menu); this.discard_copilot_suggestion(cx); cx.notify(); - } else if this.completion_tasks.is_empty() { - // If there are no more completion tasks and the last menu was - // empty, we should hide it. If it was already hidden, we should - // also show the copilot suggestion when available. + } else if this.completion_tasks.len() <= 1 { + // If there are no more completion tasks (omitting ourself) and + // the last menu was empty, we should hide it. If it was already + // hidden, we should also show the copilot suggestion when available. drop(context_menu); if this.hide_context_menu(cx).is_none() { this.update_visible_copilot_suggestion(cx); @@ -3647,10 +3653,15 @@ impl Editor { } })?; + if let Some(pre_resolve_task) = pre_resolve_task { + pre_resolve_task.await; + } + Ok::<_, anyhow::Error>(()) } .log_err() }); + self.completion_tasks.push((id, task)); } diff --git a/crates/editor2/src/editor.rs b/crates/editor2/src/editor.rs index 33b742f9df..049b77116d 100644 --- a/crates/editor2/src/editor.rs +++ b/crates/editor2/src/editor.rs @@ -966,20 +966,22 @@ impl CompletionsMenu { fn pre_resolve_completion_documentation( &self, - project: Option>, - cx: &mut ViewContext, - ) { + _editor: &Editor, + _cx: &mut ViewContext, + ) -> Option> { // todo!("implementation below "); + None } - // ) { + // { // let settings = EditorSettings::get_global(cx); // if !settings.show_completion_documentation { - // return; + // return None; // } - // let Some(project) = project else { - // return; + // let Some(project) = editor.project.clone() else { + // return None; // }; + // let client = project.read(cx).client(); // let language_registry = project.read(cx).languages().clone(); @@ -989,7 +991,7 @@ impl CompletionsMenu { // let completions = self.completions.clone(); // let completion_indices: Vec<_> = self.matches.iter().map(|m| m.candidate_id).collect(); - // cx.spawn(move |this, mut cx| async move { + // Some(cx.spawn(move |this, mut cx| async move { // if is_remote { // let Some(project_id) = project_id else { // log::error!("Remote project without remote_id"); @@ -1051,8 +1053,7 @@ impl CompletionsMenu { // _ = this.update(&mut cx, |_, cx| cx.notify()); // } // } - // }) - // .detach(); + // })) // } fn attempt_resolve_selected_completion_documentation( @@ -3596,7 +3597,8 @@ impl Editor { let id = post_inc(&mut self.next_completion_id); let task = cx.spawn(|this, mut cx| { async move { - let menu = if let Some(completions) = completions.await.log_err() { + let completions = completions.await.log_err(); + let (menu, pre_resolve_task) = if let Some(completions) = completions { let mut menu = CompletionsMenu { id, initial_position: position, @@ -3619,20 +3621,24 @@ impl Editor { }; menu.filter(query.as_deref(), cx.background_executor().clone()) .await; + if menu.matches.is_empty() { - None + (None, None) } else { - _ = this.update(&mut cx, |editor, cx| { - menu.pre_resolve_completion_documentation(editor.project.clone(), cx); - }); - Some(menu) + let pre_resolve_task = this + .update(&mut cx, |editor, cx| { + menu.pre_resolve_completion_documentation(editor, cx) + }) + .ok() + .flatten(); + (Some(menu), pre_resolve_task) } } else { - None + (None, None) }; this.update(&mut cx, |this, cx| { - this.completion_tasks.retain(|(task_id, _)| *task_id > id); + this.completion_tasks.retain(|(task_id, _)| *task_id >= id); let mut context_menu = this.context_menu.write(); match context_menu.as_ref() { @@ -3664,10 +3670,15 @@ impl Editor { } })?; + if let Some(pre_resolve_task) = pre_resolve_task { + pre_resolve_task.await; + } + Ok::<_, anyhow::Error>(()) } .log_err() }); + self.completion_tasks.push((id, task)); } diff --git a/crates/editor2/src/element.rs b/crates/editor2/src/element.rs index 5476cd8ab1..da163bd8f3 100644 --- a/crates/editor2/src/element.rs +++ b/crates/editor2/src/element.rs @@ -1051,7 +1051,7 @@ impl EditorElement { } if list_origin.y + list_height > text_bounds.lower_right().y { - list_origin.y -= layout.position_map.line_height - list_height; + list_origin.y -= layout.position_map.line_height + list_height; } context_menu.draw(list_origin, available_space, cx); diff --git a/crates/gpui2/src/app.rs b/crates/gpui2/src/app.rs index f645129706..617c0b5600 100644 --- a/crates/gpui2/src/app.rs +++ b/crates/gpui2/src/app.rs @@ -10,6 +10,7 @@ pub use entity_map::*; pub use model_context::*; use refineable::Refineable; use smallvec::SmallVec; +use smol::future::FutureExt; #[cfg(any(test, feature = "test-support"))] pub use test_context::*; @@ -983,6 +984,22 @@ impl AppContext { pub fn all_action_names(&self) -> &[SharedString] { self.actions.all_action_names() } + + pub fn on_app_quit( + &mut self, + mut on_quit: impl FnMut(&mut AppContext) -> Fut + 'static, + ) -> Subscription + where + Fut: 'static + Future, + { + self.quit_observers.insert( + (), + Box::new(move |cx| { + let future = on_quit(cx); + async move { future.await }.boxed_local() + }), + ) + } } impl Context for AppContext { diff --git a/crates/gpui2/src/element.rs b/crates/gpui2/src/element.rs index 5f62629265..b18ffb8ca6 100644 --- a/crates/gpui2/src/element.rs +++ b/crates/gpui2/src/element.rs @@ -429,10 +429,6 @@ impl AnyElement { AnyElement(Box::new(Some(DrawableElement::new(element))) as Box) } - pub fn element_id(&self) -> Option { - self.0.element_id() - } - pub fn layout(&mut self, cx: &mut WindowContext) -> LayoutId { self.0.layout(cx) } @@ -464,6 +460,10 @@ impl AnyElement { pub fn into_any(self) -> AnyElement { AnyElement::new(self) } + + pub fn inner_id(&self) -> Option { + self.0.element_id() + } } impl Element for AnyElement { @@ -487,7 +487,7 @@ impl IntoElement for AnyElement { type Element = Self; fn element_id(&self) -> Option { - AnyElement::element_id(self) + None } fn into_element(self) -> Self::Element { diff --git a/crates/gpui2/src/elements/uniform_list.rs b/crates/gpui2/src/elements/uniform_list.rs index b3b99ee680..1ddddf9c78 100644 --- a/crates/gpui2/src/elements/uniform_list.rs +++ b/crates/gpui2/src/elements/uniform_list.rs @@ -1,7 +1,7 @@ use crate::{ - point, px, size, AnyElement, AvailableSpace, Bounds, Element, ElementId, InteractiveElement, - InteractiveElementState, Interactivity, IntoElement, LayoutId, Pixels, Point, Render, Size, - StyleRefinement, Styled, View, ViewContext, WindowContext, + point, px, size, AnyElement, AvailableSpace, BorrowWindow, Bounds, ContentMask, Element, + ElementId, InteractiveElement, InteractiveElementState, Interactivity, IntoElement, LayoutId, + Pixels, Point, Render, Size, StyleRefinement, Styled, View, ViewContext, WindowContext, }; use smallvec::SmallVec; use std::{cell::RefCell, cmp, ops::Range, rc::Rc}; @@ -210,31 +210,31 @@ impl Element for UniformList { scroll_offset: shared_scroll_offset, }); } - let visible_item_count = if item_height > px(0.) { - (padded_bounds.size.height / item_height).ceil() as usize + 1 - } else { - 0 - }; let first_visible_element_ix = (-scroll_offset.y / item_height).floor() as usize; + let last_visible_element_ix = + ((-scroll_offset.y + padded_bounds.size.height) / item_height).ceil() + as usize; let visible_range = first_visible_element_ix - ..cmp::min( - first_visible_element_ix + visible_item_count, - self.item_count, - ); + ..cmp::min(last_visible_element_ix, self.item_count); let items = (self.render_items)(visible_range.clone(), cx); cx.with_z_index(1, |cx| { - for (item, ix) in items.into_iter().zip(visible_range) { - let item_origin = padded_bounds.origin - + point(px(0.), item_height * ix + scroll_offset.y); - let available_space = size( - AvailableSpace::Definite(padded_bounds.size.width), - AvailableSpace::Definite(item_height), - ); - item.draw(item_origin, available_space, cx); - } + let content_mask = ContentMask { + bounds: padded_bounds, + }; + cx.with_content_mask(Some(content_mask), |cx| { + for (item, ix) in items.into_iter().zip(visible_range) { + let item_origin = padded_bounds.origin + + point(px(0.), item_height * ix + scroll_offset.y); + let available_space = size( + AvailableSpace::Definite(padded_bounds.size.width), + AvailableSpace::Definite(item_height), + ); + item.draw(item_origin, available_space, cx); + } + }); }); } }) diff --git a/crates/gpui2/src/view.rs b/crates/gpui2/src/view.rs index 02696deb8a..f31b0ae753 100644 --- a/crates/gpui2/src/view.rs +++ b/crates/gpui2/src/view.rs @@ -248,7 +248,7 @@ impl IntoElement for View { type Element = View; fn element_id(&self) -> Option { - Some(self.model.entity_id.into()) + Some(ElementId::from_entity_id(self.model.entity_id)) } fn into_element(self) -> Self::Element { @@ -260,7 +260,7 @@ impl IntoElement for AnyView { type Element = Self; fn element_id(&self) -> Option { - Some(self.model.entity_id.into()) + Some(ElementId::from_entity_id(self.model.entity_id)) } fn into_element(self) -> Self::Element { @@ -308,27 +308,23 @@ where } mod any_view { - use crate::{AnyElement, AnyView, BorrowWindow, Element, LayoutId, Render, WindowContext}; + use crate::{AnyElement, AnyView, Element, LayoutId, Render, WindowContext}; pub(crate) fn layout( view: &AnyView, cx: &mut WindowContext, ) -> (LayoutId, AnyElement) { - cx.with_element_id(Some(view.model.entity_id), |cx| { - let view = view.clone().downcast::().unwrap(); - let mut element = view.update(cx, |view, cx| view.render(cx).into_any()); - let layout_id = element.layout(cx); - (layout_id, element) - }) + let view = view.clone().downcast::().unwrap(); + let mut element = view.update(cx, |view, cx| view.render(cx).into_any()); + let layout_id = element.layout(cx); + (layout_id, element) } pub(crate) fn paint( - view: &AnyView, + _view: &AnyView, element: AnyElement, cx: &mut WindowContext, ) { - cx.with_element_id(Some(view.model.entity_id), |cx| { - element.paint(cx); - }) + element.paint(cx); } } diff --git a/crates/gpui2/src/window.rs b/crates/gpui2/src/window.rs index 483a8fdbee..1973aa14a9 100644 --- a/crates/gpui2/src/window.rs +++ b/crates/gpui2/src/window.rs @@ -230,9 +230,15 @@ pub struct Window { pub(crate) focus: Option, } +pub(crate) struct ElementStateBox { + inner: Box, + #[cfg(debug_assertions)] + type_name: &'static str, +} + // #[derive(Default)] pub(crate) struct Frame { - pub(crate) element_states: HashMap>, + pub(crate) element_states: HashMap, mouse_listeners: HashMap>, pub(crate) dispatch_tree: DispatchTree, pub(crate) focus_listeners: Vec, @@ -1815,10 +1821,37 @@ pub trait BorrowWindow: BorrowMut + BorrowMut { .remove(&global_id) }) { + let ElementStateBox { + inner, + + #[cfg(debug_assertions)] + type_name + } = any; // Using the extra inner option to avoid needing to reallocate a new box. - let mut state_box = any + let mut state_box = inner .downcast::>() - .expect("invalid element state type for id"); + .map_err(|_| { + #[cfg(debug_assertions)] + { + anyhow!( + "invalid element state type for id, requested_type {:?}, actual type: {:?}", + std::any::type_name::(), + type_name + ) + } + + #[cfg(not(debug_assertions))] + { + anyhow!( + "invalid element state type for id, requested_type {:?}", + std::any::type_name::(), + ) + } + }) + .unwrap(); + + // Actual: Option <- View + // Requested: () <- AnyElemet let state = state_box .take() .expect("element state is already on the stack"); @@ -1827,14 +1860,27 @@ pub trait BorrowWindow: BorrowMut + BorrowMut { cx.window_mut() .current_frame .element_states - .insert(global_id, state_box); + .insert(global_id, ElementStateBox { + inner: state_box, + + #[cfg(debug_assertions)] + type_name + }); result } else { let (result, state) = f(None, cx); cx.window_mut() .current_frame .element_states - .insert(global_id, Box::new(Some(state))); + .insert(global_id, + ElementStateBox { + inner: Box::new(Some(state)), + + #[cfg(debug_assertions)] + type_name: std::any::type_name::() + } + + ); result } }) @@ -2599,6 +2645,12 @@ pub enum ElementId { FocusHandle(FocusId), } +impl ElementId { + pub(crate) fn from_entity_id(entity_id: EntityId) -> Self { + ElementId::View(entity_id) + } +} + impl TryInto for ElementId { type Error = anyhow::Error; @@ -2611,12 +2663,6 @@ impl TryInto for ElementId { } } -impl From for ElementId { - fn from(id: EntityId) -> Self { - ElementId::View(id) - } -} - impl From for ElementId { fn from(id: usize) -> Self { ElementId::Integer(id) diff --git a/crates/project2/src/worktree_tests.rs b/crates/project2/src/worktree_tests.rs index df7307f694..a77f5396e1 100644 --- a/crates/project2/src/worktree_tests.rs +++ b/crates/project2/src/worktree_tests.rs @@ -1056,7 +1056,7 @@ async fn test_create_directory_during_initial_scan(cx: &mut TestAppContext) { async fn test_create_dir_all_on_create_entry(cx: &mut TestAppContext) { init_test(cx); cx.executor().allow_parking(); - let client_fake = cx.read(|cx| Client::new(FakeHttpClient::with_404_response(), cx)); + let client_fake = cx.update(|cx| Client::new(FakeHttpClient::with_404_response(), cx)); let fs_fake = FakeFs::new(cx.background_executor.clone()); fs_fake @@ -1096,7 +1096,7 @@ async fn test_create_dir_all_on_create_entry(cx: &mut TestAppContext) { assert!(tree.entry_for_path("a/b/").unwrap().is_dir()); }); - let client_real = cx.read(|cx| Client::new(FakeHttpClient::with_404_response(), cx)); + let client_real = cx.update(|cx| Client::new(FakeHttpClient::with_404_response(), cx)); let fs_real = Arc::new(RealFs); let temp_root = temp_tree(json!({ @@ -2181,7 +2181,7 @@ async fn test_propagate_git_statuses(cx: &mut TestAppContext) { fn build_client(cx: &mut TestAppContext) -> Arc { let http_client = FakeHttpClient::with_404_response(); - cx.read(|cx| Client::new(http_client, cx)) + cx.update(|cx| Client::new(http_client, cx)) } #[track_caller] diff --git a/crates/project_panel2/src/project_panel.rs b/crates/project_panel2/src/project_panel.rs index 160c48f721..d2d30556eb 100644 --- a/crates/project_panel2/src/project_panel.rs +++ b/crates/project_panel2/src/project_panel.rs @@ -371,7 +371,7 @@ impl ProjectPanel { _entry_id: ProjectEntryId, _cx: &mut ViewContext, ) { - todo!() + // todo!() // let project = self.project.read(cx); // let worktree_id = if let Some(id) = project.worktree_id_for_entry(entry_id, cx) { diff --git a/crates/terminal_view2/src/terminal_view.rs b/crates/terminal_view2/src/terminal_view.rs index 5a5f74f9e1..9f3ed31388 100644 --- a/crates/terminal_view2/src/terminal_view.rs +++ b/crates/terminal_view2/src/terminal_view.rs @@ -31,7 +31,7 @@ use workspace::{ notifications::NotifyResultExt, register_deserializable_item, searchable::{SearchEvent, SearchOptions, SearchableItem}, - ui::{ContextMenu, Icon, IconElement, Label, ListItem}, + ui::{ContextMenu, Icon, IconElement, Label}, CloseActiveItem, NewCenterTerminal, Pane, ToolbarItemLocation, Workspace, WorkspaceId, }; @@ -299,11 +299,8 @@ impl TerminalView { cx: &mut ViewContext, ) { self.context_menu = Some(ContextMenu::build(cx, |menu, _| { - menu.action(ListItem::new("clear", Label::new("Clear")), Box::new(Clear)) - .action( - ListItem::new("close", Label::new("Close")), - Box::new(CloseActiveItem { save_intent: None }), - ) + menu.action("Clear", Box::new(Clear)) + .action("Close", Box::new(CloseActiveItem { save_intent: None })) })); dbg!(&position); // todo!() diff --git a/crates/ui2/src/components/context_menu.rs b/crates/ui2/src/components/context_menu.rs index 9ebc62f27a..d5adaf586b 100644 --- a/crates/ui2/src/components/context_menu.rs +++ b/crates/ui2/src/components/context_menu.rs @@ -1,7 +1,7 @@ use std::cell::RefCell; use std::rc::Rc; -use crate::{prelude::*, v_stack, List}; +use crate::{prelude::*, v_stack, Label, List}; use crate::{ListItem, ListSeparator, ListSubHeader}; use gpui::{ overlay, px, Action, AnchorCorner, AnyElement, AppContext, Bounds, ClickEvent, DispatchPhase, @@ -10,9 +10,9 @@ use gpui::{ }; pub enum ContextMenuItem { - Separator(ListSeparator), - Header(ListSubHeader), - Entry(ListItem, Rc), + Separator, + Header(SharedString), + Entry(SharedString, Rc), } pub struct ContextMenu { @@ -46,29 +46,30 @@ impl ContextMenu { } pub fn header(mut self, title: impl Into) -> Self { - self.items - .push(ContextMenuItem::Header(ListSubHeader::new(title))); + self.items.push(ContextMenuItem::Header(title.into())); self } pub fn separator(mut self) -> Self { - self.items.push(ContextMenuItem::Separator(ListSeparator)); + self.items.push(ContextMenuItem::Separator); self } pub fn entry( mut self, - view: ListItem, + label: impl Into, on_click: impl Fn(&ClickEvent, &mut WindowContext) + 'static, ) -> Self { self.items - .push(ContextMenuItem::Entry(view, Rc::new(on_click))); + .push(ContextMenuItem::Entry(label.into(), Rc::new(on_click))); self } - pub fn action(self, view: ListItem, action: Box) -> Self { + pub fn action(self, label: impl Into, action: Box) -> Self { // todo: add the keybindings to the list entry - self.entry(view, move |_, cx| cx.dispatch_action(action.boxed_clone())) + self.entry(label.into(), move |_, cx| { + cx.dispatch_action(action.boxed_clone()) + }) } pub fn confirm(&mut self, _: &menu::Confirm, cx: &mut ViewContext) { @@ -104,16 +105,16 @@ impl Render for ContextMenu { // .border_color(cx.theme().colors().border) .child( List::new().children(self.items.iter().map(|item| match item { - ContextMenuItem::Separator(separator) => { - separator.clone().into_any_element() + ContextMenuItem::Separator => ListSeparator::new().into_any_element(), + ContextMenuItem::Header(header) => { + ListSubHeader::new(header.clone()).into_any_element() } - ContextMenuItem::Header(header) => header.clone().into_any_element(), ContextMenuItem::Entry(entry, callback) => { let callback = callback.clone(); let dismiss = cx.listener(|_, _, cx| cx.emit(Manager::Dismiss)); - entry - .clone() + ListItem::new(entry.clone()) + .child(Label::new(entry.clone())) .on_click(move |event, cx| { callback(event, cx); dismiss(event, cx) diff --git a/crates/ui2/src/components/list.rs b/crates/ui2/src/components/list.rs index 2063475e5a..a994583666 100644 --- a/crates/ui2/src/components/list.rs +++ b/crates/ui2/src/components/list.rs @@ -245,45 +245,28 @@ pub struct ListItem { // TODO: Reintroduce this // disclosure_control_style: DisclosureControlVisibility, indent_level: u32, - label: Label, left_slot: Option, overflow: OverflowStyle, size: ListEntrySize, toggle: Toggle, variant: ListItemVariant, on_click: Option>, -} - -impl Clone for ListItem { - fn clone(&self) -> Self { - Self { - id: self.id.clone(), - disabled: self.disabled, - indent_level: self.indent_level, - label: self.label.clone(), - left_slot: self.left_slot.clone(), - overflow: self.overflow, - size: self.size, - toggle: self.toggle, - variant: self.variant, - on_click: self.on_click.clone(), - } - } + children: SmallVec<[AnyElement; 2]>, } impl ListItem { - pub fn new(id: impl Into, label: Label) -> Self { + pub fn new(id: impl Into) -> Self { Self { id: id.into(), disabled: false, indent_level: 0, - label, left_slot: None, overflow: OverflowStyle::Hidden, size: ListEntrySize::default(), toggle: Toggle::NotToggleable, variant: ListItemVariant::default(), on_click: Default::default(), + children: SmallVec::new(), } } @@ -394,11 +377,17 @@ impl RenderOnce for ListItem { .relative() .child(disclosure_control(self.toggle)) .children(left_content) - .child(self.label), + .children(self.children), ) } } +impl ParentElement for ListItem { + fn children_mut(&mut self) -> &mut SmallVec<[AnyElement; 2]> { + &mut self.children + } +} + #[derive(IntoElement, Clone)] pub struct ListSeparator; diff --git a/crates/ui2/src/components/stories/context_menu.rs b/crates/ui2/src/components/stories/context_menu.rs index dd0bc03a21..98faea70aa 100644 --- a/crates/ui2/src/components/stories/context_menu.rs +++ b/crates/ui2/src/components/stories/context_menu.rs @@ -2,7 +2,7 @@ use gpui::{actions, Action, AnchorCorner, Div, Render, View}; use story::Story; use crate::prelude::*; -use crate::{menu_handle, ContextMenu, Label, ListItem}; +use crate::{menu_handle, ContextMenu, Label}; actions!(PrintCurrentDate, PrintBestFood); @@ -10,17 +10,13 @@ fn build_menu(cx: &mut WindowContext, header: impl Into) -> View, follower_states: &HashMap, FollowerState>, - active_call: Option<&Model>, active_pane: &View, zoomed: Option<&AnyWeakView>, app_state: &Arc, @@ -137,7 +135,6 @@ impl PaneGroup { project, 0, follower_states, - active_call, active_pane, zoomed, app_state, @@ -199,7 +196,6 @@ impl Member { project: &Model, basis: usize, follower_states: &HashMap, FollowerState>, - active_call: Option<&Model>, active_pane: &View, zoomed: Option<&AnyWeakView>, app_state: &Arc, @@ -214,7 +210,7 @@ impl Member { // Some(pane) // }; - div().size_full().child(pane.clone()) + div().size_full().child(pane.clone()).into_any() // Stack::new() // .with_child(pane_element.contained().with_border(leader_border)) @@ -230,16 +226,17 @@ impl Member { // .bg(cx.theme().colors().editor) // .children(); } - Member::Axis(axis) => axis.render( - project, - basis + 1, - follower_states, - active_call, - active_pane, - zoomed, - app_state, - cx, - ), + Member::Axis(axis) => axis + .render( + project, + basis + 1, + follower_states, + active_pane, + zoomed, + app_state, + cx, + ) + .into_any(), } // enum FollowIntoExternalProject {} @@ -556,7 +553,6 @@ impl PaneAxis { project: &Model, basis: usize, follower_states: &HashMap, FollowerState>, - active_call: Option<&Model>, active_pane: &View, zoomed: Option<&AnyWeakView>, app_state: &Arc, @@ -578,7 +574,6 @@ impl PaneAxis { project, basis, follower_states, - active_call, active_pane, zoomed, app_state, diff --git a/crates/workspace2/src/workspace2.rs b/crates/workspace2/src/workspace2.rs index 22a7b57058..b09b47d24c 100644 --- a/crates/workspace2/src/workspace2.rs +++ b/crates/workspace2/src/workspace2.rs @@ -16,7 +16,7 @@ mod toolbar; mod workspace_settings; use anyhow::{anyhow, Context as _, Result}; -use call2::ActiveCall; +use async_trait::async_trait; use client2::{ proto::{self, PeerId}, Client, TypedEnvelope, UserStore, @@ -33,8 +33,8 @@ use gpui::{ AsyncWindowContext, Bounds, Context, Div, Entity, EntityId, EventEmitter, FocusHandle, FocusableView, GlobalPixels, InteractiveElement, KeyContext, ManagedView, Model, ModelContext, ParentElement, PathPromptOptions, Point, PromptLevel, Render, Size, Styled, Subscription, Task, - View, ViewContext, VisualContext, WeakView, WindowBounds, WindowContext, WindowHandle, - WindowOptions, + View, ViewContext, VisualContext, WeakModel, WeakView, WindowBounds, WindowContext, + WindowHandle, WindowOptions, }; use item::{FollowableItem, FollowableItemHandle, Item, ItemHandle, ItemSettings, ProjectItem}; use itertools::Itertools; @@ -210,7 +210,6 @@ pub fn init_settings(cx: &mut AppContext) { pub fn init(app_state: Arc, cx: &mut AppContext) { init_settings(cx); notifications::init(cx); - // cx.add_global_action({ // let app_state = Arc::downgrade(&app_state); // move |_: &Open, cx: &mut AppContext| { @@ -304,6 +303,7 @@ pub struct AppState { pub user_store: Model, pub workspace_store: Model, pub fs: Arc, + pub call_factory: CallFactory, pub build_window_options: fn(Option, Option, &mut AppContext) -> WindowOptions, pub node_runtime: Arc, @@ -322,6 +322,36 @@ struct Follower { peer_id: PeerId, } +#[cfg(any(test, feature = "test-support"))] +pub struct TestCallHandler; + +#[cfg(any(test, feature = "test-support"))] +impl CallHandler for TestCallHandler { + fn peer_state(&mut self, id: PeerId, cx: &mut ViewContext) -> Option<(bool, bool)> { + None + } + + fn shared_screen_for_peer( + &self, + peer_id: PeerId, + pane: &View, + cx: &mut ViewContext, + ) -> Option> { + None + } + + fn room_id(&self, cx: &AppContext) -> Option { + None + } + + fn hang_up(&self, cx: AsyncWindowContext) -> Result>> { + anyhow::bail!("TestCallHandler should not be hanging up") + } + + fn active_project(&self, cx: &AppContext) -> Option> { + None + } +} impl AppState { #[cfg(any(test, feature = "test-support"))] pub fn test(cx: &mut AppContext) -> Arc { @@ -352,6 +382,7 @@ impl AppState { workspace_store, node_runtime: FakeNodeRuntime::new(), build_window_options: |_, _, _| Default::default(), + call_factory: |_, _| Box::new(TestCallHandler), }) } } @@ -408,6 +439,23 @@ pub enum Event { WorkspaceCreated(WeakView), } +#[async_trait(?Send)] +pub trait CallHandler { + fn peer_state(&mut self, id: PeerId, cx: &mut ViewContext) -> Option<(bool, bool)>; + fn shared_screen_for_peer( + &self, + peer_id: PeerId, + pane: &View, + cx: &mut ViewContext, + ) -> Option>; + fn room_id(&self, cx: &AppContext) -> Option; + fn is_in_room(&self, cx: &mut ViewContext) -> bool { + self.room_id(cx).is_some() + } + fn hang_up(&self, cx: AsyncWindowContext) -> Result>>; + fn active_project(&self, cx: &AppContext) -> Option>; +} + pub struct Workspace { window_self: WindowHandle, weak_self: WeakView, @@ -428,10 +476,10 @@ pub struct Workspace { titlebar_item: Option, notifications: Vec<(TypeId, usize, Box)>, project: Model, + call_handler: Box, follower_states: HashMap, FollowerState>, last_leaders_by_pane: HashMap, PeerId>, window_edited: bool, - active_call: Option<(Model, Vec)>, leader_updates_tx: mpsc::UnboundedSender<(PeerId, proto::UpdateFollowers)>, database_id: WorkspaceId, app_state: Arc, @@ -459,6 +507,7 @@ struct FollowerState { enum WorkspaceBounds {} +type CallFactory = fn(WeakView, &mut ViewContext) -> Box; impl Workspace { pub fn new( workspace_id: WorkspaceId, @@ -550,9 +599,19 @@ impl Workspace { mpsc::unbounded::<(PeerId, proto::UpdateFollowers)>(); let _apply_leader_updates = cx.spawn(|this, mut cx| async move { while let Some((leader_id, update)) = leader_updates_rx.next().await { - Self::process_leader_update(&this, leader_id, update, &mut cx) + let mut cx2 = cx.clone(); + let t = this.clone(); + + Workspace::process_leader_update(&this, leader_id, update, &mut cx) .await .log_err(); + + // this.update(&mut cx, |this, cxx| { + // this.call_handler + // .process_leader_update(leader_id, update, cx2) + // })? + // .await + // .log_err(); } Ok(()) @@ -585,14 +644,6 @@ impl Workspace { // drag_and_drop.register_container(weak_handle.clone()); // }); - let mut active_call = None; - if cx.has_global::>() { - let call = cx.global::>().clone(); - let mut subscriptions = Vec::new(); - subscriptions.push(cx.subscribe(&call, Self::on_active_call_event)); - active_call = Some((call, subscriptions)); - } - let subscriptions = vec![ cx.observe_window_activation(Self::on_window_activation_changed), cx.observe_window_bounds(move |_, cx| { @@ -655,7 +706,8 @@ impl Workspace { follower_states: Default::default(), last_leaders_by_pane: Default::default(), window_edited: false, - active_call, + + call_handler: (app_state.call_factory)(weak_handle.clone(), cx), database_id: workspace_id, app_state, _observe_current_user, @@ -1102,7 +1154,7 @@ impl Workspace { cx: &mut ViewContext, ) -> Task> { //todo!(saveing) - let active_call = self.active_call().cloned(); + let window = cx.window_handle(); cx.spawn(|this, mut cx| async move { @@ -1113,27 +1165,27 @@ impl Workspace { .count() })?; - if let Some(active_call) = active_call { - if !quitting - && workspace_count == 1 - && active_call.read_with(&cx, |call, _| call.room().is_some())? - { - let answer = window.update(&mut cx, |_, cx| { - cx.prompt( - PromptLevel::Warning, - "Do you want to leave the current call?", - &["Close window and hang up", "Cancel"], - ) - })?; + if !quitting + && workspace_count == 1 + && this + .update(&mut cx, |this, cx| this.call_handler.is_in_room(cx)) + .log_err() + .unwrap_or_default() + { + let answer = window.update(&mut cx, |_, cx| { + cx.prompt( + PromptLevel::Warning, + "Do you want to leave the current call?", + &["Close window and hang up", "Cancel"], + ) + })?; - if answer.await.log_err() == Some(1) { - return anyhow::Ok(false); - } else { - active_call - .update(&mut cx, |call, cx| call.hang_up(cx))? - .await - .log_err(); - } + if answer.await.log_err() == Some(1) { + return anyhow::Ok(false); + } else { + this.update(&mut cx, |this, cx| this.call_handler.hang_up(cx.to_async()))?? + .await + .log_err(); } } @@ -2391,19 +2443,19 @@ impl Workspace { // } pub fn unfollow(&mut self, pane: &View, cx: &mut ViewContext) -> Option { - let state = self.follower_states.remove(pane)?; + let follower_states = &mut self.follower_states; + let state = follower_states.remove(pane)?; let leader_id = state.leader_id; for (_, item) in state.items_by_leader_view_id { item.set_leader_peer_id(None, cx); } - if self - .follower_states + if follower_states .values() .all(|state| state.leader_id != state.leader_id) { let project_id = self.project.read(cx).remote_id(); - let room_id = self.active_call()?.read(cx).room()?.read(cx).id(); + let room_id = self.call_handler.room_id(cx)?; self.app_state .client .send(proto::Unfollow { @@ -2762,8 +2814,9 @@ impl Workspace { } else { None }; + let room_id = self.call_handler.room_id(cx)?; self.app_state().workspace_store.update(cx, |store, cx| { - store.update_followers(project_id, update, cx) + store.update_followers(project_id, room_id, update, cx) }) } @@ -2771,31 +2824,12 @@ impl Workspace { self.follower_states.get(pane).map(|state| state.leader_id) } - fn leader_updated(&mut self, leader_id: PeerId, cx: &mut ViewContext) -> Option<()> { + pub fn leader_updated(&mut self, leader_id: PeerId, cx: &mut ViewContext) -> Option<()> { cx.notify(); - let call = self.active_call()?; - let room = call.read(cx).room()?.read(cx); - let participant = room.remote_participant_for_peer_id(leader_id)?; + let (leader_in_this_project, leader_in_this_app) = + self.call_handler.peer_state(leader_id, cx)?; let mut items_to_activate = Vec::new(); - - let leader_in_this_app; - let leader_in_this_project; - match participant.location { - call2::ParticipantLocation::SharedProject { project_id } => { - leader_in_this_app = true; - leader_in_this_project = Some(project_id) == self.project.read(cx).remote_id(); - } - call2::ParticipantLocation::UnsharedProject => { - leader_in_this_app = true; - leader_in_this_project = false; - } - call2::ParticipantLocation::External => { - leader_in_this_app = false; - leader_in_this_project = false; - } - }; - for (pane, state) in &self.follower_states { if state.leader_id != leader_id { continue; @@ -2825,8 +2859,8 @@ impl Workspace { if let Some(index) = pane.update(cx, |pane, _| pane.index_for_item(item.as_ref())) { pane.update(cx, |pane, cx| pane.activate_item(index, false, false, cx)); } else { - pane.update(cx, |pane, cx| { - pane.add_item(item.boxed_clone(), false, false, None, cx) + pane.update(cx, |pane, mut cx| { + pane.add_item(item.boxed_clone(), false, false, None, &mut cx) }); } @@ -2886,25 +2920,6 @@ impl Workspace { } } - fn active_call(&self) -> Option<&Model> { - self.active_call.as_ref().map(|(call, _)| call) - } - - fn on_active_call_event( - &mut self, - _: Model, - event: &call2::room::Event, - cx: &mut ViewContext, - ) { - match event { - call2::room::Event::ParticipantLocationChanged { participant_id } - | call2::room::Event::RemoteVideoTracksChanged { participant_id } => { - self.leader_updated(*participant_id, cx); - } - _ => {} - } - } - pub fn database_id(&self) -> WorkspaceId { self.database_id } @@ -3314,6 +3329,7 @@ impl Workspace { fs: project.read(cx).fs().clone(), build_window_options: |_, _, _| Default::default(), node_runtime: FakeNodeRuntime::new(), + call_factory: |_, _| Box::new(TestCallHandler), }); let workspace = Self::new(0, project, app_state, cx); workspace.active_pane.update(cx, |pane, cx| pane.focus(cx)); @@ -3672,7 +3688,6 @@ impl Render for Workspace { .child(self.center.render( &self.project, &self.follower_states, - self.active_call(), &self.active_pane, self.zoomed.as_ref(), &self.app_state, @@ -3842,14 +3857,10 @@ impl WorkspaceStore { pub fn update_followers( &self, project_id: Option, + room_id: u64, update: proto::update_followers::Variant, cx: &AppContext, ) -> Option<()> { - if !cx.has_global::>() { - return None; - } - - let room_id = ActiveCall::global(cx).read(cx).room()?.read(cx).id(); let follower_ids: Vec<_> = self .followers .iter() @@ -3885,9 +3896,17 @@ impl WorkspaceStore { project_id: envelope.payload.project_id, peer_id: envelope.original_sender_id()?, }; - let active_project = ActiveCall::global(cx).read(cx).location().cloned(); - let mut response = proto::FollowResponse::default(); + let active_project = this + .workspaces + .iter() + .next() + .and_then(|workspace| { + workspace + .read_with(cx, |this, cx| this.call_handler.active_project(cx)) + .log_err() + }) + .flatten(); for workspace in &this.workspaces { workspace .update(cx, |workspace, cx| { diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index ab8d5b7efe..0512eaca7a 100644 --- a/crates/zed/Cargo.toml +++ b/crates/zed/Cargo.toml @@ -3,7 +3,7 @@ authors = ["Nathan Sobo "] description = "The fast, collaborative code editor." edition = "2021" name = "zed" -version = "0.114.0" +version = "0.115.0" publish = false [lib] diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index 5f2a7c525e..20b93ae6bb 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -65,7 +65,8 @@ fn main() { log::info!("========== starting zed =========="); let mut app = gpui::App::new(Assets).unwrap(); - let installation_id = app.background().block(installation_id()).ok(); + let (installation_id, existing_installation_id_found) = + app.background().block(installation_id()).ok().unzip(); let session_id = Uuid::new_v4().to_string(); init_panic_hook(&app, installation_id.clone(), session_id.clone()); @@ -166,6 +167,14 @@ fn main() { .detach(); client.telemetry().start(installation_id, session_id, cx); + let telemetry_settings = *settings::get::(cx); + let event_operation = match existing_installation_id_found { + Some(false) => "first open", + _ => "open", + }; + client + .telemetry() + .report_app_event(telemetry_settings, event_operation); let app_state = Arc::new(AppState { languages, @@ -317,11 +326,11 @@ async fn authenticate(client: Arc, cx: &AsyncAppContext) -> Result<()> { Ok::<_, anyhow::Error>(()) } -async fn installation_id() -> Result { +async fn installation_id() -> Result<(String, bool)> { let legacy_key_name = "device_id"; if let Ok(Some(installation_id)) = KEY_VALUE_STORE.read_kvp(legacy_key_name) { - Ok(installation_id) + Ok((installation_id, true)) } else { let installation_id = Uuid::new_v4().to_string(); @@ -329,7 +338,7 @@ async fn installation_id() -> Result { .write_kvp(legacy_key_name.to_string(), installation_id.clone()) .await?; - Ok(installation_id) + Ok((installation_id, false)) } } diff --git a/crates/zed2/src/main.rs b/crates/zed2/src/main.rs index c6737628a9..bbf7c3ae9c 100644 --- a/crates/zed2/src/main.rs +++ b/crates/zed2/src/main.rs @@ -71,7 +71,11 @@ fn main() { log::info!("========== starting zed =========="); let app = App::production(Arc::new(Assets)); - let installation_id = app.background_executor().block(installation_id()).ok(); + let (installation_id, existing_installation_id_found) = app + .background_executor() + .block(installation_id()) + .ok() + .unzip(); let session_id = Uuid::new_v4().to_string(); init_panic_hook(&app, installation_id.clone(), session_id.clone()); @@ -173,6 +177,14 @@ fn main() { // .detach(); client.telemetry().start(installation_id, session_id, cx); + let telemetry_settings = *client::TelemetrySettings::get_global(cx); + let event_operation = match existing_installation_id_found { + Some(false) => "first open", + _ => "open", + }; + client + .telemetry() + .report_app_event(telemetry_settings, event_operation); let app_state = Arc::new(AppState { languages, @@ -180,6 +192,7 @@ fn main() { user_store, fs, build_window_options, + call_factory: call::Call::new, // background_actions: todo!("ask Mikayla"), workspace_store, node_runtime, @@ -333,11 +346,11 @@ fn main() { // Ok::<_, anyhow::Error>(()) // } -async fn installation_id() -> Result { +async fn installation_id() -> Result<(String, bool)> { let legacy_key_name = "device_id"; if let Ok(Some(installation_id)) = KEY_VALUE_STORE.read_kvp(legacy_key_name) { - Ok(installation_id) + Ok((installation_id, true)) } else { let installation_id = Uuid::new_v4().to_string(); @@ -345,7 +358,7 @@ async fn installation_id() -> Result { .write_kvp(legacy_key_name.to_string(), installation_id.clone()) .await?; - Ok(installation_id) + Ok((installation_id, false)) } }