Merge branch 'main' into v0.119.x

Joseph T. Lyons created

Change summary

Cargo.lock                                                |    7 
assets/icons/arrow_circle.svg                             |    7 
crates/ai/src/providers/open_ai/embedding.rs              |    3 
crates/assistant/src/assistant_panel.rs                   |   14 
crates/call/src/call.rs                                   |   16 
crates/channel/src/channel_store_tests.rs                 |    5 
crates/client/Cargo.toml                                  |    1 
crates/client/src/client.rs                               |   24 
crates/client/src/telemetry.rs                            |  184 
crates/client/src/user.rs                                 |    1 
crates/collab/src/db/queries/rooms.rs                     |    8 
crates/collab/src/tests/following_tests.rs                | 3667 ++++----
crates/collab_ui/Cargo.toml                               |    1 
crates/collab_ui/src/chat_panel.rs                        |    6 
crates/collab_ui/src/chat_panel/message_editor.rs         |    5 
crates/collab_ui/src/collab_panel.rs                      |   32 
crates/collab_ui/src/collab_titlebar_item.rs              |   71 
crates/collab_ui/src/collab_ui.rs                         |    4 
crates/collab_ui/src/notification_panel.rs                |   10 
crates/diagnostics/src/items.rs                           |   29 
crates/editor/src/display_map.rs                          |    2 
crates/editor/src/editor.rs                               |    3 
crates/editor/src/editor_tests.rs                         |   15 
crates/editor/src/element.rs                              |    2 
crates/editor/src/test/editor_lsp_test_context.rs         |   24 
crates/editor/src/test/editor_test_context.rs             |   14 
crates/feedback/Cargo.toml                                |    1 
crates/feedback/src/feedback_modal.rs                     |    4 
crates/file_finder/src/file_finder.rs                     |   43 
crates/gpui/src/action.rs                                 |   12 
crates/gpui/src/app.rs                                    |   12 
crates/gpui/src/app/entity_map.rs                         |    2 
crates/gpui/src/app/test_context.rs                       |   60 
crates/gpui/src/geometry.rs                               |    5 
crates/gpui/src/gpui.rs                                   |   20 
crates/gpui/src/input.rs                                  |    8 
crates/gpui/src/interactive.rs                            |    2 
crates/gpui/src/platform.rs                               |    2 
crates/gpui/src/platform/mac.rs                           |   18 
crates/gpui/src/platform/mac/dispatcher.rs                |    9 
crates/gpui/src/platform/mac/display.rs                   |    2 
crates/gpui/src/platform/mac/display_linker.rs            |    2 
crates/gpui/src/platform/mac/platform.rs                  |    5 
crates/gpui/src/platform/mac/text_system.rs               |    4 
crates/gpui/src/platform/mac/window.rs                    |    5 
crates/gpui/src/platform/test/platform.rs                 |    2 
crates/gpui/src/platform/test/window.rs                   |   11 
crates/gpui/src/style.rs                                  |    6 
crates/gpui/src/text_system.rs                            |    2 
crates/gpui/src/view.rs                                   |   12 
crates/gpui/src/window.rs                                 |   25 
crates/gpui/tests/action_macros.rs                        |    2 
crates/gpui_macros/src/register_action.rs                 |    4 
crates/journal/src/journal.rs                             |    4 
crates/language_tools/Cargo.toml                          |    1 
crates/language_tools/src/lsp_log_tests.rs                |    3 
crates/project_symbols/Cargo.toml                         |    1 
crates/project_symbols/src/project_symbols.rs             |    3 
crates/search/src/buffer_search.rs                        |    7 
crates/terminal/src/terminal.rs                           |   47 
crates/terminal_view/Cargo.toml                           |    1 
crates/terminal_view/src/terminal_element.rs              |   32 
crates/terminal_view/src/terminal_panel.rs                |   89 
crates/terminal_view/src/terminal_view.rs                 |   26 
crates/theme_importer/Cargo.toml                          |    1 
crates/theme_importer/src/main.rs                         |    1 
crates/theme_importer/src/zed1/converter.rs               |    2 
crates/theme_importer/src/zed1/theme.rs                   |    2 
crates/theme_selector/src/theme_selector.rs               |    2 
crates/vim/src/test/neovim_backed_binding_test_context.rs |   21 
crates/vim/src/test/neovim_backed_test_context.rs         |   21 
crates/vim/src/test/vim_test_context.rs                   |   18 
crates/welcome/src/base_keymap_picker.rs                  |   32 
crates/welcome/src/base_keymap_setting.rs                 |   14 
crates/welcome/src/welcome.rs                             |  356 
crates/workspace/src/dock.rs                              |   29 
crates/workspace/src/item.rs                              |    2 
crates/workspace/src/pane.rs                              |   87 
crates/workspace/src/pane_group.rs                        |   59 
crates/workspace/src/shared_screen.rs                     |   16 
crates/workspace/src/workspace.rs                         |   96 
crates/zed/Cargo.toml                                     |    5 
crates/zed/src/main.rs                                    |   18 
crates/zed/src/zed.rs                                     | 3944 ++++----
84 files changed, 4,936 insertions(+), 4,439 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -1380,6 +1380,7 @@ dependencies = [
  "schemars",
  "serde",
  "serde_derive",
+ "serde_json",
  "settings",
  "smol",
  "sum_tree",
@@ -1545,6 +1546,7 @@ dependencies = [
  "schemars",
  "serde",
  "serde_derive",
+ "serde_json",
  "settings",
  "smallvec",
  "theme",
@@ -2490,6 +2492,7 @@ dependencies = [
  "regex",
  "serde",
  "serde_derive",
+ "serde_json",
  "settings",
  "smallvec",
  "smol",
@@ -3784,6 +3787,7 @@ dependencies = [
  "lsp",
  "project",
  "serde",
+ "serde_json",
  "settings",
  "theme",
  "tree-sitter",
@@ -5511,6 +5515,7 @@ dependencies = [
  "picker",
  "postage",
  "project",
+ "serde_json",
  "settings",
  "smol",
  "text",
@@ -7764,6 +7769,7 @@ dependencies = [
  "search",
  "serde",
  "serde_derive",
+ "serde_json",
  "settings",
  "shellexpand",
  "smallvec",
@@ -7844,6 +7850,7 @@ dependencies = [
  "pathfinder_color",
  "rust-embed",
  "serde",
+ "serde_json",
  "simplelog",
  "strum",
  "theme",

assets/icons/arrow_circle.svg 🔗

@@ -1 +1,6 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-refresh-cw"><path d="M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8"/><path d="M21 3v5h-5"/><path d="M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16"/><path d="M8 16H3v5"/></svg>
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M3 8C3 6.67392 3.52678 5.40215 4.46446 4.46447C5.40214 3.52679 6.67391 3.00001 7.99999 3.00001C9.39779 3.00527 10.7394 3.55069 11.7444 4.52223L13 5.77778" stroke="black" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M13 3.00001V5.77778H10.2222" stroke="black" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M13 8C13 9.32608 12.4732 10.5978 11.5355 11.5355C10.5978 12.4732 9.32607 13 7.99999 13C6.60219 12.9947 5.26054 12.4493 4.25555 11.4778L3 10.2222" stroke="black" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M5.77777 10.2222H3V13" stroke="black" stroke-linecap="round" stroke-linejoin="round"/>
+</svg>

crates/ai/src/providers/open_ai/embedding.rs 🔗

@@ -1,8 +1,8 @@
 use anyhow::{anyhow, Result};
 use async_trait::async_trait;
 use futures::AsyncReadExt;
+use gpui::AppContext;
 use gpui::BackgroundExecutor;
-use gpui::{serde_json, AppContext};
 use isahc::http::StatusCode;
 use isahc::prelude::Configurable;
 use isahc::{AsyncBody, Response};
@@ -11,6 +11,7 @@ use parking_lot::{Mutex, RwLock};
 use parse_duration::parse;
 use postage::watch;
 use serde::{Deserialize, Serialize};
+use serde_json;
 use std::env;
 use std::ops::Add;
 use std::sync::Arc;

crates/assistant/src/assistant_panel.rs 🔗

@@ -2298,11 +2298,10 @@ impl ConversationEditor {
                         move |_cx| {
                             let message_id = message.id;
                             let sender = ButtonLike::new("role")
+                                .style(ButtonStyle::Filled)
                                 .child(match message.role {
                                     Role::User => Label::new("You").color(Color::Default),
-                                    Role::Assistant => {
-                                        Label::new("Assistant").color(Color::Modified)
-                                    }
+                                    Role::Assistant => Label::new("Assistant").color(Color::Info),
                                     Role::System => Label::new("System").color(Color::Warning),
                                 })
                                 .tooltip(|cx| {
@@ -2325,11 +2324,12 @@ impl ConversationEditor {
                                     }
                                 });
 
-                            h_stack()
+                            div()
+                                .h_flex()
                                 .id(("message_header", message_id.0))
                                 .h_11()
+                                .relative()
                                 .gap_1()
-                                .p_1()
                                 .child(sender)
                                 // TODO: Only show this if the message if the message has been sent
                                 .child(
@@ -2538,7 +2538,7 @@ impl Render for ConversationEditor {
             .child(
                 div()
                     .size_full()
-                    .pl_2()
+                    .pl_4()
                     .bg(cx.theme().colors().editor_background)
                     .child(self.editor.clone()),
             )
@@ -3538,5 +3538,5 @@ fn report_assistant_event(
         .default_open_ai_model
         .clone();
 
-    telemetry.report_assistant_event(conversation_id, assistant_kind, model.full_name(), cx)
+    telemetry.report_assistant_event(conversation_id, assistant_kind, model.full_name())
 }

crates/call/src/call.rs 🔗

@@ -310,14 +310,14 @@ impl ActiveCall {
         })
     }
 
-    pub fn decline_incoming(&mut self, cx: &mut ModelContext<Self>) -> Result<()> {
+    pub fn decline_incoming(&mut self, _: &mut ModelContext<Self>) -> Result<()> {
         let call = self
             .incoming_call
             .0
             .borrow_mut()
             .take()
             .ok_or_else(|| anyhow!("no incoming call"))?;
-        report_call_event_for_room("decline incoming", call.room_id, None, &self.client, cx);
+        report_call_event_for_room("decline incoming", call.room_id, None, &self.client);
         self.client.send(proto::DeclineCall {
             room_id: call.room_id,
         })?;
@@ -467,7 +467,7 @@ impl ActiveCall {
     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);
+            report_call_event_for_room(operation, room.id(), room.channel_id(), &self.client);
         }
     }
 }
@@ -477,11 +477,10 @@ pub fn report_call_event_for_room(
     room_id: u64,
     channel_id: Option<u64>,
     client: &Arc<Client>,
-    cx: &mut AppContext,
 ) {
     let telemetry = client.telemetry();
 
-    telemetry.report_call_event(operation, Some(room_id), channel_id, cx)
+    telemetry.report_call_event(operation, Some(room_id), channel_id)
 }
 
 pub fn report_call_event_for_channel(
@@ -494,12 +493,7 @@ pub fn report_call_event_for_channel(
 
     let telemetry = client.telemetry();
 
-    telemetry.report_call_event(
-        operation,
-        room.map(|r| r.read(cx).id()),
-        Some(channel_id),
-        cx,
-    )
+    telemetry.report_call_event(operation, room.map(|r| r.read(cx).id()), Some(channel_id))
 }
 
 #[cfg(test)]

crates/channel/src/channel_store_tests.rs 🔗

@@ -343,12 +343,13 @@ async fn test_channel_messages(cx: &mut TestAppContext) {
 }
 
 fn init_test(cx: &mut AppContext) -> Model<ChannelStore> {
+    let settings_store = SettingsStore::test(cx);
+    cx.set_global(settings_store);
+
     let http = FakeHttpClient::with_404_response();
     let client = Client::new(http.clone(), cx);
     let user_store = cx.new_model(|cx| UserStore::new(client.clone(), cx));
 
-    let settings_store = SettingsStore::test(cx);
-    cx.set_global(settings_store);
     client::init(&client, cx);
     crate::init(&client, user_store, cx);
 

crates/client/Cargo.toml 🔗

@@ -36,6 +36,7 @@ rand.workspace = true
 schemars.workspace = true
 serde.workspace = true
 serde_derive.workspace = true
+serde_json.workspace = true
 smol.workspace = true
 sysinfo.workspace = true
 tempfile = "3"

crates/client/src/client.rs 🔗

@@ -15,8 +15,8 @@ use futures::{
     TryFutureExt as _, TryStreamExt,
 };
 use gpui::{
-    actions, serde_json, AnyModel, AnyWeakModel, AppContext, AsyncAppContext, Model,
-    SemanticVersion, Task, WeakModel,
+    actions, AnyModel, AnyWeakModel, AppContext, AsyncAppContext, Model, SemanticVersion, Task,
+    WeakModel,
 };
 use lazy_static::lazy_static;
 use parking_lot::RwLock;
@@ -25,6 +25,7 @@ use rand::prelude::*;
 use rpc::proto::{AnyTypedEnvelope, EntityMessage, EnvelopedMessage, PeerId, RequestMessage};
 use schemars::JsonSchema;
 use serde::{Deserialize, Serialize};
+use serde_json;
 use settings::Settings;
 use std::{
     any::TypeId,
@@ -45,7 +46,7 @@ use util::http::HttpClient;
 use util::{ResultExt, TryFutureExt};
 
 pub use rpc::*;
-pub use telemetry::ClickhouseEvent;
+pub use telemetry::Event;
 pub use user::*;
 
 lazy_static! {
@@ -501,8 +502,7 @@ impl Client {
                 }));
             }
             Status::SignedOut | Status::UpgradeRequired => {
-                cx.update(|cx| self.telemetry.set_authenticated_user_info(None, false, cx))
-                    .log_err();
+                self.telemetry.set_authenticated_user_info(None, false);
                 state._reconnect_task.take();
             }
             _ => {}
@@ -1405,11 +1405,13 @@ mod tests {
 
     use gpui::{BackgroundExecutor, Context, TestAppContext};
     use parking_lot::Mutex;
+    use settings::SettingsStore;
     use std::future;
     use util::http::FakeHttpClient;
 
     #[gpui::test(iterations = 10)]
     async fn test_reconnection(cx: &mut TestAppContext) {
+        init_test(cx);
         let user_id = 5;
         let client = cx.update(|cx| Client::new(FakeHttpClient::with_404_response(), cx));
         let server = FakeServer::for_client(user_id, &client, cx).await;
@@ -1444,6 +1446,7 @@ mod tests {
 
     #[gpui::test(iterations = 10)]
     async fn test_connection_timeout(executor: BackgroundExecutor, cx: &mut TestAppContext) {
+        init_test(cx);
         let user_id = 5;
         let client = cx.update(|cx| Client::new(FakeHttpClient::with_404_response(), cx));
         let mut status = client.status();
@@ -1515,6 +1518,7 @@ mod tests {
         cx: &mut TestAppContext,
         executor: BackgroundExecutor,
     ) {
+        init_test(cx);
         let auth_count = Arc::new(Mutex::new(0));
         let dropped_auth_count = Arc::new(Mutex::new(0));
         let client = cx.update(|cx| Client::new(FakeHttpClient::with_404_response(), cx));
@@ -1563,6 +1567,7 @@ mod tests {
 
     #[gpui::test]
     async fn test_subscribing_to_entity(cx: &mut TestAppContext) {
+        init_test(cx);
         let user_id = 5;
         let client = cx.update(|cx| Client::new(FakeHttpClient::with_404_response(), cx));
         let server = FakeServer::for_client(user_id, &client, cx).await;
@@ -1616,6 +1621,7 @@ mod tests {
 
     #[gpui::test]
     async fn test_subscribing_after_dropping_subscription(cx: &mut TestAppContext) {
+        init_test(cx);
         let user_id = 5;
         let client = cx.update(|cx| Client::new(FakeHttpClient::with_404_response(), cx));
         let server = FakeServer::for_client(user_id, &client, cx).await;
@@ -1644,6 +1650,7 @@ mod tests {
 
     #[gpui::test]
     async fn test_dropping_subscription_in_handler(cx: &mut TestAppContext) {
+        init_test(cx);
         let user_id = 5;
         let client = cx.update(|cx| Client::new(FakeHttpClient::with_404_response(), cx));
         let server = FakeServer::for_client(user_id, &client, cx).await;
@@ -1672,4 +1679,11 @@ mod tests {
         id: usize,
         subscription: Option<Subscription>,
     }
+
+    fn init_test(cx: &mut TestAppContext) {
+        cx.update(|cx| {
+            let settings_store = SettingsStore::test(cx);
+            cx.set_global(settings_store);
+        });
+    }
 }

crates/client/src/telemetry.rs 🔗

@@ -1,11 +1,12 @@
 use crate::{TelemetrySettings, ZED_SECRET_CLIENT_TOKEN, ZED_SERVER_URL};
 use chrono::{DateTime, Utc};
 use futures::Future;
-use gpui::{serde_json, AppContext, AppMetadata, BackgroundExecutor, Task};
+use gpui::{AppContext, AppMetadata, BackgroundExecutor, Task};
 use lazy_static::lazy_static;
 use parking_lot::Mutex;
 use serde::Serialize;
-use settings::Settings;
+use serde_json;
+use settings::{Settings, SettingsStore};
 use std::{env, io::Write, mem, path::PathBuf, sync::Arc, time::Duration};
 use sysinfo::{
     CpuRefreshKind, Pid, PidExt, ProcessExt, ProcessRefreshKind, RefreshKind, System, SystemExt,
@@ -17,32 +18,32 @@ use util::{channel::ReleaseChannel, TryFutureExt};
 pub struct Telemetry {
     http_client: Arc<dyn HttpClient>,
     executor: BackgroundExecutor,
-    state: Mutex<TelemetryState>,
+    state: Arc<Mutex<TelemetryState>>,
 }
 
 struct TelemetryState {
+    settings: TelemetrySettings,
     metrics_id: Option<Arc<str>>,      // Per logged-in user
     installation_id: Option<Arc<str>>, // Per app installation (different for dev, nightly, preview, and stable)
     session_id: Option<Arc<str>>,      // Per app launch
     release_channel: Option<&'static str>,
     app_metadata: AppMetadata,
     architecture: &'static str,
-    clickhouse_events_queue: Vec<ClickhouseEventWrapper>,
-    flush_clickhouse_events_task: Option<Task<()>>,
+    events_queue: Vec<EventWrapper>,
+    flush_events_task: Option<Task<()>>,
     log_file: Option<NamedTempFile>,
     is_staff: Option<bool>,
     first_event_datetime: Option<DateTime<Utc>>,
 }
 
-const CLICKHOUSE_EVENTS_URL_PATH: &'static str = "/api/events";
+const EVENTS_URL_PATH: &'static str = "/api/events";
 
 lazy_static! {
-    static ref CLICKHOUSE_EVENTS_URL: String =
-        format!("{}{}", *ZED_SERVER_URL, CLICKHOUSE_EVENTS_URL_PATH);
+    static ref EVENTS_URL: String = format!("{}{}", *ZED_SERVER_URL, EVENTS_URL_PATH);
 }
 
 #[derive(Serialize, Debug)]
-struct ClickhouseEventRequestBody {
+struct EventRequestBody {
     token: &'static str,
     installation_id: Option<Arc<str>>,
     session_id: Option<Arc<str>>,
@@ -52,14 +53,14 @@ struct ClickhouseEventRequestBody {
     os_version: Option<String>,
     architecture: &'static str,
     release_channel: Option<&'static str>,
-    events: Vec<ClickhouseEventWrapper>,
+    events: Vec<EventWrapper>,
 }
 
 #[derive(Serialize, Debug)]
-struct ClickhouseEventWrapper {
+struct EventWrapper {
     signed_in: bool,
     #[serde(flatten)]
-    event: ClickhouseEvent,
+    event: Event,
 }
 
 #[derive(Serialize, Debug)]
@@ -71,7 +72,7 @@ pub enum AssistantKind {
 
 #[derive(Serialize, Debug)]
 #[serde(tag = "type")]
-pub enum ClickhouseEvent {
+pub enum Event {
     Editor {
         operation: &'static str,
         file_extension: Option<String>,
@@ -139,45 +140,61 @@ impl Telemetry {
             None
         };
 
+        TelemetrySettings::register(cx);
+
+        let state = Arc::new(Mutex::new(TelemetryState {
+            settings: TelemetrySettings::get_global(cx).clone(),
+            app_metadata: cx.app_metadata(),
+            architecture: env::consts::ARCH,
+            release_channel,
+            installation_id: None,
+            metrics_id: None,
+            session_id: None,
+            events_queue: Default::default(),
+            flush_events_task: Default::default(),
+            log_file: None,
+            is_staff: None,
+            first_event_datetime: None,
+        }));
+
+        cx.observe_global::<SettingsStore>({
+            let state = state.clone();
+
+            move |cx| {
+                let mut state = state.lock();
+                state.settings = TelemetrySettings::get_global(cx).clone();
+            }
+        })
+        .detach();
+
         // TODO: Replace all hardware stuff with nested SystemSpecs json
         let this = Arc::new(Self {
             http_client: client,
             executor: cx.background_executor().clone(),
-            state: Mutex::new(TelemetryState {
-                app_metadata: cx.app_metadata(),
-                architecture: env::consts::ARCH,
-                release_channel,
-                installation_id: None,
-                metrics_id: None,
-                session_id: None,
-                clickhouse_events_queue: Default::default(),
-                flush_clickhouse_events_task: Default::default(),
-                log_file: None,
-                is_staff: None,
-                first_event_datetime: None,
-            }),
+            state,
         });
 
         // 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)
+            move |_| this.shutdown_telemetry()
         }));
 
         this
     }
 
     #[cfg(any(test, feature = "test-support"))]
-    fn shutdown_telemetry(self: &Arc<Self>, _: &mut AppContext) -> impl Future<Output = ()> {
+    fn shutdown_telemetry(self: &Arc<Self>) -> impl Future<Output = ()> {
         Task::ready(())
     }
 
     // Skip calling this function in tests.
     // TestAppContext ends up calling this function on shutdown and it panics when trying to find the TelemetrySettings
     #[cfg(not(any(test, feature = "test-support")))]
-    fn shutdown_telemetry(self: &Arc<Self>, cx: &mut AppContext) -> impl Future<Output = ()> {
-        self.report_app_event("close", true, cx);
+    fn shutdown_telemetry(self: &Arc<Self>) -> impl Future<Output = ()> {
+        self.report_app_event("close");
+        self.flush_events();
         Task::ready(())
     }
 
@@ -197,7 +214,7 @@ impl Telemetry {
         drop(state);
 
         let this = self.clone();
-        cx.spawn(|cx| async move {
+        cx.spawn(|_| async move {
             // Avoiding calling `System::new_all()`, as there have been crashes related to it
             let refresh_kind = RefreshKind::new()
                 .with_memory() // For memory usage
@@ -226,11 +243,8 @@ impl Telemetry {
                     return;
                 };
 
-                cx.update(|cx| {
-                    this.report_memory_event(process.memory(), process.virtual_memory(), cx);
-                    this.report_cpu_event(process.cpu_usage(), system.cpus().len() as u32, cx);
-                })
-                .ok();
+                this.report_memory_event(process.memory(), process.virtual_memory());
+                this.report_cpu_event(process.cpu_usage(), system.cpus().len() as u32);
             }
         })
         .detach();
@@ -240,13 +254,13 @@ impl Telemetry {
         self: &Arc<Self>,
         metrics_id: Option<String>,
         is_staff: bool,
-        cx: &AppContext,
     ) {
-        if !TelemetrySettings::get_global(cx).metrics {
+        let mut state = self.state.lock();
+
+        if !state.settings.metrics {
             return;
         }
 
-        let mut state = self.state.lock();
         let metrics_id: Option<Arc<str>> = metrics_id.map(|id| id.into());
         state.metrics_id = metrics_id.clone();
         state.is_staff = Some(is_staff);
@@ -260,9 +274,8 @@ impl Telemetry {
         operation: &'static str,
         copilot_enabled: bool,
         copilot_enabled_for_language: bool,
-        cx: &AppContext,
     ) {
-        let event = ClickhouseEvent::Editor {
+        let event = Event::Editor {
             file_extension,
             vim_mode,
             operation,
@@ -271,7 +284,7 @@ impl Telemetry {
             milliseconds_since_first_event: self.milliseconds_since_first_event(),
         };
 
-        self.report_clickhouse_event(event, false, cx)
+        self.report_event(event)
     }
 
     pub fn report_copilot_event(
@@ -279,16 +292,15 @@ impl Telemetry {
         suggestion_id: Option<String>,
         suggestion_accepted: bool,
         file_extension: Option<String>,
-        cx: &AppContext,
     ) {
-        let event = ClickhouseEvent::Copilot {
+        let event = Event::Copilot {
             suggestion_id,
             suggestion_accepted,
             file_extension,
             milliseconds_since_first_event: self.milliseconds_since_first_event(),
         };
 
-        self.report_clickhouse_event(event, false, cx)
+        self.report_event(event)
     }
 
     pub fn report_assistant_event(
@@ -296,16 +308,15 @@ impl Telemetry {
         conversation_id: Option<String>,
         kind: AssistantKind,
         model: &'static str,
-        cx: &AppContext,
     ) {
-        let event = ClickhouseEvent::Assistant {
+        let event = Event::Assistant {
             conversation_id,
             kind,
             model,
             milliseconds_since_first_event: self.milliseconds_since_first_event(),
         };
 
-        self.report_clickhouse_event(event, false, cx)
+        self.report_event(event)
     }
 
     pub fn report_call_event(
@@ -313,75 +324,58 @@ impl Telemetry {
         operation: &'static str,
         room_id: Option<u64>,
         channel_id: Option<u64>,
-        cx: &AppContext,
     ) {
-        let event = ClickhouseEvent::Call {
+        let event = Event::Call {
             operation,
             room_id,
             channel_id,
             milliseconds_since_first_event: self.milliseconds_since_first_event(),
         };
 
-        self.report_clickhouse_event(event, false, cx)
+        self.report_event(event)
     }
 
-    pub fn report_cpu_event(
-        self: &Arc<Self>,
-        usage_as_percentage: f32,
-        core_count: u32,
-        cx: &AppContext,
-    ) {
-        let event = ClickhouseEvent::Cpu {
+    pub fn report_cpu_event(self: &Arc<Self>, usage_as_percentage: f32, core_count: u32) {
+        let event = Event::Cpu {
             usage_as_percentage,
             core_count,
             milliseconds_since_first_event: self.milliseconds_since_first_event(),
         };
 
-        self.report_clickhouse_event(event, false, cx)
+        self.report_event(event)
     }
 
     pub fn report_memory_event(
         self: &Arc<Self>,
         memory_in_bytes: u64,
         virtual_memory_in_bytes: u64,
-        cx: &AppContext,
     ) {
-        let event = ClickhouseEvent::Memory {
+        let event = Event::Memory {
             memory_in_bytes,
             virtual_memory_in_bytes,
             milliseconds_since_first_event: self.milliseconds_since_first_event(),
         };
 
-        self.report_clickhouse_event(event, false, cx)
+        self.report_event(event)
     }
 
-    pub fn report_app_event(
-        self: &Arc<Self>,
-        operation: &'static str,
-        immediate_flush: bool,
-        cx: &AppContext,
-    ) {
-        let event = ClickhouseEvent::App {
+    pub fn report_app_event(self: &Arc<Self>, operation: &'static str) {
+        let event = Event::App {
             operation,
             milliseconds_since_first_event: self.milliseconds_since_first_event(),
         };
 
-        self.report_clickhouse_event(event, immediate_flush, cx)
+        self.report_event(event)
     }
 
-    pub fn report_setting_event(
-        self: &Arc<Self>,
-        setting: &'static str,
-        value: String,
-        cx: &AppContext,
-    ) {
-        let event = ClickhouseEvent::Setting {
+    pub fn report_setting_event(self: &Arc<Self>, setting: &'static str, value: String) {
+        let event = Event::Setting {
             setting,
             value,
             milliseconds_since_first_event: self.milliseconds_since_first_event(),
         };
 
-        self.report_clickhouse_event(event, false, cx)
+        self.report_event(event)
     }
 
     fn milliseconds_since_first_event(&self) -> i64 {
@@ -398,32 +392,26 @@ impl Telemetry {
         }
     }
 
-    fn report_clickhouse_event(
-        self: &Arc<Self>,
-        event: ClickhouseEvent,
-        immediate_flush: bool,
-        cx: &AppContext,
-    ) {
-        if !TelemetrySettings::get_global(cx).metrics {
+    fn report_event(self: &Arc<Self>, event: Event) {
+        let mut state = self.state.lock();
+
+        if !state.settings.metrics {
             return;
         }
 
-        let mut state = self.state.lock();
         let signed_in = state.metrics_id.is_some();
-        state
-            .clickhouse_events_queue
-            .push(ClickhouseEventWrapper { signed_in, event });
+        state.events_queue.push(EventWrapper { signed_in, event });
 
         if state.installation_id.is_some() {
-            if immediate_flush || state.clickhouse_events_queue.len() >= MAX_QUEUE_LEN {
+            if state.events_queue.len() >= MAX_QUEUE_LEN {
                 drop(state);
-                self.flush_clickhouse_events();
+                self.flush_events();
             } else {
                 let this = self.clone();
                 let executor = self.executor.clone();
-                state.flush_clickhouse_events_task = Some(self.executor.spawn(async move {
+                state.flush_events_task = Some(self.executor.spawn(async move {
                     executor.timer(DEBOUNCE_INTERVAL).await;
-                    this.flush_clickhouse_events();
+                    this.flush_events();
                 }));
             }
         }
@@ -441,11 +429,11 @@ impl Telemetry {
         self.state.lock().is_staff
     }
 
-    fn flush_clickhouse_events(self: &Arc<Self>) {
+    pub fn flush_events(self: &Arc<Self>) {
         let mut state = self.state.lock();
         state.first_event_datetime = None;
-        let mut events = mem::take(&mut state.clickhouse_events_queue);
-        state.flush_clickhouse_events_task.take();
+        let mut events = mem::take(&mut state.events_queue);
+        state.flush_events_task.take();
         drop(state);
 
         let this = self.clone();
@@ -466,7 +454,7 @@ impl Telemetry {
 
                     {
                         let state = this.state.lock();
-                        let request_body = ClickhouseEventRequestBody {
+                        let request_body = EventRequestBody {
                             token: ZED_SECRET_CLIENT_TOKEN,
                             installation_id: state.installation_id.clone(),
                             session_id: state.session_id.clone(),
@@ -490,7 +478,7 @@ impl Telemetry {
                     }
 
                     this.http_client
-                        .post_json(CLICKHOUSE_EVENTS_URL.as_str(), json_bytes.into())
+                        .post_json(EVENTS_URL.as_str(), json_bytes.into())
                         .await?;
                     anyhow::Ok(())
                 }

crates/client/src/user.rs 🔗

@@ -164,7 +164,6 @@ impl UserStore {
                                         client.telemetry.set_authenticated_user_info(
                                             Some(info.metrics_id.clone()),
                                             info.staff,
-                                            cx,
                                         )
                                     }
                                 })?;

crates/collab/src/db/queries/rooms.rs 🔗

@@ -888,6 +888,14 @@ impl Database {
                     .exec(&*tx)
                     .await?;
 
+                follower::Entity::delete_many()
+                    .filter(
+                        Condition::all()
+                            .add(follower::Column::FollowerConnectionId.eq(connection.id as i32)),
+                    )
+                    .exec(&*tx)
+                    .await?;
+
                 // Unshare projects.
                 project::Entity::delete_many()
                     .filter(

crates/collab/src/tests/following_tests.rs 🔗

@@ -1,1890 +1,1777 @@
-//todo!(workspace)
-
-// use crate::{rpc::RECONNECT_TIMEOUT, tests::TestServer};
-// use call::ActiveCall;
-// use collab_ui::notifications::project_shared_notification::ProjectSharedNotification;
-// use editor::{Editor, ExcerptRange, MultiBuffer};
-// use gpui::{point, BackgroundExecutor, TestAppContext, View, VisualTestContext, WindowContext};
-// use live_kit_client::MacOSDisplay;
-// use project::project_settings::ProjectSettings;
-// use rpc::proto::PeerId;
-// use serde_json::json;
-// use settings::SettingsStore;
-// use std::borrow::Cow;
-// use workspace::{
-//     dock::{test::TestPanel, DockPosition},
-//     item::{test::TestItem, ItemHandle as _},
-//     shared_screen::SharedScreen,
-//     SplitDirection, Workspace,
-// };
-
-// #[gpui::test(iterations = 10)]
-// async fn test_basic_following(
-//     executor: BackgroundExecutor,
-//     cx_a: &mut TestAppContext,
-//     cx_b: &mut TestAppContext,
-//     cx_c: &mut TestAppContext,
-//     cx_d: &mut TestAppContext,
-// ) {
-//     let mut server = TestServer::start(executor.clone()).await;
-//     let client_a = server.create_client(cx_a, "user_a").await;
-//     let client_b = server.create_client(cx_b, "user_b").await;
-//     let client_c = server.create_client(cx_c, "user_c").await;
-//     let client_d = server.create_client(cx_d, "user_d").await;
-//     server
-//         .create_room(&mut [
-//             (&client_a, cx_a),
-//             (&client_b, cx_b),
-//             (&client_c, cx_c),
-//             (&client_d, cx_d),
-//         ])
-//         .await;
-//     let active_call_a = cx_a.read(ActiveCall::global);
-//     let active_call_b = cx_b.read(ActiveCall::global);
-
-//     cx_a.update(editor::init);
-//     cx_b.update(editor::init);
-
-//     client_a
-//         .fs()
-//         .insert_tree(
-//             "/a",
-//             json!({
-//                 "1.txt": "one\none\none",
-//                 "2.txt": "two\ntwo\ntwo",
-//                 "3.txt": "three\nthree\nthree",
-//             }),
-//         )
-//         .await;
-//     let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
-//     active_call_a
-//         .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
-//         .await
-//         .unwrap();
-
-//     let project_id = active_call_a
-//         .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
-//         .await
-//         .unwrap();
-//     let project_b = client_b.build_remote_project(project_id, cx_b).await;
-//     active_call_b
-//         .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
-//         .await
-//         .unwrap();
-
-//     let window_a = client_a.build_workspace(&project_a, cx_a);
-//     let workspace_a = window_a.root(cx_a).unwrap();
-//     let window_b = client_b.build_workspace(&project_b, cx_b);
-//     let workspace_b = window_b.root(cx_b).unwrap();
-
-//     todo!("could be wrong")
-//     let mut cx_a = VisualTestContext::from_window(*window_a, cx_a);
-//     let cx_a = &mut cx_a;
-//     let mut cx_b = VisualTestContext::from_window(*window_b, cx_b);
-//     let cx_b = &mut cx_b;
-//     let mut cx_c = VisualTestContext::from_window(*window_c, cx_c);
-//     let cx_c = &mut cx_c;
-//     let mut cx_d = VisualTestContext::from_window(*window_d, cx_d);
-//     let cx_d = &mut cx_d;
-
-//     // Client A opens some editors.
-//     let pane_a = workspace_a.update(cx_a, |workspace, _| workspace.active_pane().clone());
-//     let editor_a1 = workspace_a
-//         .update(cx_a, |workspace, cx| {
-//             workspace.open_path((worktree_id, "1.txt"), None, true, cx)
-//         })
-//         .await
-//         .unwrap()
-//         .downcast::<Editor>()
-//         .unwrap();
-//     let editor_a2 = workspace_a
-//         .update(cx_a, |workspace, cx| {
-//             workspace.open_path((worktree_id, "2.txt"), None, true, cx)
-//         })
-//         .await
-//         .unwrap()
-//         .downcast::<Editor>()
-//         .unwrap();
-
-//     // Client B opens an editor.
-//     let editor_b1 = workspace_b
-//         .update(cx_b, |workspace, cx| {
-//             workspace.open_path((worktree_id, "1.txt"), None, true, cx)
-//         })
-//         .await
-//         .unwrap()
-//         .downcast::<Editor>()
-//         .unwrap();
-
-//     let peer_id_a = client_a.peer_id().unwrap();
-//     let peer_id_b = client_b.peer_id().unwrap();
-//     let peer_id_c = client_c.peer_id().unwrap();
-//     let peer_id_d = client_d.peer_id().unwrap();
-
-//     // Client A updates their selections in those editors
-//     editor_a1.update(cx_a, |editor, cx| {
-//         editor.handle_input("a", cx);
-//         editor.handle_input("b", cx);
-//         editor.handle_input("c", cx);
-//         editor.select_left(&Default::default(), cx);
-//         assert_eq!(editor.selections.ranges(cx), vec![3..2]);
-//     });
-//     editor_a2.update(cx_a, |editor, cx| {
-//         editor.handle_input("d", cx);
-//         editor.handle_input("e", cx);
-//         editor.select_left(&Default::default(), cx);
-//         assert_eq!(editor.selections.ranges(cx), vec![2..1]);
-//     });
-
-//     // When client B starts following client A, all visible view states are replicated to client B.
-//     workspace_b
-//         .update(cx_b, |workspace, cx| {
-//             workspace.follow(peer_id_a, cx).unwrap()
-//         })
-//         .await
-//         .unwrap();
-
-//     cx_c.executor().run_until_parked();
-//     let editor_b2 = workspace_b.update(cx_b, |workspace, cx| {
-//         workspace
-//             .active_item(cx)
-//             .unwrap()
-//             .downcast::<Editor>()
-//             .unwrap()
-//     });
-//     assert_eq!(
-//         cx_b.read(|cx| editor_b2.project_path(cx)),
-//         Some((worktree_id, "2.txt").into())
-//     );
-//     assert_eq!(
-//         editor_b2.update(cx_b, |editor, cx| editor.selections.ranges(cx)),
-//         vec![2..1]
-//     );
-//     assert_eq!(
-//         editor_b1.update(cx_b, |editor, cx| editor.selections.ranges(cx)),
-//         vec![3..2]
-//     );
-
-//     cx_c.executor().run_until_parked();
-//     let active_call_c = cx_c.read(ActiveCall::global);
-//     let project_c = client_c.build_remote_project(project_id, cx_c).await;
-//     let window_c = client_c.build_workspace(&project_c, cx_c);
-//     let workspace_c = window_c.root(cx_c).unwrap();
-//     active_call_c
-//         .update(cx_c, |call, cx| call.set_location(Some(&project_c), cx))
-//         .await
-//         .unwrap();
-//     drop(project_c);
-
-//     // Client C also follows client A.
-//     workspace_c
-//         .update(cx_c, |workspace, cx| {
-//             workspace.follow(peer_id_a, cx).unwrap()
-//         })
-//         .await
-//         .unwrap();
-
-//     cx_d.executor().run_until_parked();
-//     let active_call_d = cx_d.read(ActiveCall::global);
-//     let project_d = client_d.build_remote_project(project_id, cx_d).await;
-//     let workspace_d = client_d
-//         .build_workspace(&project_d, cx_d)
-//         .root(cx_d)
-//         .unwrap();
-//     active_call_d
-//         .update(cx_d, |call, cx| call.set_location(Some(&project_d), cx))
-//         .await
-//         .unwrap();
-//     drop(project_d);
-
-//     // All clients see that clients B and C are following client A.
-//     cx_c.executor().run_until_parked();
-//     for (name, cx) in [("A", &cx_a), ("B", &cx_b), ("C", &cx_c), ("D", &cx_d)] {
-//         assert_eq!(
-//             followers_by_leader(project_id, cx),
-//             &[(peer_id_a, vec![peer_id_b, peer_id_c])],
-//             "followers seen by {name}"
-//         );
-//     }
-
-//     // Client C unfollows client A.
-//     workspace_c.update(cx_c, |workspace, cx| {
-//         workspace.unfollow(&workspace.active_pane().clone(), cx);
-//     });
-
-//     // All clients see that clients B is following client A.
-//     cx_c.executor().run_until_parked();
-//     for (name, cx) in [("A", &cx_a), ("B", &cx_b), ("C", &cx_c), ("D", &cx_d)] {
-//         assert_eq!(
-//             followers_by_leader(project_id, cx),
-//             &[(peer_id_a, vec![peer_id_b])],
-//             "followers seen by {name}"
-//         );
-//     }
-
-//     // Client C re-follows client A.
-//     workspace_c
-//         .update(cx_c, |workspace, cx| {
-//             workspace.follow(peer_id_a, cx).unwrap()
-//         })
-//         .await
-//         .unwrap();
-
-//     // All clients see that clients B and C are following client A.
-//     cx_c.executor().run_until_parked();
-//     for (name, cx) in [("A", &cx_a), ("B", &cx_b), ("C", &cx_c), ("D", &cx_d)] {
-//         assert_eq!(
-//             followers_by_leader(project_id, cx),
-//             &[(peer_id_a, vec![peer_id_b, peer_id_c])],
-//             "followers seen by {name}"
-//         );
-//     }
-
-//     // Client D follows client B, then switches to following client C.
-//     workspace_d
-//         .update(cx_d, |workspace, cx| {
-//             workspace.follow(peer_id_b, cx).unwrap()
-//         })
-//         .await
-//         .unwrap();
-//     workspace_d
-//         .update(cx_d, |workspace, cx| {
-//             workspace.follow(peer_id_c, cx).unwrap()
-//         })
-//         .await
-//         .unwrap();
-
-//     // All clients see that D is following C
-//     cx_d.executor().run_until_parked();
-//     for (name, cx) in [("A", &cx_a), ("B", &cx_b), ("C", &cx_c), ("D", &cx_d)] {
-//         assert_eq!(
-//             followers_by_leader(project_id, cx),
-//             &[
-//                 (peer_id_a, vec![peer_id_b, peer_id_c]),
-//                 (peer_id_c, vec![peer_id_d])
-//             ],
-//             "followers seen by {name}"
-//         );
-//     }
-
-//     // Client C closes the project.
-//     window_c.remove(cx_c);
-//     cx_c.drop_last(workspace_c);
-
-//     // Clients A and B see that client B is following A, and client C is not present in the followers.
-//     cx_c.executor().run_until_parked();
-//     for (name, cx) in [("A", &cx_a), ("B", &cx_b), ("C", &cx_c), ("D", &cx_d)] {
-//         assert_eq!(
-//             followers_by_leader(project_id, cx),
-//             &[(peer_id_a, vec![peer_id_b]),],
-//             "followers seen by {name}"
-//         );
-//     }
-
-//     // When client A activates a different editor, client B does so as well.
-//     workspace_a.update(cx_a, |workspace, cx| {
-//         workspace.activate_item(&editor_a1, cx)
-//     });
-//     executor.run_until_parked();
-//     workspace_b.update(cx_b, |workspace, cx| {
-//         assert_eq!(
-//             workspace.active_item(cx).unwrap().item_id(),
-//             editor_b1.item_id()
-//         );
-//     });
-
-//     // When client A opens a multibuffer, client B does so as well.
-//     let multibuffer_a = cx_a.build_model(|cx| {
-//         let buffer_a1 = project_a.update(cx, |project, cx| {
-//             project
-//                 .get_open_buffer(&(worktree_id, "1.txt").into(), cx)
-//                 .unwrap()
-//         });
-//         let buffer_a2 = project_a.update(cx, |project, cx| {
-//             project
-//                 .get_open_buffer(&(worktree_id, "2.txt").into(), cx)
-//                 .unwrap()
-//         });
-//         let mut result = MultiBuffer::new(0);
-//         result.push_excerpts(
-//             buffer_a1,
-//             [ExcerptRange {
-//                 context: 0..3,
-//                 primary: None,
-//             }],
-//             cx,
-//         );
-//         result.push_excerpts(
-//             buffer_a2,
-//             [ExcerptRange {
-//                 context: 4..7,
-//                 primary: None,
-//             }],
-//             cx,
-//         );
-//         result
-//     });
-//     let multibuffer_editor_a = workspace_a.update(cx_a, |workspace, cx| {
-//         let editor =
-//             cx.build_view(|cx| Editor::for_multibuffer(multibuffer_a, Some(project_a.clone()), cx));
-//         workspace.add_item(Box::new(editor.clone()), cx);
-//         editor
-//     });
-//     executor.run_until_parked();
-//     let multibuffer_editor_b = workspace_b.update(cx_b, |workspace, cx| {
-//         workspace
-//             .active_item(cx)
-//             .unwrap()
-//             .downcast::<Editor>()
-//             .unwrap()
-//     });
-//     assert_eq!(
-//         multibuffer_editor_a.update(cx_a, |editor, cx| editor.text(cx)),
-//         multibuffer_editor_b.update(cx_b, |editor, cx| editor.text(cx)),
-//     );
-
-//     // When client A navigates back and forth, client B does so as well.
-//     workspace_a
-//         .update(cx_a, |workspace, cx| {
-//             workspace.go_back(workspace.active_pane().downgrade(), cx)
-//         })
-//         .await
-//         .unwrap();
-//     executor.run_until_parked();
-//     workspace_b.update(cx_b, |workspace, cx| {
-//         assert_eq!(
-//             workspace.active_item(cx).unwrap().item_id(),
-//             editor_b1.item_id()
-//         );
-//     });
-
-//     workspace_a
-//         .update(cx_a, |workspace, cx| {
-//             workspace.go_back(workspace.active_pane().downgrade(), cx)
-//         })
-//         .await
-//         .unwrap();
-//     executor.run_until_parked();
-//     workspace_b.update(cx_b, |workspace, cx| {
-//         assert_eq!(
-//             workspace.active_item(cx).unwrap().item_id(),
-//             editor_b2.item_id()
-//         );
-//     });
-
-//     workspace_a
-//         .update(cx_a, |workspace, cx| {
-//             workspace.go_forward(workspace.active_pane().downgrade(), cx)
-//         })
-//         .await
-//         .unwrap();
-//     executor.run_until_parked();
-//     workspace_b.update(cx_b, |workspace, cx| {
-//         assert_eq!(
-//             workspace.active_item(cx).unwrap().item_id(),
-//             editor_b1.item_id()
-//         );
-//     });
-
-//     // Changes to client A's editor are reflected on client B.
-//     editor_a1.update(cx_a, |editor, cx| {
-//         editor.change_selections(None, cx, |s| s.select_ranges([1..1, 2..2]));
-//     });
-//     executor.run_until_parked();
-//     editor_b1.update(cx_b, |editor, cx| {
-//         assert_eq!(editor.selections.ranges(cx), &[1..1, 2..2]);
-//     });
-
-//     editor_a1.update(cx_a, |editor, cx| editor.set_text("TWO", cx));
-//     executor.run_until_parked();
-//     editor_b1.update(cx_b, |editor, cx| assert_eq!(editor.text(cx), "TWO"));
-
-//     editor_a1.update(cx_a, |editor, cx| {
-//         editor.change_selections(None, cx, |s| s.select_ranges([3..3]));
-//         editor.set_scroll_position(point(0., 100.), cx);
-//     });
-//     executor.run_until_parked();
-//     editor_b1.update(cx_b, |editor, cx| {
-//         assert_eq!(editor.selections.ranges(cx), &[3..3]);
-//     });
-
-//     // After unfollowing, client B stops receiving updates from client A.
-//     workspace_b.update(cx_b, |workspace, cx| {
-//         workspace.unfollow(&workspace.active_pane().clone(), cx)
-//     });
-//     workspace_a.update(cx_a, |workspace, cx| {
-//         workspace.activate_item(&editor_a2, cx)
-//     });
-//     executor.run_until_parked();
-//     assert_eq!(
-//         workspace_b.update(cx_b, |workspace, cx| workspace
-//             .active_item(cx)
-//             .unwrap()
-//             .item_id()),
-//         editor_b1.item_id()
-//     );
-
-//     // Client A starts following client B.
-//     workspace_a
-//         .update(cx_a, |workspace, cx| {
-//             workspace.follow(peer_id_b, cx).unwrap()
-//         })
-//         .await
-//         .unwrap();
-//     assert_eq!(
-//         workspace_a.update(cx_a, |workspace, _| workspace.leader_for_pane(&pane_a)),
-//         Some(peer_id_b)
-//     );
-//     assert_eq!(
-//         workspace_a.update(cx_a, |workspace, cx| workspace
-//             .active_item(cx)
-//             .unwrap()
-//             .item_id()),
-//         editor_a1.item_id()
-//     );
-
-//     // Client B activates an external window, which causes a new screen-sharing item to be added to the pane.
-//     let display = MacOSDisplay::new();
-//     active_call_b
-//         .update(cx_b, |call, cx| call.set_location(None, cx))
-//         .await
-//         .unwrap();
-//     active_call_b
-//         .update(cx_b, |call, cx| {
-//             call.room().unwrap().update(cx, |room, cx| {
-//                 room.set_display_sources(vec![display.clone()]);
-//                 room.share_screen(cx)
-//             })
-//         })
-//         .await
-//         .unwrap();
-//     executor.run_until_parked();
-//     let shared_screen = workspace_a.update(cx_a, |workspace, cx| {
-//         workspace
-//             .active_item(cx)
-//             .expect("no active item")
-//             .downcast::<SharedScreen>()
-//             .expect("active item isn't a shared screen")
-//     });
-
-//     // Client B activates Zed again, which causes the previous editor to become focused again.
-//     active_call_b
-//         .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
-//         .await
-//         .unwrap();
-//     executor.run_until_parked();
-//     workspace_a.update(cx_a, |workspace, cx| {
-//         assert_eq!(
-//             workspace.active_item(cx).unwrap().item_id(),
-//             editor_a1.item_id()
-//         )
-//     });
-
-//     // Client B activates a multibuffer that was created by following client A. Client A returns to that multibuffer.
-//     workspace_b.update(cx_b, |workspace, cx| {
-//         workspace.activate_item(&multibuffer_editor_b, cx)
-//     });
-//     executor.run_until_parked();
-//     workspace_a.update(cx_a, |workspace, cx| {
-//         assert_eq!(
-//             workspace.active_item(cx).unwrap().item_id(),
-//             multibuffer_editor_a.item_id()
-//         )
-//     });
-
-//     // Client B activates a panel, and the previously-opened screen-sharing item gets activated.
-//     let panel = window_b.build_view(cx_b, |_| TestPanel::new(DockPosition::Left));
-//     workspace_b.update(cx_b, |workspace, cx| {
-//         workspace.add_panel(panel, cx);
-//         workspace.toggle_panel_focus::<TestPanel>(cx);
-//     });
-//     executor.run_until_parked();
-//     assert_eq!(
-//         workspace_a.update(cx_a, |workspace, cx| workspace
-//             .active_item(cx)
-//             .unwrap()
-//             .item_id()),
-//         shared_screen.item_id()
-//     );
-
-//     // Toggling the focus back to the pane causes client A to return to the multibuffer.
-//     workspace_b.update(cx_b, |workspace, cx| {
-//         workspace.toggle_panel_focus::<TestPanel>(cx);
-//     });
-//     executor.run_until_parked();
-//     workspace_a.update(cx_a, |workspace, cx| {
-//         assert_eq!(
-//             workspace.active_item(cx).unwrap().item_id(),
-//             multibuffer_editor_a.item_id()
-//         )
-//     });
-
-//     // Client B activates an item that doesn't implement following,
-//     // so the previously-opened screen-sharing item gets activated.
-//     let unfollowable_item = window_b.build_view(cx_b, |_| TestItem::new());
-//     workspace_b.update(cx_b, |workspace, cx| {
-//         workspace.active_pane().update(cx, |pane, cx| {
-//             pane.add_item(Box::new(unfollowable_item), true, true, None, cx)
-//         })
-//     });
-//     executor.run_until_parked();
-//     assert_eq!(
-//         workspace_a.update(cx_a, |workspace, cx| workspace
-//             .active_item(cx)
-//             .unwrap()
-//             .item_id()),
-//         shared_screen.item_id()
-//     );
-
-//     // Following interrupts when client B disconnects.
-//     client_b.disconnect(&cx_b.to_async());
-//     executor.advance_clock(RECONNECT_TIMEOUT);
-//     assert_eq!(
-//         workspace_a.update(cx_a, |workspace, _| workspace.leader_for_pane(&pane_a)),
-//         None
-//     );
-// }
-
-// #[gpui::test]
-// async fn test_following_tab_order(
-//     executor: BackgroundExecutor,
-//     cx_a: &mut TestAppContext,
-//     cx_b: &mut TestAppContext,
-// ) {
-//     let mut server = TestServer::start(executor.clone()).await;
-//     let client_a = server.create_client(cx_a, "user_a").await;
-//     let client_b = server.create_client(cx_b, "user_b").await;
-//     server
-//         .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
-//         .await;
-//     let active_call_a = cx_a.read(ActiveCall::global);
-//     let active_call_b = cx_b.read(ActiveCall::global);
-
-//     cx_a.update(editor::init);
-//     cx_b.update(editor::init);
-
-//     client_a
-//         .fs()
-//         .insert_tree(
-//             "/a",
-//             json!({
-//                 "1.txt": "one",
-//                 "2.txt": "two",
-//                 "3.txt": "three",
-//             }),
-//         )
-//         .await;
-//     let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
-//     active_call_a
-//         .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
-//         .await
-//         .unwrap();
-
-//     let project_id = active_call_a
-//         .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
-//         .await
-//         .unwrap();
-//     let project_b = client_b.build_remote_project(project_id, cx_b).await;
-//     active_call_b
-//         .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
-//         .await
-//         .unwrap();
-
-//     let workspace_a = client_a
-//         .build_workspace(&project_a, cx_a)
-//         .root(cx_a)
-//         .unwrap();
-//     let pane_a = workspace_a.update(cx_a, |workspace, _| workspace.active_pane().clone());
-
-//     let workspace_b = client_b
-//         .build_workspace(&project_b, cx_b)
-//         .root(cx_b)
-//         .unwrap();
-//     let pane_b = workspace_b.update(cx_b, |workspace, _| workspace.active_pane().clone());
-
-//     let client_b_id = project_a.update(cx_a, |project, _| {
-//         project.collaborators().values().next().unwrap().peer_id
-//     });
-
-//     //Open 1, 3 in that order on client A
-//     workspace_a
-//         .update(cx_a, |workspace, cx| {
-//             workspace.open_path((worktree_id, "1.txt"), None, true, cx)
-//         })
-//         .await
-//         .unwrap();
-//     workspace_a
-//         .update(cx_a, |workspace, cx| {
-//             workspace.open_path((worktree_id, "3.txt"), None, true, cx)
-//         })
-//         .await
-//         .unwrap();
-
-//     let pane_paths = |pane: &View<workspace::Pane>, cx: &mut TestAppContext| {
-//         pane.update(cx, |pane, cx| {
-//             pane.items()
-//                 .map(|item| {
-//                     item.project_path(cx)
-//                         .unwrap()
-//                         .path
-//                         .to_str()
-//                         .unwrap()
-//                         .to_owned()
-//                 })
-//                 .collect::<Vec<_>>()
-//         })
-//     };
-
-//     //Verify that the tabs opened in the order we expect
-//     assert_eq!(&pane_paths(&pane_a, cx_a), &["1.txt", "3.txt"]);
-
-//     //Follow client B as client A
-//     workspace_a
-//         .update(cx_a, |workspace, cx| {
-//             workspace.follow(client_b_id, cx).unwrap()
-//         })
-//         .await
-//         .unwrap();
-
-//     //Open just 2 on client B
-//     workspace_b
-//         .update(cx_b, |workspace, cx| {
-//             workspace.open_path((worktree_id, "2.txt"), None, true, cx)
-//         })
-//         .await
-//         .unwrap();
-//     executor.run_until_parked();
-
-//     // Verify that newly opened followed file is at the end
-//     assert_eq!(&pane_paths(&pane_a, cx_a), &["1.txt", "3.txt", "2.txt"]);
-
-//     //Open just 1 on client B
-//     workspace_b
-//         .update(cx_b, |workspace, cx| {
-//             workspace.open_path((worktree_id, "1.txt"), None, true, cx)
-//         })
-//         .await
-//         .unwrap();
-//     assert_eq!(&pane_paths(&pane_b, cx_b), &["2.txt", "1.txt"]);
-//     executor.run_until_parked();
-
-//     // Verify that following into 1 did not reorder
-//     assert_eq!(&pane_paths(&pane_a, cx_a), &["1.txt", "3.txt", "2.txt"]);
-// }
-
-// #[gpui::test(iterations = 10)]
-// async fn test_peers_following_each_other(
-//     executor: BackgroundExecutor,
-//     cx_a: &mut TestAppContext,
-//     cx_b: &mut TestAppContext,
-// ) {
-//     let mut server = TestServer::start(executor.clone()).await;
-//     let client_a = server.create_client(cx_a, "user_a").await;
-//     let client_b = server.create_client(cx_b, "user_b").await;
-//     server
-//         .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
-//         .await;
-//     let active_call_a = cx_a.read(ActiveCall::global);
-//     let active_call_b = cx_b.read(ActiveCall::global);
-
-//     cx_a.update(editor::init);
-//     cx_b.update(editor::init);
-
-//     // Client A shares a project.
-//     client_a
-//         .fs()
-//         .insert_tree(
-//             "/a",
-//             json!({
-//                 "1.txt": "one",
-//                 "2.txt": "two",
-//                 "3.txt": "three",
-//                 "4.txt": "four",
-//             }),
-//         )
-//         .await;
-//     let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
-//     active_call_a
-//         .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
-//         .await
-//         .unwrap();
-//     let project_id = active_call_a
-//         .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
-//         .await
-//         .unwrap();
-
-//     // Client B joins the project.
-//     let project_b = client_b.build_remote_project(project_id, cx_b).await;
-//     active_call_b
-//         .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
-//         .await
-//         .unwrap();
-
-//     // Client A opens a file.
-//     let workspace_a = client_a
-//         .build_workspace(&project_a, cx_a)
-//         .root(cx_a)
-//         .unwrap();
-//     workspace_a
-//         .update(cx_a, |workspace, cx| {
-//             workspace.open_path((worktree_id, "1.txt"), None, true, cx)
-//         })
-//         .await
-//         .unwrap()
-//         .downcast::<Editor>()
-//         .unwrap();
-
-//     // Client B opens a different file.
-//     let workspace_b = client_b
-//         .build_workspace(&project_b, cx_b)
-//         .root(cx_b)
-//         .unwrap();
-//     workspace_b
-//         .update(cx_b, |workspace, cx| {
-//             workspace.open_path((worktree_id, "2.txt"), None, true, cx)
-//         })
-//         .await
-//         .unwrap()
-//         .downcast::<Editor>()
-//         .unwrap();
-
-//     // Clients A and B follow each other in split panes
-//     workspace_a.update(cx_a, |workspace, cx| {
-//         workspace.split_and_clone(workspace.active_pane().clone(), SplitDirection::Right, cx);
-//     });
-//     workspace_a
-//         .update(cx_a, |workspace, cx| {
-//             workspace.follow(client_b.peer_id().unwrap(), cx).unwrap()
-//         })
-//         .await
-//         .unwrap();
-//     workspace_b.update(cx_b, |workspace, cx| {
-//         workspace.split_and_clone(workspace.active_pane().clone(), SplitDirection::Right, cx);
-//     });
-//     workspace_b
-//         .update(cx_b, |workspace, cx| {
-//             workspace.follow(client_a.peer_id().unwrap(), cx).unwrap()
-//         })
-//         .await
-//         .unwrap();
-
-//     // Clients A and B return focus to the original files they had open
-//     workspace_a.update(cx_a, |workspace, cx| workspace.activate_next_pane(cx));
-//     workspace_b.update(cx_b, |workspace, cx| workspace.activate_next_pane(cx));
-//     executor.run_until_parked();
-
-//     // Both clients see the other client's focused file in their right pane.
-//     assert_eq!(
-//         pane_summaries(&workspace_a, cx_a),
-//         &[
-//             PaneSummary {
-//                 active: true,
-//                 leader: None,
-//                 items: vec![(true, "1.txt".into())]
-//             },
-//             PaneSummary {
-//                 active: false,
-//                 leader: client_b.peer_id(),
-//                 items: vec![(false, "1.txt".into()), (true, "2.txt".into())]
-//             },
-//         ]
-//     );
-//     assert_eq!(
-//         pane_summaries(&workspace_b, cx_b),
-//         &[
-//             PaneSummary {
-//                 active: true,
-//                 leader: None,
-//                 items: vec![(true, "2.txt".into())]
-//             },
-//             PaneSummary {
-//                 active: false,
-//                 leader: client_a.peer_id(),
-//                 items: vec![(false, "2.txt".into()), (true, "1.txt".into())]
-//             },
-//         ]
-//     );
-
-//     // Clients A and B each open a new file.
-//     workspace_a
-//         .update(cx_a, |workspace, cx| {
-//             workspace.open_path((worktree_id, "3.txt"), None, true, cx)
-//         })
-//         .await
-//         .unwrap();
-
-//     workspace_b
-//         .update(cx_b, |workspace, cx| {
-//             workspace.open_path((worktree_id, "4.txt"), None, true, cx)
-//         })
-//         .await
-//         .unwrap();
-//     executor.run_until_parked();
-
-//     // Both client's see the other client open the new file, but keep their
-//     // focus on their own active pane.
-//     assert_eq!(
-//         pane_summaries(&workspace_a, cx_a),
-//         &[
-//             PaneSummary {
-//                 active: true,
-//                 leader: None,
-//                 items: vec![(false, "1.txt".into()), (true, "3.txt".into())]
-//             },
-//             PaneSummary {
-//                 active: false,
-//                 leader: client_b.peer_id(),
-//                 items: vec![
-//                     (false, "1.txt".into()),
-//                     (false, "2.txt".into()),
-//                     (true, "4.txt".into())
-//                 ]
-//             },
-//         ]
-//     );
-//     assert_eq!(
-//         pane_summaries(&workspace_b, cx_b),
-//         &[
-//             PaneSummary {
-//                 active: true,
-//                 leader: None,
-//                 items: vec![(false, "2.txt".into()), (true, "4.txt".into())]
-//             },
-//             PaneSummary {
-//                 active: false,
-//                 leader: client_a.peer_id(),
-//                 items: vec![
-//                     (false, "2.txt".into()),
-//                     (false, "1.txt".into()),
-//                     (true, "3.txt".into())
-//                 ]
-//             },
-//         ]
-//     );
-
-//     // Client A focuses their right pane, in which they're following client B.
-//     workspace_a.update(cx_a, |workspace, cx| workspace.activate_next_pane(cx));
-//     executor.run_until_parked();
-
-//     // Client B sees that client A is now looking at the same file as them.
-//     assert_eq!(
-//         pane_summaries(&workspace_a, cx_a),
-//         &[
-//             PaneSummary {
-//                 active: false,
-//                 leader: None,
-//                 items: vec![(false, "1.txt".into()), (true, "3.txt".into())]
-//             },
-//             PaneSummary {
-//                 active: true,
-//                 leader: client_b.peer_id(),
-//                 items: vec![
-//                     (false, "1.txt".into()),
-//                     (false, "2.txt".into()),
-//                     (true, "4.txt".into())
-//                 ]
-//             },
-//         ]
-//     );
-//     assert_eq!(
-//         pane_summaries(&workspace_b, cx_b),
-//         &[
-//             PaneSummary {
-//                 active: true,
-//                 leader: None,
-//                 items: vec![(false, "2.txt".into()), (true, "4.txt".into())]
-//             },
-//             PaneSummary {
-//                 active: false,
-//                 leader: client_a.peer_id(),
-//                 items: vec![
-//                     (false, "2.txt".into()),
-//                     (false, "1.txt".into()),
-//                     (false, "3.txt".into()),
-//                     (true, "4.txt".into())
-//                 ]
-//             },
-//         ]
-//     );
-
-//     // Client B focuses their right pane, in which they're following client A,
-//     // who is following them.
-//     workspace_b.update(cx_b, |workspace, cx| workspace.activate_next_pane(cx));
-//     executor.run_until_parked();
-
-//     // Client A sees that client B is now looking at the same file as them.
-//     assert_eq!(
-//         pane_summaries(&workspace_b, cx_b),
-//         &[
-//             PaneSummary {
-//                 active: false,
-//                 leader: None,
-//                 items: vec![(false, "2.txt".into()), (true, "4.txt".into())]
-//             },
-//             PaneSummary {
-//                 active: true,
-//                 leader: client_a.peer_id(),
-//                 items: vec![
-//                     (false, "2.txt".into()),
-//                     (false, "1.txt".into()),
-//                     (false, "3.txt".into()),
-//                     (true, "4.txt".into())
-//                 ]
-//             },
-//         ]
-//     );
-//     assert_eq!(
-//         pane_summaries(&workspace_a, cx_a),
-//         &[
-//             PaneSummary {
-//                 active: false,
-//                 leader: None,
-//                 items: vec![(false, "1.txt".into()), (true, "3.txt".into())]
-//             },
-//             PaneSummary {
-//                 active: true,
-//                 leader: client_b.peer_id(),
-//                 items: vec![
-//                     (false, "1.txt".into()),
-//                     (false, "2.txt".into()),
-//                     (true, "4.txt".into())
-//                 ]
-//             },
-//         ]
-//     );
-
-//     // Client B focuses a file that they previously followed A to, breaking
-//     // the follow.
-//     workspace_b.update(cx_b, |workspace, cx| {
-//         workspace.active_pane().update(cx, |pane, cx| {
-//             pane.activate_prev_item(true, cx);
-//         });
-//     });
-//     executor.run_until_parked();
-
-//     // Both clients see that client B is looking at that previous file.
-//     assert_eq!(
-//         pane_summaries(&workspace_b, cx_b),
-//         &[
-//             PaneSummary {
-//                 active: false,
-//                 leader: None,
-//                 items: vec![(false, "2.txt".into()), (true, "4.txt".into())]
-//             },
-//             PaneSummary {
-//                 active: true,
-//                 leader: None,
-//                 items: vec![
-//                     (false, "2.txt".into()),
-//                     (false, "1.txt".into()),
-//                     (true, "3.txt".into()),
-//                     (false, "4.txt".into())
-//                 ]
-//             },
-//         ]
-//     );
-//     assert_eq!(
-//         pane_summaries(&workspace_a, cx_a),
-//         &[
-//             PaneSummary {
-//                 active: false,
-//                 leader: None,
-//                 items: vec![(false, "1.txt".into()), (true, "3.txt".into())]
-//             },
-//             PaneSummary {
-//                 active: true,
-//                 leader: client_b.peer_id(),
-//                 items: vec![
-//                     (false, "1.txt".into()),
-//                     (false, "2.txt".into()),
-//                     (false, "4.txt".into()),
-//                     (true, "3.txt".into()),
-//                 ]
-//             },
-//         ]
-//     );
-
-//     // Client B closes tabs, some of which were originally opened by client A,
-//     // and some of which were originally opened by client B.
-//     workspace_b.update(cx_b, |workspace, cx| {
-//         workspace.active_pane().update(cx, |pane, cx| {
-//             pane.close_inactive_items(&Default::default(), cx)
-//                 .unwrap()
-//                 .detach();
-//         });
-//     });
-
-//     executor.run_until_parked();
-
-//     // Both clients see that Client B is looking at the previous tab.
-//     assert_eq!(
-//         pane_summaries(&workspace_b, cx_b),
-//         &[
-//             PaneSummary {
-//                 active: false,
-//                 leader: None,
-//                 items: vec![(false, "2.txt".into()), (true, "4.txt".into())]
-//             },
-//             PaneSummary {
-//                 active: true,
-//                 leader: None,
-//                 items: vec![(true, "3.txt".into()),]
-//             },
-//         ]
-//     );
-//     assert_eq!(
-//         pane_summaries(&workspace_a, cx_a),
-//         &[
-//             PaneSummary {
-//                 active: false,
-//                 leader: None,
-//                 items: vec![(false, "1.txt".into()), (true, "3.txt".into())]
-//             },
-//             PaneSummary {
-//                 active: true,
-//                 leader: client_b.peer_id(),
-//                 items: vec![
-//                     (false, "1.txt".into()),
-//                     (false, "2.txt".into()),
-//                     (false, "4.txt".into()),
-//                     (true, "3.txt".into()),
-//                 ]
-//             },
-//         ]
-//     );
-
-//     // Client B follows client A again.
-//     workspace_b
-//         .update(cx_b, |workspace, cx| {
-//             workspace.follow(client_a.peer_id().unwrap(), cx).unwrap()
-//         })
-//         .await
-//         .unwrap();
-
-//     // Client A cycles through some tabs.
-//     workspace_a.update(cx_a, |workspace, cx| {
-//         workspace.active_pane().update(cx, |pane, cx| {
-//             pane.activate_prev_item(true, cx);
-//         });
-//     });
-//     executor.run_until_parked();
-
-//     // Client B follows client A into those tabs.
-//     assert_eq!(
-//         pane_summaries(&workspace_a, cx_a),
-//         &[
-//             PaneSummary {
-//                 active: false,
-//                 leader: None,
-//                 items: vec![(false, "1.txt".into()), (true, "3.txt".into())]
-//             },
-//             PaneSummary {
-//                 active: true,
-//                 leader: None,
-//                 items: vec![
-//                     (false, "1.txt".into()),
-//                     (false, "2.txt".into()),
-//                     (true, "4.txt".into()),
-//                     (false, "3.txt".into()),
-//                 ]
-//             },
-//         ]
-//     );
-//     assert_eq!(
-//         pane_summaries(&workspace_b, cx_b),
-//         &[
-//             PaneSummary {
-//                 active: false,
-//                 leader: None,
-//                 items: vec![(false, "2.txt".into()), (true, "4.txt".into())]
-//             },
-//             PaneSummary {
-//                 active: true,
-//                 leader: client_a.peer_id(),
-//                 items: vec![(false, "3.txt".into()), (true, "4.txt".into())]
-//             },
-//         ]
-//     );
-
-//     workspace_a.update(cx_a, |workspace, cx| {
-//         workspace.active_pane().update(cx, |pane, cx| {
-//             pane.activate_prev_item(true, cx);
-//         });
-//     });
-//     executor.run_until_parked();
-
-//     assert_eq!(
-//         pane_summaries(&workspace_a, cx_a),
-//         &[
-//             PaneSummary {
-//                 active: false,
-//                 leader: None,
-//                 items: vec![(false, "1.txt".into()), (true, "3.txt".into())]
-//             },
-//             PaneSummary {
-//                 active: true,
-//                 leader: None,
-//                 items: vec![
-//                     (false, "1.txt".into()),
-//                     (true, "2.txt".into()),
-//                     (false, "4.txt".into()),
-//                     (false, "3.txt".into()),
-//                 ]
-//             },
-//         ]
-//     );
-//     assert_eq!(
-//         pane_summaries(&workspace_b, cx_b),
-//         &[
-//             PaneSummary {
-//                 active: false,
-//                 leader: None,
-//                 items: vec![(false, "2.txt".into()), (true, "4.txt".into())]
-//             },
-//             PaneSummary {
-//                 active: true,
-//                 leader: client_a.peer_id(),
-//                 items: vec![
-//                     (false, "3.txt".into()),
-//                     (false, "4.txt".into()),
-//                     (true, "2.txt".into())
-//                 ]
-//             },
-//         ]
-//     );
-
-//     workspace_a.update(cx_a, |workspace, cx| {
-//         workspace.active_pane().update(cx, |pane, cx| {
-//             pane.activate_prev_item(true, cx);
-//         });
-//     });
-//     executor.run_until_parked();
-
-//     assert_eq!(
-//         pane_summaries(&workspace_a, cx_a),
-//         &[
-//             PaneSummary {
-//                 active: false,
-//                 leader: None,
-//                 items: vec![(false, "1.txt".into()), (true, "3.txt".into())]
-//             },
-//             PaneSummary {
-//                 active: true,
-//                 leader: None,
-//                 items: vec![
-//                     (true, "1.txt".into()),
-//                     (false, "2.txt".into()),
-//                     (false, "4.txt".into()),
-//                     (false, "3.txt".into()),
-//                 ]
-//             },
-//         ]
-//     );
-//     assert_eq!(
-//         pane_summaries(&workspace_b, cx_b),
-//         &[
-//             PaneSummary {
-//                 active: false,
-//                 leader: None,
-//                 items: vec![(false, "2.txt".into()), (true, "4.txt".into())]
-//             },
-//             PaneSummary {
-//                 active: true,
-//                 leader: client_a.peer_id(),
-//                 items: vec![
-//                     (false, "3.txt".into()),
-//                     (false, "4.txt".into()),
-//                     (false, "2.txt".into()),
-//                     (true, "1.txt".into()),
-//                 ]
-//             },
-//         ]
-//     );
-// }
-
-// #[gpui::test(iterations = 10)]
-// async fn test_auto_unfollowing(
-//     executor: BackgroundExecutor,
-//     cx_a: &mut TestAppContext,
-//     cx_b: &mut TestAppContext,
-// ) {
-//     // 2 clients connect to a server.
-//     let mut server = TestServer::start(executor.clone()).await;
-//     let client_a = server.create_client(cx_a, "user_a").await;
-//     let client_b = server.create_client(cx_b, "user_b").await;
-//     server
-//         .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
-//         .await;
-//     let active_call_a = cx_a.read(ActiveCall::global);
-//     let active_call_b = cx_b.read(ActiveCall::global);
-
-//     cx_a.update(editor::init);
-//     cx_b.update(editor::init);
-
-//     // Client A shares a project.
-//     client_a
-//         .fs()
-//         .insert_tree(
-//             "/a",
-//             json!({
-//                 "1.txt": "one",
-//                 "2.txt": "two",
-//                 "3.txt": "three",
-//             }),
-//         )
-//         .await;
-//     let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
-//     active_call_a
-//         .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
-//         .await
-//         .unwrap();
-
-//     let project_id = active_call_a
-//         .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
-//         .await
-//         .unwrap();
-//     let project_b = client_b.build_remote_project(project_id, cx_b).await;
-//     active_call_b
-//         .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
-//         .await
-//         .unwrap();
-
-//     todo!("could be wrong")
-//     let mut cx_a = VisualTestContext::from_window(*window_a, cx_a);
-//     let cx_a = &mut cx_a;
-//     let mut cx_b = VisualTestContext::from_window(*window_b, cx_b);
-//     let cx_b = &mut cx_b;
-
-//     // Client A opens some editors.
-//     let workspace_a = client_a
-//         .build_workspace(&project_a, cx_a)
-//         .root(cx_a)
-//         .unwrap();
-//     let _editor_a1 = workspace_a
-//         .update(cx_a, |workspace, cx| {
-//             workspace.open_path((worktree_id, "1.txt"), None, true, cx)
-//         })
-//         .await
-//         .unwrap()
-//         .downcast::<Editor>()
-//         .unwrap();
-
-//     // Client B starts following client A.
-//     let workspace_b = client_b
-//         .build_workspace(&project_b, cx_b)
-//         .root(cx_b)
-//         .unwrap();
-//     let pane_b = workspace_b.update(cx_b, |workspace, _| workspace.active_pane().clone());
-//     let leader_id = project_b.update(cx_b, |project, _| {
-//         project.collaborators().values().next().unwrap().peer_id
-//     });
-//     workspace_b
-//         .update(cx_b, |workspace, cx| {
-//             workspace.follow(leader_id, cx).unwrap()
-//         })
-//         .await
-//         .unwrap();
-//     assert_eq!(
-//         workspace_b.update(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
-//         Some(leader_id)
-//     );
-//     let editor_b2 = workspace_b.update(cx_b, |workspace, cx| {
-//         workspace
-//             .active_item(cx)
-//             .unwrap()
-//             .downcast::<Editor>()
-//             .unwrap()
-//     });
-
-//     // When client B moves, it automatically stops following client A.
-//     editor_b2.update(cx_b, |editor, cx| editor.move_right(&editor::MoveRight, cx));
-//     assert_eq!(
-//         workspace_b.update(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
-//         None
-//     );
-
-//     workspace_b
-//         .update(cx_b, |workspace, cx| {
-//             workspace.follow(leader_id, cx).unwrap()
-//         })
-//         .await
-//         .unwrap();
-//     assert_eq!(
-//         workspace_b.update(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
-//         Some(leader_id)
-//     );
-
-//     // When client B edits, it automatically stops following client A.
-//     editor_b2.update(cx_b, |editor, cx| editor.insert("X", cx));
-//     assert_eq!(
-//         workspace_b.update(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
-//         None
-//     );
-
-//     workspace_b
-//         .update(cx_b, |workspace, cx| {
-//             workspace.follow(leader_id, cx).unwrap()
-//         })
-//         .await
-//         .unwrap();
-//     assert_eq!(
-//         workspace_b.update(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
-//         Some(leader_id)
-//     );
-
-//     // When client B scrolls, it automatically stops following client A.
-//     editor_b2.update(cx_b, |editor, cx| {
-//         editor.set_scroll_position(point(0., 3.), cx)
-//     });
-//     assert_eq!(
-//         workspace_b.update(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
-//         None
-//     );
-
-//     workspace_b
-//         .update(cx_b, |workspace, cx| {
-//             workspace.follow(leader_id, cx).unwrap()
-//         })
-//         .await
-//         .unwrap();
-//     assert_eq!(
-//         workspace_b.update(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
-//         Some(leader_id)
-//     );
-
-//     // When client B activates a different pane, it continues following client A in the original pane.
-//     workspace_b.update(cx_b, |workspace, cx| {
-//         workspace.split_and_clone(pane_b.clone(), SplitDirection::Right, cx)
-//     });
-//     assert_eq!(
-//         workspace_b.update(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
-//         Some(leader_id)
-//     );
-
-//     workspace_b.update(cx_b, |workspace, cx| workspace.activate_next_pane(cx));
-//     assert_eq!(
-//         workspace_b.update(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
-//         Some(leader_id)
-//     );
-
-//     // When client B activates a different item in the original pane, it automatically stops following client A.
-//     workspace_b
-//         .update(cx_b, |workspace, cx| {
-//             workspace.open_path((worktree_id, "2.txt"), None, true, cx)
-//         })
-//         .await
-//         .unwrap();
-//     assert_eq!(
-//         workspace_b.update(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
-//         None
-//     );
-// }
-
-// #[gpui::test(iterations = 10)]
-// async fn test_peers_simultaneously_following_each_other(
-//     executor: BackgroundExecutor,
-//     cx_a: &mut TestAppContext,
-//     cx_b: &mut TestAppContext,
-// ) {
-//     let mut server = TestServer::start(executor.clone()).await;
-//     let client_a = server.create_client(cx_a, "user_a").await;
-//     let client_b = server.create_client(cx_b, "user_b").await;
-//     server
-//         .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
-//         .await;
-//     let active_call_a = cx_a.read(ActiveCall::global);
-
-//     cx_a.update(editor::init);
-//     cx_b.update(editor::init);
-
-//     client_a.fs().insert_tree("/a", json!({})).await;
-//     let (project_a, _) = client_a.build_local_project("/a", cx_a).await;
-//     let workspace_a = client_a
-//         .build_workspace(&project_a, cx_a)
-//         .root(cx_a)
-//         .unwrap();
-//     let project_id = active_call_a
-//         .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
-//         .await
-//         .unwrap();
-
-//     let project_b = client_b.build_remote_project(project_id, cx_b).await;
-//     let workspace_b = client_b
-//         .build_workspace(&project_b, cx_b)
-//         .root(cx_b)
-//         .unwrap();
-
-//     executor.run_until_parked();
-//     let client_a_id = project_b.update(cx_b, |project, _| {
-//         project.collaborators().values().next().unwrap().peer_id
-//     });
-//     let client_b_id = project_a.update(cx_a, |project, _| {
-//         project.collaborators().values().next().unwrap().peer_id
-//     });
-
-//     let a_follow_b = workspace_a.update(cx_a, |workspace, cx| {
-//         workspace.follow(client_b_id, cx).unwrap()
-//     });
-//     let b_follow_a = workspace_b.update(cx_b, |workspace, cx| {
-//         workspace.follow(client_a_id, cx).unwrap()
-//     });
-
-//     futures::try_join!(a_follow_b, b_follow_a).unwrap();
-//     workspace_a.update(cx_a, |workspace, _| {
-//         assert_eq!(
-//             workspace.leader_for_pane(workspace.active_pane()),
-//             Some(client_b_id)
-//         );
-//     });
-//     workspace_b.update(cx_b, |workspace, _| {
-//         assert_eq!(
-//             workspace.leader_for_pane(workspace.active_pane()),
-//             Some(client_a_id)
-//         );
-//     });
-// }
-
-// #[gpui::test(iterations = 10)]
-// async fn test_following_across_workspaces(
-//     executor: BackgroundExecutor,
-//     cx_a: &mut TestAppContext,
-//     cx_b: &mut TestAppContext,
-// ) {
-//     // a and b join a channel/call
-//     // a shares project 1
-//     // b shares project 2
-//     //
-//     // b follows a: causes project 2 to be joined, and b to follow a.
-//     // b opens a different file in project 2, a follows b
-//     // b opens a different file in project 1, a cannot follow b
-//     // b shares the project, a joins the project and follows b
-//     let mut server = TestServer::start(executor.clone()).await;
-//     let client_a = server.create_client(cx_a, "user_a").await;
-//     let client_b = server.create_client(cx_b, "user_b").await;
-//     cx_a.update(editor::init);
-//     cx_b.update(editor::init);
-
-//     client_a
-//         .fs()
-//         .insert_tree(
-//             "/a",
-//             json!({
-//                 "w.rs": "",
-//                 "x.rs": "",
-//             }),
-//         )
-//         .await;
-
-//     client_b
-//         .fs()
-//         .insert_tree(
-//             "/b",
-//             json!({
-//                 "y.rs": "",
-//                 "z.rs": "",
-//             }),
-//         )
-//         .await;
-
-//     server
-//         .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
-//         .await;
-//     let active_call_a = cx_a.read(ActiveCall::global);
-//     let active_call_b = cx_b.read(ActiveCall::global);
-
-//     let (project_a, worktree_id_a) = client_a.build_local_project("/a", cx_a).await;
-//     let (project_b, worktree_id_b) = client_b.build_local_project("/b", cx_b).await;
-
-//     let workspace_a = client_a
-//         .build_workspace(&project_a, cx_a)
-//         .root(cx_a)
-//         .unwrap();
-//     let workspace_b = client_b
-//         .build_workspace(&project_b, cx_b)
-//         .root(cx_b)
-//         .unwrap();
-
-//     cx_a.update(|cx| collab_ui::init(&client_a.app_state, cx));
-//     cx_b.update(|cx| collab_ui::init(&client_b.app_state, cx));
-
-//     active_call_a
-//         .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
-//         .await
-//         .unwrap();
-
-//     active_call_a
-//         .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
-//         .await
-//         .unwrap();
-//     active_call_b
-//         .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
-//         .await
-//         .unwrap();
-
-//     todo!("could be wrong")
-//     let mut cx_a = VisualTestContext::from_window(*window_a, cx_a);
-//     let cx_a = &mut cx_a;
-//     let mut cx_b = VisualTestContext::from_window(*window_b, cx_b);
-//     let cx_b = &mut cx_b;
-
-//     workspace_a
-//         .update(cx_a, |workspace, cx| {
-//             workspace.open_path((worktree_id_a, "w.rs"), None, true, cx)
-//         })
-//         .await
-//         .unwrap();
-
-//     executor.run_until_parked();
-//     assert_eq!(visible_push_notifications(cx_b).len(), 1);
-
-//     workspace_b.update(cx_b, |workspace, cx| {
-//         workspace
-//             .follow(client_a.peer_id().unwrap(), cx)
-//             .unwrap()
-//             .detach()
-//     });
-
-//     executor.run_until_parked();
-//     let workspace_b_project_a = cx_b
-//         .windows()
-//         .iter()
-//         .max_by_key(|window| window.item_id())
-//         .unwrap()
-//         .downcast::<Workspace>()
-//         .unwrap()
-//         .root(cx_b)
-//         .unwrap();
-
-//     // assert that b is following a in project a in w.rs
-//     workspace_b_project_a.update(cx_b, |workspace, cx| {
-//         assert!(workspace.is_being_followed(client_a.peer_id().unwrap()));
-//         assert_eq!(
-//             client_a.peer_id(),
-//             workspace.leader_for_pane(workspace.active_pane())
-//         );
-//         let item = workspace.active_item(cx).unwrap();
-//         assert_eq!(item.tab_description(0, cx).unwrap(), Cow::Borrowed("w.rs"));
-//     });
-
-//     // TODO: in app code, this would be done by the collab_ui.
-//     active_call_b
-//         .update(cx_b, |call, cx| {
-//             let project = workspace_b_project_a.read(cx).project().clone();
-//             call.set_location(Some(&project), cx)
-//         })
-//         .await
-//         .unwrap();
-
-//     // assert that there are no share notifications open
-//     assert_eq!(visible_push_notifications(cx_b).len(), 0);
-
-//     // b moves to x.rs in a's project, and a follows
-//     workspace_b_project_a
-//         .update(cx_b, |workspace, cx| {
-//             workspace.open_path((worktree_id_a, "x.rs"), None, true, cx)
-//         })
-//         .await
-//         .unwrap();
-
-//     executor.run_until_parked();
-//     workspace_b_project_a.update(cx_b, |workspace, cx| {
-//         let item = workspace.active_item(cx).unwrap();
-//         assert_eq!(item.tab_description(0, cx).unwrap(), Cow::Borrowed("x.rs"));
-//     });
-
-//     workspace_a.update(cx_a, |workspace, cx| {
-//         workspace
-//             .follow(client_b.peer_id().unwrap(), cx)
-//             .unwrap()
-//             .detach()
-//     });
-
-//     executor.run_until_parked();
-//     workspace_a.update(cx_a, |workspace, cx| {
-//         assert!(workspace.is_being_followed(client_b.peer_id().unwrap()));
-//         assert_eq!(
-//             client_b.peer_id(),
-//             workspace.leader_for_pane(workspace.active_pane())
-//         );
-//         let item = workspace.active_pane().read(cx).active_item().unwrap();
-//         assert_eq!(item.tab_description(0, cx).unwrap(), "x.rs".into());
-//     });
-
-//     // b moves to y.rs in b's project, a is still following but can't yet see
-//     workspace_b
-//         .update(cx_b, |workspace, cx| {
-//             workspace.open_path((worktree_id_b, "y.rs"), None, true, cx)
-//         })
-//         .await
-//         .unwrap();
-
-//     // TODO: in app code, this would be done by the collab_ui.
-//     active_call_b
-//         .update(cx_b, |call, cx| {
-//             let project = workspace_b.read(cx).project().clone();
-//             call.set_location(Some(&project), cx)
-//         })
-//         .await
-//         .unwrap();
-
-//     let project_b_id = active_call_b
-//         .update(cx_b, |call, cx| call.share_project(project_b.clone(), cx))
-//         .await
-//         .unwrap();
-
-//     executor.run_until_parked();
-//     assert_eq!(visible_push_notifications(cx_a).len(), 1);
-//     cx_a.update(|cx| {
-//         workspace::join_remote_project(
-//             project_b_id,
-//             client_b.user_id().unwrap(),
-//             client_a.app_state.clone(),
-//             cx,
-//         )
-//     })
-//     .await
-//     .unwrap();
-
-//     executor.run_until_parked();
-
-//     assert_eq!(visible_push_notifications(cx_a).len(), 0);
-//     let workspace_a_project_b = cx_a
-//         .windows()
-//         .iter()
-//         .max_by_key(|window| window.item_id())
-//         .unwrap()
-//         .downcast::<Workspace>()
-//         .unwrap()
-//         .root(cx_a)
-//         .unwrap();
-
-//     workspace_a_project_b.update(cx_a, |workspace, cx| {
-//         assert_eq!(workspace.project().read(cx).remote_id(), Some(project_b_id));
-//         assert!(workspace.is_being_followed(client_b.peer_id().unwrap()));
-//         assert_eq!(
-//             client_b.peer_id(),
-//             workspace.leader_for_pane(workspace.active_pane())
-//         );
-//         let item = workspace.active_item(cx).unwrap();
-//         assert_eq!(item.tab_description(0, cx).unwrap(), Cow::Borrowed("y.rs"));
-//     });
-// }
-
-// #[gpui::test]
-// async fn test_following_into_excluded_file(
-//     executor: BackgroundExecutor,
-//     mut cx_a: &mut TestAppContext,
-//     mut cx_b: &mut TestAppContext,
-// ) {
-//     let mut server = TestServer::start(executor.clone()).await;
-//     let client_a = server.create_client(cx_a, "user_a").await;
-//     let client_b = server.create_client(cx_b, "user_b").await;
-//     for cx in [&mut cx_a, &mut cx_b] {
-//         cx.update(|cx| {
-//             cx.update_global::<SettingsStore, _>(|store, cx| {
-//                 store.update_user_settings::<ProjectSettings>(cx, |project_settings| {
-//                     project_settings.file_scan_exclusions = Some(vec!["**/.git".to_string()]);
-//                 });
-//             });
-//         });
-//     }
-//     server
-//         .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
-//         .await;
-//     let active_call_a = cx_a.read(ActiveCall::global);
-//     let active_call_b = cx_b.read(ActiveCall::global);
-
-//     cx_a.update(editor::init);
-//     cx_b.update(editor::init);
-
-//     client_a
-//         .fs()
-//         .insert_tree(
-//             "/a",
-//             json!({
-//                 ".git": {
-//                     "COMMIT_EDITMSG": "write your commit message here",
-//                 },
-//                 "1.txt": "one\none\none",
-//                 "2.txt": "two\ntwo\ntwo",
-//                 "3.txt": "three\nthree\nthree",
-//             }),
-//         )
-//         .await;
-//     let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
-//     active_call_a
-//         .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
-//         .await
-//         .unwrap();
-
-//     let project_id = active_call_a
-//         .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
-//         .await
-//         .unwrap();
-//     let project_b = client_b.build_remote_project(project_id, cx_b).await;
-//     active_call_b
-//         .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
-//         .await
-//         .unwrap();
-
-//     let window_a = client_a.build_workspace(&project_a, cx_a);
-//     let workspace_a = window_a.root(cx_a).unwrap();
-//     let peer_id_a = client_a.peer_id().unwrap();
-//     let window_b = client_b.build_workspace(&project_b, cx_b);
-//     let workspace_b = window_b.root(cx_b).unwrap();
-
-//     todo!("could be wrong")
-//     let mut cx_a = VisualTestContext::from_window(*window_a, cx_a);
-//     let cx_a = &mut cx_a;
-//     let mut cx_b = VisualTestContext::from_window(*window_b, cx_b);
-//     let cx_b = &mut cx_b;
-
-//     // Client A opens editors for a regular file and an excluded file.
-//     let editor_for_regular = workspace_a
-//         .update(cx_a, |workspace, cx| {
-//             workspace.open_path((worktree_id, "1.txt"), None, true, cx)
-//         })
-//         .await
-//         .unwrap()
-//         .downcast::<Editor>()
-//         .unwrap();
-//     let editor_for_excluded_a = workspace_a
-//         .update(cx_a, |workspace, cx| {
-//             workspace.open_path((worktree_id, ".git/COMMIT_EDITMSG"), None, true, cx)
-//         })
-//         .await
-//         .unwrap()
-//         .downcast::<Editor>()
-//         .unwrap();
-
-//     // Client A updates their selections in those editors
-//     editor_for_regular.update(cx_a, |editor, cx| {
-//         editor.handle_input("a", cx);
-//         editor.handle_input("b", cx);
-//         editor.handle_input("c", cx);
-//         editor.select_left(&Default::default(), cx);
-//         assert_eq!(editor.selections.ranges(cx), vec![3..2]);
-//     });
-//     editor_for_excluded_a.update(cx_a, |editor, cx| {
-//         editor.select_all(&Default::default(), cx);
-//         editor.handle_input("new commit message", cx);
-//         editor.select_left(&Default::default(), cx);
-//         assert_eq!(editor.selections.ranges(cx), vec![18..17]);
-//     });
-
-//     // When client B starts following client A, currently visible file is replicated
-//     workspace_b
-//         .update(cx_b, |workspace, cx| {
-//             workspace.follow(peer_id_a, cx).unwrap()
-//         })
-//         .await
-//         .unwrap();
-
-//     let editor_for_excluded_b = workspace_b.update(cx_b, |workspace, cx| {
-//         workspace
-//             .active_item(cx)
-//             .unwrap()
-//             .downcast::<Editor>()
-//             .unwrap()
-//     });
-//     assert_eq!(
-//         cx_b.read(|cx| editor_for_excluded_b.project_path(cx)),
-//         Some((worktree_id, ".git/COMMIT_EDITMSG").into())
-//     );
-//     assert_eq!(
-//         editor_for_excluded_b.update(cx_b, |editor, cx| editor.selections.ranges(cx)),
-//         vec![18..17]
-//     );
-
-//     // Changes from B to the excluded file are replicated in A's editor
-//     editor_for_excluded_b.update(cx_b, |editor, cx| {
-//         editor.handle_input("\nCo-Authored-By: B <b@b.b>", cx);
-//     });
-//     executor.run_until_parked();
-//     editor_for_excluded_a.update(cx_a, |editor, cx| {
-//         assert_eq!(
-//             editor.text(cx),
-//             "new commit messag\nCo-Authored-By: B <b@b.b>"
-//         );
-//     });
-// }
-
-// fn visible_push_notifications(
-//     cx: &mut TestAppContext,
-// ) -> Vec<gpui::View<ProjectSharedNotification>> {
-//     let mut ret = Vec::new();
-//     for window in cx.windows() {
-//         window.update(cx, |window| {
-//             if let Some(handle) = window
-//                 .root_view()
-//                 .clone()
-//                 .downcast::<ProjectSharedNotification>()
-//             {
-//                 ret.push(handle)
-//             }
-//         });
-//     }
-//     ret
-// }
-
-// #[derive(Debug, PartialEq, Eq)]
-// struct PaneSummary {
-//     active: bool,
-//     leader: Option<PeerId>,
-//     items: Vec<(bool, String)>,
-// }
-
-// fn followers_by_leader(project_id: u64, cx: &TestAppContext) -> Vec<(PeerId, Vec<PeerId>)> {
-//     cx.read(|cx| {
-//         let active_call = ActiveCall::global(cx).read(cx);
-//         let peer_id = active_call.client().peer_id();
-//         let room = active_call.room().unwrap().read(cx);
-//         let mut result = room
-//             .remote_participants()
-//             .values()
-//             .map(|participant| participant.peer_id)
-//             .chain(peer_id)
-//             .filter_map(|peer_id| {
-//                 let followers = room.followers_for(peer_id, project_id);
-//                 if followers.is_empty() {
-//                     None
-//                 } else {
-//                     Some((peer_id, followers.to_vec()))
-//                 }
-//             })
-//             .collect::<Vec<_>>();
-//         result.sort_by_key(|e| e.0);
-//         result
-//     })
-// }
-
-// fn pane_summaries(workspace: &View<Workspace>, cx: &mut WindowContext<'_>) -> Vec<PaneSummary> {
-//     workspace.update(cx, |workspace, cx| {
-//         let active_pane = workspace.active_pane();
-//         workspace
-//             .panes()
-//             .iter()
-//             .map(|pane| {
-//                 let leader = workspace.leader_for_pane(pane);
-//                 let active = pane == active_pane;
-//                 let pane = pane.read(cx);
-//                 let active_ix = pane.active_item_index();
-//                 PaneSummary {
-//                     active,
-//                     leader,
-//                     items: pane
-//                         .items()
-//                         .enumerate()
-//                         .map(|(ix, item)| {
-//                             (
-//                                 ix == active_ix,
-//                                 item.tab_description(0, cx)
-//                                     .map_or(String::new(), |s| s.to_string()),
-//                             )
-//                         })
-//                         .collect(),
-//                 }
-//             })
-//             .collect()
-//     })
-// }
+use crate::{rpc::RECONNECT_TIMEOUT, tests::TestServer};
+use call::ActiveCall;
+use collab_ui::notifications::project_shared_notification::ProjectSharedNotification;
+use editor::{Editor, ExcerptRange, MultiBuffer};
+use gpui::{
+    point, BackgroundExecutor, Context, SharedString, TestAppContext, View, VisualContext,
+    VisualTestContext,
+};
+use language::Capability;
+use live_kit_client::MacOSDisplay;
+use project::project_settings::ProjectSettings;
+use rpc::proto::PeerId;
+use serde_json::json;
+use settings::SettingsStore;
+use workspace::{
+    dock::{test::TestPanel, DockPosition},
+    item::{test::TestItem, ItemHandle as _},
+    shared_screen::SharedScreen,
+    SplitDirection, Workspace,
+};
+
+#[gpui::test(iterations = 10)]
+async fn test_basic_following(
+    cx_a: &mut TestAppContext,
+    cx_b: &mut TestAppContext,
+    cx_c: &mut TestAppContext,
+    cx_d: &mut TestAppContext,
+) {
+    let executor = cx_a.executor();
+    let mut server = TestServer::start(executor.clone()).await;
+    let client_a = server.create_client(cx_a, "user_a").await;
+    let client_b = server.create_client(cx_b, "user_b").await;
+    let client_c = server.create_client(cx_c, "user_c").await;
+    let client_d = server.create_client(cx_d, "user_d").await;
+    server
+        .create_room(&mut [
+            (&client_a, cx_a),
+            (&client_b, cx_b),
+            (&client_c, cx_c),
+            (&client_d, cx_d),
+        ])
+        .await;
+    let active_call_a = cx_a.read(ActiveCall::global);
+    let active_call_b = cx_b.read(ActiveCall::global);
+
+    cx_a.update(editor::init);
+    cx_b.update(editor::init);
+
+    client_a
+        .fs()
+        .insert_tree(
+            "/a",
+            json!({
+                "1.txt": "one\none\none",
+                "2.txt": "two\ntwo\ntwo",
+                "3.txt": "three\nthree\nthree",
+            }),
+        )
+        .await;
+    let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
+    active_call_a
+        .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
+        .await
+        .unwrap();
+
+    let project_id = active_call_a
+        .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
+        .await
+        .unwrap();
+    let project_b = client_b.build_remote_project(project_id, cx_b).await;
+    active_call_b
+        .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
+        .await
+        .unwrap();
+
+    let (workspace_a, cx_a) = client_a.build_workspace(&project_a, cx_a);
+    let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
+
+    // Client A opens some editors.
+    let pane_a = workspace_a.update(cx_a, |workspace, _| workspace.active_pane().clone());
+    let editor_a1 = workspace_a
+        .update(cx_a, |workspace, cx| {
+            workspace.open_path((worktree_id, "1.txt"), None, true, cx)
+        })
+        .await
+        .unwrap()
+        .downcast::<Editor>()
+        .unwrap();
+    let editor_a2 = workspace_a
+        .update(cx_a, |workspace, cx| {
+            workspace.open_path((worktree_id, "2.txt"), None, true, cx)
+        })
+        .await
+        .unwrap()
+        .downcast::<Editor>()
+        .unwrap();
+
+    // Client B opens an editor.
+    let editor_b1 = workspace_b
+        .update(cx_b, |workspace, cx| {
+            workspace.open_path((worktree_id, "1.txt"), None, true, cx)
+        })
+        .await
+        .unwrap()
+        .downcast::<Editor>()
+        .unwrap();
+
+    let peer_id_a = client_a.peer_id().unwrap();
+    let peer_id_b = client_b.peer_id().unwrap();
+    let peer_id_c = client_c.peer_id().unwrap();
+    let peer_id_d = client_d.peer_id().unwrap();
+
+    // Client A updates their selections in those editors
+    editor_a1.update(cx_a, |editor, cx| {
+        editor.handle_input("a", cx);
+        editor.handle_input("b", cx);
+        editor.handle_input("c", cx);
+        editor.select_left(&Default::default(), cx);
+        assert_eq!(editor.selections.ranges(cx), vec![3..2]);
+    });
+    editor_a2.update(cx_a, |editor, cx| {
+        editor.handle_input("d", cx);
+        editor.handle_input("e", cx);
+        editor.select_left(&Default::default(), cx);
+        assert_eq!(editor.selections.ranges(cx), vec![2..1]);
+    });
+
+    // When client B starts following client A, all visible view states are replicated to client B.
+    workspace_b.update(cx_b, |workspace, cx| workspace.follow(peer_id_a, cx));
+
+    cx_c.executor().run_until_parked();
+    let editor_b2 = workspace_b.update(cx_b, |workspace, cx| {
+        workspace
+            .active_item(cx)
+            .unwrap()
+            .downcast::<Editor>()
+            .unwrap()
+    });
+    assert_eq!(
+        cx_b.read(|cx| editor_b2.project_path(cx)),
+        Some((worktree_id, "2.txt").into())
+    );
+    assert_eq!(
+        editor_b2.update(cx_b, |editor, cx| editor.selections.ranges(cx)),
+        vec![2..1]
+    );
+    assert_eq!(
+        editor_b1.update(cx_b, |editor, cx| editor.selections.ranges(cx)),
+        vec![3..2]
+    );
+
+    executor.run_until_parked();
+    let active_call_c = cx_c.read(ActiveCall::global);
+    let project_c = client_c.build_remote_project(project_id, cx_c).await;
+    let (workspace_c, cx_c) = client_c.build_workspace(&project_c, cx_c);
+    active_call_c
+        .update(cx_c, |call, cx| call.set_location(Some(&project_c), cx))
+        .await
+        .unwrap();
+    let weak_project_c = project_c.downgrade();
+    drop(project_c);
+
+    // Client C also follows client A.
+    workspace_c.update(cx_c, |workspace, cx| workspace.follow(peer_id_a, cx));
+
+    cx_d.executor().run_until_parked();
+    let active_call_d = cx_d.read(ActiveCall::global);
+    let project_d = client_d.build_remote_project(project_id, cx_d).await;
+    let (workspace_d, cx_d) = client_d.build_workspace(&project_d, cx_d);
+    active_call_d
+        .update(cx_d, |call, cx| call.set_location(Some(&project_d), cx))
+        .await
+        .unwrap();
+    drop(project_d);
+
+    // All clients see that clients B and C are following client A.
+    cx_c.executor().run_until_parked();
+    for (name, cx) in [("A", &cx_a), ("B", &cx_b), ("C", &cx_c), ("D", &cx_d)] {
+        assert_eq!(
+            followers_by_leader(project_id, cx),
+            &[(peer_id_a, vec![peer_id_b, peer_id_c])],
+            "followers seen by {name}"
+        );
+    }
+
+    // Client C unfollows client A.
+    workspace_c.update(cx_c, |workspace, cx| {
+        workspace.unfollow(&workspace.active_pane().clone(), cx);
+    });
+
+    // All clients see that clients B is following client A.
+    cx_c.executor().run_until_parked();
+    for (name, cx) in [("A", &cx_a), ("B", &cx_b), ("C", &cx_c), ("D", &cx_d)] {
+        assert_eq!(
+            followers_by_leader(project_id, cx),
+            &[(peer_id_a, vec![peer_id_b])],
+            "followers seen by {name}"
+        );
+    }
+
+    // Client C re-follows client A.
+    workspace_c.update(cx_c, |workspace, cx| workspace.follow(peer_id_a, cx));
+
+    // All clients see that clients B and C are following client A.
+    cx_c.executor().run_until_parked();
+    for (name, cx) in [("A", &cx_a), ("B", &cx_b), ("C", &cx_c), ("D", &cx_d)] {
+        assert_eq!(
+            followers_by_leader(project_id, cx),
+            &[(peer_id_a, vec![peer_id_b, peer_id_c])],
+            "followers seen by {name}"
+        );
+    }
+
+    // Client D follows client B, then switches to following client C.
+    workspace_d.update(cx_d, |workspace, cx| workspace.follow(peer_id_b, cx));
+    cx_a.executor().run_until_parked();
+    workspace_d.update(cx_d, |workspace, cx| workspace.follow(peer_id_c, cx));
+
+    // All clients see that D is following C
+    cx_a.executor().run_until_parked();
+    for (name, cx) in [("A", &cx_a), ("B", &cx_b), ("C", &cx_c), ("D", &cx_d)] {
+        assert_eq!(
+            followers_by_leader(project_id, cx),
+            &[
+                (peer_id_a, vec![peer_id_b, peer_id_c]),
+                (peer_id_c, vec![peer_id_d])
+            ],
+            "followers seen by {name}"
+        );
+    }
+
+    // Client C closes the project.
+    let weak_workspace_c = workspace_c.downgrade();
+    workspace_c.update(cx_c, |workspace, cx| {
+        workspace.close_window(&Default::default(), cx);
+    });
+    cx_c.update(|_| {
+        drop(workspace_c);
+    });
+    cx_b.executor().run_until_parked();
+    // are you sure you want to leave the call?
+    cx_c.simulate_prompt_answer(0);
+    cx_b.executor().run_until_parked();
+    executor.run_until_parked();
+
+    weak_workspace_c.assert_dropped();
+    weak_project_c.assert_dropped();
+
+    // Clients A and B see that client B is following A, and client C is not present in the followers.
+    executor.run_until_parked();
+    for (name, cx) in [("A", &cx_a), ("B", &cx_b), ("D", &cx_d)] {
+        assert_eq!(
+            followers_by_leader(project_id, cx),
+            &[(peer_id_a, vec![peer_id_b]),],
+            "followers seen by {name}"
+        );
+    }
+
+    // When client A activates a different editor, client B does so as well.
+    workspace_a.update(cx_a, |workspace, cx| {
+        workspace.activate_item(&editor_a1, cx)
+    });
+    executor.run_until_parked();
+    workspace_b.update(cx_b, |workspace, cx| {
+        assert_eq!(
+            workspace.active_item(cx).unwrap().item_id(),
+            editor_b1.item_id()
+        );
+    });
+
+    // When client A opens a multibuffer, client B does so as well.
+    let multibuffer_a = cx_a.new_model(|cx| {
+        let buffer_a1 = project_a.update(cx, |project, cx| {
+            project
+                .get_open_buffer(&(worktree_id, "1.txt").into(), cx)
+                .unwrap()
+        });
+        let buffer_a2 = project_a.update(cx, |project, cx| {
+            project
+                .get_open_buffer(&(worktree_id, "2.txt").into(), cx)
+                .unwrap()
+        });
+        let mut result = MultiBuffer::new(0, Capability::ReadWrite);
+        result.push_excerpts(
+            buffer_a1,
+            [ExcerptRange {
+                context: 0..3,
+                primary: None,
+            }],
+            cx,
+        );
+        result.push_excerpts(
+            buffer_a2,
+            [ExcerptRange {
+                context: 4..7,
+                primary: None,
+            }],
+            cx,
+        );
+        result
+    });
+    let multibuffer_editor_a = workspace_a.update(cx_a, |workspace, cx| {
+        let editor =
+            cx.new_view(|cx| Editor::for_multibuffer(multibuffer_a, Some(project_a.clone()), cx));
+        workspace.add_item(Box::new(editor.clone()), cx);
+        editor
+    });
+    executor.run_until_parked();
+    let multibuffer_editor_b = workspace_b.update(cx_b, |workspace, cx| {
+        workspace
+            .active_item(cx)
+            .unwrap()
+            .downcast::<Editor>()
+            .unwrap()
+    });
+    assert_eq!(
+        multibuffer_editor_a.update(cx_a, |editor, cx| editor.text(cx)),
+        multibuffer_editor_b.update(cx_b, |editor, cx| editor.text(cx)),
+    );
+
+    // When client A navigates back and forth, client B does so as well.
+    workspace_a
+        .update(cx_a, |workspace, cx| {
+            workspace.go_back(workspace.active_pane().downgrade(), cx)
+        })
+        .await
+        .unwrap();
+    executor.run_until_parked();
+    workspace_b.update(cx_b, |workspace, cx| {
+        assert_eq!(
+            workspace.active_item(cx).unwrap().item_id(),
+            editor_b1.item_id()
+        );
+    });
+
+    workspace_a
+        .update(cx_a, |workspace, cx| {
+            workspace.go_back(workspace.active_pane().downgrade(), cx)
+        })
+        .await
+        .unwrap();
+    executor.run_until_parked();
+    workspace_b.update(cx_b, |workspace, cx| {
+        assert_eq!(
+            workspace.active_item(cx).unwrap().item_id(),
+            editor_b2.item_id()
+        );
+    });
+
+    workspace_a
+        .update(cx_a, |workspace, cx| {
+            workspace.go_forward(workspace.active_pane().downgrade(), cx)
+        })
+        .await
+        .unwrap();
+    executor.run_until_parked();
+    workspace_b.update(cx_b, |workspace, cx| {
+        assert_eq!(
+            workspace.active_item(cx).unwrap().item_id(),
+            editor_b1.item_id()
+        );
+    });
+
+    // Changes to client A's editor are reflected on client B.
+    editor_a1.update(cx_a, |editor, cx| {
+        editor.change_selections(None, cx, |s| s.select_ranges([1..1, 2..2]));
+    });
+    executor.run_until_parked();
+    cx_b.background_executor.run_until_parked();
+    editor_b1.update(cx_b, |editor, cx| {
+        assert_eq!(editor.selections.ranges(cx), &[1..1, 2..2]);
+    });
+
+    editor_a1.update(cx_a, |editor, cx| editor.set_text("TWO", cx));
+    executor.run_until_parked();
+    editor_b1.update(cx_b, |editor, cx| assert_eq!(editor.text(cx), "TWO"));
+
+    editor_a1.update(cx_a, |editor, cx| {
+        editor.change_selections(None, cx, |s| s.select_ranges([3..3]));
+        editor.set_scroll_position(point(0., 100.), cx);
+    });
+    executor.run_until_parked();
+    editor_b1.update(cx_b, |editor, cx| {
+        assert_eq!(editor.selections.ranges(cx), &[3..3]);
+    });
+
+    // After unfollowing, client B stops receiving updates from client A.
+    workspace_b.update(cx_b, |workspace, cx| {
+        workspace.unfollow(&workspace.active_pane().clone(), cx)
+    });
+    workspace_a.update(cx_a, |workspace, cx| {
+        workspace.activate_item(&editor_a2, cx)
+    });
+    executor.run_until_parked();
+    assert_eq!(
+        workspace_b.update(cx_b, |workspace, cx| workspace
+            .active_item(cx)
+            .unwrap()
+            .item_id()),
+        editor_b1.item_id()
+    );
+
+    // Client A starts following client B.
+    workspace_a.update(cx_a, |workspace, cx| workspace.follow(peer_id_b, cx));
+    executor.run_until_parked();
+    assert_eq!(
+        workspace_a.update(cx_a, |workspace, _| workspace.leader_for_pane(&pane_a)),
+        Some(peer_id_b)
+    );
+    assert_eq!(
+        workspace_a.update(cx_a, |workspace, cx| workspace
+            .active_item(cx)
+            .unwrap()
+            .item_id()),
+        editor_a1.item_id()
+    );
+
+    // Client B activates an external window, which causes a new screen-sharing item to be added to the pane.
+    let display = MacOSDisplay::new();
+    active_call_b
+        .update(cx_b, |call, cx| call.set_location(None, cx))
+        .await
+        .unwrap();
+    active_call_b
+        .update(cx_b, |call, cx| {
+            call.room().unwrap().update(cx, |room, cx| {
+                room.set_display_sources(vec![display.clone()]);
+                room.share_screen(cx)
+            })
+        })
+        .await
+        .unwrap();
+    executor.run_until_parked();
+    let shared_screen = workspace_a.update(cx_a, |workspace, cx| {
+        workspace
+            .active_item(cx)
+            .expect("no active item")
+            .downcast::<SharedScreen>()
+            .expect("active item isn't a shared screen")
+    });
+
+    // Client B activates Zed again, which causes the previous editor to become focused again.
+    active_call_b
+        .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
+        .await
+        .unwrap();
+    executor.run_until_parked();
+    workspace_a.update(cx_a, |workspace, cx| {
+        assert_eq!(
+            workspace.active_item(cx).unwrap().item_id(),
+            editor_a1.item_id()
+        )
+    });
+
+    // Client B activates a multibuffer that was created by following client A. Client A returns to that multibuffer.
+    workspace_b.update(cx_b, |workspace, cx| {
+        workspace.activate_item(&multibuffer_editor_b, cx)
+    });
+    executor.run_until_parked();
+    workspace_a.update(cx_a, |workspace, cx| {
+        assert_eq!(
+            workspace.active_item(cx).unwrap().item_id(),
+            multibuffer_editor_a.item_id()
+        )
+    });
+
+    // Client B activates a panel, and the previously-opened screen-sharing item gets activated.
+    let panel = cx_b.new_view(|cx| TestPanel::new(DockPosition::Left, cx));
+    workspace_b.update(cx_b, |workspace, cx| {
+        workspace.add_panel(panel, cx);
+        workspace.toggle_panel_focus::<TestPanel>(cx);
+    });
+    executor.run_until_parked();
+    assert_eq!(
+        workspace_a.update(cx_a, |workspace, cx| workspace
+            .active_item(cx)
+            .unwrap()
+            .item_id()),
+        shared_screen.item_id()
+    );
+
+    // Toggling the focus back to the pane causes client A to return to the multibuffer.
+    workspace_b.update(cx_b, |workspace, cx| {
+        workspace.toggle_panel_focus::<TestPanel>(cx);
+    });
+    executor.run_until_parked();
+    workspace_a.update(cx_a, |workspace, cx| {
+        assert_eq!(
+            workspace.active_item(cx).unwrap().item_id(),
+            multibuffer_editor_a.item_id()
+        )
+    });
+
+    // Client B activates an item that doesn't implement following,
+    // so the previously-opened screen-sharing item gets activated.
+    let unfollowable_item = cx_b.new_view(|cx| TestItem::new(cx));
+    workspace_b.update(cx_b, |workspace, cx| {
+        workspace.active_pane().update(cx, |pane, cx| {
+            pane.add_item(Box::new(unfollowable_item), true, true, None, cx)
+        })
+    });
+    executor.run_until_parked();
+    assert_eq!(
+        workspace_a.update(cx_a, |workspace, cx| workspace
+            .active_item(cx)
+            .unwrap()
+            .item_id()),
+        shared_screen.item_id()
+    );
+
+    // Following interrupts when client B disconnects.
+    client_b.disconnect(&cx_b.to_async());
+    executor.advance_clock(RECONNECT_TIMEOUT);
+    assert_eq!(
+        workspace_a.update(cx_a, |workspace, _| workspace.leader_for_pane(&pane_a)),
+        None
+    );
+}
+
+#[gpui::test]
+async fn test_following_tab_order(
+    executor: BackgroundExecutor,
+    cx_a: &mut TestAppContext,
+    cx_b: &mut TestAppContext,
+) {
+    let mut server = TestServer::start(executor.clone()).await;
+    let client_a = server.create_client(cx_a, "user_a").await;
+    let client_b = server.create_client(cx_b, "user_b").await;
+    server
+        .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
+        .await;
+    let active_call_a = cx_a.read(ActiveCall::global);
+    let active_call_b = cx_b.read(ActiveCall::global);
+
+    cx_a.update(editor::init);
+    cx_b.update(editor::init);
+
+    client_a
+        .fs()
+        .insert_tree(
+            "/a",
+            json!({
+                "1.txt": "one",
+                "2.txt": "two",
+                "3.txt": "three",
+            }),
+        )
+        .await;
+    let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
+    active_call_a
+        .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
+        .await
+        .unwrap();
+
+    let project_id = active_call_a
+        .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
+        .await
+        .unwrap();
+    let project_b = client_b.build_remote_project(project_id, cx_b).await;
+    active_call_b
+        .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
+        .await
+        .unwrap();
+
+    let (workspace_a, cx_a) = client_a.build_workspace(&project_a, cx_a);
+    let pane_a = workspace_a.update(cx_a, |workspace, _| workspace.active_pane().clone());
+
+    let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
+    let pane_b = workspace_b.update(cx_b, |workspace, _| workspace.active_pane().clone());
+
+    let client_b_id = project_a.update(cx_a, |project, _| {
+        project.collaborators().values().next().unwrap().peer_id
+    });
+
+    //Open 1, 3 in that order on client A
+    workspace_a
+        .update(cx_a, |workspace, cx| {
+            workspace.open_path((worktree_id, "1.txt"), None, true, cx)
+        })
+        .await
+        .unwrap();
+    workspace_a
+        .update(cx_a, |workspace, cx| {
+            workspace.open_path((worktree_id, "3.txt"), None, true, cx)
+        })
+        .await
+        .unwrap();
+
+    let pane_paths = |pane: &View<workspace::Pane>, cx: &mut VisualTestContext| {
+        pane.update(cx, |pane, cx| {
+            pane.items()
+                .map(|item| {
+                    item.project_path(cx)
+                        .unwrap()
+                        .path
+                        .to_str()
+                        .unwrap()
+                        .to_owned()
+                })
+                .collect::<Vec<_>>()
+        })
+    };
+
+    //Verify that the tabs opened in the order we expect
+    assert_eq!(&pane_paths(&pane_a, cx_a), &["1.txt", "3.txt"]);
+
+    //Follow client B as client A
+    workspace_a.update(cx_a, |workspace, cx| workspace.follow(client_b_id, cx));
+    executor.run_until_parked();
+
+    //Open just 2 on client B
+    workspace_b
+        .update(cx_b, |workspace, cx| {
+            workspace.open_path((worktree_id, "2.txt"), None, true, cx)
+        })
+        .await
+        .unwrap();
+    executor.run_until_parked();
+
+    // Verify that newly opened followed file is at the end
+    assert_eq!(&pane_paths(&pane_a, cx_a), &["1.txt", "3.txt", "2.txt"]);
+
+    //Open just 1 on client B
+    workspace_b
+        .update(cx_b, |workspace, cx| {
+            workspace.open_path((worktree_id, "1.txt"), None, true, cx)
+        })
+        .await
+        .unwrap();
+    assert_eq!(&pane_paths(&pane_b, cx_b), &["2.txt", "1.txt"]);
+    executor.run_until_parked();
+
+    // Verify that following into 1 did not reorder
+    assert_eq!(&pane_paths(&pane_a, cx_a), &["1.txt", "3.txt", "2.txt"]);
+}
+
+#[gpui::test(iterations = 10)]
+async fn test_peers_following_each_other(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
+    let executor = cx_a.executor();
+    let mut server = TestServer::start(executor.clone()).await;
+    let client_a = server.create_client(cx_a, "user_a").await;
+    let client_b = server.create_client(cx_b, "user_b").await;
+    server
+        .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
+        .await;
+    let active_call_a = cx_a.read(ActiveCall::global);
+    let active_call_b = cx_b.read(ActiveCall::global);
+
+    cx_a.update(editor::init);
+    cx_b.update(editor::init);
+
+    // Client A shares a project.
+    client_a
+        .fs()
+        .insert_tree(
+            "/a",
+            json!({
+                "1.txt": "one",
+                "2.txt": "two",
+                "3.txt": "three",
+                "4.txt": "four",
+            }),
+        )
+        .await;
+    let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
+    active_call_a
+        .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
+        .await
+        .unwrap();
+    let project_id = active_call_a
+        .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
+        .await
+        .unwrap();
+
+    // Client B joins the project.
+    let project_b = client_b.build_remote_project(project_id, cx_b).await;
+    active_call_b
+        .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
+        .await
+        .unwrap();
+
+    // Client A opens a file.
+    let (workspace_a, cx_a) = client_a.build_workspace(&project_a, cx_a);
+    workspace_a
+        .update(cx_a, |workspace, cx| {
+            workspace.open_path((worktree_id, "1.txt"), None, true, cx)
+        })
+        .await
+        .unwrap()
+        .downcast::<Editor>()
+        .unwrap();
+
+    // Client B opens a different file.
+    let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
+    workspace_b
+        .update(cx_b, |workspace, cx| {
+            workspace.open_path((worktree_id, "2.txt"), None, true, cx)
+        })
+        .await
+        .unwrap()
+        .downcast::<Editor>()
+        .unwrap();
+
+    // Clients A and B follow each other in split panes
+    workspace_a.update(cx_a, |workspace, cx| {
+        workspace.split_and_clone(workspace.active_pane().clone(), SplitDirection::Right, cx);
+    });
+    workspace_a.update(cx_a, |workspace, cx| {
+        workspace.follow(client_b.peer_id().unwrap(), cx)
+    });
+    executor.run_until_parked();
+    workspace_b.update(cx_b, |workspace, cx| {
+        workspace.split_and_clone(workspace.active_pane().clone(), SplitDirection::Right, cx);
+    });
+    workspace_b.update(cx_b, |workspace, cx| {
+        workspace.follow(client_a.peer_id().unwrap(), cx)
+    });
+    executor.run_until_parked();
+
+    // Clients A and B return focus to the original files they had open
+    workspace_a.update(cx_a, |workspace, cx| workspace.activate_next_pane(cx));
+    workspace_b.update(cx_b, |workspace, cx| workspace.activate_next_pane(cx));
+    executor.run_until_parked();
+
+    // Both clients see the other client's focused file in their right pane.
+    assert_eq!(
+        pane_summaries(&workspace_a, cx_a),
+        &[
+            PaneSummary {
+                active: true,
+                leader: None,
+                items: vec![(true, "1.txt".into())]
+            },
+            PaneSummary {
+                active: false,
+                leader: client_b.peer_id(),
+                items: vec![(false, "1.txt".into()), (true, "2.txt".into())]
+            },
+        ]
+    );
+    assert_eq!(
+        pane_summaries(&workspace_b, cx_b),
+        &[
+            PaneSummary {
+                active: true,
+                leader: None,
+                items: vec![(true, "2.txt".into())]
+            },
+            PaneSummary {
+                active: false,
+                leader: client_a.peer_id(),
+                items: vec![(false, "2.txt".into()), (true, "1.txt".into())]
+            },
+        ]
+    );
+
+    // Clients A and B each open a new file.
+    workspace_a
+        .update(cx_a, |workspace, cx| {
+            workspace.open_path((worktree_id, "3.txt"), None, true, cx)
+        })
+        .await
+        .unwrap();
+
+    workspace_b
+        .update(cx_b, |workspace, cx| {
+            workspace.open_path((worktree_id, "4.txt"), None, true, cx)
+        })
+        .await
+        .unwrap();
+    executor.run_until_parked();
+
+    // Both client's see the other client open the new file, but keep their
+    // focus on their own active pane.
+    assert_eq!(
+        pane_summaries(&workspace_a, cx_a),
+        &[
+            PaneSummary {
+                active: true,
+                leader: None,
+                items: vec![(false, "1.txt".into()), (true, "3.txt".into())]
+            },
+            PaneSummary {
+                active: false,
+                leader: client_b.peer_id(),
+                items: vec![
+                    (false, "1.txt".into()),
+                    (false, "2.txt".into()),
+                    (true, "4.txt".into())
+                ]
+            },
+        ]
+    );
+    assert_eq!(
+        pane_summaries(&workspace_b, cx_b),
+        &[
+            PaneSummary {
+                active: true,
+                leader: None,
+                items: vec![(false, "2.txt".into()), (true, "4.txt".into())]
+            },
+            PaneSummary {
+                active: false,
+                leader: client_a.peer_id(),
+                items: vec![
+                    (false, "2.txt".into()),
+                    (false, "1.txt".into()),
+                    (true, "3.txt".into())
+                ]
+            },
+        ]
+    );
+
+    // Client A focuses their right pane, in which they're following client B.
+    workspace_a.update(cx_a, |workspace, cx| workspace.activate_next_pane(cx));
+    executor.run_until_parked();
+
+    // Client B sees that client A is now looking at the same file as them.
+    assert_eq!(
+        pane_summaries(&workspace_a, cx_a),
+        &[
+            PaneSummary {
+                active: false,
+                leader: None,
+                items: vec![(false, "1.txt".into()), (true, "3.txt".into())]
+            },
+            PaneSummary {
+                active: true,
+                leader: client_b.peer_id(),
+                items: vec![
+                    (false, "1.txt".into()),
+                    (false, "2.txt".into()),
+                    (true, "4.txt".into())
+                ]
+            },
+        ]
+    );
+    assert_eq!(
+        pane_summaries(&workspace_b, cx_b),
+        &[
+            PaneSummary {
+                active: true,
+                leader: None,
+                items: vec![(false, "2.txt".into()), (true, "4.txt".into())]
+            },
+            PaneSummary {
+                active: false,
+                leader: client_a.peer_id(),
+                items: vec![
+                    (false, "2.txt".into()),
+                    (false, "1.txt".into()),
+                    (false, "3.txt".into()),
+                    (true, "4.txt".into())
+                ]
+            },
+        ]
+    );
+
+    // Client B focuses their right pane, in which they're following client A,
+    // who is following them.
+    workspace_b.update(cx_b, |workspace, cx| workspace.activate_next_pane(cx));
+    executor.run_until_parked();
+
+    // Client A sees that client B is now looking at the same file as them.
+    assert_eq!(
+        pane_summaries(&workspace_b, cx_b),
+        &[
+            PaneSummary {
+                active: false,
+                leader: None,
+                items: vec![(false, "2.txt".into()), (true, "4.txt".into())]
+            },
+            PaneSummary {
+                active: true,
+                leader: client_a.peer_id(),
+                items: vec![
+                    (false, "2.txt".into()),
+                    (false, "1.txt".into()),
+                    (false, "3.txt".into()),
+                    (true, "4.txt".into())
+                ]
+            },
+        ]
+    );
+    assert_eq!(
+        pane_summaries(&workspace_a, cx_a),
+        &[
+            PaneSummary {
+                active: false,
+                leader: None,
+                items: vec![(false, "1.txt".into()), (true, "3.txt".into())]
+            },
+            PaneSummary {
+                active: true,
+                leader: client_b.peer_id(),
+                items: vec![
+                    (false, "1.txt".into()),
+                    (false, "2.txt".into()),
+                    (true, "4.txt".into())
+                ]
+            },
+        ]
+    );
+
+    // Client B focuses a file that they previously followed A to, breaking
+    // the follow.
+    workspace_b.update(cx_b, |workspace, cx| {
+        workspace.active_pane().update(cx, |pane, cx| {
+            pane.activate_prev_item(true, cx);
+        });
+    });
+    executor.run_until_parked();
+
+    // Both clients see that client B is looking at that previous file.
+    assert_eq!(
+        pane_summaries(&workspace_b, cx_b),
+        &[
+            PaneSummary {
+                active: false,
+                leader: None,
+                items: vec![(false, "2.txt".into()), (true, "4.txt".into())]
+            },
+            PaneSummary {
+                active: true,
+                leader: None,
+                items: vec![
+                    (false, "2.txt".into()),
+                    (false, "1.txt".into()),
+                    (true, "3.txt".into()),
+                    (false, "4.txt".into())
+                ]
+            },
+        ]
+    );
+    assert_eq!(
+        pane_summaries(&workspace_a, cx_a),
+        &[
+            PaneSummary {
+                active: false,
+                leader: None,
+                items: vec![(false, "1.txt".into()), (true, "3.txt".into())]
+            },
+            PaneSummary {
+                active: true,
+                leader: client_b.peer_id(),
+                items: vec![
+                    (false, "1.txt".into()),
+                    (false, "2.txt".into()),
+                    (false, "4.txt".into()),
+                    (true, "3.txt".into()),
+                ]
+            },
+        ]
+    );
+
+    // Client B closes tabs, some of which were originally opened by client A,
+    // and some of which were originally opened by client B.
+    workspace_b.update(cx_b, |workspace, cx| {
+        workspace.active_pane().update(cx, |pane, cx| {
+            pane.close_inactive_items(&Default::default(), cx)
+                .unwrap()
+                .detach();
+        });
+    });
+
+    executor.run_until_parked();
+
+    // Both clients see that Client B is looking at the previous tab.
+    assert_eq!(
+        pane_summaries(&workspace_b, cx_b),
+        &[
+            PaneSummary {
+                active: false,
+                leader: None,
+                items: vec![(false, "2.txt".into()), (true, "4.txt".into())]
+            },
+            PaneSummary {
+                active: true,
+                leader: None,
+                items: vec![(true, "3.txt".into()),]
+            },
+        ]
+    );
+    assert_eq!(
+        pane_summaries(&workspace_a, cx_a),
+        &[
+            PaneSummary {
+                active: false,
+                leader: None,
+                items: vec![(false, "1.txt".into()), (true, "3.txt".into())]
+            },
+            PaneSummary {
+                active: true,
+                leader: client_b.peer_id(),
+                items: vec![
+                    (false, "1.txt".into()),
+                    (false, "2.txt".into()),
+                    (false, "4.txt".into()),
+                    (true, "3.txt".into()),
+                ]
+            },
+        ]
+    );
+
+    // Client B follows client A again.
+    workspace_b.update(cx_b, |workspace, cx| {
+        workspace.follow(client_a.peer_id().unwrap(), cx)
+    });
+    executor.run_until_parked();
+    // Client A cycles through some tabs.
+    workspace_a.update(cx_a, |workspace, cx| {
+        workspace.active_pane().update(cx, |pane, cx| {
+            pane.activate_prev_item(true, cx);
+        });
+    });
+    executor.run_until_parked();
+
+    // Client B follows client A into those tabs.
+    assert_eq!(
+        pane_summaries(&workspace_a, cx_a),
+        &[
+            PaneSummary {
+                active: false,
+                leader: None,
+                items: vec![(false, "1.txt".into()), (true, "3.txt".into())]
+            },
+            PaneSummary {
+                active: true,
+                leader: None,
+                items: vec![
+                    (false, "1.txt".into()),
+                    (false, "2.txt".into()),
+                    (true, "4.txt".into()),
+                    (false, "3.txt".into()),
+                ]
+            },
+        ]
+    );
+    assert_eq!(
+        pane_summaries(&workspace_b, cx_b),
+        &[
+            PaneSummary {
+                active: false,
+                leader: None,
+                items: vec![(false, "2.txt".into()), (true, "4.txt".into())]
+            },
+            PaneSummary {
+                active: true,
+                leader: client_a.peer_id(),
+                items: vec![(false, "3.txt".into()), (true, "4.txt".into())]
+            },
+        ]
+    );
+
+    workspace_a.update(cx_a, |workspace, cx| {
+        workspace.active_pane().update(cx, |pane, cx| {
+            pane.activate_prev_item(true, cx);
+        });
+    });
+    executor.run_until_parked();
+
+    assert_eq!(
+        pane_summaries(&workspace_a, cx_a),
+        &[
+            PaneSummary {
+                active: false,
+                leader: None,
+                items: vec![(false, "1.txt".into()), (true, "3.txt".into())]
+            },
+            PaneSummary {
+                active: true,
+                leader: None,
+                items: vec![
+                    (false, "1.txt".into()),
+                    (true, "2.txt".into()),
+                    (false, "4.txt".into()),
+                    (false, "3.txt".into()),
+                ]
+            },
+        ]
+    );
+    assert_eq!(
+        pane_summaries(&workspace_b, cx_b),
+        &[
+            PaneSummary {
+                active: false,
+                leader: None,
+                items: vec![(false, "2.txt".into()), (true, "4.txt".into())]
+            },
+            PaneSummary {
+                active: true,
+                leader: client_a.peer_id(),
+                items: vec![
+                    (false, "3.txt".into()),
+                    (false, "4.txt".into()),
+                    (true, "2.txt".into())
+                ]
+            },
+        ]
+    );
+
+    workspace_a.update(cx_a, |workspace, cx| {
+        workspace.active_pane().update(cx, |pane, cx| {
+            pane.activate_prev_item(true, cx);
+        });
+    });
+    executor.run_until_parked();
+
+    assert_eq!(
+        pane_summaries(&workspace_a, cx_a),
+        &[
+            PaneSummary {
+                active: false,
+                leader: None,
+                items: vec![(false, "1.txt".into()), (true, "3.txt".into())]
+            },
+            PaneSummary {
+                active: true,
+                leader: None,
+                items: vec![
+                    (true, "1.txt".into()),
+                    (false, "2.txt".into()),
+                    (false, "4.txt".into()),
+                    (false, "3.txt".into()),
+                ]
+            },
+        ]
+    );
+    assert_eq!(
+        pane_summaries(&workspace_b, cx_b),
+        &[
+            PaneSummary {
+                active: false,
+                leader: None,
+                items: vec![(false, "2.txt".into()), (true, "4.txt".into())]
+            },
+            PaneSummary {
+                active: true,
+                leader: client_a.peer_id(),
+                items: vec![
+                    (false, "3.txt".into()),
+                    (false, "4.txt".into()),
+                    (false, "2.txt".into()),
+                    (true, "1.txt".into()),
+                ]
+            },
+        ]
+    );
+}
+
+#[gpui::test(iterations = 10)]
+async fn test_auto_unfollowing(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
+    // 2 clients connect to a server.
+    let executor = cx_a.executor();
+    let mut server = TestServer::start(executor.clone()).await;
+    let client_a = server.create_client(cx_a, "user_a").await;
+    let client_b = server.create_client(cx_b, "user_b").await;
+    server
+        .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
+        .await;
+    let active_call_a = cx_a.read(ActiveCall::global);
+    let active_call_b = cx_b.read(ActiveCall::global);
+
+    cx_a.update(editor::init);
+    cx_b.update(editor::init);
+
+    // Client A shares a project.
+    client_a
+        .fs()
+        .insert_tree(
+            "/a",
+            json!({
+                "1.txt": "one",
+                "2.txt": "two",
+                "3.txt": "three",
+            }),
+        )
+        .await;
+    let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
+    active_call_a
+        .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
+        .await
+        .unwrap();
+
+    let project_id = active_call_a
+        .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
+        .await
+        .unwrap();
+    let project_b = client_b.build_remote_project(project_id, cx_b).await;
+    active_call_b
+        .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
+        .await
+        .unwrap();
+
+    let (workspace_a, cx_a) = client_a.build_workspace(&project_a, cx_a);
+    let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
+
+    let _editor_a1 = workspace_a
+        .update(cx_a, |workspace, cx| {
+            workspace.open_path((worktree_id, "1.txt"), None, true, cx)
+        })
+        .await
+        .unwrap()
+        .downcast::<Editor>()
+        .unwrap();
+
+    // Client B starts following client A.
+    let pane_b = workspace_b.update(cx_b, |workspace, _| workspace.active_pane().clone());
+    let leader_id = project_b.update(cx_b, |project, _| {
+        project.collaborators().values().next().unwrap().peer_id
+    });
+    workspace_b.update(cx_b, |workspace, cx| workspace.follow(leader_id, cx));
+    executor.run_until_parked();
+    assert_eq!(
+        workspace_b.update(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
+        Some(leader_id)
+    );
+    let editor_b2 = workspace_b.update(cx_b, |workspace, cx| {
+        workspace
+            .active_item(cx)
+            .unwrap()
+            .downcast::<Editor>()
+            .unwrap()
+    });
+
+    // When client B moves, it automatically stops following client A.
+    editor_b2.update(cx_b, |editor, cx| editor.move_right(&editor::MoveRight, cx));
+    assert_eq!(
+        workspace_b.update(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
+        None
+    );
+
+    workspace_b.update(cx_b, |workspace, cx| workspace.follow(leader_id, cx));
+    executor.run_until_parked();
+    assert_eq!(
+        workspace_b.update(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
+        Some(leader_id)
+    );
+
+    // When client B edits, it automatically stops following client A.
+    editor_b2.update(cx_b, |editor, cx| editor.insert("X", cx));
+    assert_eq!(
+        workspace_b.update(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
+        None
+    );
+
+    workspace_b.update(cx_b, |workspace, cx| workspace.follow(leader_id, cx));
+    executor.run_until_parked();
+    assert_eq!(
+        workspace_b.update(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
+        Some(leader_id)
+    );
+
+    // When client B scrolls, it automatically stops following client A.
+    editor_b2.update(cx_b, |editor, cx| {
+        editor.set_scroll_position(point(0., 3.), cx)
+    });
+    assert_eq!(
+        workspace_b.update(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
+        None
+    );
+
+    workspace_b.update(cx_b, |workspace, cx| workspace.follow(leader_id, cx));
+    executor.run_until_parked();
+    assert_eq!(
+        workspace_b.update(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
+        Some(leader_id)
+    );
+
+    // When client B activates a different pane, it continues following client A in the original pane.
+    workspace_b.update(cx_b, |workspace, cx| {
+        workspace.split_and_clone(pane_b.clone(), SplitDirection::Right, cx)
+    });
+    assert_eq!(
+        workspace_b.update(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
+        Some(leader_id)
+    );
+
+    workspace_b.update(cx_b, |workspace, cx| workspace.activate_next_pane(cx));
+    assert_eq!(
+        workspace_b.update(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
+        Some(leader_id)
+    );
+
+    // When client B activates a different item in the original pane, it automatically stops following client A.
+    workspace_b
+        .update(cx_b, |workspace, cx| {
+            workspace.open_path((worktree_id, "2.txt"), None, true, cx)
+        })
+        .await
+        .unwrap();
+    assert_eq!(
+        workspace_b.update(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
+        None
+    );
+}
+
+#[gpui::test(iterations = 10)]
+async fn test_peers_simultaneously_following_each_other(
+    cx_a: &mut TestAppContext,
+    cx_b: &mut TestAppContext,
+) {
+    let executor = cx_a.executor();
+    let mut server = TestServer::start(executor.clone()).await;
+    let client_a = server.create_client(cx_a, "user_a").await;
+    let client_b = server.create_client(cx_b, "user_b").await;
+    server
+        .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
+        .await;
+    let active_call_a = cx_a.read(ActiveCall::global);
+
+    cx_a.update(editor::init);
+    cx_b.update(editor::init);
+
+    client_a.fs().insert_tree("/a", json!({})).await;
+    let (project_a, _) = client_a.build_local_project("/a", cx_a).await;
+    let (workspace_a, cx_a) = client_a.build_workspace(&project_a, cx_a);
+    let project_id = active_call_a
+        .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
+        .await
+        .unwrap();
+
+    let project_b = client_b.build_remote_project(project_id, cx_b).await;
+    let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
+
+    executor.run_until_parked();
+    let client_a_id = project_b.update(cx_b, |project, _| {
+        project.collaborators().values().next().unwrap().peer_id
+    });
+    let client_b_id = project_a.update(cx_a, |project, _| {
+        project.collaborators().values().next().unwrap().peer_id
+    });
+
+    workspace_a.update(cx_a, |workspace, cx| workspace.follow(client_b_id, cx));
+    workspace_b.update(cx_b, |workspace, cx| workspace.follow(client_a_id, cx));
+    executor.run_until_parked();
+
+    workspace_a.update(cx_a, |workspace, _| {
+        assert_eq!(
+            workspace.leader_for_pane(workspace.active_pane()),
+            Some(client_b_id)
+        );
+    });
+    workspace_b.update(cx_b, |workspace, _| {
+        assert_eq!(
+            workspace.leader_for_pane(workspace.active_pane()),
+            Some(client_a_id)
+        );
+    });
+}
+
+#[gpui::test(iterations = 10)]
+async fn test_following_across_workspaces(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
+    // a and b join a channel/call
+    // a shares project 1
+    // b shares project 2
+    //
+    // b follows a: causes project 2 to be joined, and b to follow a.
+    // b opens a different file in project 2, a follows b
+    // b opens a different file in project 1, a cannot follow b
+    // b shares the project, a joins the project and follows b
+    let executor = cx_a.executor();
+    let mut server = TestServer::start(executor.clone()).await;
+    let client_a = server.create_client(cx_a, "user_a").await;
+    let client_b = server.create_client(cx_b, "user_b").await;
+    cx_a.update(editor::init);
+    cx_b.update(editor::init);
+
+    client_a
+        .fs()
+        .insert_tree(
+            "/a",
+            json!({
+                "w.rs": "",
+                "x.rs": "",
+            }),
+        )
+        .await;
+
+    client_b
+        .fs()
+        .insert_tree(
+            "/b",
+            json!({
+                "y.rs": "",
+                "z.rs": "",
+            }),
+        )
+        .await;
+
+    server
+        .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
+        .await;
+    let active_call_a = cx_a.read(ActiveCall::global);
+    let active_call_b = cx_b.read(ActiveCall::global);
+
+    let (project_a, worktree_id_a) = client_a.build_local_project("/a", cx_a).await;
+    let (project_b, worktree_id_b) = client_b.build_local_project("/b", cx_b).await;
+
+    let (workspace_a, cx_a) = client_a.build_workspace(&project_a, cx_a);
+    let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
+
+    cx_a.update(|cx| collab_ui::init(&client_a.app_state, cx));
+    cx_b.update(|cx| collab_ui::init(&client_b.app_state, cx));
+
+    active_call_a
+        .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
+        .await
+        .unwrap();
+
+    active_call_a
+        .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
+        .await
+        .unwrap();
+    active_call_b
+        .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
+        .await
+        .unwrap();
+
+    workspace_a
+        .update(cx_a, |workspace, cx| {
+            workspace.open_path((worktree_id_a, "w.rs"), None, true, cx)
+        })
+        .await
+        .unwrap();
+
+    executor.run_until_parked();
+    assert_eq!(visible_push_notifications(cx_b).len(), 1);
+
+    workspace_b.update(cx_b, |workspace, cx| {
+        workspace.follow(client_a.peer_id().unwrap(), cx)
+    });
+
+    executor.run_until_parked();
+    let window_b_project_a = cx_b
+        .windows()
+        .iter()
+        .max_by_key(|window| window.window_id())
+        .unwrap()
+        .clone();
+
+    let mut cx_b2 = VisualTestContext::from_window(window_b_project_a.clone(), cx_b);
+
+    let workspace_b_project_a = window_b_project_a
+        .downcast::<Workspace>()
+        .unwrap()
+        .root(cx_b)
+        .unwrap();
+
+    // assert that b is following a in project a in w.rs
+    workspace_b_project_a.update(&mut cx_b2, |workspace, cx| {
+        assert!(workspace.is_being_followed(client_a.peer_id().unwrap()));
+        assert_eq!(
+            client_a.peer_id(),
+            workspace.leader_for_pane(workspace.active_pane())
+        );
+        let item = workspace.active_item(cx).unwrap();
+        assert_eq!(
+            item.tab_description(0, cx).unwrap(),
+            SharedString::from("w.rs")
+        );
+    });
+
+    // TODO: in app code, this would be done by the collab_ui.
+    active_call_b
+        .update(&mut cx_b2, |call, cx| {
+            let project = workspace_b_project_a.read(cx).project().clone();
+            call.set_location(Some(&project), cx)
+        })
+        .await
+        .unwrap();
+
+    // assert that there are no share notifications open
+    assert_eq!(visible_push_notifications(cx_b).len(), 0);
+
+    // b moves to x.rs in a's project, and a follows
+    workspace_b_project_a
+        .update(&mut cx_b2, |workspace, cx| {
+            workspace.open_path((worktree_id_a, "x.rs"), None, true, cx)
+        })
+        .await
+        .unwrap();
+
+    executor.run_until_parked();
+    workspace_b_project_a.update(&mut cx_b2, |workspace, cx| {
+        let item = workspace.active_item(cx).unwrap();
+        assert_eq!(
+            item.tab_description(0, cx).unwrap(),
+            SharedString::from("x.rs")
+        );
+    });
+
+    workspace_a.update(cx_a, |workspace, cx| {
+        workspace.follow(client_b.peer_id().unwrap(), cx)
+    });
+
+    executor.run_until_parked();
+    workspace_a.update(cx_a, |workspace, cx| {
+        assert!(workspace.is_being_followed(client_b.peer_id().unwrap()));
+        assert_eq!(
+            client_b.peer_id(),
+            workspace.leader_for_pane(workspace.active_pane())
+        );
+        let item = workspace.active_pane().read(cx).active_item().unwrap();
+        assert_eq!(item.tab_description(0, cx).unwrap(), "x.rs");
+    });
+
+    // b moves to y.rs in b's project, a is still following but can't yet see
+    workspace_b
+        .update(cx_b, |workspace, cx| {
+            workspace.open_path((worktree_id_b, "y.rs"), None, true, cx)
+        })
+        .await
+        .unwrap();
+
+    // TODO: in app code, this would be done by the collab_ui.
+    active_call_b
+        .update(cx_b, |call, cx| {
+            let project = workspace_b.read(cx).project().clone();
+            call.set_location(Some(&project), cx)
+        })
+        .await
+        .unwrap();
+
+    let project_b_id = active_call_b
+        .update(cx_b, |call, cx| call.share_project(project_b.clone(), cx))
+        .await
+        .unwrap();
+
+    executor.run_until_parked();
+    assert_eq!(visible_push_notifications(cx_a).len(), 1);
+    cx_a.update(|cx| {
+        workspace::join_remote_project(
+            project_b_id,
+            client_b.user_id().unwrap(),
+            client_a.app_state.clone(),
+            cx,
+        )
+    })
+    .await
+    .unwrap();
+
+    executor.run_until_parked();
+
+    assert_eq!(visible_push_notifications(cx_a).len(), 0);
+    let window_a_project_b = cx_a
+        .windows()
+        .iter()
+        .max_by_key(|window| window.window_id())
+        .unwrap()
+        .clone();
+    let cx_a2 = &mut VisualTestContext::from_window(window_a_project_b.clone(), cx_a);
+    let workspace_a_project_b = window_a_project_b
+        .downcast::<Workspace>()
+        .unwrap()
+        .root(cx_a)
+        .unwrap();
+
+    workspace_a_project_b.update(cx_a2, |workspace, cx| {
+        assert_eq!(workspace.project().read(cx).remote_id(), Some(project_b_id));
+        assert!(workspace.is_being_followed(client_b.peer_id().unwrap()));
+        assert_eq!(
+            client_b.peer_id(),
+            workspace.leader_for_pane(workspace.active_pane())
+        );
+        let item = workspace.active_item(cx).unwrap();
+        assert_eq!(
+            item.tab_description(0, cx).unwrap(),
+            SharedString::from("y.rs")
+        );
+    });
+}
+
+#[gpui::test]
+async fn test_following_into_excluded_file(
+    mut cx_a: &mut TestAppContext,
+    mut cx_b: &mut TestAppContext,
+) {
+    let executor = cx_a.executor();
+    let mut server = TestServer::start(executor.clone()).await;
+    let client_a = server.create_client(cx_a, "user_a").await;
+    let client_b = server.create_client(cx_b, "user_b").await;
+    for cx in [&mut cx_a, &mut cx_b] {
+        cx.update(|cx| {
+            cx.update_global::<SettingsStore, _>(|store, cx| {
+                store.update_user_settings::<ProjectSettings>(cx, |project_settings| {
+                    project_settings.file_scan_exclusions = Some(vec!["**/.git".to_string()]);
+                });
+            });
+        });
+    }
+    server
+        .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
+        .await;
+    let active_call_a = cx_a.read(ActiveCall::global);
+    let active_call_b = cx_b.read(ActiveCall::global);
+    let peer_id_a = client_a.peer_id().unwrap();
+
+    cx_a.update(editor::init);
+    cx_b.update(editor::init);
+
+    client_a
+        .fs()
+        .insert_tree(
+            "/a",
+            json!({
+                ".git": {
+                    "COMMIT_EDITMSG": "write your commit message here",
+                },
+                "1.txt": "one\none\none",
+                "2.txt": "two\ntwo\ntwo",
+                "3.txt": "three\nthree\nthree",
+            }),
+        )
+        .await;
+    let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
+    active_call_a
+        .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
+        .await
+        .unwrap();
+
+    let project_id = active_call_a
+        .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
+        .await
+        .unwrap();
+    let project_b = client_b.build_remote_project(project_id, cx_b).await;
+    active_call_b
+        .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
+        .await
+        .unwrap();
+
+    let (workspace_a, cx_a) = client_a.build_workspace(&project_a, cx_a);
+    let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
+
+    // Client A opens editors for a regular file and an excluded file.
+    let editor_for_regular = workspace_a
+        .update(cx_a, |workspace, cx| {
+            workspace.open_path((worktree_id, "1.txt"), None, true, cx)
+        })
+        .await
+        .unwrap()
+        .downcast::<Editor>()
+        .unwrap();
+    let editor_for_excluded_a = workspace_a
+        .update(cx_a, |workspace, cx| {
+            workspace.open_path((worktree_id, ".git/COMMIT_EDITMSG"), None, true, cx)
+        })
+        .await
+        .unwrap()
+        .downcast::<Editor>()
+        .unwrap();
+
+    // Client A updates their selections in those editors
+    editor_for_regular.update(cx_a, |editor, cx| {
+        editor.handle_input("a", cx);
+        editor.handle_input("b", cx);
+        editor.handle_input("c", cx);
+        editor.select_left(&Default::default(), cx);
+        assert_eq!(editor.selections.ranges(cx), vec![3..2]);
+    });
+    editor_for_excluded_a.update(cx_a, |editor, cx| {
+        editor.select_all(&Default::default(), cx);
+        editor.handle_input("new commit message", cx);
+        editor.select_left(&Default::default(), cx);
+        assert_eq!(editor.selections.ranges(cx), vec![18..17]);
+    });
+
+    // When client B starts following client A, currently visible file is replicated
+    workspace_b.update(cx_b, |workspace, cx| workspace.follow(peer_id_a, cx));
+    executor.run_until_parked();
+
+    let editor_for_excluded_b = workspace_b.update(cx_b, |workspace, cx| {
+        workspace
+            .active_item(cx)
+            .unwrap()
+            .downcast::<Editor>()
+            .unwrap()
+    });
+    assert_eq!(
+        cx_b.read(|cx| editor_for_excluded_b.project_path(cx)),
+        Some((worktree_id, ".git/COMMIT_EDITMSG").into())
+    );
+    assert_eq!(
+        editor_for_excluded_b.update(cx_b, |editor, cx| editor.selections.ranges(cx)),
+        vec![18..17]
+    );
+
+    // Changes from B to the excluded file are replicated in A's editor
+    editor_for_excluded_b.update(cx_b, |editor, cx| {
+        editor.handle_input("\nCo-Authored-By: B <b@b.b>", cx);
+    });
+    executor.run_until_parked();
+    editor_for_excluded_a.update(cx_a, |editor, cx| {
+        assert_eq!(
+            editor.text(cx),
+            "new commit messag\nCo-Authored-By: B <b@b.b>"
+        );
+    });
+}
+
+fn visible_push_notifications(
+    cx: &mut TestAppContext,
+) -> Vec<gpui::View<ProjectSharedNotification>> {
+    let mut ret = Vec::new();
+    for window in cx.windows() {
+        window
+            .update(cx, |window, _| {
+                if let Ok(handle) = window.downcast::<ProjectSharedNotification>() {
+                    ret.push(handle)
+                }
+            })
+            .unwrap();
+    }
+    ret
+}
+
+#[derive(Debug, PartialEq, Eq)]
+struct PaneSummary {
+    active: bool,
+    leader: Option<PeerId>,
+    items: Vec<(bool, String)>,
+}
+
+fn followers_by_leader(project_id: u64, cx: &TestAppContext) -> Vec<(PeerId, Vec<PeerId>)> {
+    cx.read(|cx| {
+        let active_call = ActiveCall::global(cx).read(cx);
+        let peer_id = active_call.client().peer_id();
+        let room = active_call.room().unwrap().read(cx);
+        let mut result = room
+            .remote_participants()
+            .values()
+            .map(|participant| participant.peer_id)
+            .chain(peer_id)
+            .filter_map(|peer_id| {
+                let followers = room.followers_for(peer_id, project_id);
+                if followers.is_empty() {
+                    None
+                } else {
+                    Some((peer_id, followers.to_vec()))
+                }
+            })
+            .collect::<Vec<_>>();
+        result.sort_by_key(|e| e.0);
+        result
+    })
+}
+
+fn pane_summaries(workspace: &View<Workspace>, cx: &mut VisualTestContext) -> Vec<PaneSummary> {
+    workspace.update(cx, |workspace, cx| {
+        let active_pane = workspace.active_pane();
+        workspace
+            .panes()
+            .iter()
+            .map(|pane| {
+                let leader = workspace.leader_for_pane(pane);
+                let active = pane == active_pane;
+                let pane = pane.read(cx);
+                let active_ix = pane.active_item_index();
+                PaneSummary {
+                    active,
+                    leader,
+                    items: pane
+                        .items()
+                        .enumerate()
+                        .map(|(ix, item)| {
+                            (
+                                ix == active_ix,
+                                item.tab_description(0, cx)
+                                    .map_or(String::new(), |s| s.to_string()),
+                            )
+                        })
+                        .collect(),
+                }
+            })
+            .collect()
+    })
+}

crates/collab_ui/Cargo.toml 🔗

@@ -61,6 +61,7 @@ schemars.workspace = true
 postage.workspace = true
 serde.workspace = true
 serde_derive.workspace = true
+serde_json.workspace = true
 time.workspace = true
 smallvec.workspace = true
 

crates/collab_ui/src/chat_panel.rs 🔗

@@ -7,9 +7,9 @@ use collections::HashMap;
 use db::kvp::KEY_VALUE_STORE;
 use editor::Editor;
 use gpui::{
-    actions, div, list, prelude::*, px, serde_json, AnyElement, AppContext, AsyncWindowContext,
-    ClickEvent, ElementId, EventEmitter, FocusableView, ListOffset, ListScrollEvent, ListState,
-    Model, Render, Subscription, Task, View, ViewContext, VisualContext, WeakView,
+    actions, div, list, prelude::*, px, AnyElement, AppContext, AsyncWindowContext, ClickEvent,
+    ElementId, EventEmitter, FocusableView, ListOffset, ListScrollEvent, ListState, Model, Render,
+    Subscription, Task, View, ViewContext, VisualContext, WeakView,
 };
 use language::LanguageRegistry;
 use menu::Confirm;

crates/collab_ui/src/chat_panel/message_editor.rs 🔗

@@ -271,11 +271,12 @@ mod tests {
 
     fn init_test(cx: &mut TestAppContext) -> Arc<LanguageRegistry> {
         cx.update(|cx| {
+            let settings = SettingsStore::test(cx);
+            cx.set_global(settings);
+
             let http = FakeHttpClient::with_404_response();
             let client = Client::new(http.clone(), cx);
             let user_store = cx.new_model(|cx| UserStore::new(client.clone(), cx));
-            let settings = SettingsStore::test(cx);
-            cx.set_global(settings);
             theme::init(theme::LoadThemes::JustBase, cx);
             language::init(cx);
             editor::init(cx);

crates/collab_ui/src/collab_panel.rs 🔗

@@ -15,12 +15,12 @@ use editor::{Editor, EditorElement, EditorStyle};
 use feature_flags::{ChannelsAlpha, FeatureFlagAppExt, FeatureFlagViewExt};
 use fuzzy::{match_strings, StringMatchCandidate};
 use gpui::{
-    actions, canvas, div, fill, list, overlay, point, prelude::*, px, serde_json, AnyElement,
-    AppContext, AsyncWindowContext, Bounds, ClipboardItem, DismissEvent, Div, EventEmitter,
-    FocusHandle, FocusableView, FontStyle, FontWeight, InteractiveElement, IntoElement, ListOffset,
-    ListState, Model, MouseDownEvent, ParentElement, Pixels, Point, PromptLevel, Render,
-    RenderOnce, SharedString, Styled, Subscription, Task, TextStyle, View, ViewContext,
-    VisualContext, WeakView, WhiteSpace,
+    actions, canvas, div, fill, list, overlay, point, prelude::*, px, AnyElement, AppContext,
+    AsyncWindowContext, Bounds, ClipboardItem, DismissEvent, Div, EventEmitter, FocusHandle,
+    FocusableView, FontStyle, FontWeight, InteractiveElement, IntoElement, ListOffset, ListState,
+    Model, MouseDownEvent, ParentElement, Pixels, Point, PromptLevel, Render, RenderOnce,
+    SharedString, Styled, Subscription, Task, TextStyle, View, ViewContext, VisualContext,
+    WeakView, WhiteSpace,
 };
 use menu::{Cancel, Confirm, SelectNext, SelectPrev};
 use project::{Fs, Project};
@@ -896,7 +896,7 @@ impl CollabPanel {
             .start_slot(
                 h_stack()
                     .gap_1()
-                    .child(render_tree_branch(is_last, cx))
+                    .child(render_tree_branch(is_last, false, cx))
                     .child(IconButton::new(0, Icon::Folder)),
             )
             .child(Label::new(project_name.clone()))
@@ -917,7 +917,7 @@ impl CollabPanel {
             .start_slot(
                 h_stack()
                     .gap_1()
-                    .child(render_tree_branch(is_last, cx))
+                    .child(render_tree_branch(is_last, false, cx))
                     .child(IconButton::new(0, Icon::Screen)),
             )
             .child(Label::new("Screen"))
@@ -958,7 +958,7 @@ impl CollabPanel {
             .start_slot(
                 h_stack()
                     .gap_1()
-                    .child(render_tree_branch(false, cx))
+                    .child(render_tree_branch(false, true, cx))
                     .child(IconButton::new(0, Icon::File)),
             )
             .child(div().h_7().w_full().child(Label::new("notes")))
@@ -979,7 +979,7 @@ impl CollabPanel {
             .start_slot(
                 h_stack()
                     .gap_1()
-                    .child(render_tree_branch(false, cx))
+                    .child(render_tree_branch(false, false, cx))
                     .child(IconButton::new(0, Icon::MessageBubbles)),
             )
             .child(Label::new("chat"))
@@ -1007,7 +1007,7 @@ impl CollabPanel {
             .start_slot(
                 h_stack()
                     .gap_1()
-                    .child(render_tree_branch(!has_visible_participants, cx))
+                    .child(render_tree_branch(!has_visible_participants, false, cx))
                     .child(""),
             )
             .child(Label::new(if count == 1 {
@@ -2404,11 +2404,11 @@ impl CollabPanel {
     }
 }
 
-fn render_tree_branch(is_last: bool, cx: &mut WindowContext) -> impl IntoElement {
+fn render_tree_branch(is_last: bool, overdraw: bool, cx: &mut WindowContext) -> impl IntoElement {
     let rem_size = cx.rem_size();
     let line_height = cx.text_style().line_height_in_pixels(rem_size);
     let width = rem_size * 1.5;
-    let thickness = px(2.);
+    let thickness = px(1.);
     let color = cx.theme().colors().text;
 
     canvas(move |bounds, cx| {
@@ -2422,7 +2422,11 @@ fn render_tree_branch(is_last: bool, cx: &mut WindowContext) -> impl IntoElement
                 point(start_x, top),
                 point(
                     start_x + thickness,
-                    if is_last { start_y } else { bounds.bottom() },
+                    if is_last {
+                        start_y
+                    } else {
+                        bounds.bottom() + if overdraw { px(1.) } else { px(0.) }
+                    },
                 ),
             ),
             color,

crates/collab_ui/src/collab_titlebar_item.rs 🔗

@@ -41,6 +41,7 @@ pub fn init(cx: &mut AppContext) {
         workspace.set_titlebar_item(titlebar_item.into(), cx)
     })
     .detach();
+    // todo!()
     // cx.add_action(CollabTitlebarItem::share_project);
     // cx.add_action(CollabTitlebarItem::unshare_project);
     // cx.add_action(CollabTitlebarItem::toggle_user_menu);
@@ -92,7 +93,7 @@ impl Render for CollabTitlebarItem {
                     .gap_1()
                     .children(self.render_project_host(cx))
                     .child(self.render_project_name(cx))
-                    .children(self.render_project_branch(cx))
+                    .child(div().pr_1().children(self.render_project_branch(cx)))
                     .when_some(
                         current_user.clone().zip(client.peer_id()).zip(room.clone()),
                         |this, ((current_user, peer_id), room)| {
@@ -184,6 +185,16 @@ impl Render for CollabTitlebarItem {
                                     "toggle_sharing",
                                     if is_shared { "Unshare" } else { "Share" },
                                 )
+                                .tooltip(move |cx| {
+                                    Tooltip::text(
+                                        if is_shared {
+                                            "Stop sharing project with call participants"
+                                        } else {
+                                            "Share project with call participants"
+                                        },
+                                        cx,
+                                    )
+                                })
                                 .style(ButtonStyle::Subtle)
                                 .selected_style(ButtonStyle::Tinted(TintColor::Accent))
                                 .selected(is_shared)
@@ -200,14 +211,19 @@ impl Render for CollabTitlebarItem {
                             )
                         })
                         .child(
-                            IconButton::new("leave-call", ui::Icon::Exit)
-                                .style(ButtonStyle::Subtle)
-                                .icon_size(IconSize::Small)
-                                .on_click(move |_, cx| {
-                                    ActiveCall::global(cx)
-                                        .update(cx, |call, cx| call.hang_up(cx))
-                                        .detach_and_log_err(cx);
-                                }),
+                            div()
+                                .child(
+                                    IconButton::new("leave-call", ui::Icon::Exit)
+                                        .style(ButtonStyle::Subtle)
+                                        .tooltip(|cx| Tooltip::text("Leave call", cx))
+                                        .icon_size(IconSize::Small)
+                                        .on_click(move |_, cx| {
+                                            ActiveCall::global(cx)
+                                                .update(cx, |call, cx| call.hang_up(cx))
+                                                .detach_and_log_err(cx);
+                                        }),
+                                )
+                                .pr_2(),
                         )
                         .when(!read_only, |this| {
                             this.child(
@@ -219,6 +235,16 @@ impl Render for CollabTitlebarItem {
                                         ui::Icon::Mic
                                     },
                                 )
+                                .tooltip(move |cx| {
+                                    Tooltip::text(
+                                        if is_muted {
+                                            "Unmute microphone"
+                                        } else {
+                                            "Mute microphone"
+                                        },
+                                        cx,
+                                    )
+                                })
                                 .style(ButtonStyle::Subtle)
                                 .icon_size(IconSize::Small)
                                 .selected(is_muted)
@@ -260,11 +286,22 @@ impl Render for CollabTitlebarItem {
                                     .icon_size(IconSize::Small)
                                     .selected(is_screen_sharing)
                                     .selected_style(ButtonStyle::Tinted(TintColor::Accent))
+                                    .tooltip(move |cx| {
+                                        Tooltip::text(
+                                            if is_screen_sharing {
+                                                "Stop Sharing Screen"
+                                            } else {
+                                                "Share Screen"
+                                            },
+                                            cx,
+                                        )
+                                    })
                                     .on_click(move |_, cx| {
                                         crate::toggle_screen_sharing(&Default::default(), cx)
                                     }),
                             )
                         })
+                        .child(div().pr_2())
                     })
                     .map(|el| {
                         let status = self.client.status();
@@ -284,11 +321,19 @@ impl Render for CollabTitlebarItem {
 fn render_color_ribbon(participant_index: ParticipantIndex, colors: &PlayerColors) -> gpui::Canvas {
     let color = colors.color_for_participant(participant_index.0).cursor;
     canvas(move |bounds, cx| {
-        let mut path = Path::new(bounds.lower_left());
         let height = bounds.size.height;
-        path.curve_to(bounds.origin + point(height, px(0.)), bounds.origin);
-        path.line_to(bounds.upper_right() - point(height, px(0.)));
-        path.curve_to(bounds.lower_right(), bounds.upper_right());
+        let horizontal_offset = height;
+        let vertical_offset = px(height.0 / 2.0);
+        let mut path = Path::new(bounds.lower_left());
+        path.curve_to(
+            bounds.origin + point(horizontal_offset, vertical_offset),
+            bounds.origin + point(px(0.0), vertical_offset),
+        );
+        path.line_to(bounds.upper_right() + point(-horizontal_offset, vertical_offset));
+        path.curve_to(
+            bounds.lower_right(),
+            bounds.upper_right() + point(px(0.0), vertical_offset),
+        );
         path.line_to(bounds.lower_left());
         cx.paint_path(path, color);
     })

crates/collab_ui/src/collab_ui.rs 🔗

@@ -58,7 +58,6 @@ pub fn toggle_screen_sharing(_: &ToggleScreenSharing, cx: &mut AppContext) {
                     room.id(),
                     room.channel_id(),
                     &client,
-                    cx,
                 );
                 Task::ready(room.unshare_screen(cx))
             } else {
@@ -67,7 +66,6 @@ pub fn toggle_screen_sharing(_: &ToggleScreenSharing, cx: &mut AppContext) {
                     room.id(),
                     room.channel_id(),
                     &client,
-                    cx,
                 );
                 room.share_screen(cx)
             }
@@ -86,7 +84,7 @@ pub fn toggle_mute(_: &ToggleMute, cx: &mut AppContext) {
             } else {
                 "disable microphone"
             };
-            report_call_event_for_room(operation, room.id(), room.channel_id(), &client, cx);
+            report_call_event_for_room(operation, room.id(), room.channel_id(), &client);
 
             room.toggle_mute(cx)
         })

crates/collab_ui/src/notification_panel.rs 🔗

@@ -6,11 +6,11 @@ use collections::HashMap;
 use db::kvp::KEY_VALUE_STORE;
 use futures::StreamExt;
 use gpui::{
-    actions, div, img, list, px, serde_json, AnyElement, AppContext, AsyncWindowContext,
-    CursorStyle, DismissEvent, Element, EventEmitter, FocusHandle, FocusableView,
-    InteractiveElement, IntoElement, ListAlignment, ListScrollEvent, ListState, Model,
-    ParentElement, Render, StatefulInteractiveElement, Styled, Task, View, ViewContext,
-    VisualContext, WeakView, WindowContext,
+    actions, div, img, list, px, AnyElement, AppContext, AsyncWindowContext, CursorStyle,
+    DismissEvent, Element, EventEmitter, FocusHandle, FocusableView, InteractiveElement,
+    IntoElement, ListAlignment, ListScrollEvent, ListState, Model, ParentElement, Render,
+    StatefulInteractiveElement, Styled, Task, View, ViewContext, VisualContext, WeakView,
+    WindowContext,
 };
 use notifications::{NotificationEntry, NotificationEvent, NotificationStore};
 use project::Fs;

crates/diagnostics/src/items.rs 🔗

@@ -24,19 +24,11 @@ impl Render for DiagnosticIndicator {
     fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
         let diagnostic_indicator = match (self.summary.error_count, self.summary.warning_count) {
             (0, 0) => h_stack().map(|this| {
-                if !self.in_progress_checks.is_empty() {
-                    this.child(
-                        IconElement::new(Icon::ArrowCircle)
-                            .size(IconSize::Small)
-                            .color(Color::Muted),
-                    )
-                } else {
-                    this.child(
-                        IconElement::new(Icon::Check)
-                            .size(IconSize::Small)
-                            .color(Color::Default),
-                    )
-                }
+                this.child(
+                    IconElement::new(Icon::Check)
+                        .size(IconSize::Small)
+                        .color(Color::Default),
+                )
             }),
             (0, warning_count) => h_stack()
                 .gap_1()
@@ -72,9 +64,14 @@ impl Render for DiagnosticIndicator {
 
         let status = if !self.in_progress_checks.is_empty() {
             Some(
-                Label::new("Checking…")
-                    .size(LabelSize::Small)
-                    .color(Color::Muted)
+                h_stack()
+                    .gap_2()
+                    .child(IconElement::new(Icon::ArrowCircle).size(IconSize::Small))
+                    .child(
+                        Label::new("Checking…")
+                            .size(LabelSize::Small)
+                            .into_any_element(),
+                    )
                     .into_any_element(),
             )
         } else if let Some(diagnostic) = &self.current_diagnostic {

crates/editor/src/display_map.rs 🔗

@@ -240,7 +240,7 @@ impl DisplayMap {
     }
     pub fn clear_highlights(&mut self, type_id: TypeId) -> bool {
         let mut cleared = self.text_highlights.remove(&Some(type_id)).is_some();
-        cleared |= self.inlay_highlights.remove(&type_id).is_none();
+        cleared |= self.inlay_highlights.remove(&type_id).is_some();
         cleared
     }
 

crates/editor/src/editor.rs 🔗

@@ -8874,7 +8874,7 @@ impl Editor {
 
         let telemetry = project.read(cx).client().telemetry().clone();
 
-        telemetry.report_copilot_event(suggestion_id, suggestion_accepted, file_extension, cx)
+        telemetry.report_copilot_event(suggestion_id, suggestion_accepted, file_extension)
     }
 
     #[cfg(any(test, feature = "test-support"))]
@@ -8926,7 +8926,6 @@ impl Editor {
             operation,
             copilot_enabled,
             copilot_enabled_for_language,
-            cx,
         )
     }
 

crates/editor/src/editor_tests.rs 🔗

@@ -9,11 +9,7 @@ use crate::{
 };
 
 use futures::StreamExt;
-use gpui::{
-    div,
-    serde_json::{self, json},
-    TestAppContext, VisualTestContext, WindowBounds, WindowOptions,
-};
+use gpui::{div, TestAppContext, VisualTestContext, WindowBounds, WindowOptions};
 use indoc::indoc;
 use language::{
     language_settings::{AllLanguageSettings, AllLanguageSettingsContent, LanguageSettingsContent},
@@ -24,6 +20,7 @@ use language::{
 use parking_lot::Mutex;
 use project::project_settings::{LspSettings, ProjectSettings};
 use project::FakeFs;
+use serde_json::{self, json};
 use std::sync::atomic;
 use std::sync::atomic::AtomicUsize;
 use std::{cell::RefCell, future::Future, rc::Rc, time::Instant};
@@ -8131,8 +8128,8 @@ fn assert_selection_ranges(marked_text: &str, view: &mut Editor, cx: &mut ViewCo
 /// Handle completion request passing a marked string specifying where the completion
 /// should be triggered from using '|' character, what range should be replaced, and what completions
 /// should be returned using '<' and '>' to delimit the range
-pub fn handle_completion_request<'a>(
-    cx: &mut EditorLspTestContext<'a>,
+pub fn handle_completion_request(
+    cx: &mut EditorLspTestContext,
     marked_string: &str,
     completions: Vec<&'static str>,
 ) -> impl Future<Output = ()> {
@@ -8177,8 +8174,8 @@ pub fn handle_completion_request<'a>(
     }
 }
 
-fn handle_resolve_completion_request<'a>(
-    cx: &mut EditorLspTestContext<'a>,
+fn handle_resolve_completion_request(
+    cx: &mut EditorLspTestContext,
     edits: Option<Vec<(&'static str, &'static str)>>,
 ) -> impl Future<Output = ()> {
     let edits = edits.map(|edits| {

crates/editor/src/element.rs 🔗

@@ -795,7 +795,7 @@ impl EditorElement {
                     cx.paint_quad(quad(
                         highlight_bounds,
                         Corners::all(1. * line_height),
-                        gpui::yellow(), // todo!("use the right color")
+                        cx.theme().status().modified,
                         Edges::default(),
                         transparent_black(),
                     ));

crates/editor/src/test/editor_lsp_test_context.rs 🔗

@@ -21,19 +21,19 @@ use workspace::{AppState, Workspace, WorkspaceHandle};
 
 use super::editor_test_context::{AssertionContextManager, EditorTestContext};
 
-pub struct EditorLspTestContext<'a> {
-    pub cx: EditorTestContext<'a>,
+pub struct EditorLspTestContext {
+    pub cx: EditorTestContext,
     pub lsp: lsp::FakeLanguageServer,
     pub workspace: View<Workspace>,
     pub buffer_lsp_url: lsp::Url,
 }
 
-impl<'a> EditorLspTestContext<'a> {
+impl EditorLspTestContext {
     pub async fn new(
         mut language: Language,
         capabilities: lsp::ServerCapabilities,
-        cx: &'a mut gpui::TestAppContext,
-    ) -> EditorLspTestContext<'a> {
+        cx: &mut gpui::TestAppContext,
+    ) -> EditorLspTestContext {
         let app_state = cx.update(AppState::test);
 
         cx.update(|cx| {
@@ -110,8 +110,8 @@ impl<'a> EditorLspTestContext<'a> {
 
     pub async fn new_rust(
         capabilities: lsp::ServerCapabilities,
-        cx: &'a mut gpui::TestAppContext,
-    ) -> EditorLspTestContext<'a> {
+        cx: &mut gpui::TestAppContext,
+    ) -> EditorLspTestContext {
         let language = Language::new(
             LanguageConfig {
                 name: "Rust".into(),
@@ -152,8 +152,8 @@ impl<'a> EditorLspTestContext<'a> {
 
     pub async fn new_typescript(
         capabilities: lsp::ServerCapabilities,
-        cx: &'a mut gpui::TestAppContext,
-    ) -> EditorLspTestContext<'a> {
+        cx: &mut gpui::TestAppContext,
+    ) -> EditorLspTestContext {
         let mut word_characters: HashSet<char> = Default::default();
         word_characters.insert('$');
         word_characters.insert('#');
@@ -283,15 +283,15 @@ impl<'a> EditorLspTestContext<'a> {
     }
 }
 
-impl<'a> Deref for EditorLspTestContext<'a> {
-    type Target = EditorTestContext<'a>;
+impl Deref for EditorLspTestContext {
+    type Target = EditorTestContext;
 
     fn deref(&self) -> &Self::Target {
         &self.cx
     }
 }
 
-impl<'a> DerefMut for EditorLspTestContext<'a> {
+impl DerefMut for EditorLspTestContext {
     fn deref_mut(&mut self) -> &mut Self::Target {
         &mut self.cx
     }

crates/editor/src/test/editor_test_context.rs 🔗

@@ -26,20 +26,20 @@ use util::{
 
 use super::build_editor_with_project;
 
-pub struct EditorTestContext<'a> {
-    pub cx: gpui::VisualTestContext<'a>,
+pub struct EditorTestContext {
+    pub cx: gpui::VisualTestContext,
     pub window: AnyWindowHandle,
     pub editor: View<Editor>,
     pub assertion_cx: AssertionContextManager,
 }
 
-impl<'a> EditorTestContext<'a> {
-    pub async fn new(cx: &'a mut gpui::TestAppContext) -> EditorTestContext<'a> {
+impl EditorTestContext {
+    pub async fn new(cx: &mut gpui::TestAppContext) -> EditorTestContext {
         let fs = FakeFs::new(cx.executor());
         // fs.insert_file("/file", "".to_owned()).await;
         fs.insert_tree(
             "/root",
-            gpui::serde_json::json!({
+            serde_json::json!({
                 "file": "",
             }),
         )
@@ -342,7 +342,7 @@ impl<'a> EditorTestContext<'a> {
     }
 }
 
-impl<'a> Deref for EditorTestContext<'a> {
+impl Deref for EditorTestContext {
     type Target = gpui::TestAppContext;
 
     fn deref(&self) -> &Self::Target {
@@ -350,7 +350,7 @@ impl<'a> Deref for EditorTestContext<'a> {
     }
 }
 
-impl<'a> DerefMut for EditorTestContext<'a> {
+impl DerefMut for EditorTestContext {
     fn deref_mut(&mut self) -> &mut Self::Target {
         &mut self.cx
     }

crates/feedback/Cargo.toml 🔗

@@ -36,6 +36,7 @@ postage.workspace = true
 regex.workspace = true
 serde.workspace = true
 serde_derive.workspace = true
+serde_json.workspace = true
 smallvec.workspace = true
 smol.workspace = true
 sysinfo.workspace = true

crates/feedback/src/feedback_modal.rs 🔗

@@ -7,8 +7,8 @@ use db::kvp::KEY_VALUE_STORE;
 use editor::{Editor, EditorEvent};
 use futures::AsyncReadExt;
 use gpui::{
-    div, red, rems, serde_json, AppContext, DismissEvent, EventEmitter, FocusHandle, FocusableView,
-    Model, PromptLevel, Render, Task, View, ViewContext,
+    div, red, rems, AppContext, DismissEvent, EventEmitter, FocusHandle, FocusableView, Model,
+    PromptLevel, Render, Task, View, ViewContext,
 };
 use isahc::Request;
 use language::Buffer;

crates/file_finder/src/file_finder.rs 🔗

@@ -505,8 +505,7 @@ impl FileFinderDelegate {
             || path_match.path_prefix.to_string(),
             |file_name| file_name.to_string_lossy().to_string(),
         );
-        let file_name_start = path_match.path_prefix.chars().count() + path_string.chars().count()
-            - file_name.chars().count();
+        let file_name_start = path_match.path_prefix.len() + path_string.len() - file_name.len();
         let file_name_positions = path_positions
             .iter()
             .filter_map(|pos| {
@@ -819,6 +818,44 @@ mod tests {
         }
     }
 
+    #[gpui::test]
+    async fn test_complex_path(cx: &mut TestAppContext) {
+        let app_state = init_test(cx);
+        app_state
+            .fs
+            .as_fake()
+            .insert_tree(
+                "/root",
+                json!({
+                    "其他": {
+                        "S数据表格": {
+                            "task.xlsx": "some content",
+                        },
+                    }
+                }),
+            )
+            .await;
+
+        let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
+
+        let (picker, workspace, cx) = build_find_picker(project, cx);
+
+        cx.simulate_input("t");
+        picker.update(cx, |picker, _| {
+            assert_eq!(picker.delegate.matches.len(), 1);
+            assert_eq!(
+                collect_search_results(picker),
+                vec![PathBuf::from("其他/S数据表格/task.xlsx")],
+            )
+        });
+        cx.dispatch_action(SelectNext);
+        cx.dispatch_action(Confirm);
+        cx.read(|cx| {
+            let active_editor = workspace.read(cx).active_item_as::<Editor>(cx).unwrap();
+            assert_eq!(active_editor.read(cx).title(cx), "task.xlsx");
+        });
+    }
+
     #[gpui::test]
     async fn test_row_column_numbers_query_inside_file(cx: &mut TestAppContext) {
         let app_state = init_test(cx);
@@ -1843,7 +1880,7 @@ mod tests {
         expected_matches: usize,
         expected_editor_title: &str,
         workspace: &View<Workspace>,
-        cx: &mut gpui::VisualTestContext<'_>,
+        cx: &mut gpui::VisualTestContext,
     ) -> Vec<FoundPath> {
         let picker = open_file_picker(&workspace, cx);
         cx.simulate_input(input);

crates/gpui/src/action.rs 🔗

@@ -29,7 +29,7 @@ use std::any::{Any, TypeId};
 /// macro, which only generates the code needed to register your action before `main`.
 ///
 /// ```
-/// #[derive(gpui::serde::Deserialize, std::cmp::PartialEq, std::clone::Clone)]
+/// #[derive(gpui::private::serde::Deserialize, std::cmp::PartialEq, std::clone::Clone)]
 /// pub struct Paste {
 ///     pub content: SharedString,
 /// }
@@ -158,12 +158,12 @@ impl ActionRegistry {
 macro_rules! actions {
     ($namespace:path, [ $($name:ident),* $(,)? ]) => {
         $(
-            #[derive(::std::cmp::PartialEq, ::std::clone::Clone, ::std::default::Default, gpui::serde_derive::Deserialize)]
-            #[serde(crate = "gpui::serde")]
+            #[derive(::std::cmp::PartialEq, ::std::clone::Clone, ::std::default::Default, gpui::private::serde_derive::Deserialize)]
+            #[serde(crate = "gpui::private::serde")]
             pub struct $name;
 
             gpui::__impl_action!($namespace, $name,
-                fn build(_: gpui::serde_json::Value) -> gpui::Result<::std::boxed::Box<dyn gpui::Action>> {
+                fn build(_: gpui::private::serde_json::Value) -> gpui::Result<::std::boxed::Box<dyn gpui::Action>> {
                     Ok(Box::new(Self))
                 }
             );
@@ -179,8 +179,8 @@ macro_rules! impl_actions {
     ($namespace:path, [ $($name:ident),* $(,)? ]) => {
         $(
             gpui::__impl_action!($namespace, $name,
-                fn build(value: gpui::serde_json::Value) -> gpui::Result<::std::boxed::Box<dyn gpui::Action>> {
-                    Ok(std::boxed::Box::new(gpui::serde_json::from_value::<Self>(value)?))
+                fn build(value: gpui::private::serde_json::Value) -> gpui::Result<::std::boxed::Box<dyn gpui::Action>> {
+                    Ok(std::boxed::Box::new(gpui::private::serde_json::from_value::<Self>(value)?))
                 }
             );
 

crates/gpui/src/app.rs 🔗

@@ -43,7 +43,7 @@ use util::{
     ResultExt,
 };
 
-/// Temporary(?) wrapper around RefCell<AppContext> to help us debug any double borrows.
+/// Temporary(?) wrapper around [`RefCell<AppContext>`] to help us debug any double borrows.
 /// Strongly consider removing after stabilization.
 pub struct AppCell {
     app: RefCell<AppContext>,
@@ -964,7 +964,7 @@ impl AppContext {
 
     /// Event handlers propagate events by default. Call this method to stop dispatching to
     /// event handlers with a lower z-index (mouse) or higher in the tree (keyboard). This is
-    /// the opposite of [propagate]. It's also possible to cancel a call to [propagate] by
+    /// the opposite of [`Self::propagate`]. It's also possible to cancel a call to [`Self::propagate`] by
     /// calling this method before effects are flushed.
     pub fn stop_propagation(&mut self) {
         self.propagate_event = false;
@@ -972,7 +972,7 @@ impl AppContext {
 
     /// Action handlers stop propagation by default during the bubble phase of action dispatch
     /// dispatching to action handlers higher in the element tree. This is the opposite of
-    /// [stop_propagation]. It's also possible to cancel a call to [stop_propagate] by calling
+    /// [`Self::stop_propagation`]. It's also possible to cancel a call to [`Self::stop_propagation`] by calling
     /// this method before effects are flushed.
     pub fn propagate(&mut self) {
         self.propagate_event = true;
@@ -1099,12 +1099,6 @@ impl AppContext {
     pub fn has_active_drag(&self) -> bool {
         self.active_drag.is_some()
     }
-
-    pub fn active_drag<T: 'static>(&self) -> Option<&T> {
-        self.active_drag
-            .as_ref()
-            .and_then(|drag| drag.value.downcast_ref())
-    }
 }
 
 impl Context for AppContext {

crates/gpui/src/app/entity_map.rs 🔗

@@ -1,4 +1,4 @@
-use crate::{private::Sealed, AppContext, Context, Entity, ModelContext};
+use crate::{seal::Sealed, AppContext, Context, Entity, ModelContext};
 use anyhow::{anyhow, Result};
 use derive_more::{Deref, DerefMut};
 use parking_lot::{RwLock, RwLockUpgradableReadGuard};

crates/gpui/src/app/test_context.rs 🔗

@@ -187,6 +187,10 @@ impl TestAppContext {
         self.test_window(window_handle).simulate_resize(size);
     }
 
+    pub fn windows(&self) -> Vec<AnyWindowHandle> {
+        self.app.borrow().windows().clone()
+    }
+
     pub fn spawn<Fut, R>(&self, f: impl FnOnce(AsyncAppContext) -> Fut) -> Task<R>
     where
         Fut: Future<Output = R> + 'static,
@@ -479,21 +483,24 @@ impl<V> View<V> {
 }
 
 use derive_more::{Deref, DerefMut};
-#[derive(Deref, DerefMut)]
-pub struct VisualTestContext<'a> {
+#[derive(Deref, DerefMut, Clone)]
+pub struct VisualTestContext {
     #[deref]
     #[deref_mut]
-    cx: &'a mut TestAppContext,
+    cx: TestAppContext,
     window: AnyWindowHandle,
 }
 
-impl<'a> VisualTestContext<'a> {
+impl<'a> VisualTestContext {
     pub fn update<R>(&mut self, f: impl FnOnce(&mut WindowContext) -> R) -> R {
         self.cx.update_window(self.window, |_, cx| f(cx)).unwrap()
     }
 
-    pub fn from_window(window: AnyWindowHandle, cx: &'a mut TestAppContext) -> Self {
-        Self { cx, window }
+    pub fn from_window(window: AnyWindowHandle, cx: &TestAppContext) -> Self {
+        Self {
+            cx: cx.clone(),
+            window,
+        }
     }
 
     pub fn run_until_parked(&self) {
@@ -525,9 +532,36 @@ impl<'a> VisualTestContext<'a> {
         }
         self.background_executor.run_until_parked();
     }
+    /// Returns true if the window was closed.
+    pub fn simulate_close(&mut self) -> bool {
+        let handler = self
+            .cx
+            .update_window(self.window, |_, cx| {
+                cx.window
+                    .platform_window
+                    .as_test()
+                    .unwrap()
+                    .0
+                    .lock()
+                    .should_close_handler
+                    .take()
+            })
+            .unwrap();
+        if let Some(mut handler) = handler {
+            let should_close = handler();
+            self.cx
+                .update_window(self.window, |_, cx| {
+                    cx.window.platform_window.on_should_close(handler);
+                })
+                .unwrap();
+            should_close
+        } else {
+            false
+        }
+    }
 }
 
-impl<'a> Context for VisualTestContext<'a> {
+impl Context for VisualTestContext {
     type Result<T> = <TestAppContext as Context>::Result<T>;
 
     fn new_model<T: 'static>(
@@ -578,7 +612,7 @@ impl<'a> Context for VisualTestContext<'a> {
     }
 }
 
-impl<'a> VisualContext for VisualTestContext<'a> {
+impl VisualContext for VisualTestContext {
     fn new_view<V>(
         &mut self,
         build_view: impl FnOnce(&mut ViewContext<'_, V>) -> V,
@@ -587,7 +621,7 @@ impl<'a> VisualContext for VisualTestContext<'a> {
         V: 'static + Render,
     {
         self.window
-            .update(self.cx, |_, cx| cx.new_view(build_view))
+            .update(&mut self.cx, |_, cx| cx.new_view(build_view))
             .unwrap()
     }
 
@@ -597,7 +631,7 @@ impl<'a> VisualContext for VisualTestContext<'a> {
         update: impl FnOnce(&mut V, &mut ViewContext<'_, V>) -> R,
     ) -> Self::Result<R> {
         self.window
-            .update(self.cx, |_, cx| cx.update_view(view, update))
+            .update(&mut self.cx, |_, cx| cx.update_view(view, update))
             .unwrap()
     }
 
@@ -609,13 +643,13 @@ impl<'a> VisualContext for VisualTestContext<'a> {
         V: 'static + Render,
     {
         self.window
-            .update(self.cx, |_, cx| cx.replace_root_view(build_view))
+            .update(&mut self.cx, |_, cx| cx.replace_root_view(build_view))
             .unwrap()
     }
 
     fn focus_view<V: crate::FocusableView>(&mut self, view: &View<V>) -> Self::Result<()> {
         self.window
-            .update(self.cx, |_, cx| {
+            .update(&mut self.cx, |_, cx| {
                 view.read(cx).focus_handle(cx).clone().focus(cx)
             })
             .unwrap()
@@ -626,7 +660,7 @@ impl<'a> VisualContext for VisualTestContext<'a> {
         V: crate::ManagedView,
     {
         self.window
-            .update(self.cx, |_, cx| {
+            .update(&mut self.cx, |_, cx| {
                 view.update(cx, |_, cx| cx.emit(crate::DismissEvent))
             })
             .unwrap()

crates/gpui/src/geometry.rs 🔗

@@ -1582,7 +1582,6 @@ impl From<f32> for Edges<Pixels> {
 /// Represents the corners of a box in a 2D space, such as border radius.
 ///
 /// Each field represents the size of the corner on one side of the box: `top_left`, `top_right`, `bottom_right`, and `bottom_left`.
-/// ```
 #[derive(Refineable, Clone, Default, Debug, Eq, PartialEq)]
 #[refineable(Debug)]
 #[repr(C)]
@@ -2263,7 +2262,7 @@ impl From<f64> for GlobalPixels {
     }
 }
 
-/// Represents a length in rems, a unit based on the font-size of the window, which can be assigned with [WindowContext::set_rem_size].
+/// Represents a length in rems, a unit based on the font-size of the window, which can be assigned with [`WindowContext::set_rem_size`][set_rem_size].
 ///
 /// Rems are used for defining lengths that are scalable and consistent across different UI elements.
 /// The value of `1rem` is typically equal to the font-size of the root element (often the `<html>` element in browsers),
@@ -2271,6 +2270,8 @@ impl From<f64> for GlobalPixels {
 /// purpose, allowing for scalable and accessible design that can adjust to different display settings or user preferences.
 ///
 /// For example, if the root element's font-size is `16px`, then `1rem` equals `16px`. A length of `2rems` would then be `32px`.
+///
+/// [set_rem_size]: crate::WindowContext::set_rem_size
 #[derive(Clone, Copy, Default, Add, Sub, Mul, Div, Neg)]
 pub struct Rems(pub f32);
 

crates/gpui/src/gpui.rs 🔗

@@ -30,7 +30,16 @@ mod util;
 mod view;
 mod window;
 
-mod private {
+/// Do not touch, here be dragons for use by gpui_macros and such.
+#[doc(hidden)]
+pub mod private {
+    pub use linkme;
+    pub use serde;
+    pub use serde_derive;
+    pub use serde_json;
+}
+
+mod seal {
     /// A mechanism for restricting implementations of a trait to only those in GPUI.
     /// See: https://predr.ag/blog/definitive-guide-to-sealed-traits-in-rust/
     pub trait Sealed {}
@@ -47,22 +56,17 @@ pub use element::*;
 pub use elements::*;
 pub use executor::*;
 pub use geometry::*;
-pub use gpui_macros::*;
+pub use gpui_macros::{register_action, test, IntoElement, Render};
 pub use image_cache::*;
 pub use input::*;
 pub use interactive::*;
 pub use key_dispatch::*;
 pub use keymap::*;
-pub use linkme;
 pub use platform::*;
-use private::Sealed;
 pub use refineable::*;
 pub use scene::*;
-pub use serde;
-pub use serde_derive;
-pub use serde_json;
+use seal::Sealed;
 pub use shared_string::*;
-pub use smallvec;
 pub use smol::Timer;
 pub use style::*;
 pub use styled::*;

crates/gpui/src/input.rs 🔗

@@ -5,8 +5,8 @@ use std::ops::Range;
 
 /// Implement this trait to allow views to handle textual input when implementing an editor, field, etc.
 ///
-/// Once your view `V` implements this trait, you can use it to construct an [ElementInputHandler<V>].
-/// This input handler can then be assigned during paint by calling [WindowContext::handle_input].
+/// Once your view `V` implements this trait, you can use it to construct an [`ElementInputHandler<V>`].
+/// This input handler can then be assigned during paint by calling [`WindowContext::handle_input`].
 pub trait InputHandler: 'static + Sized {
     fn text_for_range(&mut self, range: Range<usize>, cx: &mut ViewContext<Self>)
         -> Option<String>;
@@ -43,8 +43,10 @@ pub struct ElementInputHandler<V> {
 }
 
 impl<V: 'static> ElementInputHandler<V> {
-    /// Used in [Element::paint] with the element's bounds and a view context for its
+    /// Used in [`Element::paint`][element_paint] with the element's bounds and a view context for its
     /// containing view.
+    ///
+    /// [element_paint]: crate::Element::paint
     pub fn new(element_bounds: Bounds<Pixels>, view: View<V>, cx: &mut WindowContext) -> Self {
         ElementInputHandler {
             view,

crates/gpui/src/interactive.rs 🔗

@@ -214,7 +214,7 @@ impl Render for ExternalPaths {
 pub enum FileDropEvent {
     Entered {
         position: Point<Pixels>,
-        files: ExternalPaths,
+        paths: ExternalPaths,
     },
     Pending {
         position: Point<Pixels>,

crates/gpui/src/platform.rs 🔗

@@ -37,7 +37,7 @@ pub use keystroke::*;
 pub use mac::*;
 #[cfg(any(test, feature = "test-support"))]
 pub use test::*;
-pub use time::UtcOffset;
+use time::UtcOffset;
 
 #[cfg(target_os = "macos")]
 pub(crate) fn current_platform() -> Rc<dyn Platform> {

crates/gpui/src/platform/mac.rs 🔗

@@ -106,11 +106,6 @@ impl From<NSSize> for Size<Pixels> {
     }
 }
 
-pub trait NSRectExt {
-    fn size(&self) -> Size<Pixels>;
-    fn intersects(&self, other: Self) -> bool;
-}
-
 impl From<NSRect> for Size<Pixels> {
     fn from(rect: NSRect) -> Self {
         let NSSize { width, height } = rect.size;
@@ -124,16 +119,3 @@ impl From<NSRect> for Size<GlobalPixels> {
         size(width.into(), height.into())
     }
 }
-
-// impl NSRectExt for NSRect {
-//     fn intersects(&self, other: Self) -> bool {
-//         self.size.width > 0.
-//             && self.size.height > 0.
-//             && other.size.width > 0.
-//             && other.size.height > 0.
-//             && self.origin.x <= other.origin.x + other.size.width
-//             && self.origin.x + self.size.width >= other.origin.x
-//             && self.origin.y <= other.origin.y + other.size.height
-//             && self.origin.y + self.size.height >= other.origin.y
-//     }
-// }

crates/gpui/src/platform/mac/dispatcher.rs 🔗

@@ -13,9 +13,14 @@ use parking::{Parker, Unparker};
 use parking_lot::Mutex;
 use std::{ffi::c_void, ptr::NonNull, sync::Arc, time::Duration};
 
-include!(concat!(env!("OUT_DIR"), "/dispatch_sys.rs"));
+/// All items in the generated file are marked as pub, so we're gonna wrap it in a separate mod to prevent
+/// these pub items from leaking into public API.
+pub(crate) mod dispatch_sys {
+    include!(concat!(env!("OUT_DIR"), "/dispatch_sys.rs"));
+}
 
-pub fn dispatch_get_main_queue() -> dispatch_queue_t {
+use dispatch_sys::*;
+pub(crate) fn dispatch_get_main_queue() -> dispatch_queue_t {
     unsafe { &_dispatch_main_q as *const _ as dispatch_queue_t }
 }
 

crates/gpui/src/platform/mac/display.rs 🔗

@@ -51,7 +51,7 @@ impl MacDisplay {
 
 #[link(name = "ApplicationServices", kind = "framework")]
 extern "C" {
-    pub fn CGDisplayCreateUUIDFromDisplayID(display: CGDirectDisplayID) -> CFUUIDRef;
+    fn CGDisplayCreateUUIDFromDisplayID(display: CGDirectDisplayID) -> CFUUIDRef;
 }
 
 /// Convert the given rectangle from CoreGraphics' native coordinate space to GPUI's coordinate space.

crates/gpui/src/platform/mac/display_linker.rs 🔗

@@ -94,7 +94,7 @@ unsafe extern "C" fn trampoline(
 
 mod sys {
     //! Derived from display-link crate under the fololwing license:
-    //! https://github.com/BrainiumLLC/display-link/blob/master/LICENSE-MIT
+    //! <https://github.com/BrainiumLLC/display-link/blob/master/LICENSE-MIT>
     //! Apple docs: [CVDisplayLink](https://developer.apple.com/documentation/corevideo/cvdisplaylinkoutputcallback?language=objc)
     #![allow(dead_code, non_upper_case_globals)]
 

crates/gpui/src/platform/mac/platform.rs 🔗

@@ -56,9 +56,6 @@ use time::UtcOffset;
 #[allow(non_upper_case_globals)]
 const NSUTF8StringEncoding: NSUInteger = 4;
 
-#[allow(non_upper_case_globals)]
-pub const NSViewLayerContentsRedrawDuringViewResize: NSInteger = 2;
-
 const MAC_PLATFORM_IVAR: &str = "platform";
 static mut APP_CLASS: *const Class = ptr::null();
 static mut APP_DELEGATE_CLASS: *const Class = ptr::null();
@@ -404,7 +401,7 @@ impl Platform for MacPlatform {
         // this, we make quitting the application asynchronous so that we aren't holding borrows to
         // the app state on the stack when we actually terminate the app.
 
-        use super::dispatcher::{dispatch_async_f, dispatch_get_main_queue};
+        use super::dispatcher::{dispatch_get_main_queue, dispatch_sys::dispatch_async_f};
 
         unsafe {
             dispatch_async_f(dispatch_get_main_queue(), ptr::null_mut(), Some(quit));

crates/gpui/src/platform/mac/text_system.rs 🔗

@@ -500,9 +500,9 @@ impl<'a> StringIndexConverter<'a> {
 }
 
 #[repr(C)]
-pub struct __CFTypesetter(c_void);
+pub(crate) struct __CFTypesetter(c_void);
 
-pub type CTTypesetterRef = *const __CFTypesetter;
+type CTTypesetterRef = *const __CFTypesetter;
 
 #[link(name = "CoreText", kind = "framework")]
 extern "C" {

crates/gpui/src/platform/mac/window.rs 🔗

@@ -1673,10 +1673,7 @@ extern "C" fn dragging_entered(this: &Object, _: Sel, dragging_info: id) -> NSDr
     if send_new_event(&window_state, {
         let position = drag_event_position(&window_state, dragging_info);
         let paths = external_paths_from_event(dragging_info);
-        InputEvent::FileDrop(FileDropEvent::Entered {
-            position,
-            files: paths,
-        })
+        InputEvent::FileDrop(FileDropEvent::Entered { position, paths })
     }) {
         NSDragOperationCopy
     } else {

crates/gpui/src/platform/test/platform.rs 🔗

@@ -266,7 +266,7 @@ impl Platform for TestPlatform {
     }
 
     fn local_timezone(&self) -> time::UtcOffset {
-        unimplemented!()
+        time::UtcOffset::UTC
     }
 
     fn path_for_auxiliary_executable(&self, _name: &str) -> Result<std::path::PathBuf> {

crates/gpui/src/platform/test/window.rs 🔗

@@ -18,7 +18,7 @@ pub struct TestWindowState {
     pub(crate) edited: bool,
     platform: Weak<TestPlatform>,
     sprite_atlas: Arc<dyn PlatformAtlas>,
-
+    pub(crate) should_close_handler: Option<Box<dyn FnMut() -> bool>>,
     input_callback: Option<Box<dyn FnMut(InputEvent) -> bool>>,
     active_status_change_callback: Option<Box<dyn FnMut(bool)>>,
     resize_callback: Option<Box<dyn FnMut(Size<Pixels>, f32)>>,
@@ -44,7 +44,7 @@ impl TestWindow {
             sprite_atlas: Arc::new(TestAtlas::new()),
             title: Default::default(),
             edited: false,
-
+            should_close_handler: None,
             input_callback: None,
             active_status_change_callback: None,
             resize_callback: None,
@@ -117,6 +117,9 @@ impl TestWindow {
 
         self.0.lock().input_handler = Some(input_handler);
     }
+    pub fn edited(&self) -> bool {
+        self.0.lock().edited
+    }
 }
 
 impl PlatformWindow for TestWindow {
@@ -235,8 +238,8 @@ impl PlatformWindow for TestWindow {
         self.0.lock().moved_callback = Some(callback)
     }
 
-    fn on_should_close(&self, _callback: Box<dyn FnMut() -> bool>) {
-        unimplemented!()
+    fn on_should_close(&self, callback: Box<dyn FnMut() -> bool>) {
+        self.0.lock().should_close_handler = Some(callback);
     }
 
     fn on_close(&self, _callback: Box<dyn FnOnce()>) {

crates/gpui/src/style.rs 🔗

@@ -561,6 +561,12 @@ impl From<Hsla> for Fill {
     }
 }
 
+impl From<Rgba> for Fill {
+    fn from(color: Rgba) -> Self {
+        Self::Color(color.into())
+    }
+}
+
 impl From<TextStyle> for HighlightStyle {
     fn from(other: TextStyle) -> Self {
         Self::from(&other)

crates/gpui/src/text_system.rs 🔗

@@ -33,7 +33,7 @@ pub struct FontId(pub usize);
 #[derive(Hash, PartialEq, Eq, Clone, Copy, Debug)]
 pub struct FontFamilyId(pub usize);
 
-pub const SUBPIXEL_VARIANTS: u8 = 4;
+pub(crate) const SUBPIXEL_VARIANTS: u8 = 4;
 
 pub struct TextSystem {
     line_layout_cache: Arc<LineLayoutCache>,

crates/gpui/src/view.rs 🔗

@@ -1,12 +1,12 @@
 use crate::{
-    private::Sealed, AnyElement, AnyModel, AnyWeakModel, AppContext, AvailableSpace, BorrowWindow,
+    seal::Sealed, AnyElement, AnyModel, AnyWeakModel, AppContext, AvailableSpace, BorrowWindow,
     Bounds, Element, ElementId, Entity, EntityId, Flatten, FocusHandle, FocusableView, IntoElement,
     LayoutId, Model, Pixels, Point, Render, Size, ViewContext, VisualContext, WeakModel,
     WindowContext,
 };
 use anyhow::{Context, Result};
 use std::{
-    any::TypeId,
+    any::{type_name, TypeId},
     fmt,
     hash::{Hash, Hasher},
 };
@@ -104,6 +104,14 @@ impl<V> Clone for View<V> {
     }
 }
 
+impl<T> std::fmt::Debug for View<T> {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        f.debug_struct(&format!("View<{}>", type_name::<T>()))
+            .field("entity_id", &self.model.entity_id)
+            .finish_non_exhaustive()
+    }
+}
+
 impl<V> Hash for View<V> {
     fn hash<H: Hasher>(&self, state: &mut H) {
         self.model.hash(state);

crates/gpui/src/window.rs 🔗

@@ -1462,12 +1462,12 @@ impl<'a> WindowContext<'a> {
             // Translate dragging and dropping of external files from the operating system
             // to internal drag and drop events.
             InputEvent::FileDrop(file_drop) => match file_drop {
-                FileDropEvent::Entered { position, files } => {
+                FileDropEvent::Entered { position, paths } => {
                     self.window.mouse_position = position;
                     if self.active_drag.is_none() {
                         self.active_drag = Some(AnyDrag {
-                            value: Box::new(files.clone()),
-                            view: self.new_view(|_| files).into(),
+                            value: Box::new(paths.clone()),
+                            view: self.new_view(|_| paths).into(),
                             cursor_offset: position,
                         });
                     }
@@ -1826,9 +1826,11 @@ impl<'a> WindowContext<'a> {
         result
     }
 
-    /// Set an input handler, such as [ElementInputHandler], which interfaces with the
+    /// Set an input handler, such as [`ElementInputHandler`][element_input_handler], which interfaces with the
     /// platform to receive textual input with proper integration with concerns such
     /// as IME interactions.
+    ///
+    /// [element_input_handler]: crate::ElementInputHandler
     pub fn handle_input(
         &mut self,
         focus_handle: &FocusHandle,
@@ -2500,8 +2502,7 @@ impl<'a, V: 'static> ViewContext<'a, V> {
     }
 
     /// Register a listener to be called when the given focus handle receives focus.
-    /// Unlike [on_focus_changed], returns a subscription and persists until the subscription
-    /// is dropped.
+    /// Returns a subscription and persists until the subscription is dropped.
     pub fn on_focus(
         &mut self,
         handle: &FocusHandle,
@@ -2527,8 +2528,7 @@ impl<'a, V: 'static> ViewContext<'a, V> {
     }
 
     /// Register a listener to be called when the given focus handle or one of its descendants receives focus.
-    /// Unlike [on_focus_changed], returns a subscription and persists until the subscription
-    /// is dropped.
+    /// Returns a subscription and persists until the subscription is dropped.
     pub fn on_focus_in(
         &mut self,
         handle: &FocusHandle,
@@ -2554,8 +2554,7 @@ impl<'a, V: 'static> ViewContext<'a, V> {
     }
 
     /// Register a listener to be called when the given focus handle loses focus.
-    /// Unlike [on_focus_changed], returns a subscription and persists until the subscription
-    /// is dropped.
+    /// Returns a subscription and persists until the subscription is dropped.
     pub fn on_blur(
         &mut self,
         handle: &FocusHandle,
@@ -2581,8 +2580,7 @@ impl<'a, V: 'static> ViewContext<'a, V> {
     }
 
     /// Register a listener to be called when the window loses focus.
-    /// Unlike [on_focus_changed], returns a subscription and persists until the subscription
-    /// is dropped.
+    /// Returns a subscription and persists until the subscription is dropped.
     pub fn on_blur_window(
         &mut self,
         mut listener: impl FnMut(&mut V, &mut ViewContext<V>) + 'static,
@@ -2597,8 +2595,7 @@ impl<'a, V: 'static> ViewContext<'a, V> {
     }
 
     /// Register a listener to be called when the given focus handle or one of its descendants loses focus.
-    /// Unlike [on_focus_changed], returns a subscription and persists until the subscription
-    /// is dropped.
+    /// Returns a subscription and persists until the subscription is dropped.
     pub fn on_focus_out(
         &mut self,
         handle: &FocusHandle,

crates/gpui/tests/action_macros.rs 🔗

@@ -11,7 +11,7 @@ fn test_action_macros() {
 
     impl_actions!(test, [AnotherTestAction]);
 
-    #[derive(PartialEq, Clone, gpui::serde_derive::Deserialize)]
+    #[derive(PartialEq, Clone, gpui::private::serde_derive::Deserialize)]
     struct RegisterableAction {}
 
     register_action!(RegisterableAction);

crates/gpui_macros/src/register_action.rs 🔗

@@ -36,8 +36,8 @@ pub(crate) fn register_action(type_name: &Ident) -> proc_macro2::TokenStream {
 
     quote! {
         #[doc(hidden)]
-        #[gpui::linkme::distributed_slice(gpui::__GPUI_ACTIONS)]
-        #[linkme(crate = gpui::linkme)]
+        #[gpui::private::linkme::distributed_slice(gpui::__GPUI_ACTIONS)]
+        #[linkme(crate = gpui::private::linkme)]
         static #static_slice_name: gpui::MacroActionBuilder = #action_builder_fn_name;
 
         /// This is an auto generated function, do not use.

crates/journal/src/journal.rs 🔗

@@ -11,7 +11,7 @@ use std::{
     path::{Path, PathBuf},
     sync::Arc,
 };
-use workspace::{AppState, Workspace};
+use workspace::{AppState, OpenVisible, Workspace};
 
 actions!(journal, [NewJournalEntry]);
 
@@ -100,7 +100,7 @@ pub fn new_journal_entry(app_state: Arc<AppState>, cx: &mut WindowContext) {
 
         let opened = workspace
             .update(&mut cx, |workspace, cx| {
-                workspace.open_paths(vec![entry_path], true, cx)
+                workspace.open_paths(vec![entry_path], OpenVisible::All, None, cx)
             })?
             .await;
 

crates/language_tools/Cargo.toml 🔗

@@ -24,6 +24,7 @@ futures.workspace = true
 serde.workspace = true
 anyhow.workspace = true
 tree-sitter.workspace = true
+serde_json.workspace = true
 
 [dev-dependencies]
 client = { path = "../client", features = ["test-support"] }

crates/language_tools/src/lsp_log_tests.rs 🔗

@@ -4,9 +4,10 @@ use crate::lsp_log::LogMenuItem;
 
 use super::*;
 use futures::StreamExt;
-use gpui::{serde_json::json, Context, TestAppContext, VisualTestContext};
+use gpui::{Context, TestAppContext, VisualTestContext};
 use language::{tree_sitter_rust, FakeLspAdapter, Language, LanguageConfig, LanguageServerName};
 use project::{FakeFs, Project};
+use serde_json::json;
 use settings::SettingsStore;
 
 #[gpui::test]

crates/project_symbols/Cargo.toml 🔗

@@ -24,6 +24,7 @@ anyhow.workspace = true
 ordered-float.workspace = true
 postage.workspace = true
 smol.workspace = true
+serde_json.workspace = true
 
 [dev-dependencies]
 futures.workspace = true

crates/project_symbols/src/project_symbols.rs 🔗

@@ -260,9 +260,10 @@ impl PickerDelegate for ProjectSymbolsDelegate {
 mod tests {
     use super::*;
     use futures::StreamExt;
-    use gpui::{serde_json::json, TestAppContext, VisualContext};
+    use gpui::{TestAppContext, VisualContext};
     use language::{FakeLspAdapter, Language, LanguageConfig};
     use project::FakeFs;
+    use serde_json::json;
     use settings::SettingsStore;
     use std::{path::Path, sync::Arc};
 

crates/search/src/buffer_search.rs 🔗

@@ -1091,13 +1091,10 @@ mod tests {
             theme::init(theme::LoadThemes::JustBase, cx);
         });
     }
+
     fn init_test(
         cx: &mut TestAppContext,
-    ) -> (
-        View<Editor>,
-        View<BufferSearchBar>,
-        &mut VisualTestContext<'_>,
-    ) {
+    ) -> (View<Editor>, View<BufferSearchBar>, &mut VisualTestContext) {
         init_globals(cx);
         let buffer = cx.new_model(|cx| {
             Buffer::new(

crates/terminal/src/terminal.rs 🔗

@@ -1311,34 +1311,33 @@ impl Terminal {
         })
     }
 
-    pub fn title(&self) -> String {
+    pub fn title(&self, truncate: bool) -> String {
         self.foreground_process_info
             .as_ref()
             .map(|fpi| {
-                format!(
-                    "{} — {}",
-                    truncate_and_trailoff(
-                        &fpi.cwd
-                            .file_name()
-                            .map(|name| name.to_string_lossy().to_string())
-                            .unwrap_or_default(),
-                        25
-                    ),
-                    truncate_and_trailoff(
-                        &{
-                            format!(
-                                "{}{}",
-                                fpi.name,
-                                if fpi.argv.len() >= 1 {
-                                    format!(" {}", (&fpi.argv[1..]).join(" "))
-                                } else {
-                                    "".to_string()
-                                }
-                            )
-                        },
-                        25
+                let process_file = fpi
+                    .cwd
+                    .file_name()
+                    .map(|name| name.to_string_lossy().to_string())
+                    .unwrap_or_default();
+                let process_name = format!(
+                    "{}{}",
+                    fpi.name,
+                    if fpi.argv.len() >= 1 {
+                        format!(" {}", (&fpi.argv[1..]).join(" "))
+                    } else {
+                        "".to_string()
+                    }
+                );
+                let (process_file, process_name) = if truncate {
+                    (
+                        truncate_and_trailoff(&process_file, 25),
+                        truncate_and_trailoff(&process_name, 25),
                     )
-                )
+                } else {
+                    (process_file, process_name)
+                };
+                format!("{process_file} — {process_name}")
             })
             .unwrap_or_else(|| "Terminal".to_string())
     }

crates/terminal_view/Cargo.toml 🔗

@@ -36,6 +36,7 @@ thiserror.workspace = true
 lazy_static.workspace = true
 serde.workspace = true
 serde_derive.workspace = true
+serde_json.workspace = true
 
 [dev-dependencies]
 editor = { path = "../editor", features = ["test-support"] }

crates/terminal_view/src/terminal_element.rs 🔗

@@ -1,12 +1,12 @@
 use editor::{Cursor, HighlightedRange, HighlightedRangeLine};
 use gpui::{
     div, fill, point, px, red, relative, AnyElement, AsyncWindowContext, AvailableSpace,
-    BorrowWindow, Bounds, DispatchPhase, Element, ElementId, ExternalPaths, FocusHandle, Font,
-    FontStyle, FontWeight, HighlightStyle, Hsla, InteractiveBounds, InteractiveElement,
+    BorrowWindow, Bounds, DispatchPhase, Element, ElementId, FocusHandle, Font, FontStyle,
+    FontWeight, HighlightStyle, Hsla, InteractiveBounds, InteractiveElement,
     InteractiveElementState, Interactivity, IntoElement, LayoutId, Model, ModelContext,
     ModifiersChangedEvent, MouseButton, MouseMoveEvent, Pixels, PlatformInputHandler, Point,
-    ShapedLine, StatefulInteractiveElement, StyleRefinement, Styled, TextRun, TextStyle,
-    TextSystem, UnderlineStyle, WhiteSpace, WindowContext,
+    ShapedLine, StatefulInteractiveElement, Styled, TextRun, TextStyle, TextSystem, UnderlineStyle,
+    WhiteSpace, WindowContext,
 };
 use itertools::Itertools;
 use language::CursorShape;
@@ -25,7 +25,7 @@ use terminal::{
 use theme::{ActiveTheme, Theme, ThemeSettings};
 use ui::Tooltip;
 
-use std::{any::TypeId, mem};
+use std::mem;
 use std::{fmt::Debug, ops::RangeInclusive};
 
 ///The information generated during layout that is necessary for painting
@@ -677,28 +677,6 @@ impl TerminalElement {
             }
         });
 
-        self.interactivity.drag_over_styles.push((
-            TypeId::of::<ExternalPaths>(),
-            StyleRefinement::default().bg(cx.theme().colors().drop_target_background),
-        ));
-        self.interactivity.on_drop::<ExternalPaths>({
-            let focus = focus.clone();
-            let terminal = terminal.clone();
-            move |external_paths, cx| {
-                cx.focus(&focus);
-                let mut new_text = external_paths
-                    .paths()
-                    .iter()
-                    .map(|path| format!(" {path:?}"))
-                    .join("");
-                new_text.push(' ');
-                terminal.update(cx, |terminal, _| {
-                    // todo!() long paths are not displayed properly albeit the text is there
-                    terminal.paste(&new_text);
-                });
-            }
-        });
-
         // Mouse mode handlers:
         // All mouse modes need the extra click handlers
         if mode.intersects(TermMode::MOUSE_MODE) {

crates/terminal_view/src/terminal_panel.rs 🔗

@@ -1,13 +1,14 @@
-use std::{path::PathBuf, sync::Arc};
+use std::{ops::ControlFlow, path::PathBuf, sync::Arc};
 
 use crate::TerminalView;
 use db::kvp::KEY_VALUE_STORE;
 use gpui::{
-    actions, serde_json, AppContext, AsyncWindowContext, Entity, EventEmitter, ExternalPaths,
-    FocusHandle, FocusableView, IntoElement, ParentElement, Pixels, Render, Styled, Subscription,
-    Task, View, ViewContext, VisualContext, WeakView, WindowContext,
+    actions, AppContext, AsyncWindowContext, Entity, EventEmitter, ExternalPaths, FocusHandle,
+    FocusableView, IntoElement, ParentElement, Pixels, Render, Styled, Subscription, Task, View,
+    ViewContext, VisualContext, WeakView, WindowContext,
 };
-use project::Fs;
+use itertools::Itertools;
+use project::{Fs, ProjectEntryId};
 use search::{buffer_search::DivRegistrar, BufferSearchBar};
 use serde::{Deserialize, Serialize};
 use settings::{Settings, SettingsStore};
@@ -19,7 +20,7 @@ use workspace::{
     item::Item,
     pane,
     ui::Icon,
-    Pane, Workspace,
+    DraggedTab, Pane, Workspace,
 };
 
 use anyhow::Result;
@@ -59,18 +60,7 @@ impl TerminalPanel {
                 workspace.weak_handle(),
                 workspace.project().clone(),
                 Default::default(),
-                Some(Arc::new(|a, cx| {
-                    if let Some(tab) = a.downcast_ref::<workspace::pane::DraggedTab>() {
-                        if let Some(item) = tab.pane.read(cx).item_for_index(tab.ix) {
-                            return item.downcast::<TerminalView>().is_some();
-                        }
-                    }
-                    if a.downcast_ref::<ExternalPaths>().is_some() {
-                        return true;
-                    }
-
-                    false
-                })),
+                None,
                 cx,
             );
             pane.set_can_split(false, cx);
@@ -105,6 +95,52 @@ impl TerminalPanel {
                     })
                     .into_any_element()
             });
+
+            let workspace = workspace.weak_handle();
+            pane.set_custom_drop_handle(cx, move |pane, dropped_item, cx| {
+                if let Some(tab) = dropped_item.downcast_ref::<DraggedTab>() {
+                    let item = if &tab.pane == cx.view() {
+                        pane.item_for_index(tab.ix)
+                    } else {
+                        tab.pane.read(cx).item_for_index(tab.ix)
+                    };
+                    if let Some(item) = item {
+                        if item.downcast::<TerminalView>().is_some() {
+                            return ControlFlow::Continue(());
+                        } else if let Some(project_path) = item.project_path(cx) {
+                            if let Some(entry_path) = workspace
+                                .update(cx, |workspace, cx| {
+                                    workspace
+                                        .project()
+                                        .read(cx)
+                                        .absolute_path(&project_path, cx)
+                                })
+                                .log_err()
+                                .flatten()
+                            {
+                                add_paths_to_terminal(pane, &[entry_path], cx);
+                            }
+                        }
+                    }
+                } else if let Some(&entry_id) = dropped_item.downcast_ref::<ProjectEntryId>() {
+                    if let Some(entry_path) = workspace
+                        .update(cx, |workspace, cx| {
+                            let project = workspace.project().read(cx);
+                            project
+                                .path_for_entry(entry_id, cx)
+                                .and_then(|project_path| project.absolute_path(&project_path, cx))
+                        })
+                        .log_err()
+                        .flatten()
+                    {
+                        add_paths_to_terminal(pane, &[entry_path], cx);
+                    }
+                } else if let Some(paths) = dropped_item.downcast_ref::<ExternalPaths>() {
+                    add_paths_to_terminal(pane, paths.paths(), cx);
+                }
+
+                ControlFlow::Break(())
+            });
             let buffer_search_bar = cx.new_view(search::BufferSearchBar::new);
             pane.toolbar()
                 .update(cx, |toolbar, cx| toolbar.add_item(buffer_search_bar, cx));
@@ -217,7 +253,6 @@ impl TerminalPanel {
             pane::Event::Remove => cx.emit(PanelEvent::Close),
             pane::Event::ZoomIn => cx.emit(PanelEvent::ZoomIn),
             pane::Event::ZoomOut => cx.emit(PanelEvent::ZoomOut),
-            pane::Event::Focus => cx.emit(PanelEvent::Focus),
 
             pane::Event::AddItem { item } => {
                 if let Some(workspace) = self.workspace.upgrade() {
@@ -330,6 +365,22 @@ impl TerminalPanel {
     }
 }
 
+fn add_paths_to_terminal(pane: &mut Pane, paths: &[PathBuf], cx: &mut ViewContext<'_, Pane>) {
+    if let Some(terminal_view) = pane
+        .active_item()
+        .and_then(|item| item.downcast::<TerminalView>())
+    {
+        cx.focus_view(&terminal_view);
+        let mut new_text = paths.iter().map(|path| format!(" {path:?}")).join("");
+        new_text.push(' ');
+        terminal_view.update(cx, |terminal_view, cx| {
+            terminal_view.terminal().update(cx, |terminal, _| {
+                terminal.paste(&new_text);
+            });
+        });
+    }
+}
+
 impl EventEmitter<PanelEvent> for TerminalPanel {}
 
 impl Render for TerminalPanel {

crates/terminal_view/src/terminal_view.rs 🔗

@@ -29,7 +29,8 @@ use workspace::{
     notifications::NotifyResultExt,
     register_deserializable_item,
     searchable::{SearchEvent, SearchOptions, SearchableItem, SearchableItemHandle},
-    CloseActiveItem, NewCenterTerminal, Pane, ToolbarItemLocation, Workspace, WorkspaceId,
+    CloseActiveItem, NewCenterTerminal, OpenVisible, Pane, ToolbarItemLocation, Workspace,
+    WorkspaceId,
 };
 
 use anyhow::Context;
@@ -192,12 +193,26 @@ impl TerminalView {
                     }
                     let potential_abs_paths = possible_open_targets(&workspace, maybe_path, cx);
                     if let Some(path) = potential_abs_paths.into_iter().next() {
-                        let is_dir = path.path_like.is_dir();
                         let task_workspace = workspace.clone();
                         cx.spawn(|_, mut cx| async move {
+                            let fs = task_workspace.update(&mut cx, |workspace, cx| {
+                                workspace.project().read(cx).fs().clone()
+                            })?;
+                            let is_dir = fs
+                                .metadata(&path.path_like)
+                                .await?
+                                .with_context(|| {
+                                    format!("Missing metadata for file {:?}", path.path_like)
+                                })?
+                                .is_dir;
                             let opened_items = task_workspace
                                 .update(&mut cx, |workspace, cx| {
-                                    workspace.open_paths(vec![path.path_like], is_dir, cx)
+                                    workspace.open_paths(
+                                        vec![path.path_like],
+                                        OpenVisible::OnlyDirectories,
+                                        None,
+                                        cx,
+                                    )
                                 })
                                 .context("workspace update")?
                                 .await;
@@ -665,7 +680,7 @@ impl Item for TerminalView {
     type Event = ItemEvent;
 
     fn tab_tooltip_text(&self, cx: &AppContext) -> Option<SharedString> {
-        Some(self.terminal().read(cx).title().into())
+        Some(self.terminal().read(cx).title(false).into())
     }
 
     fn tab_content(
@@ -674,8 +689,7 @@ impl Item for TerminalView {
         selected: bool,
         cx: &WindowContext,
     ) -> AnyElement {
-        let title = self.terminal().read(cx).title();
-
+        let title = self.terminal().read(cx).title(true);
         h_stack()
             .gap_2()
             .child(IconElement::new(Icon::Terminal))

crates/theme_importer/Cargo.toml 🔗

@@ -18,6 +18,7 @@ palette = { version = "0.7.3", default-features = false, features = ["std"] }
 pathfinder_color = "0.5"
 rust-embed.workspace = true
 serde.workspace = true
+serde_json.workspace = true
 simplelog = "0.9"
 strum = { version = "0.25.0", features = ["derive"] }
 theme = { path = "../theme", features = ["importing-themes"] }

crates/theme_importer/src/main.rs 🔗

@@ -16,7 +16,6 @@ use any_ascii::any_ascii;
 use anyhow::{anyhow, Context, Result};
 use clap::Parser;
 use convert_case::{Case, Casing};
-use gpui::serde_json;
 use indexmap::IndexMap;
 use indoc::formatdoc;
 use json_comments::StripComments;

crates/theme_importer/src/zed1/converter.rs 🔗

@@ -1,5 +1,5 @@
 use anyhow::{Context, Result};
-use gpui::{serde_json, Hsla, Rgba};
+use gpui::{Hsla, Rgba};
 use theme::{
     color_alpha, Appearance, PlayerColor, PlayerColors, StatusColorsRefinement,
     ThemeColorsRefinement, UserFontStyle, UserFontWeight, UserHighlightStyle, UserSyntaxTheme,

crates/theme_importer/src/zed1/theme.rs 🔗

@@ -6,10 +6,10 @@ use std::fmt;
 use std::ops::{Deref, DerefMut};
 use std::sync::Arc;
 
-use gpui::serde_json::{self, Value};
 use pathfinder_color::ColorU;
 use serde::de::{self, DeserializeOwned, Unexpected};
 use serde::{Deserialize, Deserializer};
+use serde_json::{self, Value};
 
 #[derive(Clone, Copy, Default, PartialEq, Eq, Hash, PartialOrd, Ord)]
 #[repr(transparent)]

crates/theme_selector/src/theme_selector.rs 🔗

@@ -182,7 +182,7 @@ impl PickerDelegate for ThemeSelectorDelegate {
         let theme_name = cx.theme().name.clone();
 
         self.telemetry
-            .report_setting_event("theme", theme_name.to_string(), cx);
+            .report_setting_event("theme", theme_name.to_string());
 
         update_settings_file::<ThemeSettings>(self.fs.clone(), cx, move |settings| {
             settings.theme = Some(theme_name.to_string());

crates/vim/src/test/neovim_backed_binding_test_context.rs 🔗

@@ -4,30 +4,27 @@ use crate::state::Mode;
 
 use super::{ExemptionFeatures, NeovimBackedTestContext, SUPPORTED_FEATURES};
 
-pub struct NeovimBackedBindingTestContext<'a, const COUNT: usize> {
-    cx: NeovimBackedTestContext<'a>,
+pub struct NeovimBackedBindingTestContext<const COUNT: usize> {
+    cx: NeovimBackedTestContext,
     keystrokes_under_test: [&'static str; COUNT],
 }
 
-impl<'a, const COUNT: usize> NeovimBackedBindingTestContext<'a, COUNT> {
-    pub fn new(
-        keystrokes_under_test: [&'static str; COUNT],
-        cx: NeovimBackedTestContext<'a>,
-    ) -> Self {
+impl<const COUNT: usize> NeovimBackedBindingTestContext<COUNT> {
+    pub fn new(keystrokes_under_test: [&'static str; COUNT], cx: NeovimBackedTestContext) -> Self {
         Self {
             cx,
             keystrokes_under_test,
         }
     }
 
-    pub fn consume(self) -> NeovimBackedTestContext<'a> {
+    pub fn consume(self) -> NeovimBackedTestContext {
         self.cx
     }
 
     pub fn binding<const NEW_COUNT: usize>(
         self,
         keystrokes: [&'static str; NEW_COUNT],
-    ) -> NeovimBackedBindingTestContext<'a, NEW_COUNT> {
+    ) -> NeovimBackedBindingTestContext<NEW_COUNT> {
         self.consume().binding(keystrokes)
     }
 
@@ -80,15 +77,15 @@ impl<'a, const COUNT: usize> NeovimBackedBindingTestContext<'a, COUNT> {
     }
 }
 
-impl<'a, const COUNT: usize> Deref for NeovimBackedBindingTestContext<'a, COUNT> {
-    type Target = NeovimBackedTestContext<'a>;
+impl<const COUNT: usize> Deref for NeovimBackedBindingTestContext<COUNT> {
+    type Target = NeovimBackedTestContext;
 
     fn deref(&self) -> &Self::Target {
         &self.cx
     }
 }
 
-impl<'a, const COUNT: usize> DerefMut for NeovimBackedBindingTestContext<'a, COUNT> {
+impl<const COUNT: usize> DerefMut for NeovimBackedBindingTestContext<COUNT> {
     fn deref_mut(&mut self) -> &mut Self::Target {
         &mut self.cx
     }

crates/vim/src/test/neovim_backed_test_context.rs 🔗

@@ -47,8 +47,8 @@ impl ExemptionFeatures {
     }
 }
 
-pub struct NeovimBackedTestContext<'a> {
-    cx: VimTestContext<'a>,
+pub struct NeovimBackedTestContext {
+    cx: VimTestContext,
     // Lookup for exempted assertions. Keyed by the insertion text, and with a value indicating which
     // bindings are exempted. If None, all bindings are ignored for that insertion text.
     exemptions: HashMap<String, Option<HashSet<String>>>,
@@ -60,8 +60,8 @@ pub struct NeovimBackedTestContext<'a> {
     is_dirty: bool,
 }
 
-impl<'a> NeovimBackedTestContext<'a> {
-    pub async fn new(cx: &'a mut gpui::TestAppContext) -> NeovimBackedTestContext<'a> {
+impl NeovimBackedTestContext {
+    pub async fn new(cx: &mut gpui::TestAppContext) -> NeovimBackedTestContext {
         // rust stores the name of the test on the current thread.
         // We use this to automatically name a file that will store
         // the neovim connection's requests/responses so that we can
@@ -393,20 +393,20 @@ impl<'a> NeovimBackedTestContext<'a> {
     pub fn binding<const COUNT: usize>(
         self,
         keystrokes: [&'static str; COUNT],
-    ) -> NeovimBackedBindingTestContext<'a, COUNT> {
+    ) -> NeovimBackedBindingTestContext<COUNT> {
         NeovimBackedBindingTestContext::new(keystrokes, self)
     }
 }
 
-impl<'a> Deref for NeovimBackedTestContext<'a> {
-    type Target = VimTestContext<'a>;
+impl Deref for NeovimBackedTestContext {
+    type Target = VimTestContext;
 
     fn deref(&self) -> &Self::Target {
         &self.cx
     }
 }
 
-impl<'a> DerefMut for NeovimBackedTestContext<'a> {
+impl DerefMut for NeovimBackedTestContext {
     fn deref_mut(&mut self) -> &mut Self::Target {
         &mut self.cx
     }
@@ -415,7 +415,7 @@ impl<'a> DerefMut for NeovimBackedTestContext<'a> {
 // a common mistake in tests is to call set_shared_state when
 // you mean asswert_shared_state. This notices that and lets
 // you know.
-impl<'a> Drop for NeovimBackedTestContext<'a> {
+impl Drop for NeovimBackedTestContext {
     fn drop(&mut self) {
         if self.is_dirty {
             panic!("Test context was dropped after set_shared_state before assert_shared_state")
@@ -425,9 +425,8 @@ impl<'a> Drop for NeovimBackedTestContext<'a> {
 
 #[cfg(test)]
 mod test {
-    use gpui::TestAppContext;
-
     use crate::test::NeovimBackedTestContext;
+    use gpui::TestAppContext;
 
     #[gpui::test]
     async fn neovim_backed_test_context_works(cx: &mut TestAppContext) {

crates/vim/src/test/vim_test_context.rs 🔗

@@ -10,11 +10,11 @@ use search::BufferSearchBar;
 
 use crate::{state::Operator, *};
 
-pub struct VimTestContext<'a> {
-    cx: EditorLspTestContext<'a>,
+pub struct VimTestContext {
+    cx: EditorLspTestContext,
 }
 
-impl<'a> VimTestContext<'a> {
+impl VimTestContext {
     pub fn init(cx: &mut gpui::TestAppContext) {
         if cx.has_global::<Vim>() {
             dbg!("OOPS");
@@ -29,13 +29,13 @@ impl<'a> VimTestContext<'a> {
         });
     }
 
-    pub async fn new(cx: &'a mut gpui::TestAppContext, enabled: bool) -> VimTestContext<'a> {
+    pub async fn new(cx: &mut gpui::TestAppContext, enabled: bool) -> VimTestContext {
         Self::init(cx);
         let lsp = EditorLspTestContext::new_rust(Default::default(), cx).await;
         Self::new_with_lsp(lsp, enabled)
     }
 
-    pub async fn new_typescript(cx: &'a mut gpui::TestAppContext) -> VimTestContext<'a> {
+    pub async fn new_typescript(cx: &mut gpui::TestAppContext) -> VimTestContext {
         Self::init(cx);
         Self::new_with_lsp(
             EditorLspTestContext::new_typescript(Default::default(), cx).await,
@@ -43,7 +43,7 @@ impl<'a> VimTestContext<'a> {
         )
     }
 
-    pub fn new_with_lsp(mut cx: EditorLspTestContext<'a>, enabled: bool) -> VimTestContext<'a> {
+    pub fn new_with_lsp(mut cx: EditorLspTestContext, enabled: bool) -> VimTestContext {
         cx.update(|cx| {
             cx.update_global(|store: &mut SettingsStore, cx| {
                 store.update_user_settings::<VimModeSetting>(cx, |s| *s = Some(enabled));
@@ -162,15 +162,15 @@ impl<'a> VimTestContext<'a> {
     }
 }
 
-impl<'a> Deref for VimTestContext<'a> {
-    type Target = EditorTestContext<'a>;
+impl Deref for VimTestContext {
+    type Target = EditorTestContext;
 
     fn deref(&self) -> &Self::Target {
         &self.cx
     }
 }
 
-impl<'a> DerefMut for VimTestContext<'a> {
+impl DerefMut for VimTestContext {
     fn deref_mut(&mut self) -> &mut Self::Target {
         &mut self.cx
     }

crates/welcome/src/base_keymap_picker.rs 🔗

@@ -1,4 +1,5 @@
 use super::base_keymap_setting::BaseKeymap;
+use client::telemetry::Telemetry;
 use fuzzy::{match_strings, StringMatch, StringMatchCandidate};
 use gpui::{
     actions, AppContext, DismissEvent, EventEmitter, FocusableView, Render, Task, View,
@@ -27,22 +28,22 @@ pub fn toggle(
     cx: &mut ViewContext<Workspace>,
 ) {
     let fs = workspace.app_state().fs.clone();
+    let telemetry = workspace.client().telemetry().clone();
     workspace.toggle_modal(cx, |cx| {
         BaseKeymapSelector::new(
-            BaseKeymapSelectorDelegate::new(cx.view().downgrade(), fs, cx),
+            BaseKeymapSelectorDelegate::new(cx.view().downgrade(), fs, telemetry, cx),
             cx,
         )
     });
 }
 
 pub struct BaseKeymapSelector {
-    focus_handle: gpui::FocusHandle,
     picker: View<Picker<BaseKeymapSelectorDelegate>>,
 }
 
 impl FocusableView for BaseKeymapSelector {
-    fn focus_handle(&self, _cx: &AppContext) -> gpui::FocusHandle {
-        self.focus_handle.clone()
+    fn focus_handle(&self, cx: &AppContext) -> gpui::FocusHandle {
+        self.picker.focus_handle(cx)
     }
 }
 
@@ -55,17 +56,13 @@ impl BaseKeymapSelector {
         cx: &mut ViewContext<BaseKeymapSelector>,
     ) -> Self {
         let picker = cx.new_view(|cx| Picker::new(delegate, cx));
-        let focus_handle = cx.focus_handle();
-        Self {
-            focus_handle,
-            picker,
-        }
+        Self { picker }
     }
 }
 
 impl Render for BaseKeymapSelector {
     fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
-        self.picker.clone()
+        v_stack().w(rems(34.)).child(self.picker.clone())
     }
 }
 
@@ -73,6 +70,7 @@ pub struct BaseKeymapSelectorDelegate {
     view: WeakView<BaseKeymapSelector>,
     matches: Vec<StringMatch>,
     selected_index: usize,
+    telemetry: Arc<Telemetry>,
     fs: Arc<dyn Fs>,
 }
 
@@ -80,6 +78,7 @@ impl BaseKeymapSelectorDelegate {
     fn new(
         weak_view: WeakView<BaseKeymapSelector>,
         fs: Arc<dyn Fs>,
+        telemetry: Arc<Telemetry>,
         cx: &mut ViewContext<BaseKeymapSelector>,
     ) -> Self {
         let base = BaseKeymap::get(None, cx);
@@ -91,6 +90,7 @@ impl BaseKeymapSelectorDelegate {
             view: weak_view,
             matches: Vec::new(),
             selected_index,
+            telemetry,
             fs,
         }
     }
@@ -172,6 +172,10 @@ impl PickerDelegate for BaseKeymapSelectorDelegate {
     fn confirm(&mut self, _: bool, cx: &mut ViewContext<Picker<BaseKeymapSelectorDelegate>>) {
         if let Some(selection) = self.matches.get(self.selected_index) {
             let base_keymap = BaseKeymap::from_names(&selection.string);
+
+            self.telemetry
+                .report_setting_event("keymap", base_keymap.to_string());
+
             update_settings_file::<BaseKeymap>(self.fs.clone(), cx, move |setting| {
                 *setting = Some(base_keymap)
             });
@@ -184,7 +188,13 @@ impl PickerDelegate for BaseKeymapSelectorDelegate {
             .ok();
     }
 
-    fn dismissed(&mut self, _cx: &mut ViewContext<Picker<BaseKeymapSelectorDelegate>>) {}
+    fn dismissed(&mut self, cx: &mut ViewContext<Picker<BaseKeymapSelectorDelegate>>) {
+        self.view
+            .update(cx, |_, cx| {
+                cx.emit(DismissEvent);
+            })
+            .log_err();
+    }
 
     fn render_match(
         &self,

crates/welcome/src/base_keymap_setting.rs 🔗

@@ -1,3 +1,5 @@
+use std::fmt::{Display, Formatter};
+
 use schemars::JsonSchema;
 use serde::{Deserialize, Serialize};
 use settings::Settings;
@@ -12,6 +14,18 @@ pub enum BaseKeymap {
     TextMate,
 }
 
+impl Display for BaseKeymap {
+    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
+        match self {
+            BaseKeymap::VSCode => write!(f, "VSCode"),
+            BaseKeymap::JetBrains => write!(f, "JetBrains"),
+            BaseKeymap::SublimeText => write!(f, "Sublime Text"),
+            BaseKeymap::Atom => write!(f, "Atom"),
+            BaseKeymap::TextMate => write!(f, "TextMate"),
+        }
+    }
+}
+
 impl BaseKeymap {
     pub const OPTIONS: [(&'static str, Self); 5] = [
         ("VSCode (Default)", Self::VSCode),

crates/welcome/src/welcome.rs 🔗

@@ -1,7 +1,7 @@
 mod base_keymap_picker;
 mod base_keymap_setting;
 
-use client::TelemetrySettings;
+use client::{telemetry::Telemetry, TelemetrySettings};
 use db::kvp::KEY_VALUE_STORE;
 use gpui::{
     svg, AnyElement, AppContext, EventEmitter, FocusHandle, FocusableView, InteractiveElement,
@@ -27,7 +27,7 @@ pub fn init(cx: &mut AppContext) {
 
     cx.observe_new_views(|workspace: &mut Workspace, _cx| {
         workspace.register_action(|workspace, _: &Welcome, cx| {
-            let welcome_page = cx.new_view(|cx| WelcomePage::new(workspace, cx));
+            let welcome_page = WelcomePage::new(workspace, cx);
             workspace.add_item(Box::new(welcome_page), cx)
         });
     })
@@ -39,7 +39,7 @@ pub fn init(cx: &mut AppContext) {
 pub fn show_welcome_view(app_state: &Arc<AppState>, cx: &mut AppContext) {
     open_new(&app_state, cx, |workspace, cx| {
         workspace.toggle_dock(DockPosition::Left, cx);
-        let welcome_page = cx.new_view(|cx| WelcomePage::new(workspace, cx));
+        let welcome_page = WelcomePage::new(workspace, cx);
         workspace.add_item_to_center(Box::new(welcome_page.clone()), cx);
         cx.focus_view(&welcome_page);
         cx.notify();
@@ -54,174 +54,213 @@ pub fn show_welcome_view(app_state: &Arc<AppState>, cx: &mut AppContext) {
 pub struct WelcomePage {
     workspace: WeakView<Workspace>,
     focus_handle: FocusHandle,
+    telemetry: Arc<Telemetry>,
     _settings_subscription: Subscription,
 }
 
 impl Render for WelcomePage {
     fn render(&mut self, cx: &mut gpui::ViewContext<Self>) -> impl IntoElement {
-        h_stack()
-            .full()
-            .bg(cx.theme().colors().editor_background)
-            .track_focus(&self.focus_handle)
-            .child(
-                v_stack()
-                    .w_96()
-                    .gap_4()
-                    .mx_auto()
-                    .child(
-                        svg()
-                            .path("icons/logo_96.svg")
-                            .text_color(gpui::white())
-                            .w(px(96.))
-                            .h(px(96.))
-                            .mx_auto(),
-                    )
-                    .child(
-                        h_stack()
-                            .justify_center()
-                            .child(Label::new("Code at the speed of thought")),
-                    )
-                    .child(
-                        v_stack()
-                            .gap_2()
-                            .child(
-                                Button::new("choose-theme", "Choose a theme")
-                                    .full_width()
-                                    .on_click(cx.listener(|this, _, cx| {
-                                        this.workspace
-                                            .update(cx, |workspace, cx| {
-                                                theme_selector::toggle(
-                                                    workspace,
-                                                    &Default::default(),
-                                                    cx,
-                                                )
-                                            })
-                                            .ok();
-                                    })),
-                            )
-                            .child(
-                                Button::new("choose-keymap", "Choose a keymap")
-                                    .full_width()
-                                    .on_click(cx.listener(|this, _, cx| {
-                                        this.workspace
-                                            .update(cx, |workspace, cx| {
-                                                base_keymap_picker::toggle(
-                                                    workspace,
-                                                    &Default::default(),
-                                                    cx,
-                                                )
-                                            })
-                                            .ok();
-                                    })),
-                            )
-                            .child(
-                                Button::new("install-cli", "Install the CLI")
-                                    .full_width()
-                                    .on_click(cx.listener(|_, _, cx| {
-                                        cx.app_mut()
-                                            .spawn(|cx| async move {
-                                                install_cli::install_cli(&cx).await
-                                            })
-                                            .detach_and_log_err(cx);
-                                    })),
-                            ),
-                    )
-                    .child(
-                        v_stack()
-                            .p_3()
-                            .gap_2()
-                            .bg(cx.theme().colors().elevated_surface_background)
-                            .border_1()
-                            .border_color(cx.theme().colors().border)
-                            .rounded_md()
-                            .child(
-                                h_stack()
-                                    .gap_2()
-                                    .child(
-                                        Checkbox::new(
-                                            "enable-vim",
-                                            if VimModeSetting::get_global(cx).0 {
-                                                ui::Selection::Selected
-                                            } else {
-                                                ui::Selection::Unselected
-                                            },
+        h_stack().full().track_focus(&self.focus_handle).child(
+            v_stack()
+                .w_96()
+                .gap_4()
+                .mx_auto()
+                .child(
+                    svg()
+                        .path("icons/logo_96.svg")
+                        .text_color(gpui::white())
+                        .w(px(96.))
+                        .h(px(96.))
+                        .mx_auto(),
+                )
+                .child(
+                    h_stack()
+                        .justify_center()
+                        .child(Label::new("Code at the speed of thought")),
+                )
+                .child(
+                    v_stack()
+                        .gap_2()
+                        .child(
+                            Button::new("choose-theme", "Choose a theme")
+                                .full_width()
+                                .on_click(cx.listener(|this, _, cx| {
+                                    this.telemetry
+                                        .report_app_event("welcome page: change theme");
+                                    this.workspace
+                                        .update(cx, |workspace, cx| {
+                                            theme_selector::toggle(
+                                                workspace,
+                                                &Default::default(),
+                                                cx,
+                                            )
+                                        })
+                                        .ok();
+                                })),
+                        )
+                        .child(
+                            Button::new("choose-keymap", "Choose a keymap")
+                                .full_width()
+                                .on_click(cx.listener(|this, _, cx| {
+                                    this.telemetry
+                                        .report_app_event("welcome page: change keymap");
+                                    this.workspace
+                                        .update(cx, |workspace, cx| {
+                                            base_keymap_picker::toggle(
+                                                workspace,
+                                                &Default::default(),
+                                                cx,
+                                            )
+                                        })
+                                        .ok();
+                                })),
+                        )
+                        .child(
+                            Button::new("install-cli", "Install the CLI")
+                                .full_width()
+                                .on_click(cx.listener(|this, _, cx| {
+                                    this.telemetry.report_app_event("welcome page: install cli");
+                                    cx.app_mut()
+                                        .spawn(
+                                            |cx| async move { install_cli::install_cli(&cx).await },
                                         )
-                                        .on_click(
-                                            cx.listener(move |this, selection, cx| {
-                                                this.update_settings::<VimModeSetting>(
-                                                    selection,
-                                                    cx,
-                                                    |setting, value| *setting = Some(value),
-                                                );
-                                            }),
-                                        ),
+                                        .detach_and_log_err(cx);
+                                })),
+                        ),
+                )
+                .child(
+                    v_stack()
+                        .p_3()
+                        .gap_2()
+                        .bg(cx.theme().colors().elevated_surface_background)
+                        .border_1()
+                        .border_color(cx.theme().colors().border)
+                        .rounded_md()
+                        .child(
+                            h_stack()
+                                .gap_2()
+                                .child(
+                                    Checkbox::new(
+                                        "enable-vim",
+                                        if VimModeSetting::get_global(cx).0 {
+                                            ui::Selection::Selected
+                                        } else {
+                                            ui::Selection::Unselected
+                                        },
                                     )
-                                    .child(Label::new("Enable vim mode")),
-                            )
-                            .child(
-                                h_stack()
-                                    .gap_2()
-                                    .child(
-                                        Checkbox::new(
-                                            "enable-telemetry",
-                                            if TelemetrySettings::get_global(cx).metrics {
-                                                ui::Selection::Selected
-                                            } else {
-                                                ui::Selection::Unselected
-                                            },
-                                        )
-                                        .on_click(
-                                            cx.listener(move |this, selection, cx| {
-                                                this.update_settings::<TelemetrySettings>(
-                                                    selection,
-                                                    cx,
-                                                    |settings, value| {
-                                                        settings.metrics = Some(value)
-                                                    },
-                                                );
-                                            }),
-                                        ),
+                                    .on_click(cx.listener(
+                                        move |this, selection, cx| {
+                                            this.telemetry
+                                                .report_app_event("welcome page: toggle vim");
+                                            this.update_settings::<VimModeSetting>(
+                                                selection,
+                                                cx,
+                                                |setting, value| *setting = Some(value),
+                                            );
+                                        },
+                                    )),
+                                )
+                                .child(Label::new("Enable vim mode")),
+                        )
+                        .child(
+                            h_stack()
+                                .gap_2()
+                                .child(
+                                    Checkbox::new(
+                                        "enable-telemetry",
+                                        if TelemetrySettings::get_global(cx).metrics {
+                                            ui::Selection::Selected
+                                        } else {
+                                            ui::Selection::Unselected
+                                        },
                                     )
-                                    .child(Label::new("Send anonymous usage data")),
-                            )
-                            .child(
-                                h_stack()
-                                    .gap_2()
-                                    .child(
-                                        Checkbox::new(
-                                            "enable-crash",
-                                            if TelemetrySettings::get_global(cx).diagnostics {
-                                                ui::Selection::Selected
-                                            } else {
-                                                ui::Selection::Unselected
-                                            },
-                                        )
-                                        .on_click(
-                                            cx.listener(move |this, selection, cx| {
-                                                this.update_settings::<TelemetrySettings>(
-                                                    selection,
-                                                    cx,
-                                                    |settings, value| {
-                                                        settings.diagnostics = Some(value)
-                                                    },
-                                                );
-                                            }),
-                                        ),
+                                    .on_click(cx.listener(
+                                        move |this, selection, cx| {
+                                            this.telemetry.report_app_event(
+                                                "welcome page: toggle metric telemetry",
+                                            );
+                                            this.update_settings::<TelemetrySettings>(
+                                                selection,
+                                                cx,
+                                                {
+                                                    let telemetry = this.telemetry.clone();
+
+                                                    move |settings, value| {
+                                                        settings.metrics = Some(value);
+
+                                                        telemetry.report_setting_event(
+                                                            "metric telemetry",
+                                                            value.to_string(),
+                                                        );
+                                                    }
+                                                },
+                                            );
+                                        },
+                                    )),
+                                )
+                                .child(Label::new("Send anonymous usage data")),
+                        )
+                        .child(
+                            h_stack()
+                                .gap_2()
+                                .child(
+                                    Checkbox::new(
+                                        "enable-crash",
+                                        if TelemetrySettings::get_global(cx).diagnostics {
+                                            ui::Selection::Selected
+                                        } else {
+                                            ui::Selection::Unselected
+                                        },
                                     )
-                                    .child(Label::new("Send crash reports")),
-                            ),
-                    ),
-            )
+                                    .on_click(cx.listener(
+                                        move |this, selection, cx| {
+                                            this.telemetry.report_app_event(
+                                                "welcome page: toggle diagnostic telemetry",
+                                            );
+                                            this.update_settings::<TelemetrySettings>(
+                                                selection,
+                                                cx,
+                                                {
+                                                    let telemetry = this.telemetry.clone();
+
+                                                    move |settings, value| {
+                                                        settings.diagnostics = Some(value);
+
+                                                        telemetry.report_setting_event(
+                                                            "diagnostic telemetry",
+                                                            value.to_string(),
+                                                        );
+                                                    }
+                                                },
+                                            );
+                                        },
+                                    )),
+                                )
+                                .child(Label::new("Send crash reports")),
+                        ),
+                ),
+        )
     }
 }
 
 impl WelcomePage {
-    pub fn new(workspace: &Workspace, cx: &mut ViewContext<Self>) -> Self {
-        WelcomePage {
-            focus_handle: cx.focus_handle(),
-            workspace: workspace.weak_handle(),
-            _settings_subscription: cx.observe_global::<SettingsStore>(move |_, cx| cx.notify()),
-        }
+    pub fn new(workspace: &Workspace, cx: &mut ViewContext<Workspace>) -> View<Self> {
+        let this = cx.new_view(|cx| {
+            cx.on_release(|this: &mut Self, _, _| {
+                this.telemetry.report_app_event("welcome page: close");
+            })
+            .detach();
+
+            WelcomePage {
+                focus_handle: cx.focus_handle(),
+                workspace: workspace.weak_handle(),
+                telemetry: workspace.client().telemetry().clone(),
+                _settings_subscription: cx
+                    .observe_global::<SettingsStore>(move |_, cx| cx.notify()),
+            }
+        });
+
+        this
     }
 
     fn update_settings<T: Settings>(
@@ -279,6 +318,7 @@ impl Item for WelcomePage {
         Some(cx.new_view(|cx| WelcomePage {
             focus_handle: cx.focus_handle(),
             workspace: self.workspace.clone(),
+            telemetry: self.telemetry.clone(),
             _settings_subscription: cx.observe_global::<SettingsStore>(move |_, cx| cx.notify()),
         }))
     }

crates/workspace/src/dock.rs 🔗

@@ -19,7 +19,6 @@ pub enum PanelEvent {
     ZoomOut,
     Activate,
     Close,
-    Focus,
 }
 
 pub trait Panel: FocusableView + EventEmitter<PanelEvent> {
@@ -216,6 +215,28 @@ impl Dock {
             }
         });
 
+        cx.on_focus_in(&focus_handle, {
+            let dock = dock.downgrade();
+            move |workspace, cx| {
+                let Some(dock) = dock.upgrade() else {
+                    return;
+                };
+                let Some(panel) = dock.read(cx).active_panel() else {
+                    return;
+                };
+                if panel.is_zoomed(cx) {
+                    workspace.zoomed = Some(panel.to_any().downgrade().into());
+                    workspace.zoomed_position = Some(position);
+                } else {
+                    workspace.zoomed = None;
+                    workspace.zoomed_position = None;
+                }
+                workspace.dismiss_zoomed_items_to_reveal(Some(position), cx);
+                workspace.update_active_view_for_followers(cx)
+            }
+        })
+        .detach();
+
         cx.observe(&dock, move |workspace, dock, cx| {
             if dock.read(cx).is_open() {
                 if let Some(panel) = dock.read(cx).active_panel() {
@@ -394,7 +415,6 @@ impl Dock {
                         this.set_open(false, cx);
                     }
                 }
-                PanelEvent::Focus => {}
             }),
         ];
 
@@ -561,6 +581,7 @@ impl Render for Dock {
             }
 
             div()
+                .track_focus(&self.focus_handle)
                 .flex()
                 .bg(cx.theme().colors().panel_background)
                 .border_color(cx.theme().colors().border)
@@ -584,7 +605,7 @@ impl Render for Dock {
                 )
                 .child(handle)
         } else {
-            div()
+            div().track_focus(&self.focus_handle)
         }
     }
 }
@@ -724,7 +745,7 @@ pub mod test {
 
     impl Render for TestPanel {
         fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
-            div()
+            div().id("test").track_focus(&self.focus_handle)
         }
     }
 

crates/workspace/src/item.rs 🔗

@@ -442,7 +442,7 @@ impl<T: Item> ItemHandle for View<T> {
                         ) && !pending_update_scheduled.load(Ordering::SeqCst)
                         {
                             pending_update_scheduled.store(true, Ordering::SeqCst);
-                            cx.on_next_frame({
+                            cx.defer({
                                 let pending_update = pending_update.clone();
                                 let pending_update_scheduled = pending_update_scheduled.clone();
                                 move |this, cx| {

crates/workspace/src/pane.rs 🔗

@@ -2,15 +2,16 @@ use crate::{
     item::{ClosePosition, Item, ItemHandle, ItemSettings, WeakItemHandle},
     toolbar::Toolbar,
     workspace_settings::{AutosaveSetting, WorkspaceSettings},
-    NewCenterTerminal, NewFile, NewSearch, SplitDirection, ToggleZoom, Workspace,
+    NewCenterTerminal, NewFile, NewSearch, OpenVisible, SplitDirection, ToggleZoom, Workspace,
 };
 use anyhow::Result;
 use collections::{HashMap, HashSet, VecDeque};
 use gpui::{
     actions, impl_actions, overlay, prelude::*, Action, AnchorCorner, AnyElement, AppContext,
-    AsyncWindowContext, DismissEvent, Div, DragMoveEvent, EntityId, EventEmitter, FocusHandle,
-    FocusableView, Model, MouseButton, NavigationDirection, Pixels, Point, PromptLevel, Render,
-    ScrollHandle, Subscription, Task, View, ViewContext, VisualContext, WeakView, WindowContext,
+    AsyncWindowContext, DismissEvent, Div, DragMoveEvent, EntityId, EventEmitter, ExternalPaths,
+    FocusHandle, FocusableView, Model, MouseButton, NavigationDirection, Pixels, Point,
+    PromptLevel, Render, ScrollHandle, Subscription, Task, View, ViewContext, VisualContext,
+    WeakView, WindowContext,
 };
 use parking_lot::Mutex;
 use project::{Project, ProjectEntryId, ProjectPath};
@@ -19,6 +20,7 @@ use settings::Settings;
 use std::{
     any::Any,
     cmp, fmt, mem,
+    ops::ControlFlow,
     path::{Path, PathBuf},
     rc::Rc,
     sync::{
@@ -182,6 +184,8 @@ pub struct Pane {
     project: Model<Project>,
     drag_split_direction: Option<SplitDirection>,
     can_drop_predicate: Option<Arc<dyn Fn(&dyn Any, &mut WindowContext) -> bool>>,
+    custom_drop_handle:
+        Option<Arc<dyn Fn(&mut Pane, &dyn Any, &mut ViewContext<Pane>) -> ControlFlow<(), ()>>>,
     can_split: bool,
     render_tab_bar_buttons: Rc<dyn Fn(&mut Pane, &mut ViewContext<Pane>) -> AnyElement>,
     _subscriptions: Vec<Subscription>,
@@ -374,6 +378,7 @@ impl Pane {
             workspace,
             project,
             can_drop_predicate,
+            custom_drop_handle: None,
             can_split: true,
             render_tab_bar_buttons: Rc::new(move |pane, cx| {
                 h_stack()
@@ -500,13 +505,6 @@ impl Pane {
         self.active_item_index
     }
 
-    //     pub fn on_can_drop<F>(&mut self, can_drop: F)
-    //     where
-    //         F: 'static + Fn(&DragAndDrop<Workspace>, &WindowContext) -> bool,
-    //     {
-    //         self.can_drop = Rc::new(can_drop);
-    //     }
-
     pub fn set_can_split(&mut self, can_split: bool, cx: &mut ViewContext<Self>) {
         self.can_split = can_split;
         cx.notify();
@@ -527,6 +525,14 @@ impl Pane {
         cx.notify();
     }
 
+    pub fn set_custom_drop_handle<F>(&mut self, cx: &mut ViewContext<Self>, handle: F)
+    where
+        F: 'static + Fn(&mut Pane, &dyn Any, &mut ViewContext<Pane>) -> ControlFlow<(), ()>,
+    {
+        self.custom_drop_handle = Some(Arc::new(handle));
+        cx.notify();
+    }
+
     pub fn nav_history_for_item<T: Item>(&self, item: &View<T>) -> ItemNavHistory {
         ItemNavHistory {
             history: self.nav_history.clone(),
@@ -1555,6 +1561,10 @@ impl Pane {
                 this.drag_split_direction = None;
                 this.handle_project_entry_drop(entry_id, cx)
             }))
+            .on_drop(cx.listener(move |this, paths, cx| {
+                this.drag_split_direction = None;
+                this.handle_external_paths_drop(paths, cx)
+            }))
             .when_some(item.tab_tooltip_text(cx), |tab, text| {
                 tab.tooltip(move |cx| Tooltip::text(text.clone(), cx))
             })
@@ -1721,6 +1731,10 @@ impl Pane {
                     .on_drop(cx.listener(move |this, entry_id: &ProjectEntryId, cx| {
                         this.drag_split_direction = None;
                         this.handle_project_entry_drop(entry_id, cx)
+                    }))
+                    .on_drop(cx.listener(move |this, paths, cx| {
+                        this.drag_split_direction = None;
+                        this.handle_external_paths_drop(paths, cx)
                     })),
             )
     }
@@ -1809,8 +1823,13 @@ impl Pane {
         &mut self,
         dragged_tab: &DraggedTab,
         ix: usize,
-        cx: &mut ViewContext<'_, Pane>,
+        cx: &mut ViewContext<'_, Self>,
     ) {
+        if let Some(custom_drop_handle) = self.custom_drop_handle.clone() {
+            if let ControlFlow::Break(()) = custom_drop_handle(self, dragged_tab, cx) {
+                return;
+            }
+        }
         let mut to_pane = cx.view().clone();
         let split_direction = self.drag_split_direction;
         let item_id = dragged_tab.item_id;
@@ -1830,8 +1849,13 @@ impl Pane {
     fn handle_project_entry_drop(
         &mut self,
         project_entry_id: &ProjectEntryId,
-        cx: &mut ViewContext<'_, Pane>,
+        cx: &mut ViewContext<'_, Self>,
     ) {
+        if let Some(custom_drop_handle) = self.custom_drop_handle.clone() {
+            if let ControlFlow::Break(()) = custom_drop_handle(self, project_entry_id, cx) {
+                return;
+            }
+        }
         let mut to_pane = cx.view().clone();
         let split_direction = self.drag_split_direction;
         let project_entry_id = *project_entry_id;
@@ -1855,6 +1879,38 @@ impl Pane {
             .log_err();
     }
 
+    fn handle_external_paths_drop(
+        &mut self,
+        paths: &ExternalPaths,
+        cx: &mut ViewContext<'_, Self>,
+    ) {
+        if let Some(custom_drop_handle) = self.custom_drop_handle.clone() {
+            if let ControlFlow::Break(()) = custom_drop_handle(self, paths, cx) {
+                return;
+            }
+        }
+        let mut to_pane = cx.view().clone();
+        let split_direction = self.drag_split_direction;
+        let paths = paths.paths().to_vec();
+        self.workspace
+            .update(cx, |_, cx| {
+                cx.defer(move |workspace, cx| {
+                    if let Some(split_direction) = split_direction {
+                        to_pane = workspace.split_pane(to_pane, split_direction, cx);
+                    }
+                    workspace
+                        .open_paths(
+                            paths,
+                            OpenVisible::OnlyDirectories,
+                            Some(to_pane.downgrade()),
+                            cx,
+                        )
+                        .detach();
+                });
+            })
+            .log_err();
+    }
+
     pub fn display_nav_history_buttons(&mut self, display: bool) {
         self.display_nav_history_buttons = display;
     }
@@ -1956,6 +2012,7 @@ impl Render for Pane {
                     .group("")
                     .on_drag_move::<DraggedTab>(cx.listener(Self::handle_drag_move))
                     .on_drag_move::<ProjectEntryId>(cx.listener(Self::handle_drag_move))
+                    .on_drag_move::<ExternalPaths>(cx.listener(Self::handle_drag_move))
                     .map(|div| {
                         if let Some(item) = self.active_item() {
                             div.v_flex()
@@ -1985,6 +2042,7 @@ impl Render for Pane {
                             ))
                             .group_drag_over::<DraggedTab>("", |style| style.visible())
                             .group_drag_over::<ProjectEntryId>("", |style| style.visible())
+                            .group_drag_over::<ExternalPaths>("", |style| style.visible())
                             .when_some(self.can_drop_predicate.clone(), |this, p| {
                                 this.can_drop(move |a, cx| p(a, cx))
                             })
@@ -1994,6 +2052,9 @@ impl Render for Pane {
                             .on_drop(cx.listener(move |this, entry_id, cx| {
                                 this.handle_project_entry_drop(entry_id, cx)
                             }))
+                            .on_drop(cx.listener(move |this, paths, cx| {
+                                this.handle_external_paths_drop(paths, cx)
+                            }))
                             .map(|div| match self.drag_split_direction {
                                 None => div.top_0().left_0().right_0().bottom_0(),
                                 Some(SplitDirection::Up) => div.top_0().left_0().right_0().h_32(),

crates/workspace/src/pane_group.rs 🔗

@@ -487,6 +487,7 @@ impl PaneAxis {
             basis,
             self.flexes.clone(),
             self.bounding_boxes.clone(),
+            cx.view().downgrade(),
         )
         .children(self.members.iter().enumerate().map(|(ix, member)| {
             if member.contains(active_pane) {
@@ -575,21 +576,25 @@ mod element {
     use gpui::{
         px, relative, Along, AnyElement, Axis, Bounds, CursorStyle, Element, InteractiveBounds,
         IntoElement, MouseDownEvent, MouseMoveEvent, MouseUpEvent, ParentElement, Pixels, Point,
-        Size, Style, WindowContext,
+        Size, Style, WeakView, WindowContext,
     };
     use parking_lot::Mutex;
     use smallvec::SmallVec;
     use ui::prelude::*;
+    use util::ResultExt;
+
+    use crate::Workspace;
 
     use super::{HANDLE_HITBOX_SIZE, HORIZONTAL_MIN_SIZE, VERTICAL_MIN_SIZE};
 
     const DIVIDER_SIZE: f32 = 1.0;
 
-    pub fn pane_axis(
+    pub(super) fn pane_axis(
         axis: Axis,
         basis: usize,
         flexes: Arc<Mutex<Vec<f32>>>,
         bounding_boxes: Arc<Mutex<Vec<Option<Bounds<Pixels>>>>>,
+        workspace: WeakView<Workspace>,
     ) -> PaneAxisElement {
         PaneAxisElement {
             axis,
@@ -598,6 +603,7 @@ mod element {
             bounding_boxes,
             children: SmallVec::new(),
             active_pane_ix: None,
+            workspace,
         }
     }
 
@@ -608,6 +614,7 @@ mod element {
         bounding_boxes: Arc<Mutex<Vec<Option<Bounds<Pixels>>>>>,
         children: SmallVec<[AnyElement; 2]>,
         active_pane_ix: Option<usize>,
+        workspace: WeakView<Workspace>,
     }
 
     impl PaneAxisElement {
@@ -623,6 +630,7 @@ mod element {
             axis: Axis,
             child_start: Point<Pixels>,
             container_size: Size<Pixels>,
+            workspace: WeakView<Workspace>,
             cx: &mut WindowContext,
         ) {
             let min_size = match axis {
@@ -697,7 +705,9 @@ mod element {
             }
 
             // todo!(schedule serialize)
-            // workspace.schedule_serialize(cx);
+            workspace
+                .update(cx, |this, cx| this.schedule_serialize(cx))
+                .log_err();
             cx.notify();
         }
 
@@ -708,6 +718,7 @@ mod element {
             ix: usize,
             pane_bounds: Bounds<Pixels>,
             axis_bounds: Bounds<Pixels>,
+            workspace: WeakView<Workspace>,
             cx: &mut WindowContext,
         ) {
             let handle_bounds = Bounds {
@@ -742,24 +753,39 @@ mod element {
 
                 cx.on_mouse_event({
                     let dragged_handle = dragged_handle.clone();
-                    move |e: &MouseDownEvent, phase, _cx| {
+                    let flexes = flexes.clone();
+                    let workspace = workspace.clone();
+                    move |e: &MouseDownEvent, phase, cx| {
                         if phase.bubble() && handle_bounds.contains(&e.position) {
                             dragged_handle.replace(Some(ix));
+                            if e.click_count >= 2 {
+                                let mut borrow = flexes.lock();
+                                *borrow = vec![1.; borrow.len()];
+                                workspace
+                                    .update(cx, |this, cx| this.schedule_serialize(cx))
+                                    .log_err();
+                                cx.notify();
+                            }
                         }
                     }
                 });
-                cx.on_mouse_event(move |e: &MouseMoveEvent, phase, cx| {
-                    let dragged_handle = dragged_handle.borrow();
-                    if phase.bubble() && *dragged_handle == Some(ix) {
-                        Self::compute_resize(
-                            &flexes,
-                            e,
-                            ix,
-                            axis,
-                            pane_bounds.origin,
-                            axis_bounds.size,
-                            cx,
-                        )
+                cx.on_mouse_event({
+                    let workspace = workspace.clone();
+                    move |e: &MouseMoveEvent, phase, cx| {
+                        let dragged_handle = dragged_handle.borrow();
+
+                        if phase.bubble() && *dragged_handle == Some(ix) {
+                            Self::compute_resize(
+                                &flexes,
+                                e,
+                                ix,
+                                axis,
+                                pane_bounds.origin,
+                                axis_bounds.size,
+                                workspace.clone(),
+                                cx,
+                            )
+                        }
                     }
                 });
             });
@@ -840,6 +866,7 @@ mod element {
                             ix,
                             child_bounds,
                             bounds,
+                            self.workspace.clone(),
                             cx,
                         );
                     }

crates/workspace/src/shared_screen.rs 🔗

@@ -66,12 +66,16 @@ impl FocusableView for SharedScreen {
     }
 }
 impl Render for SharedScreen {
-    fn render(&mut self, _: &mut ViewContext<Self>) -> impl IntoElement {
-        div().track_focus(&self.focus).size_full().children(
-            self.frame
-                .as_ref()
-                .map(|frame| img(frame.image()).size_full()),
-        )
+    fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
+        div()
+            .bg(cx.theme().colors().editor_background)
+            .track_focus(&self.focus)
+            .size_full()
+            .children(
+                self.frame
+                    .as_ref()
+                    .map(|frame| img(frame.image()).size_full()),
+            )
     }
 }
 

crates/workspace/src/workspace.rs 🔗

@@ -431,6 +431,13 @@ pub enum Event {
     WorkspaceCreated(WeakView<Workspace>),
 }
 
+pub enum OpenVisible {
+    All,
+    None,
+    OnlyFiles,
+    OnlyDirectories,
+}
+
 pub struct Workspace {
     weak_self: WeakView<Self>,
     workspace_actions: Vec<Box<dyn Fn(Div, &mut ViewContext<Self>) -> Div>>,
@@ -651,7 +658,7 @@ impl Workspace {
             cx.on_release(|this, window, cx| {
                 this.app_state.workspace_store.update(cx, |store, _| {
                     let window = window.downcast::<Self>().unwrap();
-                    debug_assert!(store.workspaces.remove(&window));
+                    store.workspaces.remove(&window);
                 })
             }),
         ];
@@ -1258,9 +1265,7 @@ impl Workspace {
     }
 
     pub fn open(&mut self, _: &Open, cx: &mut ViewContext<Self>) {
-        self.client()
-            .telemetry()
-            .report_app_event("open project", false, cx);
+        self.client().telemetry().report_app_event("open project");
         let paths = cx.prompt_for_paths(PathPromptOptions {
             files: true,
             directories: true,
@@ -1317,7 +1322,8 @@ impl Workspace {
     pub fn open_paths(
         &mut self,
         mut abs_paths: Vec<PathBuf>,
-        visible: bool,
+        visible: OpenVisible,
+        pane: Option<WeakView<Pane>>,
         cx: &mut ViewContext<Self>,
     ) -> Task<Vec<Option<Result<Box<dyn ItemHandle>, anyhow::Error>>>> {
         log::info!("open paths {abs_paths:?}");
@@ -1328,31 +1334,56 @@ impl Workspace {
         abs_paths.sort_unstable();
         cx.spawn(move |this, mut cx| async move {
             let mut tasks = Vec::with_capacity(abs_paths.len());
+
             for abs_path in &abs_paths {
-                let project_path = match this
-                    .update(&mut cx, |this, cx| {
-                        Workspace::project_path_for_path(
-                            this.project.clone(),
-                            abs_path,
-                            visible,
-                            cx,
-                        )
-                    })
-                    .log_err()
-                {
-                    Some(project_path) => project_path.await.log_err(),
+                let visible = match visible {
+                    OpenVisible::All => Some(true),
+                    OpenVisible::None => Some(false),
+                    OpenVisible::OnlyFiles => match fs.metadata(abs_path).await.log_err() {
+                        Some(Some(metadata)) => Some(!metadata.is_dir),
+                        Some(None) => {
+                            log::error!("No metadata for file {abs_path:?}");
+                            None
+                        }
+                        None => None,
+                    },
+                    OpenVisible::OnlyDirectories => match fs.metadata(abs_path).await.log_err() {
+                        Some(Some(metadata)) => Some(metadata.is_dir),
+                        Some(None) => {
+                            log::error!("No metadata for file {abs_path:?}");
+                            None
+                        }
+                        None => None,
+                    },
+                };
+                let project_path = match visible {
+                    Some(visible) => match this
+                        .update(&mut cx, |this, cx| {
+                            Workspace::project_path_for_path(
+                                this.project.clone(),
+                                abs_path,
+                                visible,
+                                cx,
+                            )
+                        })
+                        .log_err()
+                    {
+                        Some(project_path) => project_path.await.log_err(),
+                        None => None,
+                    },
                     None => None,
                 };
 
                 let this = this.clone();
                 let abs_path = abs_path.clone();
                 let fs = fs.clone();
+                let pane = pane.clone();
                 let task = cx.spawn(move |mut cx| async move {
                     let (worktree, project_path) = project_path?;
                     if fs.is_file(&abs_path).await {
                         Some(
                             this.update(&mut cx, |this, cx| {
-                                this.open_path(project_path, None, true, cx)
+                                this.open_path(project_path, pane, true, cx)
                             })
                             .log_err()?
                             .await,
@@ -1398,7 +1429,9 @@ impl Workspace {
         cx.spawn(|this, mut cx| async move {
             if let Some(paths) = paths.await.log_err().flatten() {
                 let results = this
-                    .update(&mut cx, |this, cx| this.open_paths(paths, true, cx))?
+                    .update(&mut cx, |this, cx| {
+                        this.open_paths(paths, OpenVisible::All, None, cx)
+                    })?
                     .await;
                 for result in results.into_iter().flatten() {
                     result.log_err();
@@ -1784,7 +1817,16 @@ impl Workspace {
         cx.spawn(|workspace, mut cx| async move {
             let open_paths_task_result = workspace
                 .update(&mut cx, |workspace, cx| {
-                    workspace.open_paths(vec![abs_path.clone()], visible, cx)
+                    workspace.open_paths(
+                        vec![abs_path.clone()],
+                        if visible {
+                            OpenVisible::All
+                        } else {
+                            OpenVisible::None
+                        },
+                        None,
+                        cx,
+                    )
                 })
                 .with_context(|| format!("open abs path {abs_path:?} task spawn"))?
                 .await;
@@ -2457,11 +2499,11 @@ impl Workspace {
         Some(leader_id)
     }
 
-    //     pub fn is_being_followed(&self, peer_id: PeerId) -> bool {
-    //         self.follower_states
-    //             .values()
-    //             .any(|state| state.leader_id == peer_id)
-    //     }
+    pub fn is_being_followed(&self, peer_id: PeerId) -> bool {
+        self.follower_states
+            .values()
+            .any(|state| state.leader_id == peer_id)
+    }
 
     fn active_item_path_changed(&mut self, cx: &mut ViewContext<Self>) {
         let active_entry = self.active_project_path(cx);
@@ -4083,7 +4125,7 @@ pub fn open_paths(
                 existing.clone(),
                 existing
                     .update(&mut cx, |workspace, cx| {
-                        workspace.open_paths(abs_paths, true, cx)
+                        workspace.open_paths(abs_paths, OpenVisible::All, None, cx)
                     })?
                     .await,
             ))
@@ -4131,7 +4173,7 @@ pub fn create_and_open_local_file(
         let mut items = workspace
             .update(&mut cx, |workspace, cx| {
                 workspace.with_local_workspace(cx, |workspace, cx| {
-                    workspace.open_paths(vec![path.to_path_buf()], false, cx)
+                    workspace.open_paths(vec![path.to_path_buf()], OpenVisible::None, None, cx)
                 })
             })?
             .await?

crates/zed/Cargo.toml 🔗

@@ -146,8 +146,7 @@ uuid.workspace = true
 [dev-dependencies]
 call = { path = "../call", features = ["test-support"] }
 # client = { path = "../client", features = ["test-support"] }
-# editor = { path = "../editor", features = ["test-support"] }
-# gpui = { path = "../gpui", features = ["test-support"] }
+editor = { path = "../editor", features = ["test-support"] }
 gpui = { path = "../gpui", features = ["test-support"] }
 language = { path = "../language", features = ["test-support"] }
 # lsp = { path = "../lsp", features = ["test-support"] }
@@ -156,7 +155,7 @@ project = { path = "../project", features = ["test-support"] }
 # settings = { path = "../settings", features = ["test-support"] }
 text = { path = "../text", features = ["test-support"] }
 # util = { path = "../util", features = ["test-support"] }
-# workspace = { path = "../workspace", features = ["test-support"] }
+workspace = { path = "../workspace", features = ["test-support"] }
 unindent.workspace = true
 
 [package.metadata.bundle-dev]

crates/zed/src/main.rs 🔗

@@ -45,7 +45,7 @@ use util::{
     paths, ResultExt,
 };
 use uuid::Uuid;
-use welcome::{show_welcome_view, FIRST_OPEN};
+use welcome::{show_welcome_view, BaseKeymap, FIRST_OPEN};
 use workspace::{AppState, WorkspaceStore};
 use zed::{
     app_menus, build_window_options, ensure_only_instance, handle_cli_connection,
@@ -171,17 +171,15 @@ fn main() {
         })
         .detach();
 
-        client.telemetry().start(installation_id, session_id, cx);
-        client
-            .telemetry()
-            .report_setting_event("theme", cx.theme().name.to_string(), cx);
-        let event_operation = match existing_installation_id_found {
+        let telemetry = client.telemetry();
+        telemetry.start(installation_id, session_id, cx);
+        telemetry.report_setting_event("theme", cx.theme().name.to_string());
+        telemetry.report_setting_event("keymap", BaseKeymap::get_global(cx).to_string());
+        telemetry.report_app_event(match existing_installation_id_found {
             Some(false) => "first open",
             _ => "open",
-        };
-        client
-            .telemetry()
-            .report_app_event(event_operation, true, cx);
+        });
+        telemetry.flush_events();
 
         let app_state = Arc::new(AppState {
             languages: languages.clone(),

crates/zed/src/zed.rs 🔗

@@ -33,11 +33,11 @@ use util::{
 };
 use uuid::Uuid;
 use welcome::BaseKeymap;
-use workspace::Pane;
 use workspace::{
     create_and_open_local_file, notifications::simple_message_notification::MessageNotification,
     open_new, AppState, NewFile, NewWindow, Workspace, WorkspaceSettings,
 };
+use workspace::{dock::Panel, Pane};
 use zed_actions::{OpenBrowser, OpenSettings, OpenZedURL, Quit};
 
 actions!(
@@ -114,9 +114,7 @@ pub fn initialize_workspace(app_state: Arc<AppState>, cx: &mut AppContext) {
         })
         .detach();
 
-        //     cx.emit(workspace2::Event::PaneAdded(
-        //         workspace.active_pane().clone(),
-        //     ));
+        // cx.emit(workspace::Event::PaneAdded(workspace.active_pane().clone()));
 
         //     let collab_titlebar_item =
         //         cx.add_view(|cx| CollabTitlebarItem::new(workspace, &workspace_handle, cx));
@@ -187,6 +185,7 @@ pub fn initialize_workspace(app_state: Arc<AppState>, cx: &mut AppContext) {
             )?;
 
             workspace_handle.update(&mut cx, |workspace, cx| {
+                let position = project_panel.read(cx).position(cx);
                 workspace.add_panel(project_panel, cx);
                 workspace.add_panel(terminal_panel, cx);
                 workspace.add_panel(assistant_panel, cx);
@@ -194,19 +193,18 @@ pub fn initialize_workspace(app_state: Arc<AppState>, cx: &mut AppContext) {
                 workspace.add_panel(chat_panel, cx);
                 workspace.add_panel(notification_panel, cx);
 
-                // if !was_deserialized
-                //     && workspace
-                //         .project()
-                //         .read(cx)
-                //         .visible_worktrees(cx)
-                //         .any(|tree| {
-                //             tree.read(cx)
-                //                 .root_entry()
-                //                 .map_or(false, |entry| entry.is_dir())
-                //         })
-                // {
-                //     workspace.toggle_dock(project_panel_position, cx);
-                // }
+                if workspace
+                    .project()
+                    .read(cx)
+                    .visible_worktrees(cx)
+                    .any(|tree| {
+                        tree.read(cx)
+                            .root_entry()
+                            .map_or(false, |entry| entry.is_dir())
+                    })
+                {
+                    workspace.toggle_dock(position, cx);
+                }
                 cx.focus_self();
             })
         })
@@ -587,7 +585,6 @@ pub fn handle_keymap_file_changes(
                     }
                 }
             }
-
             cx.update(|cx| reload_keymaps(cx, &user_keymap)).ok();
         }
     })
@@ -770,1844 +767,2073 @@ fn open_bundled_file(
 }
 
 // todo!()
-// #[cfg(test)]
-// mod tests {
-//     use super::*;
-//     use assets::Assets;
-//     use editor::{scroll::autoscroll::Autoscroll, DisplayPoint, Editor};
-//     use fs::{FakeFs, Fs};
-//     use gpui::{
-//         actions, elements::Empty, executor::Deterministic, Action, AnyElement, AnyWindowHandle,
-//         AppContext, AssetSource, Element, Entity, TestAppContext, View, ViewHandle,
-//     };
-//     use language::LanguageRegistry;
-//     use project::{project_settings::ProjectSettings, Project, ProjectPath};
-//     use serde_json::json;
-//     use settings::{handle_settings_file_changes, watch_config_file, SettingsStore};
-//     use std::{
-//         collections::HashSet,
-//         path::{Path, PathBuf},
-//     };
-//     use theme::{ThemeRegistry, ThemeSettings};
-//     use workspace::{
-//         item::{Item, ItemHandle},
-//         open_new, open_paths, pane, NewFile, SaveIntent, SplitDirection, WorkspaceHandle,
-//     };
-
-//     #[gpui::test]
-//     async fn test_open_paths_action(cx: &mut TestAppContext) {
-//         let app_state = init_test(cx);
-//         app_state
-//             .fs
-//             .as_fake()
-//             .insert_tree(
-//                 "/root",
-//                 json!({
-//                     "a": {
-//                         "aa": null,
-//                         "ab": null,
-//                     },
-//                     "b": {
-//                         "ba": null,
-//                         "bb": null,
-//                     },
-//                     "c": {
-//                         "ca": null,
-//                         "cb": null,
-//                     },
-//                     "d": {
-//                         "da": null,
-//                         "db": null,
-//                     },
-//                 }),
-//             )
-//             .await;
-
-//         cx.update(|cx| {
-//             open_paths(
-//                 &[PathBuf::from("/root/a"), PathBuf::from("/root/b")],
-//                 &app_state,
-//                 None,
-//                 cx,
-//             )
-//         })
-//         .await
-//         .unwrap();
-//         assert_eq!(cx.windows().len(), 1);
-
-//         cx.update(|cx| open_paths(&[PathBuf::from("/root/a")], &app_state, None, cx))
-//             .await
-//             .unwrap();
-//         assert_eq!(cx.windows().len(), 1);
-//         let workspace_1 = cx.windows()[0].downcast::<Workspace>().unwrap().root(cx);
-//         workspace_1.update(cx, |workspace, cx| {
-//             assert_eq!(workspace.worktrees(cx).count(), 2);
-//             assert!(workspace.left_dock().read(cx).is_open());
-//             assert!(workspace.active_pane().is_focused(cx));
-//         });
-
-//         cx.update(|cx| {
-//             open_paths(
-//                 &[PathBuf::from("/root/b"), PathBuf::from("/root/c")],
-//                 &app_state,
-//                 None,
-//                 cx,
-//             )
-//         })
-//         .await
-//         .unwrap();
-//         assert_eq!(cx.windows().len(), 2);
-
-//         // Replace existing windows
-//         let window = cx.windows()[0].downcast::<Workspace>().unwrap();
-//         cx.update(|cx| {
-//             open_paths(
-//                 &[PathBuf::from("/root/c"), PathBuf::from("/root/d")],
-//                 &app_state,
-//                 Some(window),
-//                 cx,
-//             )
-//         })
-//         .await
-//         .unwrap();
-//         assert_eq!(cx.windows().len(), 2);
-//         let workspace_1 = cx.windows()[0].downcast::<Workspace>().unwrap().root(cx);
-//         workspace_1.update(cx, |workspace, cx| {
-//             assert_eq!(
-//                 workspace
-//                     .worktrees(cx)
-//                     .map(|w| w.read(cx).abs_path())
-//                     .collect::<Vec<_>>(),
-//                 &[Path::new("/root/c").into(), Path::new("/root/d").into()]
-//             );
-//             assert!(workspace.left_dock().read(cx).is_open());
-//             assert!(workspace.active_pane().is_focused(cx));
-//         });
-//     }
-
-//     #[gpui::test]
-//     async fn test_window_edit_state(executor: Arc<Deterministic>, cx: &mut TestAppContext) {
-//         let app_state = init_test(cx);
-//         app_state
-//             .fs
-//             .as_fake()
-//             .insert_tree("/root", json!({"a": "hey"}))
-//             .await;
-
-//         cx.update(|cx| open_paths(&[PathBuf::from("/root/a")], &app_state, None, cx))
-//             .await
-//             .unwrap();
-//         assert_eq!(cx.windows().len(), 1);
-
-//         // When opening the workspace, the window is not in a edited state.
-//         let window = cx.windows()[0].downcast::<Workspace>().unwrap();
-//         let workspace = window.root(cx);
-//         let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
-//         let editor = workspace.read_with(cx, |workspace, cx| {
-//             workspace
-//                 .active_item(cx)
-//                 .unwrap()
-//                 .downcast::<Editor>()
-//                 .unwrap()
-//         });
-//         assert!(!window.is_edited(cx));
-
-//         // Editing a buffer marks the window as edited.
-//         editor.update(cx, |editor, cx| editor.insert("EDIT", cx));
-//         assert!(window.is_edited(cx));
-
-//         // Undoing the edit restores the window's edited state.
-//         editor.update(cx, |editor, cx| editor.undo(&Default::default(), cx));
-//         assert!(!window.is_edited(cx));
-
-//         // Redoing the edit marks the window as edited again.
-//         editor.update(cx, |editor, cx| editor.redo(&Default::default(), cx));
-//         assert!(window.is_edited(cx));
-
-//         // Closing the item restores the window's edited state.
-//         let close = pane.update(cx, |pane, cx| {
-//             drop(editor);
-//             pane.close_active_item(&Default::default(), cx).unwrap()
-//         });
-//         executor.run_until_parked();
-
-//         window.simulate_prompt_answer(1, cx);
-//         close.await.unwrap();
-//         assert!(!window.is_edited(cx));
-
-//         // Opening the buffer again doesn't impact the window's edited state.
-//         cx.update(|cx| open_paths(&[PathBuf::from("/root/a")], &app_state, None, cx))
-//             .await
-//             .unwrap();
-//         let editor = workspace.read_with(cx, |workspace, cx| {
-//             workspace
-//                 .active_item(cx)
-//                 .unwrap()
-//                 .downcast::<Editor>()
-//                 .unwrap()
-//         });
-//         assert!(!window.is_edited(cx));
-
-//         // Editing the buffer marks the window as edited.
-//         editor.update(cx, |editor, cx| editor.insert("EDIT", cx));
-//         assert!(window.is_edited(cx));
-
-//         // Ensure closing the window via the mouse gets preempted due to the
-//         // buffer having unsaved changes.
-//         assert!(!window.simulate_close(cx));
-//         executor.run_until_parked();
-//         assert_eq!(cx.windows().len(), 1);
-
-//         // The window is successfully closed after the user dismisses the prompt.
-//         window.simulate_prompt_answer(1, cx);
-//         executor.run_until_parked();
-//         assert_eq!(cx.windows().len(), 0);
-//     }
-
-//     #[gpui::test]
-//     async fn test_new_empty_workspace(cx: &mut TestAppContext) {
-//         let app_state = init_test(cx);
-//         cx.update(|cx| {
-//             open_new(&app_state, cx, |workspace, cx| {
-//                 Editor::new_file(workspace, &Default::default(), cx)
-//             })
-//         })
-//         .await;
-
-//         let window = cx
-//             .windows()
-//             .first()
-//             .unwrap()
-//             .downcast::<Workspace>()
-//             .unwrap();
-//         let workspace = window.root(cx);
-
-//         let editor = workspace.update(cx, |workspace, cx| {
-//             workspace
-//                 .active_item(cx)
-//                 .unwrap()
-//                 .downcast::<editor::Editor>()
-//                 .unwrap()
-//         });
-
-//         editor.update(cx, |editor, cx| {
-//             assert!(editor.text(cx).is_empty());
-//             assert!(!editor.is_dirty(cx));
-//         });
-
-//         let save_task = workspace.update(cx, |workspace, cx| {
-//             workspace.save_active_item(SaveIntent::Save, cx)
-//         });
-//         app_state.fs.create_dir(Path::new("/root")).await.unwrap();
-//         cx.foreground().run_until_parked();
-//         cx.simulate_new_path_selection(|_| Some(PathBuf::from("/root/the-new-name")));
-//         save_task.await.unwrap();
-//         editor.read_with(cx, |editor, cx| {
-//             assert!(!editor.is_dirty(cx));
-//             assert_eq!(editor.title(cx), "the-new-name");
-//         });
-//     }
-
-//     #[gpui::test]
-//     async fn test_open_entry(cx: &mut TestAppContext) {
-//         let app_state = init_test(cx);
-//         app_state
-//             .fs
-//             .as_fake()
-//             .insert_tree(
-//                 "/root",
-//                 json!({
-//                     "a": {
-//                         "file1": "contents 1",
-//                         "file2": "contents 2",
-//                         "file3": "contents 3",
-//                     },
-//                 }),
-//             )
-//             .await;
-
-//         let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
-//         let window = cx.add_window(|cx| Workspace::test_new(project, cx));
-//         let workspace = window.root(cx);
-
-//         let entries = cx.read(|cx| workspace.file_project_paths(cx));
-//         let file1 = entries[0].clone();
-//         let file2 = entries[1].clone();
-//         let file3 = entries[2].clone();
-
-//         // Open the first entry
-//         let entry_1 = workspace
-//             .update(cx, |w, cx| w.open_path(file1.clone(), None, true, cx))
-//             .await
-//             .unwrap();
-//         cx.read(|cx| {
-//             let pane = workspace.read(cx).active_pane().read(cx);
-//             assert_eq!(
-//                 pane.active_item().unwrap().project_path(cx),
-//                 Some(file1.clone())
-//             );
-//             assert_eq!(pane.items_len(), 1);
-//         });
-
-//         // Open the second entry
-//         workspace
-//             .update(cx, |w, cx| w.open_path(file2.clone(), None, true, cx))
-//             .await
-//             .unwrap();
-//         cx.read(|cx| {
-//             let pane = workspace.read(cx).active_pane().read(cx);
-//             assert_eq!(
-//                 pane.active_item().unwrap().project_path(cx),
-//                 Some(file2.clone())
-//             );
-//             assert_eq!(pane.items_len(), 2);
-//         });
-
-//         // Open the first entry again. The existing pane item is activated.
-//         let entry_1b = workspace
-//             .update(cx, |w, cx| w.open_path(file1.clone(), None, true, cx))
-//             .await
-//             .unwrap();
-//         assert_eq!(entry_1.id(), entry_1b.id());
-
-//         cx.read(|cx| {
-//             let pane = workspace.read(cx).active_pane().read(cx);
-//             assert_eq!(
-//                 pane.active_item().unwrap().project_path(cx),
-//                 Some(file1.clone())
-//             );
-//             assert_eq!(pane.items_len(), 2);
-//         });
-
-//         // Split the pane with the first entry, then open the second entry again.
-//         workspace
-//             .update(cx, |w, cx| {
-//                 w.split_and_clone(w.active_pane().clone(), SplitDirection::Right, cx);
-//                 w.open_path(file2.clone(), None, true, cx)
-//             })
-//             .await
-//             .unwrap();
-
-//         workspace.read_with(cx, |w, cx| {
-//             assert_eq!(
-//                 w.active_pane()
-//                     .read(cx)
-//                     .active_item()
-//                     .unwrap()
-//                     .project_path(cx),
-//                 Some(file2.clone())
-//             );
-//         });
-
-//         // Open the third entry twice concurrently. Only one pane item is added.
-//         let (t1, t2) = workspace.update(cx, |w, cx| {
-//             (
-//                 w.open_path(file3.clone(), None, true, cx),
-//                 w.open_path(file3.clone(), None, true, cx),
-//             )
-//         });
-//         t1.await.unwrap();
-//         t2.await.unwrap();
-//         cx.read(|cx| {
-//             let pane = workspace.read(cx).active_pane().read(cx);
-//             assert_eq!(
-//                 pane.active_item().unwrap().project_path(cx),
-//                 Some(file3.clone())
-//             );
-//             let pane_entries = pane
-//                 .items()
-//                 .map(|i| i.project_path(cx).unwrap())
-//                 .collect::<Vec<_>>();
-//             assert_eq!(pane_entries, &[file1, file2, file3]);
-//         });
-//     }
-
-//     #[gpui::test]
-//     async fn test_open_paths(cx: &mut TestAppContext) {
-//         let app_state = init_test(cx);
-
-//         app_state
-//             .fs
-//             .as_fake()
-//             .insert_tree(
-//                 "/",
-//                 json!({
-//                     "dir1": {
-//                         "a.txt": ""
-//                     },
-//                     "dir2": {
-//                         "b.txt": ""
-//                     },
-//                     "dir3": {
-//                         "c.txt": ""
-//                     },
-//                     "d.txt": ""
-//                 }),
-//             )
-//             .await;
-
-//         cx.update(|cx| open_paths(&[PathBuf::from("/dir1/")], &app_state, None, cx))
-//             .await
-//             .unwrap();
-//         assert_eq!(cx.windows().len(), 1);
-//         let workspace = cx.windows()[0].downcast::<Workspace>().unwrap().root(cx);
-
-//         #[track_caller]
-//         fn assert_project_panel_selection(
-//             workspace: &Workspace,
-//             expected_worktree_path: &Path,
-//             expected_entry_path: &Path,
-//             cx: &AppContext,
-//         ) {
-//             let project_panel = [
-//                 workspace.left_dock().read(cx).panel::<ProjectPanel>(),
-//                 workspace.right_dock().read(cx).panel::<ProjectPanel>(),
-//                 workspace.bottom_dock().read(cx).panel::<ProjectPanel>(),
-//             ]
-//             .into_iter()
-//             .find_map(std::convert::identity)
-//             .expect("found no project panels")
-//             .read(cx);
-//             let (selected_worktree, selected_entry) = project_panel
-//                 .selected_entry(cx)
-//                 .expect("project panel should have a selected entry");
-//             assert_eq!(
-//                 selected_worktree.abs_path().as_ref(),
-//                 expected_worktree_path,
-//                 "Unexpected project panel selected worktree path"
-//             );
-//             assert_eq!(
-//                 selected_entry.path.as_ref(),
-//                 expected_entry_path,
-//                 "Unexpected project panel selected entry path"
-//             );
-//         }
-
-//         // Open a file within an existing worktree.
-//         workspace
-//             .update(cx, |view, cx| {
-//                 view.open_paths(vec!["/dir1/a.txt".into()], true, cx)
-//             })
-//             .await;
-//         cx.read(|cx| {
-//             let workspace = workspace.read(cx);
-//             assert_project_panel_selection(workspace, Path::new("/dir1"), Path::new("a.txt"), cx);
-//             assert_eq!(
-//                 workspace
-//                     .active_pane()
-//                     .read(cx)
-//                     .active_item()
-//                     .unwrap()
-//                     .as_any()
-//                     .downcast_ref::<Editor>()
-//                     .unwrap()
-//                     .read(cx)
-//                     .title(cx),
-//                 "a.txt"
-//             );
-//         });
-
-//         // Open a file outside of any existing worktree.
-//         workspace
-//             .update(cx, |view, cx| {
-//                 view.open_paths(vec!["/dir2/b.txt".into()], true, cx)
-//             })
-//             .await;
-//         cx.read(|cx| {
-//             let workspace = workspace.read(cx);
-//             assert_project_panel_selection(workspace, Path::new("/dir2/b.txt"), Path::new(""), cx);
-//             let worktree_roots = workspace
-//                 .worktrees(cx)
-//                 .map(|w| w.read(cx).as_local().unwrap().abs_path().as_ref())
-//                 .collect::<HashSet<_>>();
-//             assert_eq!(
-//                 worktree_roots,
-//                 vec!["/dir1", "/dir2/b.txt"]
-//                     .into_iter()
-//                     .map(Path::new)
-//                     .collect(),
-//             );
-//             assert_eq!(
-//                 workspace
-//                     .active_pane()
-//                     .read(cx)
-//                     .active_item()
-//                     .unwrap()
-//                     .as_any()
-//                     .downcast_ref::<Editor>()
-//                     .unwrap()
-//                     .read(cx)
-//                     .title(cx),
-//                 "b.txt"
-//             );
-//         });
-
-//         // Ensure opening a directory and one of its children only adds one worktree.
-//         workspace
-//             .update(cx, |view, cx| {
-//                 view.open_paths(vec!["/dir3".into(), "/dir3/c.txt".into()], true, cx)
-//             })
-//             .await;
-//         cx.read(|cx| {
-//             let workspace = workspace.read(cx);
-//             assert_project_panel_selection(workspace, Path::new("/dir3"), Path::new("c.txt"), cx);
-//             let worktree_roots = workspace
-//                 .worktrees(cx)
-//                 .map(|w| w.read(cx).as_local().unwrap().abs_path().as_ref())
-//                 .collect::<HashSet<_>>();
-//             assert_eq!(
-//                 worktree_roots,
-//                 vec!["/dir1", "/dir2/b.txt", "/dir3"]
-//                     .into_iter()
-//                     .map(Path::new)
-//                     .collect(),
-//             );
-//             assert_eq!(
-//                 workspace
-//                     .active_pane()
-//                     .read(cx)
-//                     .active_item()
-//                     .unwrap()
-//                     .as_any()
-//                     .downcast_ref::<Editor>()
-//                     .unwrap()
-//                     .read(cx)
-//                     .title(cx),
-//                 "c.txt"
-//             );
-//         });
-
-//         // Ensure opening invisibly a file outside an existing worktree adds a new, invisible worktree.
-//         workspace
-//             .update(cx, |view, cx| {
-//                 view.open_paths(vec!["/d.txt".into()], false, cx)
-//             })
-//             .await;
-//         cx.read(|cx| {
-//             let workspace = workspace.read(cx);
-//             assert_project_panel_selection(workspace, Path::new("/d.txt"), Path::new(""), cx);
-//             let worktree_roots = workspace
-//                 .worktrees(cx)
-//                 .map(|w| w.read(cx).as_local().unwrap().abs_path().as_ref())
-//                 .collect::<HashSet<_>>();
-//             assert_eq!(
-//                 worktree_roots,
-//                 vec!["/dir1", "/dir2/b.txt", "/dir3", "/d.txt"]
-//                     .into_iter()
-//                     .map(Path::new)
-//                     .collect(),
-//             );
-
-//             let visible_worktree_roots = workspace
-//                 .visible_worktrees(cx)
-//                 .map(|w| w.read(cx).as_local().unwrap().abs_path().as_ref())
-//                 .collect::<HashSet<_>>();
-//             assert_eq!(
-//                 visible_worktree_roots,
-//                 vec!["/dir1", "/dir2/b.txt", "/dir3"]
-//                     .into_iter()
-//                     .map(Path::new)
-//                     .collect(),
-//             );
-
-//             assert_eq!(
-//                 workspace
-//                     .active_pane()
-//                     .read(cx)
-//                     .active_item()
-//                     .unwrap()
-//                     .as_any()
-//                     .downcast_ref::<Editor>()
-//                     .unwrap()
-//                     .read(cx)
-//                     .title(cx),
-//                 "d.txt"
-//             );
-//         });
-//     }
-
-//     #[gpui::test]
-//     async fn test_opening_excluded_paths(cx: &mut TestAppContext) {
-//         let app_state = init_test(cx);
-//         cx.update(|cx| {
-//             cx.update_global::<SettingsStore, _, _>(|store, cx| {
-//                 store.update_user_settings::<ProjectSettings>(cx, |project_settings| {
-//                     project_settings.file_scan_exclusions =
-//                         Some(vec!["excluded_dir".to_string(), "**/.git".to_string()]);
-//                 });
-//             });
-//         });
-//         app_state
-//             .fs
-//             .as_fake()
-//             .insert_tree(
-//                 "/root",
-//                 json!({
-//                     ".gitignore": "ignored_dir\n",
-//                     ".git": {
-//                         "HEAD": "ref: refs/heads/main",
-//                     },
-//                     "regular_dir": {
-//                         "file": "regular file contents",
-//                     },
-//                     "ignored_dir": {
-//                         "ignored_subdir": {
-//                             "file": "ignored subfile contents",
-//                         },
-//                         "file": "ignored file contents",
-//                     },
-//                     "excluded_dir": {
-//                         "file": "excluded file contents",
-//                     },
-//                 }),
-//             )
-//             .await;
-
-//         let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
-//         let window = cx.add_window(|cx| Workspace::test_new(project, cx));
-//         let workspace = window.root(cx);
-
-//         let initial_entries = cx.read(|cx| workspace.file_project_paths(cx));
-//         let paths_to_open = [
-//             Path::new("/root/excluded_dir/file").to_path_buf(),
-//             Path::new("/root/.git/HEAD").to_path_buf(),
-//             Path::new("/root/excluded_dir/ignored_subdir").to_path_buf(),
-//         ];
-//         let (opened_workspace, new_items) = cx
-//             .update(|cx| workspace::open_paths(&paths_to_open, &app_state, None, cx))
-//             .await
-//             .unwrap();
-
-//         assert_eq!(
-//             opened_workspace.id(),
-//             workspace.id(),
-//             "Excluded files in subfolders of a workspace root should be opened in the workspace"
-//         );
-//         let mut opened_paths = cx.read(|cx| {
-//             assert_eq!(
-//                 new_items.len(),
-//                 paths_to_open.len(),
-//                 "Expect to get the same number of opened items as submitted paths to open"
-//             );
-//             new_items
-//                 .iter()
-//                 .zip(paths_to_open.iter())
-//                 .map(|(i, path)| {
-//                     match i {
-//                         Some(Ok(i)) => {
-//                             Some(i.project_path(cx).map(|p| p.path.display().to_string()))
-//                         }
-//                         Some(Err(e)) => panic!("Excluded file {path:?} failed to open: {e:?}"),
-//                         None => None,
-//                     }
-//                     .flatten()
-//                 })
-//                 .collect::<Vec<_>>()
-//         });
-//         opened_paths.sort();
-//         assert_eq!(
-//             opened_paths,
-//             vec![
-//                 None,
-//                 Some(".git/HEAD".to_string()),
-//                 Some("excluded_dir/file".to_string()),
-//             ],
-//             "Excluded files should get opened, excluded dir should not get opened"
-//         );
-
-//         let entries = cx.read(|cx| workspace.file_project_paths(cx));
-//         assert_eq!(
-//             initial_entries, entries,
-//             "Workspace entries should not change after opening excluded files and directories paths"
-//         );
-
-//         cx.read(|cx| {
-//             let pane = workspace.read(cx).active_pane().read(cx);
-//             let mut opened_buffer_paths = pane
-//                 .items()
-//                 .map(|i| {
-//                     i.project_path(cx)
-//                         .expect("all excluded files that got open should have a path")
-//                         .path
-//                         .display()
-//                         .to_string()
-//                 })
-//                 .collect::<Vec<_>>();
-//             opened_buffer_paths.sort();
-//             assert_eq!(
-//                 opened_buffer_paths,
-//                 vec![".git/HEAD".to_string(), "excluded_dir/file".to_string()],
-//                 "Despite not being present in the worktrees, buffers for excluded files are opened and added to the pane"
-//             );
-//         });
-//     }
-
-//     #[gpui::test]
-//     async fn test_save_conflicting_item(cx: &mut TestAppContext) {
-//         let app_state = init_test(cx);
-//         app_state
-//             .fs
-//             .as_fake()
-//             .insert_tree("/root", json!({ "a.txt": "" }))
-//             .await;
-
-//         let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
-//         let window = cx.add_window(|cx| Workspace::test_new(project, cx));
-//         let workspace = window.root(cx);
-
-//         // Open a file within an existing worktree.
-//         workspace
-//             .update(cx, |view, cx| {
-//                 view.open_paths(vec![PathBuf::from("/root/a.txt")], true, cx)
-//             })
-//             .await;
-//         let editor = cx.read(|cx| {
-//             let pane = workspace.read(cx).active_pane().read(cx);
-//             let item = pane.active_item().unwrap();
-//             item.downcast::<Editor>().unwrap()
-//         });
-
-//         editor.update(cx, |editor, cx| editor.handle_input("x", cx));
-//         app_state
-//             .fs
-//             .as_fake()
-//             .insert_file("/root/a.txt", "changed".to_string())
-//             .await;
-//         editor
-//             .condition(cx, |editor, cx| editor.has_conflict(cx))
-//             .await;
-//         cx.read(|cx| assert!(editor.is_dirty(cx)));
-
-//         let save_task = workspace.update(cx, |workspace, cx| {
-//             workspace.save_active_item(SaveIntent::Save, cx)
-//         });
-//         cx.foreground().run_until_parked();
-//         window.simulate_prompt_answer(0, cx);
-//         save_task.await.unwrap();
-//         editor.read_with(cx, |editor, cx| {
-//             assert!(!editor.is_dirty(cx));
-//             assert!(!editor.has_conflict(cx));
-//         });
-//     }
-
-//     #[gpui::test]
-//     async fn test_open_and_save_new_file(cx: &mut TestAppContext) {
-//         let app_state = init_test(cx);
-//         app_state.fs.create_dir(Path::new("/root")).await.unwrap();
-
-//         let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
-//         project.update(cx, |project, _| project.languages().add(rust_lang()));
-//         let window = cx.add_window(|cx| Workspace::test_new(project, cx));
-//         let workspace = window.root(cx);
-//         let worktree = cx.read(|cx| workspace.read(cx).worktrees(cx).next().unwrap());
-
-//         // Create a new untitled buffer
-//         cx.dispatch_action(window.into(), NewFile);
-//         let editor = workspace.read_with(cx, |workspace, cx| {
-//             workspace
-//                 .active_item(cx)
-//                 .unwrap()
-//                 .downcast::<Editor>()
-//                 .unwrap()
-//         });
-
-//         editor.update(cx, |editor, cx| {
-//             assert!(!editor.is_dirty(cx));
-//             assert_eq!(editor.title(cx), "untitled");
-//             assert!(Arc::ptr_eq(
-//                 &editor.language_at(0, cx).unwrap(),
-//                 &languages::PLAIN_TEXT
-//             ));
-//             editor.handle_input("hi", cx);
-//             assert!(editor.is_dirty(cx));
-//         });
-
-//         // Save the buffer. This prompts for a filename.
-//         let save_task = workspace.update(cx, |workspace, cx| {
-//             workspace.save_active_item(SaveIntent::Save, cx)
-//         });
-//         cx.foreground().run_until_parked();
-//         cx.simulate_new_path_selection(|parent_dir| {
-//             assert_eq!(parent_dir, Path::new("/root"));
-//             Some(parent_dir.join("the-new-name.rs"))
-//         });
-//         cx.read(|cx| {
-//             assert!(editor.is_dirty(cx));
-//             assert_eq!(editor.read(cx).title(cx), "untitled");
-//         });
-
-//         // When the save completes, the buffer's title is updated and the language is assigned based
-//         // on the path.
-//         save_task.await.unwrap();
-//         editor.read_with(cx, |editor, cx| {
-//             assert!(!editor.is_dirty(cx));
-//             assert_eq!(editor.title(cx), "the-new-name.rs");
-//             assert_eq!(editor.language_at(0, cx).unwrap().name().as_ref(), "Rust");
-//         });
-
-//         // Edit the file and save it again. This time, there is no filename prompt.
-//         editor.update(cx, |editor, cx| {
-//             editor.handle_input(" there", cx);
-//             assert!(editor.is_dirty(cx));
-//         });
-//         let save_task = workspace.update(cx, |workspace, cx| {
-//             workspace.save_active_item(SaveIntent::Save, cx)
-//         });
-//         save_task.await.unwrap();
-//         assert!(!cx.did_prompt_for_new_path());
-//         editor.read_with(cx, |editor, cx| {
-//             assert!(!editor.is_dirty(cx));
-//             assert_eq!(editor.title(cx), "the-new-name.rs")
-//         });
-
-//         // Open the same newly-created file in another pane item. The new editor should reuse
-//         // the same buffer.
-//         cx.dispatch_action(window.into(), NewFile);
-//         workspace
-//             .update(cx, |workspace, cx| {
-//                 workspace.split_and_clone(
-//                     workspace.active_pane().clone(),
-//                     SplitDirection::Right,
-//                     cx,
-//                 );
-//                 workspace.open_path((worktree.read(cx).id(), "the-new-name.rs"), None, true, cx)
-//             })
-//             .await
-//             .unwrap();
-//         let editor2 = workspace.update(cx, |workspace, cx| {
-//             workspace
-//                 .active_item(cx)
-//                 .unwrap()
-//                 .downcast::<Editor>()
-//                 .unwrap()
-//         });
-//         cx.read(|cx| {
-//             assert_eq!(
-//                 editor2.read(cx).buffer().read(cx).as_singleton().unwrap(),
-//                 editor.read(cx).buffer().read(cx).as_singleton().unwrap()
-//             );
-//         })
-//     }
-
-//     #[gpui::test]
-//     async fn test_setting_language_when_saving_as_single_file_worktree(cx: &mut TestAppContext) {
-//         let app_state = init_test(cx);
-//         app_state.fs.create_dir(Path::new("/root")).await.unwrap();
-
-//         let project = Project::test(app_state.fs.clone(), [], cx).await;
-//         project.update(cx, |project, _| project.languages().add(rust_lang()));
-//         let window = cx.add_window(|cx| Workspace::test_new(project, cx));
-//         let workspace = window.root(cx);
-
-//         // Create a new untitled buffer
-//         cx.dispatch_action(window.into(), NewFile);
-//         let editor = workspace.read_with(cx, |workspace, cx| {
-//             workspace
-//                 .active_item(cx)
-//                 .unwrap()
-//                 .downcast::<Editor>()
-//                 .unwrap()
-//         });
-
-//         editor.update(cx, |editor, cx| {
-//             assert!(Arc::ptr_eq(
-//                 &editor.language_at(0, cx).unwrap(),
-//                 &languages::PLAIN_TEXT
-//             ));
-//             editor.handle_input("hi", cx);
-//             assert!(editor.is_dirty(cx));
-//         });
-
-//         // Save the buffer. This prompts for a filename.
-//         let save_task = workspace.update(cx, |workspace, cx| {
-//             workspace.save_active_item(SaveIntent::Save, cx)
-//         });
-//         cx.foreground().run_until_parked();
-//         cx.simulate_new_path_selection(|_| Some(PathBuf::from("/root/the-new-name.rs")));
-//         save_task.await.unwrap();
-//         // The buffer is not dirty anymore and the language is assigned based on the path.
-//         editor.read_with(cx, |editor, cx| {
-//             assert!(!editor.is_dirty(cx));
-//             assert_eq!(editor.language_at(0, cx).unwrap().name().as_ref(), "Rust")
-//         });
-//     }
-
-//     #[gpui::test]
-//     async fn test_pane_actions(cx: &mut TestAppContext) {
-//         let app_state = init_test(cx);
-//         app_state
-//             .fs
-//             .as_fake()
-//             .insert_tree(
-//                 "/root",
-//                 json!({
-//                     "a": {
-//                         "file1": "contents 1",
-//                         "file2": "contents 2",
-//                         "file3": "contents 3",
-//                     },
-//                 }),
-//             )
-//             .await;
-
-//         let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
-//         let window = cx.add_window(|cx| Workspace::test_new(project, cx));
-//         let workspace = window.root(cx);
-
-//         let entries = cx.read(|cx| workspace.file_project_paths(cx));
-//         let file1 = entries[0].clone();
-
-//         let pane_1 = cx.read(|cx| workspace.read(cx).active_pane().clone());
-
-//         workspace
-//             .update(cx, |w, cx| w.open_path(file1.clone(), None, true, cx))
-//             .await
-//             .unwrap();
-
-//         let (editor_1, buffer) = pane_1.update(cx, |pane_1, cx| {
-//             let editor = pane_1.active_item().unwrap().downcast::<Editor>().unwrap();
-//             assert_eq!(editor.project_path(cx), Some(file1.clone()));
-//             let buffer = editor.update(cx, |editor, cx| {
-//                 editor.insert("dirt", cx);
-//                 editor.buffer().downgrade()
-//             });
-//             (editor.downgrade(), buffer)
-//         });
-
-//         cx.dispatch_action(window.into(), pane::SplitRight);
-//         let editor_2 = cx.update(|cx| {
-//             let pane_2 = workspace.read(cx).active_pane().clone();
-//             assert_ne!(pane_1, pane_2);
-
-//             let pane2_item = pane_2.read(cx).active_item().unwrap();
-//             assert_eq!(pane2_item.project_path(cx), Some(file1.clone()));
-
-//             pane2_item.downcast::<Editor>().unwrap().downgrade()
-//         });
-//         cx.dispatch_action(
-//             window.into(),
-//             workspace::CloseActiveItem { save_intent: None },
-//         );
-
-//         cx.foreground().run_until_parked();
-//         workspace.read_with(cx, |workspace, _| {
-//             assert_eq!(workspace.panes().len(), 1);
-//             assert_eq!(workspace.active_pane(), &pane_1);
-//         });
-
-//         cx.dispatch_action(
-//             window.into(),
-//             workspace::CloseActiveItem { save_intent: None },
-//         );
-//         cx.foreground().run_until_parked();
-//         window.simulate_prompt_answer(1, cx);
-//         cx.foreground().run_until_parked();
-
-//         workspace.read_with(cx, |workspace, cx| {
-//             assert_eq!(workspace.panes().len(), 1);
-//             assert!(workspace.active_item(cx).is_none());
-//         });
-
-//         cx.assert_dropped(editor_1);
-//         cx.assert_dropped(editor_2);
-//         cx.assert_dropped(buffer);
-//     }
-
-//     #[gpui::test]
-//     async fn test_navigation(cx: &mut TestAppContext) {
-//         let app_state = init_test(cx);
-//         app_state
-//             .fs
-//             .as_fake()
-//             .insert_tree(
-//                 "/root",
-//                 json!({
-//                     "a": {
-//                         "file1": "contents 1\n".repeat(20),
-//                         "file2": "contents 2\n".repeat(20),
-//                         "file3": "contents 3\n".repeat(20),
-//                     },
-//                 }),
-//             )
-//             .await;
-
-//         let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
-//         let workspace = cx
-//             .add_window(|cx| Workspace::test_new(project.clone(), cx))
-//             .root(cx);
-//         let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
-
-//         let entries = cx.read(|cx| workspace.file_project_paths(cx));
-//         let file1 = entries[0].clone();
-//         let file2 = entries[1].clone();
-//         let file3 = entries[2].clone();
-
-//         let editor1 = workspace
-//             .update(cx, |w, cx| w.open_path(file1.clone(), None, true, cx))
-//             .await
-//             .unwrap()
-//             .downcast::<Editor>()
-//             .unwrap();
-//         editor1.update(cx, |editor, cx| {
-//             editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
-//                 s.select_display_ranges([DisplayPoint::new(10, 0)..DisplayPoint::new(10, 0)])
-//             });
-//         });
-//         let editor2 = workspace
-//             .update(cx, |w, cx| w.open_path(file2.clone(), None, true, cx))
-//             .await
-//             .unwrap()
-//             .downcast::<Editor>()
-//             .unwrap();
-//         let editor3 = workspace
-//             .update(cx, |w, cx| w.open_path(file3.clone(), None, true, cx))
-//             .await
-//             .unwrap()
-//             .downcast::<Editor>()
-//             .unwrap();
-
-//         editor3
-//             .update(cx, |editor, cx| {
-//                 editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
-//                     s.select_display_ranges([DisplayPoint::new(12, 0)..DisplayPoint::new(12, 0)])
-//                 });
-//                 editor.newline(&Default::default(), cx);
-//                 editor.newline(&Default::default(), cx);
-//                 editor.move_down(&Default::default(), cx);
-//                 editor.move_down(&Default::default(), cx);
-//                 editor.save(project.clone(), cx)
-//             })
-//             .await
-//             .unwrap();
-//         editor3.update(cx, |editor, cx| {
-//             editor.set_scroll_position(vec2f(0., 12.5), cx)
-//         });
-//         assert_eq!(
-//             active_location(&workspace, cx),
-//             (file3.clone(), DisplayPoint::new(16, 0), 12.5)
-//         );
-
-//         workspace
-//             .update(cx, |w, cx| w.go_back(w.active_pane().downgrade(), cx))
-//             .await
-//             .unwrap();
-//         assert_eq!(
-//             active_location(&workspace, cx),
-//             (file3.clone(), DisplayPoint::new(0, 0), 0.)
-//         );
-
-//         workspace
-//             .update(cx, |w, cx| w.go_back(w.active_pane().downgrade(), cx))
-//             .await
-//             .unwrap();
-//         assert_eq!(
-//             active_location(&workspace, cx),
-//             (file2.clone(), DisplayPoint::new(0, 0), 0.)
-//         );
-
-//         workspace
-//             .update(cx, |w, cx| w.go_back(w.active_pane().downgrade(), cx))
-//             .await
-//             .unwrap();
-//         assert_eq!(
-//             active_location(&workspace, cx),
-//             (file1.clone(), DisplayPoint::new(10, 0), 0.)
-//         );
-
-//         workspace
-//             .update(cx, |w, cx| w.go_back(w.active_pane().downgrade(), cx))
-//             .await
-//             .unwrap();
-//         assert_eq!(
-//             active_location(&workspace, cx),
-//             (file1.clone(), DisplayPoint::new(0, 0), 0.)
-//         );
-
-//         // Go back one more time and ensure we don't navigate past the first item in the history.
-//         workspace
-//             .update(cx, |w, cx| w.go_back(w.active_pane().downgrade(), cx))
-//             .await
-//             .unwrap();
-//         assert_eq!(
-//             active_location(&workspace, cx),
-//             (file1.clone(), DisplayPoint::new(0, 0), 0.)
-//         );
-
-//         workspace
-//             .update(cx, |w, cx| w.go_forward(w.active_pane().downgrade(), cx))
-//             .await
-//             .unwrap();
-//         assert_eq!(
-//             active_location(&workspace, cx),
-//             (file1.clone(), DisplayPoint::new(10, 0), 0.)
-//         );
-
-//         workspace
-//             .update(cx, |w, cx| w.go_forward(w.active_pane().downgrade(), cx))
-//             .await
-//             .unwrap();
-//         assert_eq!(
-//             active_location(&workspace, cx),
-//             (file2.clone(), DisplayPoint::new(0, 0), 0.)
-//         );
-
-//         // Go forward to an item that has been closed, ensuring it gets re-opened at the same
-//         // location.
-//         pane.update(cx, |pane, cx| {
-//             let editor3_id = editor3.id();
-//             drop(editor3);
-//             pane.close_item_by_id(editor3_id, SaveIntent::Close, cx)
-//         })
-//         .await
-//         .unwrap();
-//         workspace
-//             .update(cx, |w, cx| w.go_forward(w.active_pane().downgrade(), cx))
-//             .await
-//             .unwrap();
-//         assert_eq!(
-//             active_location(&workspace, cx),
-//             (file3.clone(), DisplayPoint::new(0, 0), 0.)
-//         );
-
-//         workspace
-//             .update(cx, |w, cx| w.go_forward(w.active_pane().downgrade(), cx))
-//             .await
-//             .unwrap();
-//         assert_eq!(
-//             active_location(&workspace, cx),
-//             (file3.clone(), DisplayPoint::new(16, 0), 12.5)
-//         );
-
-//         workspace
-//             .update(cx, |w, cx| w.go_back(w.active_pane().downgrade(), cx))
-//             .await
-//             .unwrap();
-//         assert_eq!(
-//             active_location(&workspace, cx),
-//             (file3.clone(), DisplayPoint::new(0, 0), 0.)
-//         );
-
-//         // Go back to an item that has been closed and removed from disk, ensuring it gets skipped.
-//         pane.update(cx, |pane, cx| {
-//             let editor2_id = editor2.id();
-//             drop(editor2);
-//             pane.close_item_by_id(editor2_id, SaveIntent::Close, cx)
-//         })
-//         .await
-//         .unwrap();
-//         app_state
-//             .fs
-//             .remove_file(Path::new("/root/a/file2"), Default::default())
-//             .await
-//             .unwrap();
-//         cx.foreground().run_until_parked();
-
-//         workspace
-//             .update(cx, |w, cx| w.go_back(w.active_pane().downgrade(), cx))
-//             .await
-//             .unwrap();
-//         assert_eq!(
-//             active_location(&workspace, cx),
-//             (file1.clone(), DisplayPoint::new(10, 0), 0.)
-//         );
-//         workspace
-//             .update(cx, |w, cx| w.go_forward(w.active_pane().downgrade(), cx))
-//             .await
-//             .unwrap();
-//         assert_eq!(
-//             active_location(&workspace, cx),
-//             (file3.clone(), DisplayPoint::new(0, 0), 0.)
-//         );
-
-//         // Modify file to collapse multiple nav history entries into the same location.
-//         // Ensure we don't visit the same location twice when navigating.
-//         editor1.update(cx, |editor, cx| {
-//             editor.change_selections(None, cx, |s| {
-//                 s.select_display_ranges([DisplayPoint::new(15, 0)..DisplayPoint::new(15, 0)])
-//             })
-//         });
-
-//         for _ in 0..5 {
-//             editor1.update(cx, |editor, cx| {
-//                 editor.change_selections(None, cx, |s| {
-//                     s.select_display_ranges([DisplayPoint::new(3, 0)..DisplayPoint::new(3, 0)])
-//                 });
-//             });
-//             editor1.update(cx, |editor, cx| {
-//                 editor.change_selections(None, cx, |s| {
-//                     s.select_display_ranges([DisplayPoint::new(13, 0)..DisplayPoint::new(13, 0)])
-//                 })
-//             });
-//         }
-
-//         editor1.update(cx, |editor, cx| {
-//             editor.transact(cx, |editor, cx| {
-//                 editor.change_selections(None, cx, |s| {
-//                     s.select_display_ranges([DisplayPoint::new(2, 0)..DisplayPoint::new(14, 0)])
-//                 });
-//                 editor.insert("", cx);
-//             })
-//         });
-
-//         editor1.update(cx, |editor, cx| {
-//             editor.change_selections(None, cx, |s| {
-//                 s.select_display_ranges([DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0)])
-//             })
-//         });
-//         workspace
-//             .update(cx, |w, cx| w.go_back(w.active_pane().downgrade(), cx))
-//             .await
-//             .unwrap();
-//         assert_eq!(
-//             active_location(&workspace, cx),
-//             (file1.clone(), DisplayPoint::new(2, 0), 0.)
-//         );
-//         workspace
-//             .update(cx, |w, cx| w.go_back(w.active_pane().downgrade(), cx))
-//             .await
-//             .unwrap();
-//         assert_eq!(
-//             active_location(&workspace, cx),
-//             (file1.clone(), DisplayPoint::new(3, 0), 0.)
-//         );
-
-//         fn active_location(
-//             workspace: &ViewHandle<Workspace>,
-//             cx: &mut TestAppContext,
-//         ) -> (ProjectPath, DisplayPoint, f32) {
-//             workspace.update(cx, |workspace, cx| {
-//                 let item = workspace.active_item(cx).unwrap();
-//                 let editor = item.downcast::<Editor>().unwrap();
-//                 let (selections, scroll_position) = editor.update(cx, |editor, cx| {
-//                     (
-//                         editor.selections.display_ranges(cx),
-//                         editor.scroll_position(cx),
-//                     )
-//                 });
-//                 (
-//                     item.project_path(cx).unwrap(),
-//                     selections[0].start,
-//                     scroll_position.y(),
-//                 )
-//             })
-//         }
-//     }
-
-//     #[gpui::test]
-//     async fn test_reopening_closed_items(cx: &mut TestAppContext) {
-//         let app_state = init_test(cx);
-//         app_state
-//             .fs
-//             .as_fake()
-//             .insert_tree(
-//                 "/root",
-//                 json!({
-//                     "a": {
-//                         "file1": "",
-//                         "file2": "",
-//                         "file3": "",
-//                         "file4": "",
-//                     },
-//                 }),
-//             )
-//             .await;
-
-//         let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
-//         let workspace = cx
-//             .add_window(|cx| Workspace::test_new(project, cx))
-//             .root(cx);
-//         let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
-
-//         let entries = cx.read(|cx| workspace.file_project_paths(cx));
-//         let file1 = entries[0].clone();
-//         let file2 = entries[1].clone();
-//         let file3 = entries[2].clone();
-//         let file4 = entries[3].clone();
-
-//         let file1_item_id = workspace
-//             .update(cx, |w, cx| w.open_path(file1.clone(), None, true, cx))
-//             .await
-//             .unwrap()
-//             .id();
-//         let file2_item_id = workspace
-//             .update(cx, |w, cx| w.open_path(file2.clone(), None, true, cx))
-//             .await
-//             .unwrap()
-//             .id();
-//         let file3_item_id = workspace
-//             .update(cx, |w, cx| w.open_path(file3.clone(), None, true, cx))
-//             .await
-//             .unwrap()
-//             .id();
-//         let file4_item_id = workspace
-//             .update(cx, |w, cx| w.open_path(file4.clone(), None, true, cx))
-//             .await
-//             .unwrap()
-//             .id();
-//         assert_eq!(active_path(&workspace, cx), Some(file4.clone()));
-
-//         // Close all the pane items in some arbitrary order.
-//         pane.update(cx, |pane, cx| {
-//             pane.close_item_by_id(file1_item_id, SaveIntent::Close, cx)
-//         })
-//         .await
-//         .unwrap();
-//         assert_eq!(active_path(&workspace, cx), Some(file4.clone()));
-
-//         pane.update(cx, |pane, cx| {
-//             pane.close_item_by_id(file4_item_id, SaveIntent::Close, cx)
-//         })
-//         .await
-//         .unwrap();
-//         assert_eq!(active_path(&workspace, cx), Some(file3.clone()));
-
-//         pane.update(cx, |pane, cx| {
-//             pane.close_item_by_id(file2_item_id, SaveIntent::Close, cx)
-//         })
-//         .await
-//         .unwrap();
-//         assert_eq!(active_path(&workspace, cx), Some(file3.clone()));
-
-//         pane.update(cx, |pane, cx| {
-//             pane.close_item_by_id(file3_item_id, SaveIntent::Close, cx)
-//         })
-//         .await
-//         .unwrap();
-//         assert_eq!(active_path(&workspace, cx), None);
-
-//         // Reopen all the closed items, ensuring they are reopened in the same order
-//         // in which they were closed.
-//         workspace
-//             .update(cx, Workspace::reopen_closed_item)
-//             .await
-//             .unwrap();
-//         assert_eq!(active_path(&workspace, cx), Some(file3.clone()));
-
-//         workspace
-//             .update(cx, Workspace::reopen_closed_item)
-//             .await
-//             .unwrap();
-//         assert_eq!(active_path(&workspace, cx), Some(file2.clone()));
-
-//         workspace
-//             .update(cx, Workspace::reopen_closed_item)
-//             .await
-//             .unwrap();
-//         assert_eq!(active_path(&workspace, cx), Some(file4.clone()));
-
-//         workspace
-//             .update(cx, Workspace::reopen_closed_item)
-//             .await
-//             .unwrap();
-//         assert_eq!(active_path(&workspace, cx), Some(file1.clone()));
-
-//         // Reopening past the last closed item is a no-op.
-//         workspace
-//             .update(cx, Workspace::reopen_closed_item)
-//             .await
-//             .unwrap();
-//         assert_eq!(active_path(&workspace, cx), Some(file1.clone()));
-
-//         // Reopening closed items doesn't interfere with navigation history.
-//         workspace
-//             .update(cx, |workspace, cx| {
-//                 workspace.go_back(workspace.active_pane().downgrade(), cx)
-//             })
-//             .await
-//             .unwrap();
-//         assert_eq!(active_path(&workspace, cx), Some(file4.clone()));
-
-//         workspace
-//             .update(cx, |workspace, cx| {
-//                 workspace.go_back(workspace.active_pane().downgrade(), cx)
-//             })
-//             .await
-//             .unwrap();
-//         assert_eq!(active_path(&workspace, cx), Some(file2.clone()));
-
-//         workspace
-//             .update(cx, |workspace, cx| {
-//                 workspace.go_back(workspace.active_pane().downgrade(), cx)
-//             })
-//             .await
-//             .unwrap();
-//         assert_eq!(active_path(&workspace, cx), Some(file3.clone()));
-
-//         workspace
-//             .update(cx, |workspace, cx| {
-//                 workspace.go_back(workspace.active_pane().downgrade(), cx)
-//             })
-//             .await
-//             .unwrap();
-//         assert_eq!(active_path(&workspace, cx), Some(file4.clone()));
-
-//         workspace
-//             .update(cx, |workspace, cx| {
-//                 workspace.go_back(workspace.active_pane().downgrade(), cx)
-//             })
-//             .await
-//             .unwrap();
-//         assert_eq!(active_path(&workspace, cx), Some(file3.clone()));
-
-//         workspace
-//             .update(cx, |workspace, cx| {
-//                 workspace.go_back(workspace.active_pane().downgrade(), cx)
-//             })
-//             .await
-//             .unwrap();
-//         assert_eq!(active_path(&workspace, cx), Some(file2.clone()));
-
-//         workspace
-//             .update(cx, |workspace, cx| {
-//                 workspace.go_back(workspace.active_pane().downgrade(), cx)
-//             })
-//             .await
-//             .unwrap();
-//         assert_eq!(active_path(&workspace, cx), Some(file1.clone()));
-
-//         workspace
-//             .update(cx, |workspace, cx| {
-//                 workspace.go_back(workspace.active_pane().downgrade(), cx)
-//             })
-//             .await
-//             .unwrap();
-//         assert_eq!(active_path(&workspace, cx), Some(file1.clone()));
-
-//         fn active_path(
-//             workspace: &ViewHandle<Workspace>,
-//             cx: &TestAppContext,
-//         ) -> Option<ProjectPath> {
-//             workspace.read_with(cx, |workspace, cx| {
-//                 let item = workspace.active_item(cx)?;
-//                 item.project_path(cx)
-//             })
-//         }
-//     }
-
-//     #[gpui::test]
-//     async fn test_base_keymap(cx: &mut gpui::TestAppContext) {
-//         struct TestView;
-
-//         impl Entity for TestView {
-//             type Event = ();
-//         }
-
-//         impl View for TestView {
-//             fn ui_name() -> &'static str {
-//                 "TestView"
-//             }
-
-//             fn render(&mut self, _: &mut ViewContext<Self>) -> AnyElement<Self> {
-//                 Empty::new().into_any()
-//             }
-//         }
-
-//         let executor = cx.background();
-//         let fs = FakeFs::new(executor.clone());
-
-//         actions!(test, [A, B]);
-//         // From the Atom keymap
-//         actions!(workspace, [ActivatePreviousPane]);
-//         // From the JetBrains keymap
-//         actions!(pane, [ActivatePrevItem]);
-
-//         fs.save(
-//             "/settings.json".as_ref(),
-//             &r#"
-//             {
-//                 "base_keymap": "Atom"
-//             }
-//             "#
-//             .into(),
-//             Default::default(),
-//         )
-//         .await
-//         .unwrap();
-
-//         fs.save(
-//             "/keymap.json".as_ref(),
-//             &r#"
-//             [
-//                 {
-//                     "bindings": {
-//                         "backspace": "test::A"
-//                     }
-//                 }
-//             ]
-//             "#
-//             .into(),
-//             Default::default(),
-//         )
-//         .await
-//         .unwrap();
-
-//         cx.update(|cx| {
-//             cx.set_global(SettingsStore::test(cx));
-//             theme::init(Assets, cx);
-//             welcome::init(cx);
-
-//             cx.add_global_action(|_: &A, _cx| {});
-//             cx.add_global_action(|_: &B, _cx| {});
-//             cx.add_global_action(|_: &ActivatePreviousPane, _cx| {});
-//             cx.add_global_action(|_: &ActivatePrevItem, _cx| {});
-
-//             let settings_rx = watch_config_file(
-//                 executor.clone(),
-//                 fs.clone(),
-//                 PathBuf::from("/settings.json"),
-//             );
-//             let keymap_rx =
-//                 watch_config_file(executor.clone(), fs.clone(), PathBuf::from("/keymap.json"));
-
-//             handle_keymap_file_changes(keymap_rx, cx);
-//             handle_settings_file_changes(settings_rx, cx);
-//         });
-
-//         cx.foreground().run_until_parked();
-
-//         let window = cx.add_window(|_| TestView);
-
-//         // Test loading the keymap base at all
-//         assert_key_bindings_for(
-//             window.into(),
-//             cx,
-//             vec![("backspace", &A), ("k", &ActivatePreviousPane)],
-//             line!(),
-//         );
-
-//         // Test modifying the users keymap, while retaining the base keymap
-//         fs.save(
-//             "/keymap.json".as_ref(),
-//             &r#"
-//             [
-//                 {
-//                     "bindings": {
-//                         "backspace": "test::B"
-//                     }
-//                 }
-//             ]
-//             "#
-//             .into(),
-//             Default::default(),
-//         )
-//         .await
-//         .unwrap();
-
-//         cx.foreground().run_until_parked();
-
-//         assert_key_bindings_for(
-//             window.into(),
-//             cx,
-//             vec![("backspace", &B), ("k", &ActivatePreviousPane)],
-//             line!(),
-//         );
-
-//         // Test modifying the base, while retaining the users keymap
-//         fs.save(
-//             "/settings.json".as_ref(),
-//             &r#"
-//             {
-//                 "base_keymap": "JetBrains"
-//             }
-//             "#
-//             .into(),
-//             Default::default(),
-//         )
-//         .await
-//         .unwrap();
-
-//         cx.foreground().run_until_parked();
-
-//         assert_key_bindings_for(
-//             window.into(),
-//             cx,
-//             vec![("backspace", &B), ("[", &ActivatePrevItem)],
-//             line!(),
-//         );
-
-//         #[track_caller]
-//         fn assert_key_bindings_for<'a>(
-//             window: AnyWindowHandle,
-//             cx: &TestAppContext,
-//             actions: Vec<(&'static str, &'a dyn Action)>,
-//             line: u32,
-//         ) {
-//             for (key, action) in actions {
-//                 // assert that...
-//                 assert!(
-//                     cx.available_actions(window, 0)
-//                         .into_iter()
-//                         .any(|(_, bound_action, b)| {
-//                             // action names match...
-//                             bound_action.name() == action.name()
-//                         && bound_action.namespace() == action.namespace()
-//                         // and key strokes contain the given key
-//                         && b.iter()
-//                             .any(|binding| binding.keystrokes().iter().any(|k| k.key == key))
-//                         }),
-//                     "On {} Failed to find {} with key binding {}",
-//                     line,
-//                     action.name(),
-//                     key
-//                 );
-//             }
-//         }
-//     }
-
-//     #[gpui::test]
-//     async fn test_disabled_keymap_binding(cx: &mut gpui::TestAppContext) {
-//         struct TestView;
-
-//         impl Entity for TestView {
-//             type Event = ();
-//         }
-
-//         impl View for TestView {
-//             fn ui_name() -> &'static str {
-//                 "TestView"
-//             }
-
-//             fn render(&mut self, _: &mut ViewContext<Self>) -> AnyElement<Self> {
-//                 Empty::new().into_any()
-//             }
-//         }
-
-//         let executor = cx.background();
-//         let fs = FakeFs::new(executor.clone());
-
-//         actions!(test, [A, B]);
-//         // From the Atom keymap
-//         actions!(workspace, [ActivatePreviousPane]);
-//         // From the JetBrains keymap
-//         actions!(pane, [ActivatePrevItem]);
-
-//         fs.save(
-//             "/settings.json".as_ref(),
-//             &r#"
-//             {
-//                 "base_keymap": "Atom"
-//             }
-//             "#
-//             .into(),
-//             Default::default(),
-//         )
-//         .await
-//         .unwrap();
-
-//         fs.save(
-//             "/keymap.json".as_ref(),
-//             &r#"
-//             [
-//                 {
-//                     "bindings": {
-//                         "backspace": "test::A"
-//                     }
-//                 }
-//             ]
-//             "#
-//             .into(),
-//             Default::default(),
-//         )
-//         .await
-//         .unwrap();
-
-//         cx.update(|cx| {
-//             cx.set_global(SettingsStore::test(cx));
-//             theme::init(Assets, cx);
-//             welcome::init(cx);
-
-//             cx.add_global_action(|_: &A, _cx| {});
-//             cx.add_global_action(|_: &B, _cx| {});
-//             cx.add_global_action(|_: &ActivatePreviousPane, _cx| {});
-//             cx.add_global_action(|_: &ActivatePrevItem, _cx| {});
-
-//             let settings_rx = watch_config_file(
-//                 executor.clone(),
-//                 fs.clone(),
-//                 PathBuf::from("/settings.json"),
-//             );
-//             let keymap_rx =
-//                 watch_config_file(executor.clone(), fs.clone(), PathBuf::from("/keymap.json"));
-
-//             handle_keymap_file_changes(keymap_rx, cx);
-//             handle_settings_file_changes(settings_rx, cx);
-//         });
-
-//         cx.foreground().run_until_parked();
-
-//         let window = cx.add_window(|_| TestView);
-
-//         // Test loading the keymap base at all
-//         assert_key_bindings_for(
-//             window.into(),
-//             cx,
-//             vec![("backspace", &A), ("k", &ActivatePreviousPane)],
-//             line!(),
-//         );
-
-//         // Test disabling the key binding for the base keymap
-//         fs.save(
-//             "/keymap.json".as_ref(),
-//             &r#"
-//             [
-//                 {
-//                     "bindings": {
-//                         "backspace": null
-//                     }
-//                 }
-//             ]
-//             "#
-//             .into(),
-//             Default::default(),
-//         )
-//         .await
-//         .unwrap();
-
-//         cx.foreground().run_until_parked();
-
-//         assert_key_bindings_for(
-//             window.into(),
-//             cx,
-//             vec![("k", &ActivatePreviousPane)],
-//             line!(),
-//         );
-
-//         // Test modifying the base, while retaining the users keymap
-//         fs.save(
-//             "/settings.json".as_ref(),
-//             &r#"
-//             {
-//                 "base_keymap": "JetBrains"
-//             }
-//             "#
-//             .into(),
-//             Default::default(),
-//         )
-//         .await
-//         .unwrap();
-
-//         cx.foreground().run_until_parked();
-
-//         assert_key_bindings_for(window.into(), cx, vec![("[", &ActivatePrevItem)], line!());
-
-//         #[track_caller]
-//         fn assert_key_bindings_for<'a>(
-//             window: AnyWindowHandle,
-//             cx: &TestAppContext,
-//             actions: Vec<(&'static str, &'a dyn Action)>,
-//             line: u32,
-//         ) {
-//             for (key, action) in actions {
-//                 // assert that...
-//                 assert!(
-//                     cx.available_actions(window, 0)
-//                         .into_iter()
-//                         .any(|(_, bound_action, b)| {
-//                             // action names match...
-//                             bound_action.name() == action.name()
-//                         && bound_action.namespace() == action.namespace()
-//                         // and key strokes contain the given key
-//                         && b.iter()
-//                             .any(|binding| binding.keystrokes().iter().any(|k| k.key == key))
-//                         }),
-//                     "On {} Failed to find {} with key binding {}",
-//                     line,
-//                     action.name(),
-//                     key
-//                 );
-//             }
-//         }
-//     }
-
-//     #[gpui::test]
-//     fn test_bundled_settings_and_themes(cx: &mut AppContext) {
-//         cx.platform()
-//             .fonts()
-//             .add_fonts(&[
-//                 Assets
-//                     .load("fonts/zed-sans/zed-sans-extended.ttf")
-//                     .unwrap()
-//                     .to_vec()
-//                     .into(),
-//                 Assets
-//                     .load("fonts/zed-mono/zed-mono-extended.ttf")
-//                     .unwrap()
-//                     .to_vec()
-//                     .into(),
-//                 Assets
-//                     .load("fonts/plex/IBMPlexSans-Regular.ttf")
-//                     .unwrap()
-//                     .to_vec()
-//                     .into(),
-//             ])
-//             .unwrap();
-//         let themes = ThemeRegistry::new(Assets, cx.font_cache().clone());
-//         let mut settings = SettingsStore::default();
-//         settings
-//             .set_default_settings(&settings::default_settings(), cx)
-//             .unwrap();
-//         cx.set_global(settings);
-//         theme::init(Assets, cx);
-
-//         let mut has_default_theme = false;
-//         for theme_name in themes.list(false).map(|meta| meta.name) {
-//             let theme = themes.get(&theme_name).unwrap();
-//             assert_eq!(theme.meta.name, theme_name);
-//             if theme.meta.name == settings::get::<ThemeSettings>(cx).theme.meta.name {
-//                 has_default_theme = true;
-//             }
-//         }
-//         assert!(has_default_theme);
-//     }
-
-//     #[gpui::test]
-//     fn test_bundled_languages(cx: &mut AppContext) {
-//         cx.set_global(SettingsStore::test(cx));
-//         let mut languages = LanguageRegistry::test();
-//         languages.set_executor(cx.background().clone());
-//         let languages = Arc::new(languages);
-//         let node_runtime = node_runtime::FakeNodeRuntime::new();
-//         languages::init(languages.clone(), node_runtime, cx);
-//         for name in languages.language_names() {
-//             languages.language_for_name(&name);
-//         }
-//         cx.foreground().run_until_parked();
-//     }
-
-//     fn init_test(cx: &mut TestAppContext) -> Arc<AppState> {
-//         cx.foreground().forbid_parking();
-//         cx.update(|cx| {
-//             let mut app_state = AppState::test(cx);
-//             let state = Arc::get_mut(&mut app_state).unwrap();
-//             state.initialize_workspace = initialize_workspace;
-//             state.build_window_options = build_window_options;
-//             theme::init((), cx);
-//             audio::init((), cx);
-//             channel::init(&app_state.client, app_state.user_store.clone(), cx);
-//             call::init(app_state.client.clone(), app_state.user_store.clone(), cx);
-//             notifications::init(app_state.client.clone(), app_state.user_store.clone(), cx);
-//             workspace::init(app_state.clone(), cx);
-//             Project::init_settings(cx);
-//             language::init(cx);
-//             editor::init(cx);
-//             project_panel::init_settings(cx);
-//             collab_ui::init(&app_state, cx);
-//             pane::init(cx);
-//             project_panel::init((), cx);
-//             terminal_view::init(cx);
-//             assistant::init(cx);
-//             app_state
-//         })
-//     }
-
-//     fn rust_lang() -> Arc<language::Language> {
-//         Arc::new(language::Language::new(
-//             language::LanguageConfig {
-//                 name: "Rust".into(),
-//                 path_suffixes: vec!["rs".to_string()],
-//                 ..Default::default()
-//             },
-//             Some(tree_sitter_rust::language()),
-//         ))
-//     }
-// }
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use assets::Assets;
+    use editor::{scroll::autoscroll::Autoscroll, DisplayPoint, Editor, EditorEvent};
+    use gpui::{
+        actions, Action, AnyWindowHandle, AppContext, AssetSource, Entity, TestAppContext,
+        VisualTestContext, WindowHandle,
+    };
+    use language::LanguageRegistry;
+    use project::{project_settings::ProjectSettings, Project, ProjectPath};
+    use serde_json::json;
+    use settings::{handle_settings_file_changes, watch_config_file, SettingsStore};
+    use std::{
+        collections::HashSet,
+        path::{Path, PathBuf},
+    };
+    use theme::{ThemeRegistry, ThemeSettings};
+    use workspace::{
+        item::{Item, ItemHandle},
+        open_new, open_paths, pane, NewFile, OpenVisible, SaveIntent, SplitDirection,
+        WorkspaceHandle,
+    };
+
+    // #[gpui::test]
+    // async fn test_open_paths_action(cx: &mut TestAppContext) {
+    //     let app_state = init_test(cx);
+    //     app_state
+    //         .fs
+    //         .as_fake()
+    //         .insert_tree(
+    //             "/root",
+    //             json!({
+    //                 "a": {
+    //                     "aa": null,
+    //                     "ab": null,
+    //                 },
+    //                 "b": {
+    //                     "ba": null,
+    //                     "bb": null,
+    //                 },
+    //                 "c": {
+    //                     "ca": null,
+    //                     "cb": null,
+    //                 },
+    //                 "d": {
+    //                     "da": null,
+    //                     "db": null,
+    //                 },
+    //             }),
+    //         )
+    //         .await;
+
+    //     cx.update(|cx| {
+    //         open_paths(
+    //             &[PathBuf::from("/root/a"), PathBuf::from("/root/b")],
+    //             &app_state,
+    //             None,
+    //             cx,
+    //         )
+    //     })
+    //     .await
+    //     .unwrap();
+    //     assert_eq!(cx.read(|cx| cx.windows().len()), 1);
+
+    //     cx.update(|cx| open_paths(&[PathBuf::from("/root/a")], &app_state, None, cx))
+    //         .await
+    //         .unwrap();
+    //     assert_eq!(cx.read(|cx| cx.windows().len()), 1);
+    //     let workspace_1 = cx
+    //         .read(|cx| cx.windows()[0].downcast::<Workspace>())
+    //         .unwrap();
+    //     workspace_1
+    //         .update(cx, |workspace, cx| {
+    //             assert_eq!(workspace.worktrees(cx).count(), 2);
+    //             assert!(workspace.left_dock().read(cx).is_open());
+    //             assert!(workspace
+    //                 .active_pane()
+    //                 .read(cx)
+    //                 .focus_handle(cx)
+    //                 .is_focused(cx));
+    //         })
+    //         .unwrap();
+
+    //     cx.update(|cx| {
+    //         open_paths(
+    //             &[PathBuf::from("/root/b"), PathBuf::from("/root/c")],
+    //             &app_state,
+    //             None,
+    //             cx,
+    //         )
+    //     })
+    //     .await
+    //     .unwrap();
+    //     assert_eq!(cx.read(|cx| cx.windows().len()), 2);
+
+    //     // Replace existing windows
+    //     let window = cx
+    //         .update(|cx| cx.windows()[0].downcast::<Workspace>())
+    //         .unwrap();
+    //     cx.update(|cx| {
+    //         open_paths(
+    //             &[PathBuf::from("/root/c"), PathBuf::from("/root/d")],
+    //             &app_state,
+    //             Some(window),
+    //             cx,
+    //         )
+    //     })
+    //     .await
+    //     .unwrap();
+    //     assert_eq!(cx.read(|cx| cx.windows().len()), 2);
+    //     let workspace_1 = cx
+    //         .update(|cx| cx.windows()[0].downcast::<Workspace>())
+    //         .unwrap();
+    //     workspace_1
+    //         .update(cx, |workspace, cx| {
+    //             assert_eq!(
+    //                 workspace
+    //                     .worktrees(cx)
+    //                     .map(|w| w.read(cx).abs_path())
+    //                     .collect::<Vec<_>>(),
+    //                 &[Path::new("/root/c").into(), Path::new("/root/d").into()]
+    //             );
+    //             assert!(workspace.left_dock().read(cx).is_open());
+    //             assert!(workspace.active_pane().focus_handle(cx).is_focused(cx));
+    //         })
+    //         .unwrap();
+    // }
+
+    #[gpui::test]
+    async fn test_window_edit_state(cx: &mut TestAppContext) {
+        let executor = cx.executor();
+        let app_state = init_test(cx);
+        app_state
+            .fs
+            .as_fake()
+            .insert_tree("/root", json!({"a": "hey"}))
+            .await;
+
+        cx.update(|cx| open_paths(&[PathBuf::from("/root/a")], &app_state, None, cx))
+            .await
+            .unwrap();
+        assert_eq!(cx.update(|cx| cx.windows().len()), 1);
+
+        // When opening the workspace, the window is not in a edited state.
+        let window = cx.update(|cx| cx.windows()[0].downcast::<Workspace>().unwrap());
+
+        let window_is_edited = |window: WindowHandle<Workspace>, cx: &mut TestAppContext| {
+            cx.test_window(window.into()).edited()
+        };
+        let pane = window
+            .read_with(cx, |workspace, _| workspace.active_pane().clone())
+            .unwrap();
+        let editor = window
+            .read_with(cx, |workspace, cx| {
+                workspace
+                    .active_item(cx)
+                    .unwrap()
+                    .downcast::<Editor>()
+                    .unwrap()
+            })
+            .unwrap();
+
+        assert!(!window_is_edited(window, cx));
+
+        // Editing a buffer marks the window as edited.
+        window
+            .update(cx, |_, cx| {
+                editor.update(cx, |editor, cx| editor.insert("EDIT", cx));
+            })
+            .unwrap();
+
+        assert!(window_is_edited(window, cx));
+
+        // Undoing the edit restores the window's edited state.
+        window
+            .update(cx, |_, cx| {
+                editor.update(cx, |editor, cx| editor.undo(&Default::default(), cx));
+            })
+            .unwrap();
+        assert!(!window_is_edited(window, cx));
+
+        // Redoing the edit marks the window as edited again.
+        window
+            .update(cx, |_, cx| {
+                editor.update(cx, |editor, cx| editor.redo(&Default::default(), cx));
+            })
+            .unwrap();
+        assert!(window_is_edited(window, cx));
+
+        // Closing the item restores the window's edited state.
+        let close = window
+            .update(cx, |_, cx| {
+                pane.update(cx, |pane, cx| {
+                    drop(editor);
+                    pane.close_active_item(&Default::default(), cx).unwrap()
+                })
+            })
+            .unwrap();
+        executor.run_until_parked();
+
+        cx.simulate_prompt_answer(1);
+        close.await.unwrap();
+        assert!(!window_is_edited(window, cx));
+
+        // Opening the buffer again doesn't impact the window's edited state.
+        cx.update(|cx| open_paths(&[PathBuf::from("/root/a")], &app_state, None, cx))
+            .await
+            .unwrap();
+        let editor = window
+            .read_with(cx, |workspace, cx| {
+                workspace
+                    .active_item(cx)
+                    .unwrap()
+                    .downcast::<Editor>()
+                    .unwrap()
+            })
+            .unwrap();
+        assert!(!window_is_edited(window, cx));
+
+        // Editing the buffer marks the window as edited.
+        window
+            .update(cx, |_, cx| {
+                editor.update(cx, |editor, cx| editor.insert("EDIT", cx));
+            })
+            .unwrap();
+        assert!(window_is_edited(window, cx));
+
+        // Ensure closing the window via the mouse gets preempted due to the
+        // buffer having unsaved changes.
+        assert!(!VisualTestContext::from_window(window.into(), cx).simulate_close());
+        executor.run_until_parked();
+        assert_eq!(cx.update(|cx| cx.windows().len()), 1);
+
+        // The window is successfully closed after the user dismisses the prompt.
+        cx.simulate_prompt_answer(1);
+        executor.run_until_parked();
+        assert_eq!(cx.update(|cx| cx.windows().len()), 0);
+    }
+
+    #[gpui::test]
+    async fn test_new_empty_workspace(cx: &mut TestAppContext) {
+        let app_state = init_test(cx);
+        cx.update(|cx| {
+            open_new(&app_state, cx, |workspace, cx| {
+                Editor::new_file(workspace, &Default::default(), cx)
+            })
+        })
+        .await;
+
+        let workspace = cx
+            .update(|cx| cx.windows().first().unwrap().downcast::<Workspace>())
+            .unwrap();
+
+        let editor = workspace
+            .update(cx, |workspace, cx| {
+                let editor = workspace
+                    .active_item(cx)
+                    .unwrap()
+                    .downcast::<editor::Editor>()
+                    .unwrap();
+                editor.update(cx, |editor, cx| {
+                    assert!(editor.text(cx).is_empty());
+                    assert!(!editor.is_dirty(cx));
+                });
+
+                editor
+            })
+            .unwrap();
+
+        let save_task = workspace
+            .update(cx, |workspace, cx| {
+                workspace.save_active_item(SaveIntent::Save, cx)
+            })
+            .unwrap();
+        app_state.fs.create_dir(Path::new("/root")).await.unwrap();
+        cx.background_executor.run_until_parked();
+        cx.simulate_new_path_selection(|_| Some(PathBuf::from("/root/the-new-name")));
+        save_task.await.unwrap();
+        workspace
+            .update(cx, |_, cx| {
+                editor.update(cx, |editor, cx| {
+                    assert!(!editor.is_dirty(cx));
+                    assert_eq!(editor.title(cx), "the-new-name");
+                });
+            })
+            .unwrap();
+    }
+
+    #[gpui::test]
+    async fn test_open_entry(cx: &mut TestAppContext) {
+        let app_state = init_test(cx);
+        app_state
+            .fs
+            .as_fake()
+            .insert_tree(
+                "/root",
+                json!({
+                    "a": {
+                        "file1": "contents 1",
+                        "file2": "contents 2",
+                        "file3": "contents 3",
+                    },
+                }),
+            )
+            .await;
+
+        let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
+        let window = cx.add_window(|cx| Workspace::test_new(project, cx));
+        let workspace = window.root(cx).unwrap();
+
+        let entries = cx.read(|cx| workspace.file_project_paths(cx));
+        let file1 = entries[0].clone();
+        let file2 = entries[1].clone();
+        let file3 = entries[2].clone();
+
+        // Open the first entry
+        let entry_1 = window
+            .update(cx, |w, cx| w.open_path(file1.clone(), None, true, cx))
+            .unwrap()
+            .await
+            .unwrap();
+        cx.read(|cx| {
+            let pane = workspace.read(cx).active_pane().read(cx);
+            assert_eq!(
+                pane.active_item().unwrap().project_path(cx),
+                Some(file1.clone())
+            );
+            assert_eq!(pane.items_len(), 1);
+        });
+
+        // Open the second entry
+        window
+            .update(cx, |w, cx| w.open_path(file2.clone(), None, true, cx))
+            .unwrap()
+            .await
+            .unwrap();
+        cx.read(|cx| {
+            let pane = workspace.read(cx).active_pane().read(cx);
+            assert_eq!(
+                pane.active_item().unwrap().project_path(cx),
+                Some(file2.clone())
+            );
+            assert_eq!(pane.items_len(), 2);
+        });
+
+        // Open the first entry again. The existing pane item is activated.
+        let entry_1b = window
+            .update(cx, |w, cx| w.open_path(file1.clone(), None, true, cx))
+            .unwrap()
+            .await
+            .unwrap();
+        assert_eq!(entry_1.item_id(), entry_1b.item_id());
+
+        cx.read(|cx| {
+            let pane = workspace.read(cx).active_pane().read(cx);
+            assert_eq!(
+                pane.active_item().unwrap().project_path(cx),
+                Some(file1.clone())
+            );
+            assert_eq!(pane.items_len(), 2);
+        });
+
+        // Split the pane with the first entry, then open the second entry again.
+        window
+            .update(cx, |w, cx| {
+                w.split_and_clone(w.active_pane().clone(), SplitDirection::Right, cx);
+                w.open_path(file2.clone(), None, true, cx)
+            })
+            .unwrap()
+            .await
+            .unwrap();
+
+        window
+            .read_with(cx, |w, cx| {
+                assert_eq!(
+                    w.active_pane()
+                        .read(cx)
+                        .active_item()
+                        .unwrap()
+                        .project_path(cx),
+                    Some(file2.clone())
+                );
+            })
+            .unwrap();
+
+        // Open the third entry twice concurrently. Only one pane item is added.
+        let (t1, t2) = window
+            .update(cx, |w, cx| {
+                (
+                    w.open_path(file3.clone(), None, true, cx),
+                    w.open_path(file3.clone(), None, true, cx),
+                )
+            })
+            .unwrap();
+        t1.await.unwrap();
+        t2.await.unwrap();
+        cx.read(|cx| {
+            let pane = workspace.read(cx).active_pane().read(cx);
+            assert_eq!(
+                pane.active_item().unwrap().project_path(cx),
+                Some(file3.clone())
+            );
+            let pane_entries = pane
+                .items()
+                .map(|i| i.project_path(cx).unwrap())
+                .collect::<Vec<_>>();
+            assert_eq!(pane_entries, &[file1, file2, file3]);
+        });
+    }
+
+    #[gpui::test]
+    async fn test_open_paths(cx: &mut TestAppContext) {
+        let app_state = init_test(cx);
+
+        app_state
+            .fs
+            .as_fake()
+            .insert_tree(
+                "/",
+                json!({
+                    "dir1": {
+                        "a.txt": ""
+                    },
+                    "dir2": {
+                        "b.txt": ""
+                    },
+                    "dir3": {
+                        "c.txt": ""
+                    },
+                    "d.txt": ""
+                }),
+            )
+            .await;
+
+        cx.update(|cx| open_paths(&[PathBuf::from("/dir1/")], &app_state, None, cx))
+            .await
+            .unwrap();
+        assert_eq!(cx.update(|cx| cx.windows().len()), 1);
+        let window = cx.update(|cx| cx.windows()[0].downcast::<Workspace>().unwrap());
+        let workspace = window.root(cx).unwrap();
+
+        #[track_caller]
+        fn assert_project_panel_selection(
+            workspace: &Workspace,
+            expected_worktree_path: &Path,
+            expected_entry_path: &Path,
+            cx: &AppContext,
+        ) {
+            let project_panel = [
+                workspace.left_dock().read(cx).panel::<ProjectPanel>(),
+                workspace.right_dock().read(cx).panel::<ProjectPanel>(),
+                workspace.bottom_dock().read(cx).panel::<ProjectPanel>(),
+            ]
+            .into_iter()
+            .find_map(std::convert::identity)
+            .expect("found no project panels")
+            .read(cx);
+            let (selected_worktree, selected_entry) = project_panel
+                .selected_entry(cx)
+                .expect("project panel should have a selected entry");
+            assert_eq!(
+                selected_worktree.abs_path().as_ref(),
+                expected_worktree_path,
+                "Unexpected project panel selected worktree path"
+            );
+            assert_eq!(
+                selected_entry.path.as_ref(),
+                expected_entry_path,
+                "Unexpected project panel selected entry path"
+            );
+        }
+
+        // Open a file within an existing worktree.
+        window
+            .update(cx, |view, cx| {
+                view.open_paths(vec!["/dir1/a.txt".into()], OpenVisible::All, None, cx)
+            })
+            .unwrap()
+            .await;
+        cx.read(|cx| {
+            let workspace = workspace.read(cx);
+            assert_project_panel_selection(workspace, Path::new("/dir1"), Path::new("a.txt"), cx);
+            assert_eq!(
+                workspace
+                    .active_pane()
+                    .read(cx)
+                    .active_item()
+                    .unwrap()
+                    .act_as::<Editor>(cx)
+                    .unwrap()
+                    .read(cx)
+                    .title(cx),
+                "a.txt"
+            );
+        });
+
+        // Open a file outside of any existing worktree.
+        window
+            .update(cx, |view, cx| {
+                view.open_paths(vec!["/dir2/b.txt".into()], OpenVisible::All, None, cx)
+            })
+            .unwrap()
+            .await;
+        cx.read(|cx| {
+            let workspace = workspace.read(cx);
+            assert_project_panel_selection(workspace, Path::new("/dir2/b.txt"), Path::new(""), cx);
+            let worktree_roots = workspace
+                .worktrees(cx)
+                .map(|w| w.read(cx).as_local().unwrap().abs_path().as_ref())
+                .collect::<HashSet<_>>();
+            assert_eq!(
+                worktree_roots,
+                vec!["/dir1", "/dir2/b.txt"]
+                    .into_iter()
+                    .map(Path::new)
+                    .collect(),
+            );
+            assert_eq!(
+                workspace
+                    .active_pane()
+                    .read(cx)
+                    .active_item()
+                    .unwrap()
+                    .act_as::<Editor>(cx)
+                    .unwrap()
+                    .read(cx)
+                    .title(cx),
+                "b.txt"
+            );
+        });
+
+        // Ensure opening a directory and one of its children only adds one worktree.
+        window
+            .update(cx, |view, cx| {
+                view.open_paths(
+                    vec!["/dir3".into(), "/dir3/c.txt".into()],
+                    OpenVisible::All,
+                    None,
+                    cx,
+                )
+            })
+            .unwrap()
+            .await;
+        cx.read(|cx| {
+            let workspace = workspace.read(cx);
+            assert_project_panel_selection(workspace, Path::new("/dir3"), Path::new("c.txt"), cx);
+            let worktree_roots = workspace
+                .worktrees(cx)
+                .map(|w| w.read(cx).as_local().unwrap().abs_path().as_ref())
+                .collect::<HashSet<_>>();
+            assert_eq!(
+                worktree_roots,
+                vec!["/dir1", "/dir2/b.txt", "/dir3"]
+                    .into_iter()
+                    .map(Path::new)
+                    .collect(),
+            );
+            assert_eq!(
+                workspace
+                    .active_pane()
+                    .read(cx)
+                    .active_item()
+                    .unwrap()
+                    .act_as::<Editor>(cx)
+                    .unwrap()
+                    .read(cx)
+                    .title(cx),
+                "c.txt"
+            );
+        });
+
+        // Ensure opening invisibly a file outside an existing worktree adds a new, invisible worktree.
+        window
+            .update(cx, |view, cx| {
+                view.open_paths(vec!["/d.txt".into()], OpenVisible::None, None, cx)
+            })
+            .unwrap()
+            .await;
+        cx.read(|cx| {
+            let workspace = workspace.read(cx);
+            assert_project_panel_selection(workspace, Path::new("/d.txt"), Path::new(""), cx);
+            let worktree_roots = workspace
+                .worktrees(cx)
+                .map(|w| w.read(cx).as_local().unwrap().abs_path().as_ref())
+                .collect::<HashSet<_>>();
+            assert_eq!(
+                worktree_roots,
+                vec!["/dir1", "/dir2/b.txt", "/dir3", "/d.txt"]
+                    .into_iter()
+                    .map(Path::new)
+                    .collect(),
+            );
+
+            let visible_worktree_roots = workspace
+                .visible_worktrees(cx)
+                .map(|w| w.read(cx).as_local().unwrap().abs_path().as_ref())
+                .collect::<HashSet<_>>();
+            assert_eq!(
+                visible_worktree_roots,
+                vec!["/dir1", "/dir2/b.txt", "/dir3"]
+                    .into_iter()
+                    .map(Path::new)
+                    .collect(),
+            );
+
+            assert_eq!(
+                workspace
+                    .active_pane()
+                    .read(cx)
+                    .active_item()
+                    .unwrap()
+                    .act_as::<Editor>(cx)
+                    .unwrap()
+                    .read(cx)
+                    .title(cx),
+                "d.txt"
+            );
+        });
+    }
+
+    #[gpui::test]
+    async fn test_opening_excluded_paths(cx: &mut TestAppContext) {
+        let app_state = init_test(cx);
+        cx.update(|cx| {
+            cx.update_global::<SettingsStore, _>(|store, cx| {
+                store.update_user_settings::<ProjectSettings>(cx, |project_settings| {
+                    project_settings.file_scan_exclusions =
+                        Some(vec!["excluded_dir".to_string(), "**/.git".to_string()]);
+                });
+            });
+        });
+        app_state
+            .fs
+            .as_fake()
+            .insert_tree(
+                "/root",
+                json!({
+                    ".gitignore": "ignored_dir\n",
+                    ".git": {
+                        "HEAD": "ref: refs/heads/main",
+                    },
+                    "regular_dir": {
+                        "file": "regular file contents",
+                    },
+                    "ignored_dir": {
+                        "ignored_subdir": {
+                            "file": "ignored subfile contents",
+                        },
+                        "file": "ignored file contents",
+                    },
+                    "excluded_dir": {
+                        "file": "excluded file contents",
+                    },
+                }),
+            )
+            .await;
+
+        let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
+        let window = cx.add_window(|cx| Workspace::test_new(project, cx));
+        let workspace = window.root(cx).unwrap();
+
+        let initial_entries = cx.read(|cx| workspace.file_project_paths(cx));
+        let paths_to_open = [
+            Path::new("/root/excluded_dir/file").to_path_buf(),
+            Path::new("/root/.git/HEAD").to_path_buf(),
+            Path::new("/root/excluded_dir/ignored_subdir").to_path_buf(),
+        ];
+        let (opened_workspace, new_items) = cx
+            .update(|cx| workspace::open_paths(&paths_to_open, &app_state, None, cx))
+            .await
+            .unwrap();
+
+        assert_eq!(
+            opened_workspace.root_view(cx).unwrap().entity_id(),
+            workspace.entity_id(),
+            "Excluded files in subfolders of a workspace root should be opened in the workspace"
+        );
+        let mut opened_paths = cx.read(|cx| {
+            assert_eq!(
+                new_items.len(),
+                paths_to_open.len(),
+                "Expect to get the same number of opened items as submitted paths to open"
+            );
+            new_items
+                .iter()
+                .zip(paths_to_open.iter())
+                .map(|(i, path)| {
+                    match i {
+                        Some(Ok(i)) => {
+                            Some(i.project_path(cx).map(|p| p.path.display().to_string()))
+                        }
+                        Some(Err(e)) => panic!("Excluded file {path:?} failed to open: {e:?}"),
+                        None => None,
+                    }
+                    .flatten()
+                })
+                .collect::<Vec<_>>()
+        });
+        opened_paths.sort();
+        assert_eq!(
+            opened_paths,
+            vec![
+                None,
+                Some(".git/HEAD".to_string()),
+                Some("excluded_dir/file".to_string()),
+            ],
+            "Excluded files should get opened, excluded dir should not get opened"
+        );
+
+        let entries = cx.read(|cx| workspace.file_project_paths(cx));
+        assert_eq!(
+                initial_entries, entries,
+                "Workspace entries should not change after opening excluded files and directories paths"
+            );
+
+        cx.read(|cx| {
+                let pane = workspace.read(cx).active_pane().read(cx);
+                let mut opened_buffer_paths = pane
+                    .items()
+                    .map(|i| {
+                        i.project_path(cx)
+                            .expect("all excluded files that got open should have a path")
+                            .path
+                            .display()
+                            .to_string()
+                    })
+                    .collect::<Vec<_>>();
+                opened_buffer_paths.sort();
+                assert_eq!(
+                    opened_buffer_paths,
+                    vec![".git/HEAD".to_string(), "excluded_dir/file".to_string()],
+                    "Despite not being present in the worktrees, buffers for excluded files are opened and added to the pane"
+                );
+            });
+    }
+
+    #[gpui::test]
+    async fn test_save_conflicting_item(cx: &mut TestAppContext) {
+        let app_state = init_test(cx);
+        app_state
+            .fs
+            .as_fake()
+            .insert_tree("/root", json!({ "a.txt": "" }))
+            .await;
+
+        let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
+        let window = cx.add_window(|cx| Workspace::test_new(project, cx));
+        let workspace = window.root(cx).unwrap();
+
+        // Open a file within an existing worktree.
+        window
+            .update(cx, |view, cx| {
+                view.open_paths(
+                    vec![PathBuf::from("/root/a.txt")],
+                    OpenVisible::All,
+                    None,
+                    cx,
+                )
+            })
+            .unwrap()
+            .await;
+        let editor = cx.read(|cx| {
+            let pane = workspace.read(cx).active_pane().read(cx);
+            let item = pane.active_item().unwrap();
+            item.downcast::<Editor>().unwrap()
+        });
+
+        window
+            .update(cx, |_, cx| {
+                editor.update(cx, |editor, cx| editor.handle_input("x", cx));
+            })
+            .unwrap();
+
+        app_state
+            .fs
+            .as_fake()
+            .insert_file("/root/a.txt", "changed".to_string())
+            .await;
+        editor
+            .condition::<EditorEvent>(cx, |editor, cx| editor.has_conflict(cx))
+            .await;
+        cx.read(|cx| assert!(editor.is_dirty(cx)));
+
+        let save_task = window
+            .update(cx, |workspace, cx| {
+                workspace.save_active_item(SaveIntent::Save, cx)
+            })
+            .unwrap();
+        cx.background_executor.run_until_parked();
+        cx.simulate_prompt_answer(0);
+        save_task.await.unwrap();
+        window
+            .update(cx, |_, cx| {
+                editor.update(cx, |editor, cx| {
+                    assert!(!editor.is_dirty(cx));
+                    assert!(!editor.has_conflict(cx));
+                });
+            })
+            .unwrap();
+    }
+
+    #[gpui::test]
+    async fn test_open_and_save_new_file(cx: &mut TestAppContext) {
+        let app_state = init_test(cx);
+        app_state.fs.create_dir(Path::new("/root")).await.unwrap();
+
+        let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
+        project.update(cx, |project, _| project.languages().add(rust_lang()));
+        let window = cx.add_window(|cx| Workspace::test_new(project, cx));
+        let worktree = cx.update(|cx| window.read(cx).unwrap().worktrees(cx).next().unwrap());
+
+        // Create a new untitled buffer
+        cx.dispatch_action(window.into(), NewFile);
+        let editor = window
+            .read_with(cx, |workspace, cx| {
+                workspace
+                    .active_item(cx)
+                    .unwrap()
+                    .downcast::<Editor>()
+                    .unwrap()
+            })
+            .unwrap();
+
+        window
+            .update(cx, |_, cx| {
+                editor.update(cx, |editor, cx| {
+                    assert!(!editor.is_dirty(cx));
+                    assert_eq!(editor.title(cx), "untitled");
+                    assert!(Arc::ptr_eq(
+                        &editor.buffer().read(cx).language_at(0, cx).unwrap(),
+                        &languages::PLAIN_TEXT
+                    ));
+                    editor.handle_input("hi", cx);
+                    assert!(editor.is_dirty(cx));
+                });
+            })
+            .unwrap();
+
+        // Save the buffer. This prompts for a filename.
+        let save_task = window
+            .update(cx, |workspace, cx| {
+                workspace.save_active_item(SaveIntent::Save, cx)
+            })
+            .unwrap();
+        cx.background_executor.run_until_parked();
+        cx.simulate_new_path_selection(|parent_dir| {
+            assert_eq!(parent_dir, Path::new("/root"));
+            Some(parent_dir.join("the-new-name.rs"))
+        });
+        cx.read(|cx| {
+            assert!(editor.is_dirty(cx));
+            assert_eq!(editor.read(cx).title(cx), "untitled");
+        });
+
+        // When the save completes, the buffer's title is updated and the language is assigned based
+        // on the path.
+        save_task.await.unwrap();
+        window
+            .update(cx, |_, cx| {
+                editor.update(cx, |editor, cx| {
+                    assert!(!editor.is_dirty(cx));
+                    assert_eq!(editor.title(cx), "the-new-name.rs");
+                    assert_eq!(
+                        editor
+                            .buffer()
+                            .read(cx)
+                            .language_at(0, cx)
+                            .unwrap()
+                            .name()
+                            .as_ref(),
+                        "Rust"
+                    );
+                });
+            })
+            .unwrap();
+
+        // Edit the file and save it again. This time, there is no filename prompt.
+        window
+            .update(cx, |_, cx| {
+                editor.update(cx, |editor, cx| {
+                    editor.handle_input(" there", cx);
+                    assert!(editor.is_dirty(cx));
+                });
+            })
+            .unwrap();
+
+        let save_task = window
+            .update(cx, |workspace, cx| {
+                workspace.save_active_item(SaveIntent::Save, cx)
+            })
+            .unwrap();
+        save_task.await.unwrap();
+        // todo!() po
+        //assert!(!cx.did_prompt_for_new_path());
+        window
+            .update(cx, |_, cx| {
+                editor.update(cx, |editor, cx| {
+                    assert!(!editor.is_dirty(cx));
+                    assert_eq!(editor.title(cx), "the-new-name.rs")
+                });
+            })
+            .unwrap();
+
+        // Open the same newly-created file in another pane item. The new editor should reuse
+        // the same buffer.
+        cx.dispatch_action(window.into(), NewFile);
+        window
+            .update(cx, |workspace, cx| {
+                workspace.split_and_clone(
+                    workspace.active_pane().clone(),
+                    SplitDirection::Right,
+                    cx,
+                );
+                workspace.open_path((worktree.read(cx).id(), "the-new-name.rs"), None, true, cx)
+            })
+            .unwrap()
+            .await
+            .unwrap();
+        let editor2 = window
+            .update(cx, |workspace, cx| {
+                workspace
+                    .active_item(cx)
+                    .unwrap()
+                    .downcast::<Editor>()
+                    .unwrap()
+            })
+            .unwrap();
+        cx.read(|cx| {
+            assert_eq!(
+                editor2.read(cx).buffer().read(cx).as_singleton().unwrap(),
+                editor.read(cx).buffer().read(cx).as_singleton().unwrap()
+            );
+        })
+    }
+
+    #[gpui::test]
+    async fn test_setting_language_when_saving_as_single_file_worktree(cx: &mut TestAppContext) {
+        let app_state = init_test(cx);
+        app_state.fs.create_dir(Path::new("/root")).await.unwrap();
+
+        let project = Project::test(app_state.fs.clone(), [], cx).await;
+        project.update(cx, |project, _| project.languages().add(rust_lang()));
+        let window = cx.add_window(|cx| Workspace::test_new(project, cx));
+
+        // Create a new untitled buffer
+        cx.dispatch_action(window.into(), NewFile);
+        let editor = window
+            .read_with(cx, |workspace, cx| {
+                workspace
+                    .active_item(cx)
+                    .unwrap()
+                    .downcast::<Editor>()
+                    .unwrap()
+            })
+            .unwrap();
+        window
+            .update(cx, |_, cx| {
+                editor.update(cx, |editor, cx| {
+                    assert!(Arc::ptr_eq(
+                        &editor.buffer().read(cx).language_at(0, cx).unwrap(),
+                        &languages::PLAIN_TEXT
+                    ));
+                    editor.handle_input("hi", cx);
+                    assert!(editor.is_dirty(cx));
+                });
+            })
+            .unwrap();
+
+        // Save the buffer. This prompts for a filename.
+        let save_task = window
+            .update(cx, |workspace, cx| {
+                workspace.save_active_item(SaveIntent::Save, cx)
+            })
+            .unwrap();
+        cx.background_executor.run_until_parked();
+        cx.simulate_new_path_selection(|_| Some(PathBuf::from("/root/the-new-name.rs")));
+        save_task.await.unwrap();
+        // The buffer is not dirty anymore and the language is assigned based on the path.
+        window
+            .update(cx, |_, cx| {
+                editor.update(cx, |editor, cx| {
+                    assert!(!editor.is_dirty(cx));
+                    assert_eq!(
+                        editor
+                            .buffer()
+                            .read(cx)
+                            .language_at(0, cx)
+                            .unwrap()
+                            .name()
+                            .as_ref(),
+                        "Rust"
+                    )
+                });
+            })
+            .unwrap();
+    }
+
+    #[gpui::test]
+    async fn test_pane_actions(cx: &mut TestAppContext) {
+        let app_state = init_test(cx);
+        app_state
+            .fs
+            .as_fake()
+            .insert_tree(
+                "/root",
+                json!({
+                    "a": {
+                        "file1": "contents 1",
+                        "file2": "contents 2",
+                        "file3": "contents 3",
+                    },
+                }),
+            )
+            .await;
+
+        let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
+        let window = cx.add_window(|cx| Workspace::test_new(project, cx));
+        let workspace = window.root(cx).unwrap();
+
+        let entries = cx.read(|cx| workspace.file_project_paths(cx));
+        let file1 = entries[0].clone();
+
+        let pane_1 = cx.read(|cx| workspace.read(cx).active_pane().clone());
+
+        window
+            .update(cx, |w, cx| w.open_path(file1.clone(), None, true, cx))
+            .unwrap()
+            .await
+            .unwrap();
+
+        let (editor_1, buffer) = window
+            .update(cx, |_, cx| {
+                pane_1.update(cx, |pane_1, cx| {
+                    let editor = pane_1.active_item().unwrap().downcast::<Editor>().unwrap();
+                    assert_eq!(editor.project_path(cx), Some(file1.clone()));
+                    let buffer = editor.update(cx, |editor, cx| {
+                        editor.insert("dirt", cx);
+                        editor.buffer().downgrade()
+                    });
+                    (editor.downgrade(), buffer)
+                })
+            })
+            .unwrap();
+
+        cx.dispatch_action(window.into(), pane::SplitRight);
+        let editor_2 = cx.update(|cx| {
+            let pane_2 = workspace.read(cx).active_pane().clone();
+            assert_ne!(pane_1, pane_2);
+
+            let pane2_item = pane_2.read(cx).active_item().unwrap();
+            assert_eq!(pane2_item.project_path(cx), Some(file1.clone()));
+
+            pane2_item.downcast::<Editor>().unwrap().downgrade()
+        });
+        cx.dispatch_action(
+            window.into(),
+            workspace::CloseActiveItem { save_intent: None },
+        );
+
+        cx.background_executor.run_until_parked();
+        window
+            .read_with(cx, |workspace, _| {
+                assert_eq!(workspace.panes().len(), 1);
+                assert_eq!(workspace.active_pane(), &pane_1);
+            })
+            .unwrap();
+
+        cx.dispatch_action(
+            window.into(),
+            workspace::CloseActiveItem { save_intent: None },
+        );
+        cx.background_executor.run_until_parked();
+        cx.simulate_prompt_answer(1);
+        cx.background_executor.run_until_parked();
+
+        window
+            .read_with(cx, |workspace, cx| {
+                assert_eq!(workspace.panes().len(), 1);
+                assert!(workspace.active_item(cx).is_none());
+            })
+            .unwrap();
+        editor_1.assert_dropped();
+        editor_2.assert_dropped();
+        buffer.assert_dropped();
+    }
+
+    #[gpui::test]
+    async fn test_navigation(cx: &mut TestAppContext) {
+        let app_state = init_test(cx);
+        app_state
+            .fs
+            .as_fake()
+            .insert_tree(
+                "/root",
+                json!({
+                    "a": {
+                        "file1": "contents 1\n".repeat(20),
+                        "file2": "contents 2\n".repeat(20),
+                        "file3": "contents 3\n".repeat(20),
+                    },
+                }),
+            )
+            .await;
+
+        let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
+        let workspace = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
+        let pane = workspace
+            .read_with(cx, |workspace, _| workspace.active_pane().clone())
+            .unwrap();
+
+        let entries = cx.update(|cx| workspace.root(cx).unwrap().file_project_paths(cx));
+        let file1 = entries[0].clone();
+        let file2 = entries[1].clone();
+        let file3 = entries[2].clone();
+
+        let editor1 = workspace
+            .update(cx, |w, cx| w.open_path(file1.clone(), None, true, cx))
+            .unwrap()
+            .await
+            .unwrap()
+            .downcast::<Editor>()
+            .unwrap();
+        workspace
+            .update(cx, |_, cx| {
+                editor1.update(cx, |editor, cx| {
+                    editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
+                        s.select_display_ranges(
+                            [DisplayPoint::new(10, 0)..DisplayPoint::new(10, 0)],
+                        )
+                    });
+                });
+            })
+            .unwrap();
+
+        let editor2 = workspace
+            .update(cx, |w, cx| w.open_path(file2.clone(), None, true, cx))
+            .unwrap()
+            .await
+            .unwrap()
+            .downcast::<Editor>()
+            .unwrap();
+        let editor3 = workspace
+            .update(cx, |w, cx| w.open_path(file3.clone(), None, true, cx))
+            .unwrap()
+            .await
+            .unwrap()
+            .downcast::<Editor>()
+            .unwrap();
+
+        workspace
+            .update(cx, |_, cx| {
+                editor3.update(cx, |editor, cx| {
+                    editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
+                        s.select_display_ranges(
+                            [DisplayPoint::new(12, 0)..DisplayPoint::new(12, 0)],
+                        )
+                    });
+                    editor.newline(&Default::default(), cx);
+                    editor.newline(&Default::default(), cx);
+                    editor.move_down(&Default::default(), cx);
+                    editor.move_down(&Default::default(), cx);
+                    editor.save(project.clone(), cx)
+                })
+            })
+            .unwrap()
+            .await
+            .unwrap();
+        workspace
+            .update(cx, |_, cx| {
+                editor3.update(cx, |editor, cx| {
+                    editor.set_scroll_position(point(0., 12.5), cx)
+                });
+            })
+            .unwrap();
+        assert_eq!(
+            active_location(&workspace, cx),
+            (file3.clone(), DisplayPoint::new(16, 0), 12.5)
+        );
+
+        workspace
+            .update(cx, |w, cx| w.go_back(w.active_pane().downgrade(), cx))
+            .unwrap()
+            .await
+            .unwrap();
+        assert_eq!(
+            active_location(&workspace, cx),
+            (file3.clone(), DisplayPoint::new(0, 0), 0.)
+        );
+
+        workspace
+            .update(cx, |w, cx| w.go_back(w.active_pane().downgrade(), cx))
+            .unwrap()
+            .await
+            .unwrap();
+        assert_eq!(
+            active_location(&workspace, cx),
+            (file2.clone(), DisplayPoint::new(0, 0), 0.)
+        );
+
+        workspace
+            .update(cx, |w, cx| w.go_back(w.active_pane().downgrade(), cx))
+            .unwrap()
+            .await
+            .unwrap();
+        assert_eq!(
+            active_location(&workspace, cx),
+            (file1.clone(), DisplayPoint::new(10, 0), 0.)
+        );
+
+        workspace
+            .update(cx, |w, cx| w.go_back(w.active_pane().downgrade(), cx))
+            .unwrap()
+            .await
+            .unwrap();
+        assert_eq!(
+            active_location(&workspace, cx),
+            (file1.clone(), DisplayPoint::new(0, 0), 0.)
+        );
+
+        // Go back one more time and ensure we don't navigate past the first item in the history.
+        workspace
+            .update(cx, |w, cx| w.go_back(w.active_pane().downgrade(), cx))
+            .unwrap()
+            .await
+            .unwrap();
+        assert_eq!(
+            active_location(&workspace, cx),
+            (file1.clone(), DisplayPoint::new(0, 0), 0.)
+        );
+
+        workspace
+            .update(cx, |w, cx| w.go_forward(w.active_pane().downgrade(), cx))
+            .unwrap()
+            .await
+            .unwrap();
+        assert_eq!(
+            active_location(&workspace, cx),
+            (file1.clone(), DisplayPoint::new(10, 0), 0.)
+        );
+
+        workspace
+            .update(cx, |w, cx| w.go_forward(w.active_pane().downgrade(), cx))
+            .unwrap()
+            .await
+            .unwrap();
+        assert_eq!(
+            active_location(&workspace, cx),
+            (file2.clone(), DisplayPoint::new(0, 0), 0.)
+        );
+
+        // Go forward to an item that has been closed, ensuring it gets re-opened at the same
+        // location.
+        workspace
+            .update(cx, |_, cx| {
+                pane.update(cx, |pane, cx| {
+                    let editor3_id = editor3.entity_id();
+                    drop(editor3);
+                    pane.close_item_by_id(editor3_id, SaveIntent::Close, cx)
+                })
+            })
+            .unwrap()
+            .await
+            .unwrap();
+        workspace
+            .update(cx, |w, cx| w.go_forward(w.active_pane().downgrade(), cx))
+            .unwrap()
+            .await
+            .unwrap();
+        assert_eq!(
+            active_location(&workspace, cx),
+            (file3.clone(), DisplayPoint::new(0, 0), 0.)
+        );
+
+        workspace
+            .update(cx, |w, cx| w.go_forward(w.active_pane().downgrade(), cx))
+            .unwrap()
+            .await
+            .unwrap();
+        assert_eq!(
+            active_location(&workspace, cx),
+            (file3.clone(), DisplayPoint::new(16, 0), 12.5)
+        );
+
+        workspace
+            .update(cx, |w, cx| w.go_back(w.active_pane().downgrade(), cx))
+            .unwrap()
+            .await
+            .unwrap();
+        assert_eq!(
+            active_location(&workspace, cx),
+            (file3.clone(), DisplayPoint::new(0, 0), 0.)
+        );
+
+        // Go back to an item that has been closed and removed from disk, ensuring it gets skipped.
+        workspace
+            .update(cx, |_, cx| {
+                pane.update(cx, |pane, cx| {
+                    let editor2_id = editor2.entity_id();
+                    drop(editor2);
+                    pane.close_item_by_id(editor2_id, SaveIntent::Close, cx)
+                })
+            })
+            .unwrap()
+            .await
+            .unwrap();
+        app_state
+            .fs
+            .remove_file(Path::new("/root/a/file2"), Default::default())
+            .await
+            .unwrap();
+        cx.background_executor.run_until_parked();
+
+        workspace
+            .update(cx, |w, cx| w.go_back(w.active_pane().downgrade(), cx))
+            .unwrap()
+            .await
+            .unwrap();
+        assert_eq!(
+            active_location(&workspace, cx),
+            (file1.clone(), DisplayPoint::new(10, 0), 0.)
+        );
+        workspace
+            .update(cx, |w, cx| w.go_forward(w.active_pane().downgrade(), cx))
+            .unwrap()
+            .await
+            .unwrap();
+        assert_eq!(
+            active_location(&workspace, cx),
+            (file3.clone(), DisplayPoint::new(0, 0), 0.)
+        );
+
+        // Modify file to collapse multiple nav history entries into the same location.
+        // Ensure we don't visit the same location twice when navigating.
+        workspace
+            .update(cx, |_, cx| {
+                editor1.update(cx, |editor, cx| {
+                    editor.change_selections(None, cx, |s| {
+                        s.select_display_ranges(
+                            [DisplayPoint::new(15, 0)..DisplayPoint::new(15, 0)],
+                        )
+                    })
+                });
+            })
+            .unwrap();
+        for _ in 0..5 {
+            workspace
+                .update(cx, |_, cx| {
+                    editor1.update(cx, |editor, cx| {
+                        editor.change_selections(None, cx, |s| {
+                            s.select_display_ranges([
+                                DisplayPoint::new(3, 0)..DisplayPoint::new(3, 0)
+                            ])
+                        });
+                    });
+                })
+                .unwrap();
+
+            workspace
+                .update(cx, |_, cx| {
+                    editor1.update(cx, |editor, cx| {
+                        editor.change_selections(None, cx, |s| {
+                            s.select_display_ranges([
+                                DisplayPoint::new(13, 0)..DisplayPoint::new(13, 0)
+                            ])
+                        })
+                    });
+                })
+                .unwrap();
+        }
+        workspace
+            .update(cx, |_, cx| {
+                editor1.update(cx, |editor, cx| {
+                    editor.transact(cx, |editor, cx| {
+                        editor.change_selections(None, cx, |s| {
+                            s.select_display_ranges([
+                                DisplayPoint::new(2, 0)..DisplayPoint::new(14, 0)
+                            ])
+                        });
+                        editor.insert("", cx);
+                    })
+                });
+            })
+            .unwrap();
+
+        workspace
+            .update(cx, |_, cx| {
+                editor1.update(cx, |editor, cx| {
+                    editor.change_selections(None, cx, |s| {
+                        s.select_display_ranges([DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0)])
+                    })
+                });
+            })
+            .unwrap();
+        workspace
+            .update(cx, |w, cx| w.go_back(w.active_pane().downgrade(), cx))
+            .unwrap()
+            .await
+            .unwrap();
+        assert_eq!(
+            active_location(&workspace, cx),
+            (file1.clone(), DisplayPoint::new(2, 0), 0.)
+        );
+        workspace
+            .update(cx, |w, cx| w.go_back(w.active_pane().downgrade(), cx))
+            .unwrap()
+            .await
+            .unwrap();
+        assert_eq!(
+            active_location(&workspace, cx),
+            (file1.clone(), DisplayPoint::new(3, 0), 0.)
+        );
+
+        fn active_location(
+            workspace: &WindowHandle<Workspace>,
+            cx: &mut TestAppContext,
+        ) -> (ProjectPath, DisplayPoint, f32) {
+            workspace
+                .update(cx, |workspace, cx| {
+                    let item = workspace.active_item(cx).unwrap();
+                    let editor = item.downcast::<Editor>().unwrap();
+                    let (selections, scroll_position) = editor.update(cx, |editor, cx| {
+                        (
+                            editor.selections.display_ranges(cx),
+                            editor.scroll_position(cx),
+                        )
+                    });
+                    (
+                        item.project_path(cx).unwrap(),
+                        selections[0].start,
+                        scroll_position.y,
+                    )
+                })
+                .unwrap()
+        }
+    }
+
+    #[gpui::test]
+    async fn test_reopening_closed_items(cx: &mut TestAppContext) {
+        let app_state = init_test(cx);
+        app_state
+            .fs
+            .as_fake()
+            .insert_tree(
+                "/root",
+                json!({
+                    "a": {
+                        "file1": "",
+                        "file2": "",
+                        "file3": "",
+                        "file4": "",
+                    },
+                }),
+            )
+            .await;
+
+        let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
+        let workspace = cx.add_window(|cx| Workspace::test_new(project, cx));
+        let pane = workspace
+            .read_with(cx, |workspace, _| workspace.active_pane().clone())
+            .unwrap();
+
+        let entries = cx.update(|cx| workspace.root(cx).unwrap().file_project_paths(cx));
+        let file1 = entries[0].clone();
+        let file2 = entries[1].clone();
+        let file3 = entries[2].clone();
+        let file4 = entries[3].clone();
+
+        let file1_item_id = workspace
+            .update(cx, |w, cx| w.open_path(file1.clone(), None, true, cx))
+            .unwrap()
+            .await
+            .unwrap()
+            .item_id();
+        let file2_item_id = workspace
+            .update(cx, |w, cx| w.open_path(file2.clone(), None, true, cx))
+            .unwrap()
+            .await
+            .unwrap()
+            .item_id();
+        let file3_item_id = workspace
+            .update(cx, |w, cx| w.open_path(file3.clone(), None, true, cx))
+            .unwrap()
+            .await
+            .unwrap()
+            .item_id();
+        let file4_item_id = workspace
+            .update(cx, |w, cx| w.open_path(file4.clone(), None, true, cx))
+            .unwrap()
+            .await
+            .unwrap()
+            .item_id();
+        assert_eq!(active_path(&workspace, cx), Some(file4.clone()));
+
+        // Close all the pane items in some arbitrary order.
+        workspace
+            .update(cx, |_, cx| {
+                pane.update(cx, |pane, cx| {
+                    pane.close_item_by_id(file1_item_id, SaveIntent::Close, cx)
+                })
+            })
+            .unwrap()
+            .await
+            .unwrap();
+        assert_eq!(active_path(&workspace, cx), Some(file4.clone()));
+
+        workspace
+            .update(cx, |_, cx| {
+                pane.update(cx, |pane, cx| {
+                    pane.close_item_by_id(file4_item_id, SaveIntent::Close, cx)
+                })
+            })
+            .unwrap()
+            .await
+            .unwrap();
+        assert_eq!(active_path(&workspace, cx), Some(file3.clone()));
+
+        workspace
+            .update(cx, |_, cx| {
+                pane.update(cx, |pane, cx| {
+                    pane.close_item_by_id(file2_item_id, SaveIntent::Close, cx)
+                })
+            })
+            .unwrap()
+            .await
+            .unwrap();
+        assert_eq!(active_path(&workspace, cx), Some(file3.clone()));
+        workspace
+            .update(cx, |_, cx| {
+                pane.update(cx, |pane, cx| {
+                    pane.close_item_by_id(file3_item_id, SaveIntent::Close, cx)
+                })
+            })
+            .unwrap()
+            .await
+            .unwrap();
+
+        assert_eq!(active_path(&workspace, cx), None);
+
+        // Reopen all the closed items, ensuring they are reopened in the same order
+        // in which they were closed.
+        workspace
+            .update(cx, Workspace::reopen_closed_item)
+            .unwrap()
+            .await
+            .unwrap();
+        assert_eq!(active_path(&workspace, cx), Some(file3.clone()));
+
+        workspace
+            .update(cx, Workspace::reopen_closed_item)
+            .unwrap()
+            .await
+            .unwrap();
+        assert_eq!(active_path(&workspace, cx), Some(file2.clone()));
+
+        workspace
+            .update(cx, Workspace::reopen_closed_item)
+            .unwrap()
+            .await
+            .unwrap();
+        assert_eq!(active_path(&workspace, cx), Some(file4.clone()));
+
+        workspace
+            .update(cx, Workspace::reopen_closed_item)
+            .unwrap()
+            .await
+            .unwrap();
+        assert_eq!(active_path(&workspace, cx), Some(file1.clone()));
+
+        // Reopening past the last closed item is a no-op.
+        workspace
+            .update(cx, Workspace::reopen_closed_item)
+            .unwrap()
+            .await
+            .unwrap();
+        assert_eq!(active_path(&workspace, cx), Some(file1.clone()));
+
+        // Reopening closed items doesn't interfere with navigation history.
+        workspace
+            .update(cx, |workspace, cx| {
+                workspace.go_back(workspace.active_pane().downgrade(), cx)
+            })
+            .unwrap()
+            .await
+            .unwrap();
+        assert_eq!(active_path(&workspace, cx), Some(file4.clone()));
+
+        workspace
+            .update(cx, |workspace, cx| {
+                workspace.go_back(workspace.active_pane().downgrade(), cx)
+            })
+            .unwrap()
+            .await
+            .unwrap();
+        assert_eq!(active_path(&workspace, cx), Some(file2.clone()));
+
+        workspace
+            .update(cx, |workspace, cx| {
+                workspace.go_back(workspace.active_pane().downgrade(), cx)
+            })
+            .unwrap()
+            .await
+            .unwrap();
+        assert_eq!(active_path(&workspace, cx), Some(file3.clone()));
+
+        workspace
+            .update(cx, |workspace, cx| {
+                workspace.go_back(workspace.active_pane().downgrade(), cx)
+            })
+            .unwrap()
+            .await
+            .unwrap();
+        assert_eq!(active_path(&workspace, cx), Some(file4.clone()));
+
+        workspace
+            .update(cx, |workspace, cx| {
+                workspace.go_back(workspace.active_pane().downgrade(), cx)
+            })
+            .unwrap()
+            .await
+            .unwrap();
+        assert_eq!(active_path(&workspace, cx), Some(file3.clone()));
+
+        workspace
+            .update(cx, |workspace, cx| {
+                workspace.go_back(workspace.active_pane().downgrade(), cx)
+            })
+            .unwrap()
+            .await
+            .unwrap();
+        assert_eq!(active_path(&workspace, cx), Some(file2.clone()));
+
+        workspace
+            .update(cx, |workspace, cx| {
+                workspace.go_back(workspace.active_pane().downgrade(), cx)
+            })
+            .unwrap()
+            .await
+            .unwrap();
+        assert_eq!(active_path(&workspace, cx), Some(file1.clone()));
+
+        workspace
+            .update(cx, |workspace, cx| {
+                workspace.go_back(workspace.active_pane().downgrade(), cx)
+            })
+            .unwrap()
+            .await
+            .unwrap();
+        assert_eq!(active_path(&workspace, cx), Some(file1.clone()));
+
+        fn active_path(
+            workspace: &WindowHandle<Workspace>,
+            cx: &TestAppContext,
+        ) -> Option<ProjectPath> {
+            workspace
+                .read_with(cx, |workspace, cx| {
+                    let item = workspace.active_item(cx)?;
+                    item.project_path(cx)
+                })
+                .unwrap()
+        }
+    }
+    fn init_keymap_test(cx: &mut TestAppContext) -> Arc<AppState> {
+        cx.update(|cx| {
+            let app_state = AppState::test(cx);
+
+            theme::init(theme::LoadThemes::JustBase, cx);
+            client::init(&app_state.client, cx);
+            language::init(cx);
+            workspace::init(app_state.clone(), cx);
+            welcome::init(cx);
+            Project::init_settings(cx);
+            app_state
+        })
+    }
+    #[gpui::test]
+    async fn test_base_keymap(cx: &mut gpui::TestAppContext) {
+        let executor = cx.executor();
+        let app_state = init_keymap_test(cx);
+        let project = Project::test(app_state.fs.clone(), [], cx).await;
+        let workspace = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
+
+        actions!(test1, [A, B]);
+        // From the Atom keymap
+        use workspace::ActivatePreviousPane;
+        // From the JetBrains keymap
+        use workspace::ActivatePrevItem;
+
+        app_state
+            .fs
+            .save(
+                "/settings.json".as_ref(),
+                &r#"
+                {
+                    "base_keymap": "Atom"
+                }
+                "#
+                .into(),
+                Default::default(),
+            )
+            .await
+            .unwrap();
+
+        app_state
+            .fs
+            .save(
+                "/keymap.json".as_ref(),
+                &r#"
+                [
+                    {
+                        "bindings": {
+                            "backspace": "test1::A"
+                        }
+                    }
+                ]
+                "#
+                .into(),
+                Default::default(),
+            )
+            .await
+            .unwrap();
+        executor.run_until_parked();
+        cx.update(|cx| {
+            let settings_rx = watch_config_file(
+                &executor,
+                app_state.fs.clone(),
+                PathBuf::from("/settings.json"),
+            );
+            let keymap_rx = watch_config_file(
+                &executor,
+                app_state.fs.clone(),
+                PathBuf::from("/keymap.json"),
+            );
+            handle_settings_file_changes(settings_rx, cx);
+            handle_keymap_file_changes(keymap_rx, cx);
+        });
+        workspace
+            .update(cx, |workspace, _| {
+                workspace.register_action(|_, _: &A, _cx| {});
+                workspace.register_action(|_, _: &B, _cx| {});
+                workspace.register_action(|_, _: &ActivatePreviousPane, _cx| {});
+                workspace.register_action(|_, _: &ActivatePrevItem, _cx| {});
+            })
+            .unwrap();
+        executor.run_until_parked();
+        // Test loading the keymap base at all
+        assert_key_bindings_for(
+            workspace.into(),
+            cx,
+            vec![("backspace", &A), ("k", &ActivatePreviousPane)],
+            line!(),
+        );
+
+        // Test modifying the users keymap, while retaining the base keymap
+        app_state
+            .fs
+            .save(
+                "/keymap.json".as_ref(),
+                &r#"
+                [
+                    {
+                        "bindings": {
+                            "backspace": "test1::B"
+                        }
+                    }
+                ]
+                "#
+                .into(),
+                Default::default(),
+            )
+            .await
+            .unwrap();
+
+        executor.run_until_parked();
+
+        assert_key_bindings_for(
+            workspace.into(),
+            cx,
+            vec![("backspace", &B), ("k", &ActivatePreviousPane)],
+            line!(),
+        );
+
+        // Test modifying the base, while retaining the users keymap
+        app_state
+            .fs
+            .save(
+                "/settings.json".as_ref(),
+                &r#"
+                {
+                    "base_keymap": "JetBrains"
+                }
+                "#
+                .into(),
+                Default::default(),
+            )
+            .await
+            .unwrap();
+
+        executor.run_until_parked();
+
+        assert_key_bindings_for(
+            workspace.into(),
+            cx,
+            vec![("backspace", &B), ("[", &ActivatePrevItem)],
+            line!(),
+        );
+    }
+
+    #[gpui::test]
+    async fn test_disabled_keymap_binding(cx: &mut gpui::TestAppContext) {
+        let executor = cx.executor();
+        let app_state = init_keymap_test(cx);
+        let project = Project::test(app_state.fs.clone(), [], cx).await;
+        let workspace = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
+
+        actions!(test2, [A, B]);
+        // From the Atom keymap
+        use workspace::ActivatePreviousPane;
+        // From the JetBrains keymap
+        use pane::ActivatePrevItem;
+        workspace
+            .update(cx, |workspace, _| {
+                workspace
+                    .register_action(|_, _: &A, _| {})
+                    .register_action(|_, _: &B, _| {});
+            })
+            .unwrap();
+        app_state
+            .fs
+            .save(
+                "/settings.json".as_ref(),
+                &r#"
+                {
+                    "base_keymap": "Atom"
+                }
+                "#
+                .into(),
+                Default::default(),
+            )
+            .await
+            .unwrap();
+        app_state
+            .fs
+            .save(
+                "/keymap.json".as_ref(),
+                &r#"
+                [
+                    {
+                        "bindings": {
+                            "backspace": "test2::A"
+                        }
+                    }
+                ]
+                "#
+                .into(),
+                Default::default(),
+            )
+            .await
+            .unwrap();
+
+        cx.update(|cx| {
+            let settings_rx = watch_config_file(
+                &executor,
+                app_state.fs.clone(),
+                PathBuf::from("/settings.json"),
+            );
+            let keymap_rx = watch_config_file(
+                &executor,
+                app_state.fs.clone(),
+                PathBuf::from("/keymap.json"),
+            );
+
+            handle_settings_file_changes(settings_rx, cx);
+            handle_keymap_file_changes(keymap_rx, cx);
+        });
+
+        cx.background_executor.run_until_parked();
+
+        cx.background_executor.run_until_parked();
+        // Test loading the keymap base at all
+        assert_key_bindings_for(
+            workspace.into(),
+            cx,
+            vec![("backspace", &A), ("k", &ActivatePreviousPane)],
+            line!(),
+        );
+
+        // Test disabling the key binding for the base keymap
+        app_state
+            .fs
+            .save(
+                "/keymap.json".as_ref(),
+                &r#"
+                [
+                    {
+                        "bindings": {
+                            "backspace": null
+                        }
+                    }
+                ]
+                "#
+                .into(),
+                Default::default(),
+            )
+            .await
+            .unwrap();
+
+        cx.background_executor.run_until_parked();
+
+        assert_key_bindings_for(
+            workspace.into(),
+            cx,
+            vec![("k", &ActivatePreviousPane)],
+            line!(),
+        );
+
+        // Test modifying the base, while retaining the users keymap
+        app_state
+            .fs
+            .save(
+                "/settings.json".as_ref(),
+                &r#"
+                {
+                    "base_keymap": "JetBrains"
+                }
+                "#
+                .into(),
+                Default::default(),
+            )
+            .await
+            .unwrap();
+
+        cx.background_executor.run_until_parked();
+
+        assert_key_bindings_for(
+            workspace.into(),
+            cx,
+            vec![("[", &ActivatePrevItem)],
+            line!(),
+        );
+    }
+
+    #[gpui::test]
+    fn test_bundled_settings_and_themes(cx: &mut AppContext) {
+        cx.text_system()
+            .add_fonts(&[
+                Assets
+                    .load("fonts/zed-sans/zed-sans-extended.ttf")
+                    .unwrap()
+                    .to_vec()
+                    .into(),
+                Assets
+                    .load("fonts/zed-mono/zed-mono-extended.ttf")
+                    .unwrap()
+                    .to_vec()
+                    .into(),
+                Assets
+                    .load("fonts/plex/IBMPlexSans-Regular.ttf")
+                    .unwrap()
+                    .to_vec()
+                    .into(),
+            ])
+            .unwrap();
+        let themes = ThemeRegistry::default();
+        let mut settings = SettingsStore::default();
+        settings
+            .set_default_settings(&settings::default_settings(), cx)
+            .unwrap();
+        cx.set_global(settings);
+        theme::init(theme::LoadThemes::JustBase, cx);
+
+        let mut has_default_theme = false;
+        for theme_name in themes.list(false).map(|meta| meta.name) {
+            let theme = themes.get(&theme_name).unwrap();
+            assert_eq!(theme.name, theme_name);
+            if theme.name == ThemeSettings::get(None, cx).active_theme.name {
+                has_default_theme = true;
+            }
+        }
+        assert!(has_default_theme);
+    }
+
+    #[gpui::test]
+    fn test_bundled_languages(cx: &mut AppContext) {
+        let settings = SettingsStore::test(cx);
+        cx.set_global(settings);
+        let mut languages = LanguageRegistry::test();
+        languages.set_executor(cx.background_executor().clone());
+        let languages = Arc::new(languages);
+        let node_runtime = node_runtime::FakeNodeRuntime::new();
+        languages::init(languages.clone(), node_runtime, cx);
+        for name in languages.language_names() {
+            languages.language_for_name(&name);
+        }
+        cx.background_executor().run_until_parked();
+    }
+
+    fn init_test(cx: &mut TestAppContext) -> Arc<AppState> {
+        cx.update(|cx| {
+            let mut app_state = AppState::test(cx);
+
+            let state = Arc::get_mut(&mut app_state).unwrap();
+
+            state.build_window_options = build_window_options;
+            theme::init(theme::LoadThemes::JustBase, cx);
+            audio::init((), cx);
+            channel::init(&app_state.client, app_state.user_store.clone(), cx);
+            call::init(app_state.client.clone(), app_state.user_store.clone(), cx);
+            notifications::init(app_state.client.clone(), app_state.user_store.clone(), cx);
+            workspace::init(app_state.clone(), cx);
+            Project::init_settings(cx);
+            language::init(cx);
+            editor::init(cx);
+            project_panel::init_settings(cx);
+            collab_ui::init(&app_state, cx);
+            project_panel::init((), cx);
+            terminal_view::init(cx);
+            assistant::init(cx);
+            initialize_workspace(app_state.clone(), cx);
+            app_state
+        })
+    }
+
+    fn rust_lang() -> Arc<language::Language> {
+        Arc::new(language::Language::new(
+            language::LanguageConfig {
+                name: "Rust".into(),
+                path_suffixes: vec!["rs".to_string()],
+                ..Default::default()
+            },
+            Some(tree_sitter_rust::language()),
+        ))
+    }
+    #[track_caller]
+    fn assert_key_bindings_for<'a>(
+        window: AnyWindowHandle,
+        cx: &TestAppContext,
+        actions: Vec<(&'static str, &'a dyn Action)>,
+        line: u32,
+    ) {
+        let available_actions = cx
+            .update(|cx| window.update(cx, |_, cx| cx.available_actions()))
+            .unwrap();
+        for (key, action) in actions {
+            let bindings = cx
+                .update(|cx| window.update(cx, |_, cx| cx.bindings_for_action(action)))
+                .unwrap();
+            // assert that...
+            assert!(
+                available_actions.iter().any(|bound_action| {
+                    // actions match...
+                    bound_action.partial_eq(action)
+                }),
+                "On {} Failed to find {}",
+                line,
+                action.name(),
+            );
+            assert!(
+                // and key strokes contain the given key
+                bindings
+                    .into_iter()
+                    .any(|binding| binding.keystrokes().iter().any(|k| k.key == key)),
+                "On {} Failed to find {} with key binding {}",
+                line,
+                action.name(),
+                key
+            );
+        }
+    }
+}