Merge branch 'main' into project-panel-with-new-mouse-events

Antonio Scandurra created

Change summary

.github/workflows/ci.yml                   |    3 
assets/keymaps/default.json                |    1 
assets/keymaps/vim.json                    |   33 
crates/collab/src/bin/seed.rs              |   19 
crates/collab/src/integration_tests.rs     | 5359 +++++++++++++++++++++
crates/collab/src/lib.rs                   |   69 
crates/collab/src/main.rs                  |   74 
crates/collab/src/rpc.rs                   | 5900 -----------------------
crates/editor/src/context_menu.rs          |  272 -
crates/editor/src/display_map.rs           |   17 
crates/editor/src/editor.rs                |  624 +-
crates/editor/src/element.rs               |   63 
crates/editor/src/items.rs                 |    6 
crates/editor/src/multi_buffer.rs          |    8 
crates/editor/src/selections_collection.rs |   91 
crates/editor/src/test.rs                  |  314 +
crates/gpui/src/app.rs                     |   72 
crates/gpui/src/platform.rs                |    1 
crates/gpui/src/platform/mac/platform.rs   |    5 
crates/gpui/src/platform/mac/window.rs     |    9 
crates/gpui/src/platform/test.rs           |   10 
crates/language/src/buffer.rs              |   27 
crates/language/src/proto.rs               |    3 
crates/language/src/tests.rs               |    4 
crates/project/src/project.rs              |   18 
crates/rpc/proto/zed.proto                 |    2 
crates/rpc/src/rpc.rs                      |    2 
crates/settings/src/settings.rs            |    4 
crates/text/src/selection.rs               |    5 
crates/util/src/test/marked_text.rs        |   96 
crates/vim/src/editor_events.rs            |   31 
crates/vim/src/motion.rs                   |    4 
crates/vim/src/normal.rs                   |  181 
crates/vim/src/normal/change.rs            |    4 
crates/vim/src/normal/delete.rs            |    3 
crates/vim/src/normal/yank.rs              |   26 
crates/vim/src/state.rs                    |   20 
crates/vim/src/utils.rs                    |   25 
crates/vim/src/vim.rs                      |   45 
crates/vim/src/vim_test_context.rs         |  274 
crates/vim/src/visual.rs                   |  355 +
crates/workspace/src/pane.rs               |   31 
crates/workspace/src/workspace.rs          |  223 
crates/zed/src/main.rs                     |    4 
crates/zed/src/menus.rs                    |   39 
crates/zed/src/zed.rs                      |   17 
46 files changed, 7,326 insertions(+), 7,067 deletions(-)

Detailed changes

.github/workflows/ci.yml 🔗

@@ -43,6 +43,9 @@ jobs:
 
       - name: Run tests
         run: cargo test --workspace --no-fail-fast
+    
+      - name: Build collab binaries
+        run: cargo build --bins --all-features 
 
   bundle:
     name: Bundle app

assets/keymaps/default.json 🔗

@@ -20,6 +20,7 @@
             "cmd-shift-S": "workspace::SaveAs",
             "cmd-=": "zed::IncreaseBufferFontSize",
             "cmd--": "zed::DecreaseBufferFontSize",
+            "cmd-0": "zed::ResetBufferFontSize",
             "cmd-,": "zed::OpenSettings",
             "cmd-q": "zed::Quit",
             "cmd-n": "workspace::NewFile",

assets/keymaps/vim.json 🔗

@@ -57,6 +57,10 @@
                 "Delete"
             ],
             "shift-D": "vim::DeleteToEndOfLine",
+            "y": [
+                "vim::PushOperator",
+                "Yank"
+            ],
             "i": [
                 "vim::SwitchMode",
                 "Insert"
@@ -71,8 +75,24 @@
             "shift-O": "vim::InsertLineAbove",
             "v": [
                 "vim::SwitchMode",
-                "Visual"
-            ]
+                {
+                    "Visual": {
+                        "line": false
+                    }
+                }
+            ],
+            "shift-V": [
+                "vim::SwitchMode",
+                {
+                    "Visual": {
+                        "line": true
+                    }
+                }
+            ],
+            "p": "vim::Paste",
+            "u": "editor::Undo",
+            "ctrl-r": "editor::Redo",
+            "ctrl-o": "pane::GoBack"
         }
     },
     {
@@ -104,12 +124,19 @@
             "d": "vim::CurrentLine"
         }
     },
+    {
+        "context": "Editor && vim_operator == y",
+        "bindings": {
+            "y": "vim::CurrentLine"
+        }
+    },
     {
         "context": "Editor && vim_mode == visual",
         "bindings": {
             "c": "vim::VisualChange",
             "d": "vim::VisualDelete",
-            "x": "vim::VisualDelete"
+            "x": "vim::VisualDelete",
+            "y": "vim::VisualYank"
         }
     },
     {

crates/collab/src/bin/seed.rs 🔗

@@ -1,4 +1,5 @@
 use clap::Parser;
+use collab::{Error, Result};
 use db::{Db, PostgresDb, UserId};
 use rand::prelude::*;
 use serde::Deserialize;
@@ -32,12 +33,12 @@ async fn main() {
         .expect("failed to connect to postgres database");
 
     let mut zed_users = vec![
-        "nathansobo".to_string(),
-        "maxbrunsfeld".to_string(),
-        "as-cii".to_string(),
-        "iamnbutler".to_string(),
-        "gibusu".to_string(),
-        "Kethku".to_string(),
+        ("nathansobo".to_string(), Some("nathan@zed.dev")),
+        ("maxbrunsfeld".to_string(), Some("max@zed.dev")),
+        ("as-cii".to_string(), Some("antonio@zed.dev")),
+        ("iamnbutler".to_string(), Some("nate@zed.dev")),
+        ("gibusu".to_string(), Some("greg@zed.dev")),
+        ("Kethku".to_string(), Some("keith@zed.dev")),
     ];
 
     if args.github_users {
@@ -61,7 +62,7 @@ async fn main() {
                 .json::<Vec<GitHubUser>>()
                 .await
                 .expect("failed to deserialize github user");
-            zed_users.extend(users.iter().map(|user| user.login.clone()));
+            zed_users.extend(users.iter().map(|user| (user.login.clone(), None)));
 
             if let Some(last_user) = users.last() {
                 last_user_id = Some(last_user.id);
@@ -72,7 +73,7 @@ async fn main() {
     }
 
     let mut zed_user_ids = Vec::<UserId>::new();
-    for zed_user in zed_users {
+    for (zed_user, email) in zed_users {
         if let Some(user) = db
             .get_user_by_github_login(&zed_user)
             .await
@@ -81,7 +82,7 @@ async fn main() {
             zed_user_ids.push(user.id);
         } else {
             zed_user_ids.push(
-                db.create_user(&zed_user, true)
+                db.create_user(&zed_user, email, true)
                     .await
                     .expect("failed to insert user"),
             );

crates/collab/src/integration_tests.rs 🔗

@@ -0,0 +1,5359 @@
+use crate::{
+    db::{tests::TestDb, UserId},
+    rpc::{Executor, Server, Store},
+    AppState,
+};
+use ::rpc::Peer;
+use anyhow::anyhow;
+use client::{
+    self, proto, test::FakeHttpClient, Channel, ChannelDetails, ChannelList, Client, Connection,
+    Credentials, EstablishConnectionError, UserStore, RECEIVE_TIMEOUT,
+};
+use collections::{BTreeMap, HashMap, HashSet};
+use editor::{
+    self, ConfirmCodeAction, ConfirmCompletion, ConfirmRename, Editor, Input, Redo, Rename,
+    ToOffset, ToggleCodeActions, Undo,
+};
+use futures::{channel::mpsc, Future, StreamExt as _};
+use gpui::{
+    executor::{self, Deterministic},
+    geometry::vector::vec2f,
+    ModelHandle, Task, TestAppContext, ViewHandle,
+};
+use language::{
+    range_to_lsp, tree_sitter_rust, Diagnostic, DiagnosticEntry, FakeLspAdapter, Language,
+    LanguageConfig, LanguageRegistry, OffsetRangeExt, Point, Rope,
+};
+use lsp::{self, FakeLanguageServer};
+use parking_lot::Mutex;
+use project::{
+    fs::{FakeFs, Fs as _},
+    search::SearchQuery,
+    worktree::WorktreeHandle,
+    DiagnosticSummary, Project, ProjectPath, WorktreeId,
+};
+use rand::prelude::*;
+use rpc::PeerId;
+use serde_json::json;
+use settings::Settings;
+use sqlx::types::time::OffsetDateTime;
+use std::{
+    cell::RefCell,
+    env,
+    ops::Deref,
+    path::{Path, PathBuf},
+    rc::Rc,
+    sync::{
+        atomic::{AtomicBool, Ordering::SeqCst},
+        Arc,
+    },
+    time::Duration,
+};
+use theme::ThemeRegistry;
+use tokio::sync::RwLockReadGuard;
+use workspace::{Item, SplitDirection, ToggleFollow, Workspace};
+
+#[ctor::ctor]
+fn init_logger() {
+    if std::env::var("RUST_LOG").is_ok() {
+        env_logger::init();
+    }
+}
+
+#[gpui::test(iterations = 10)]
+async fn test_share_project(
+    deterministic: Arc<Deterministic>,
+    cx_a: &mut TestAppContext,
+    cx_b: &mut TestAppContext,
+    cx_b2: &mut TestAppContext,
+) {
+    cx_a.foreground().forbid_parking();
+    let (window_b, _) = cx_b.add_window(|_| EmptyView);
+    let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
+    let mut client_a = server.create_client(cx_a, "user_a").await;
+    let mut client_b = server.create_client(cx_b, "user_b").await;
+    server
+        .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)])
+        .await;
+
+    let fs = FakeFs::new(cx_a.background());
+    fs.insert_tree(
+        "/a",
+        json!({
+            ".gitignore": "ignored-dir",
+            "a.txt": "a-contents",
+            "b.txt": "b-contents",
+            "ignored-dir": {
+                "c.txt": "",
+                "d.txt": "",
+            }
+        }),
+    )
+    .await;
+
+    let (project_a, worktree_id) = client_a.build_local_project(fs, "/a", cx_a).await;
+    let project_id = project_a.read_with(cx_a, |project, _| project.remote_id().unwrap());
+
+    // Join that project as client B
+    let client_b_peer_id = client_b.peer_id;
+    let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await;
+    let replica_id_b = project_b.read_with(cx_b, |project, _| {
+        assert_eq!(
+            project
+                .collaborators()
+                .get(&client_a.peer_id)
+                .unwrap()
+                .user
+                .github_login,
+            "user_a"
+        );
+        project.replica_id()
+    });
+
+    deterministic.run_until_parked();
+    project_a.read_with(cx_a, |project, _| {
+        let client_b_collaborator = project.collaborators().get(&client_b_peer_id).unwrap();
+        assert_eq!(client_b_collaborator.replica_id, replica_id_b);
+        assert_eq!(client_b_collaborator.user.github_login, "user_b");
+    });
+    project_b.read_with(cx_b, |project, cx| {
+        let worktree = project.worktrees(cx).next().unwrap().read(cx);
+        assert_eq!(
+            worktree.paths().map(AsRef::as_ref).collect::<Vec<_>>(),
+            [
+                Path::new(".gitignore"),
+                Path::new("a.txt"),
+                Path::new("b.txt"),
+                Path::new("ignored-dir"),
+                Path::new("ignored-dir/c.txt"),
+                Path::new("ignored-dir/d.txt"),
+            ]
+        );
+    });
+
+    // Open the same file as client B and client A.
+    let buffer_b = project_b
+        .update(cx_b, |p, cx| p.open_buffer((worktree_id, "b.txt"), cx))
+        .await
+        .unwrap();
+    buffer_b.read_with(cx_b, |buf, _| assert_eq!(buf.text(), "b-contents"));
+    project_a.read_with(cx_a, |project, cx| {
+        assert!(project.has_open_buffer((worktree_id, "b.txt"), cx))
+    });
+    let buffer_a = project_a
+        .update(cx_a, |p, cx| p.open_buffer((worktree_id, "b.txt"), cx))
+        .await
+        .unwrap();
+
+    let editor_b = cx_b.add_view(window_b, |cx| Editor::for_buffer(buffer_b, None, cx));
+
+    // TODO
+    // // Create a selection set as client B and see that selection set as client A.
+    // buffer_a
+    //     .condition(&cx_a, |buffer, _| buffer.selection_sets().count() == 1)
+    //     .await;
+
+    // Edit the buffer as client B and see that edit as client A.
+    editor_b.update(cx_b, |editor, cx| {
+        editor.handle_input(&Input("ok, ".into()), cx)
+    });
+    buffer_a
+        .condition(&cx_a, |buffer, _| buffer.text() == "ok, b-contents")
+        .await;
+
+    // TODO
+    // // Remove the selection set as client B, see those selections disappear as client A.
+    cx_b.update(move |_| drop(editor_b));
+    // buffer_a
+    //     .condition(&cx_a, |buffer, _| buffer.selection_sets().count() == 0)
+    //     .await;
+
+    // Client B can join again on a different window because they are already a participant.
+    let client_b2 = server.create_client(cx_b2, "user_b").await;
+    let project_b2 = Project::remote(
+        project_id,
+        client_b2.client.clone(),
+        client_b2.user_store.clone(),
+        client_b2.language_registry.clone(),
+        FakeFs::new(cx_b2.background()),
+        &mut cx_b2.to_async(),
+    )
+    .await
+    .unwrap();
+    deterministic.run_until_parked();
+    project_a.read_with(cx_a, |project, _| {
+        assert_eq!(project.collaborators().len(), 2);
+    });
+    project_b.read_with(cx_b, |project, _| {
+        assert_eq!(project.collaborators().len(), 2);
+    });
+    project_b2.read_with(cx_b2, |project, _| {
+        assert_eq!(project.collaborators().len(), 2);
+    });
+
+    // Dropping client B's first project removes only that from client A's collaborators.
+    cx_b.update(move |_| {
+        drop(client_b.project.take());
+        drop(project_b);
+    });
+    deterministic.run_until_parked();
+    project_a.read_with(cx_a, |project, _| {
+        assert_eq!(project.collaborators().len(), 1);
+    });
+    project_b2.read_with(cx_b2, |project, _| {
+        assert_eq!(project.collaborators().len(), 1);
+    });
+}
+
+#[gpui::test(iterations = 10)]
+async fn test_unshare_project(
+    deterministic: Arc<Deterministic>,
+    cx_a: &mut TestAppContext,
+    cx_b: &mut TestAppContext,
+) {
+    cx_a.foreground().forbid_parking();
+    let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
+    let mut client_a = server.create_client(cx_a, "user_a").await;
+    let mut client_b = server.create_client(cx_b, "user_b").await;
+    server
+        .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)])
+        .await;
+
+    let fs = FakeFs::new(cx_a.background());
+    fs.insert_tree(
+        "/a",
+        json!({
+            "a.txt": "a-contents",
+            "b.txt": "b-contents",
+        }),
+    )
+    .await;
+
+    let (project_a, worktree_id) = client_a.build_local_project(fs, "/a", cx_a).await;
+    let worktree_a = project_a.read_with(cx_a, |project, cx| project.worktrees(cx).next().unwrap());
+    let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await;
+    assert!(worktree_a.read_with(cx_a, |tree, _| tree.as_local().unwrap().is_shared()));
+
+    project_b
+        .update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx))
+        .await
+        .unwrap();
+
+    // When client B leaves the project, it gets automatically unshared.
+    cx_b.update(|_| {
+        drop(client_b.project.take());
+        drop(project_b);
+    });
+    deterministic.run_until_parked();
+    assert!(worktree_a.read_with(cx_a, |tree, _| !tree.as_local().unwrap().is_shared()));
+
+    // When client B joins again, the project gets re-shared.
+    let project_b2 = client_b.build_remote_project(&project_a, cx_a, cx_b).await;
+    assert!(worktree_a.read_with(cx_a, |tree, _| tree.as_local().unwrap().is_shared()));
+    project_b2
+        .update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx))
+        .await
+        .unwrap();
+
+    // When client A (the host) leaves, the project gets unshared and guests are notified.
+    cx_a.update(|_| {
+        drop(project_a);
+        client_a.project.take();
+    });
+    deterministic.run_until_parked();
+    project_b2.read_with(cx_b, |project, _| {
+        assert!(project.is_read_only());
+        assert!(project.collaborators().is_empty());
+    });
+}
+
+#[gpui::test(iterations = 10)]
+async fn test_host_disconnect(
+    deterministic: Arc<Deterministic>,
+    cx_a: &mut TestAppContext,
+    cx_b: &mut TestAppContext,
+    cx_c: &mut TestAppContext,
+) {
+    cx_a.foreground().forbid_parking();
+    let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
+    let mut client_a = server.create_client(cx_a, "user_a").await;
+    let mut client_b = server.create_client(cx_b, "user_b").await;
+    let client_c = server.create_client(cx_c, "user_c").await;
+    server
+        .make_contacts(vec![
+            (&client_a, cx_a),
+            (&client_b, cx_b),
+            (&client_c, cx_c),
+        ])
+        .await;
+
+    let fs = FakeFs::new(cx_a.background());
+    fs.insert_tree(
+        "/a",
+        json!({
+            "a.txt": "a-contents",
+            "b.txt": "b-contents",
+        }),
+    )
+    .await;
+
+    let (project_a, worktree_id) = client_a.build_local_project(fs, "/a", cx_a).await;
+    let worktree_a = project_a.read_with(cx_a, |project, cx| project.worktrees(cx).next().unwrap());
+    let project_id = project_a.read_with(cx_a, |project, _| project.remote_id().unwrap());
+
+    let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await;
+    assert!(worktree_a.read_with(cx_a, |tree, _| tree.as_local().unwrap().is_shared()));
+
+    project_b
+        .update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx))
+        .await
+        .unwrap();
+
+    // Request to join that project as client C
+    let project_c = cx_c.spawn(|mut cx| async move {
+        Project::remote(
+            project_id,
+            client_c.client.clone(),
+            client_c.user_store.clone(),
+            client_c.language_registry.clone(),
+            FakeFs::new(cx.background()),
+            &mut cx,
+        )
+        .await
+    });
+    deterministic.run_until_parked();
+
+    // Drop client A's connection. Collaborators should disappear and the project should not be shown as shared.
+    server.disconnect_client(client_a.current_user_id(cx_a));
+    cx_a.foreground().advance_clock(rpc::RECEIVE_TIMEOUT);
+    project_a
+        .condition(cx_a, |project, _| project.collaborators().is_empty())
+        .await;
+    project_a.read_with(cx_a, |project, _| assert!(!project.is_shared()));
+    project_b
+        .condition(cx_b, |project, _| project.is_read_only())
+        .await;
+    assert!(worktree_a.read_with(cx_a, |tree, _| !tree.as_local().unwrap().is_shared()));
+    cx_b.update(|_| {
+        drop(project_b);
+    });
+    assert!(matches!(
+        project_c.await.unwrap_err(),
+        project::JoinProjectError::HostWentOffline
+    ));
+
+    // Ensure guests can still join.
+    let project_b2 = client_b.build_remote_project(&project_a, cx_a, cx_b).await;
+    assert!(worktree_a.read_with(cx_a, |tree, _| tree.as_local().unwrap().is_shared()));
+    project_b2
+        .update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx))
+        .await
+        .unwrap();
+}
+
+#[gpui::test(iterations = 10)]
+async fn test_decline_join_request(
+    deterministic: Arc<Deterministic>,
+    cx_a: &mut TestAppContext,
+    cx_b: &mut TestAppContext,
+) {
+    cx_a.foreground().forbid_parking();
+    let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
+    let mut client_a = server.create_client(cx_a, "user_a").await;
+    let client_b = server.create_client(cx_b, "user_b").await;
+    server
+        .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)])
+        .await;
+
+    let fs = FakeFs::new(cx_a.background());
+    fs.insert_tree("/a", json!({})).await;
+
+    let (project_a, _) = client_a.build_local_project(fs, "/a", cx_a).await;
+    let project_id = project_a.read_with(cx_a, |project, _| project.remote_id().unwrap());
+
+    // Request to join that project as client B
+    let project_b = cx_b.spawn(|mut cx| {
+        let client = client_b.client.clone();
+        let user_store = client_b.user_store.clone();
+        let language_registry = client_b.language_registry.clone();
+        async move {
+            Project::remote(
+                project_id,
+                client,
+                user_store,
+                language_registry,
+                FakeFs::new(cx.background()),
+                &mut cx,
+            )
+            .await
+        }
+    });
+    deterministic.run_until_parked();
+    project_a.update(cx_a, |project, cx| {
+        project.respond_to_join_request(client_b.user_id().unwrap(), false, cx)
+    });
+    assert!(matches!(
+        project_b.await.unwrap_err(),
+        project::JoinProjectError::HostDeclined
+    ));
+
+    // Request to join the project again as client B
+    let project_b = cx_b.spawn(|mut cx| {
+        let client = client_b.client.clone();
+        let user_store = client_b.user_store.clone();
+        async move {
+            Project::remote(
+                project_id,
+                client,
+                user_store,
+                client_b.language_registry.clone(),
+                FakeFs::new(cx.background()),
+                &mut cx,
+            )
+            .await
+        }
+    });
+
+    // Close the project on the host
+    deterministic.run_until_parked();
+    cx_a.update(|_| {
+        drop(project_a);
+        client_a.project.take();
+    });
+    deterministic.run_until_parked();
+    assert!(matches!(
+        project_b.await.unwrap_err(),
+        project::JoinProjectError::HostClosedProject
+    ));
+}
+
+#[gpui::test(iterations = 10)]
+async fn test_cancel_join_request(
+    deterministic: Arc<Deterministic>,
+    cx_a: &mut TestAppContext,
+    cx_b: &mut TestAppContext,
+) {
+    cx_a.foreground().forbid_parking();
+    let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
+    let mut client_a = server.create_client(cx_a, "user_a").await;
+    let client_b = server.create_client(cx_b, "user_b").await;
+    server
+        .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)])
+        .await;
+
+    let fs = FakeFs::new(cx_a.background());
+    fs.insert_tree("/a", json!({})).await;
+
+    let (project_a, _) = client_a.build_local_project(fs, "/a", cx_a).await;
+    let project_id = project_a.read_with(cx_a, |project, _| project.remote_id().unwrap());
+
+    let user_b = client_a
+        .user_store
+        .update(cx_a, |store, cx| {
+            store.fetch_user(client_b.user_id().unwrap(), cx)
+        })
+        .await
+        .unwrap();
+
+    let project_a_events = Rc::new(RefCell::new(Vec::new()));
+    project_a.update(cx_a, {
+        let project_a_events = project_a_events.clone();
+        move |_, cx| {
+            cx.subscribe(&cx.handle(), move |_, _, event, _| {
+                project_a_events.borrow_mut().push(event.clone());
+            })
+            .detach();
+        }
+    });
+
+    // Request to join that project as client B
+    let project_b = cx_b.spawn(|mut cx| {
+        let client = client_b.client.clone();
+        let user_store = client_b.user_store.clone();
+        let language_registry = client_b.language_registry.clone();
+        async move {
+            Project::remote(
+                project_id,
+                client,
+                user_store,
+                language_registry.clone(),
+                FakeFs::new(cx.background()),
+                &mut cx,
+            )
+            .await
+        }
+    });
+    deterministic.run_until_parked();
+    assert_eq!(
+        &*project_a_events.borrow(),
+        &[project::Event::ContactRequestedJoin(user_b.clone())]
+    );
+    project_a_events.borrow_mut().clear();
+
+    // Cancel the join request by leaving the project
+    client_b
+        .client
+        .send(proto::LeaveProject { project_id })
+        .unwrap();
+    drop(project_b);
+
+    deterministic.run_until_parked();
+    assert_eq!(
+        &*project_a_events.borrow(),
+        &[project::Event::ContactCancelledJoinRequest(user_b.clone())]
+    );
+}
+
+#[gpui::test(iterations = 10)]
+async fn test_propagate_saves_and_fs_changes(
+    cx_a: &mut TestAppContext,
+    cx_b: &mut TestAppContext,
+    cx_c: &mut TestAppContext,
+) {
+    cx_a.foreground().forbid_parking();
+    let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
+    let mut client_a = server.create_client(cx_a, "user_a").await;
+    let mut client_b = server.create_client(cx_b, "user_b").await;
+    let mut client_c = server.create_client(cx_c, "user_c").await;
+    server
+        .make_contacts(vec![
+            (&client_a, cx_a),
+            (&client_b, cx_b),
+            (&client_c, cx_c),
+        ])
+        .await;
+
+    let fs = FakeFs::new(cx_a.background());
+    fs.insert_tree(
+        "/a",
+        json!({
+            "file1": "",
+            "file2": ""
+        }),
+    )
+    .await;
+
+    let (project_a, worktree_id) = client_a.build_local_project(fs.clone(), "/a", cx_a).await;
+    let worktree_a = project_a.read_with(cx_a, |p, cx| p.worktrees(cx).next().unwrap());
+
+    // Join that worktree as clients B and C.
+    let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await;
+    let project_c = client_c.build_remote_project(&project_a, cx_a, cx_c).await;
+    let worktree_b = project_b.read_with(cx_b, |p, cx| p.worktrees(cx).next().unwrap());
+    let worktree_c = project_c.read_with(cx_c, |p, cx| p.worktrees(cx).next().unwrap());
+
+    // Open and edit a buffer as both guests B and C.
+    let buffer_b = project_b
+        .update(cx_b, |p, cx| p.open_buffer((worktree_id, "file1"), cx))
+        .await
+        .unwrap();
+    let buffer_c = project_c
+        .update(cx_c, |p, cx| p.open_buffer((worktree_id, "file1"), cx))
+        .await
+        .unwrap();
+    buffer_b.update(cx_b, |buf, cx| buf.edit([(0..0, "i-am-b, ")], cx));
+    buffer_c.update(cx_c, |buf, cx| buf.edit([(0..0, "i-am-c, ")], cx));
+
+    // Open and edit that buffer as the host.
+    let buffer_a = project_a
+        .update(cx_a, |p, cx| p.open_buffer((worktree_id, "file1"), cx))
+        .await
+        .unwrap();
+
+    buffer_a
+        .condition(cx_a, |buf, _| buf.text() == "i-am-c, i-am-b, ")
+        .await;
+    buffer_a.update(cx_a, |buf, cx| {
+        buf.edit([(buf.len()..buf.len(), "i-am-a")], cx)
+    });
+
+    // Wait for edits to propagate
+    buffer_a
+        .condition(cx_a, |buf, _| buf.text() == "i-am-c, i-am-b, i-am-a")
+        .await;
+    buffer_b
+        .condition(cx_b, |buf, _| buf.text() == "i-am-c, i-am-b, i-am-a")
+        .await;
+    buffer_c
+        .condition(cx_c, |buf, _| buf.text() == "i-am-c, i-am-b, i-am-a")
+        .await;
+
+    // Edit the buffer as the host and concurrently save as guest B.
+    let save_b = buffer_b.update(cx_b, |buf, cx| buf.save(cx));
+    buffer_a.update(cx_a, |buf, cx| buf.edit([(0..0, "hi-a, ")], cx));
+    save_b.await.unwrap();
+    assert_eq!(
+        fs.load("/a/file1".as_ref()).await.unwrap(),
+        "hi-a, i-am-c, i-am-b, i-am-a"
+    );
+    buffer_a.read_with(cx_a, |buf, _| assert!(!buf.is_dirty()));
+    buffer_b.read_with(cx_b, |buf, _| assert!(!buf.is_dirty()));
+    buffer_c.condition(cx_c, |buf, _| !buf.is_dirty()).await;
+
+    worktree_a.flush_fs_events(cx_a).await;
+
+    // Make changes on host's file system, see those changes on guest worktrees.
+    fs.rename(
+        "/a/file1".as_ref(),
+        "/a/file1-renamed".as_ref(),
+        Default::default(),
+    )
+    .await
+    .unwrap();
+
+    fs.rename("/a/file2".as_ref(), "/a/file3".as_ref(), Default::default())
+        .await
+        .unwrap();
+    fs.insert_file(Path::new("/a/file4"), "4".into()).await;
+
+    worktree_a
+        .condition(&cx_a, |tree, _| {
+            tree.paths()
+                .map(|p| p.to_string_lossy())
+                .collect::<Vec<_>>()
+                == ["file1-renamed", "file3", "file4"]
+        })
+        .await;
+    worktree_b
+        .condition(&cx_b, |tree, _| {
+            tree.paths()
+                .map(|p| p.to_string_lossy())
+                .collect::<Vec<_>>()
+                == ["file1-renamed", "file3", "file4"]
+        })
+        .await;
+    worktree_c
+        .condition(&cx_c, |tree, _| {
+            tree.paths()
+                .map(|p| p.to_string_lossy())
+                .collect::<Vec<_>>()
+                == ["file1-renamed", "file3", "file4"]
+        })
+        .await;
+
+    // Ensure buffer files are updated as well.
+    buffer_a
+        .condition(&cx_a, |buf, _| {
+            buf.file().unwrap().path().to_str() == Some("file1-renamed")
+        })
+        .await;
+    buffer_b
+        .condition(&cx_b, |buf, _| {
+            buf.file().unwrap().path().to_str() == Some("file1-renamed")
+        })
+        .await;
+    buffer_c
+        .condition(&cx_c, |buf, _| {
+            buf.file().unwrap().path().to_str() == Some("file1-renamed")
+        })
+        .await;
+}
+
+#[gpui::test(iterations = 10)]
+async fn test_fs_operations(
+    executor: Arc<Deterministic>,
+    cx_a: &mut TestAppContext,
+    cx_b: &mut TestAppContext,
+) {
+    executor.forbid_parking();
+    let fs = FakeFs::new(cx_a.background());
+
+    // Connect to a server as 2 clients.
+    let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
+    let mut client_a = server.create_client(cx_a, "user_a").await;
+    let mut client_b = server.create_client(cx_b, "user_b").await;
+    server
+        .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)])
+        .await;
+
+    // Share a project as client A
+    fs.insert_tree(
+        "/dir",
+        json!({
+            "a.txt": "a-contents",
+            "b.txt": "b-contents",
+        }),
+    )
+    .await;
+
+    let (project_a, worktree_id) = client_a.build_local_project(fs, "/dir", cx_a).await;
+    let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await;
+
+    let worktree_a = project_a.read_with(cx_a, |project, cx| project.worktrees(cx).next().unwrap());
+    let worktree_b = project_b.read_with(cx_b, |project, cx| project.worktrees(cx).next().unwrap());
+
+    let entry = project_b
+        .update(cx_b, |project, cx| {
+            project
+                .create_entry((worktree_id, "c.txt"), false, cx)
+                .unwrap()
+        })
+        .await
+        .unwrap();
+    worktree_a.read_with(cx_a, |worktree, _| {
+        assert_eq!(
+            worktree
+                .paths()
+                .map(|p| p.to_string_lossy())
+                .collect::<Vec<_>>(),
+            ["a.txt", "b.txt", "c.txt"]
+        );
+    });
+    worktree_b.read_with(cx_b, |worktree, _| {
+        assert_eq!(
+            worktree
+                .paths()
+                .map(|p| p.to_string_lossy())
+                .collect::<Vec<_>>(),
+            ["a.txt", "b.txt", "c.txt"]
+        );
+    });
+
+    project_b
+        .update(cx_b, |project, cx| {
+            project.rename_entry(entry.id, Path::new("d.txt"), cx)
+        })
+        .unwrap()
+        .await
+        .unwrap();
+    worktree_a.read_with(cx_a, |worktree, _| {
+        assert_eq!(
+            worktree
+                .paths()
+                .map(|p| p.to_string_lossy())
+                .collect::<Vec<_>>(),
+            ["a.txt", "b.txt", "d.txt"]
+        );
+    });
+    worktree_b.read_with(cx_b, |worktree, _| {
+        assert_eq!(
+            worktree
+                .paths()
+                .map(|p| p.to_string_lossy())
+                .collect::<Vec<_>>(),
+            ["a.txt", "b.txt", "d.txt"]
+        );
+    });
+
+    let dir_entry = project_b
+        .update(cx_b, |project, cx| {
+            project
+                .create_entry((worktree_id, "DIR"), true, cx)
+                .unwrap()
+        })
+        .await
+        .unwrap();
+    worktree_a.read_with(cx_a, |worktree, _| {
+        assert_eq!(
+            worktree
+                .paths()
+                .map(|p| p.to_string_lossy())
+                .collect::<Vec<_>>(),
+            ["DIR", "a.txt", "b.txt", "d.txt"]
+        );
+    });
+    worktree_b.read_with(cx_b, |worktree, _| {
+        assert_eq!(
+            worktree
+                .paths()
+                .map(|p| p.to_string_lossy())
+                .collect::<Vec<_>>(),
+            ["DIR", "a.txt", "b.txt", "d.txt"]
+        );
+    });
+
+    project_b
+        .update(cx_b, |project, cx| {
+            project
+                .create_entry((worktree_id, "DIR/e.txt"), false, cx)
+                .unwrap()
+        })
+        .await
+        .unwrap();
+    project_b
+        .update(cx_b, |project, cx| {
+            project
+                .create_entry((worktree_id, "DIR/SUBDIR"), true, cx)
+                .unwrap()
+        })
+        .await
+        .unwrap();
+    project_b
+        .update(cx_b, |project, cx| {
+            project
+                .create_entry((worktree_id, "DIR/SUBDIR/f.txt"), false, cx)
+                .unwrap()
+        })
+        .await
+        .unwrap();
+    worktree_a.read_with(cx_a, |worktree, _| {
+        assert_eq!(
+            worktree
+                .paths()
+                .map(|p| p.to_string_lossy())
+                .collect::<Vec<_>>(),
+            [
+                "DIR",
+                "DIR/SUBDIR",
+                "DIR/SUBDIR/f.txt",
+                "DIR/e.txt",
+                "a.txt",
+                "b.txt",
+                "d.txt"
+            ]
+        );
+    });
+    worktree_b.read_with(cx_b, |worktree, _| {
+        assert_eq!(
+            worktree
+                .paths()
+                .map(|p| p.to_string_lossy())
+                .collect::<Vec<_>>(),
+            [
+                "DIR",
+                "DIR/SUBDIR",
+                "DIR/SUBDIR/f.txt",
+                "DIR/e.txt",
+                "a.txt",
+                "b.txt",
+                "d.txt"
+            ]
+        );
+    });
+
+    project_b
+        .update(cx_b, |project, cx| {
+            project
+                .copy_entry(dir_entry.id, Path::new("DIR2"), cx)
+                .unwrap()
+        })
+        .await
+        .unwrap();
+    worktree_a.read_with(cx_a, |worktree, _| {
+        assert_eq!(
+            worktree
+                .paths()
+                .map(|p| p.to_string_lossy())
+                .collect::<Vec<_>>(),
+            [
+                "DIR",
+                "DIR/SUBDIR",
+                "DIR/SUBDIR/f.txt",
+                "DIR/e.txt",
+                "DIR2",
+                "DIR2/SUBDIR",
+                "DIR2/SUBDIR/f.txt",
+                "DIR2/e.txt",
+                "a.txt",
+                "b.txt",
+                "d.txt"
+            ]
+        );
+    });
+    worktree_b.read_with(cx_b, |worktree, _| {
+        assert_eq!(
+            worktree
+                .paths()
+                .map(|p| p.to_string_lossy())
+                .collect::<Vec<_>>(),
+            [
+                "DIR",
+                "DIR/SUBDIR",
+                "DIR/SUBDIR/f.txt",
+                "DIR/e.txt",
+                "DIR2",
+                "DIR2/SUBDIR",
+                "DIR2/SUBDIR/f.txt",
+                "DIR2/e.txt",
+                "a.txt",
+                "b.txt",
+                "d.txt"
+            ]
+        );
+    });
+
+    project_b
+        .update(cx_b, |project, cx| {
+            project.delete_entry(dir_entry.id, cx).unwrap()
+        })
+        .await
+        .unwrap();
+    worktree_a.read_with(cx_a, |worktree, _| {
+        assert_eq!(
+            worktree
+                .paths()
+                .map(|p| p.to_string_lossy())
+                .collect::<Vec<_>>(),
+            [
+                "DIR2",
+                "DIR2/SUBDIR",
+                "DIR2/SUBDIR/f.txt",
+                "DIR2/e.txt",
+                "a.txt",
+                "b.txt",
+                "d.txt"
+            ]
+        );
+    });
+    worktree_b.read_with(cx_b, |worktree, _| {
+        assert_eq!(
+            worktree
+                .paths()
+                .map(|p| p.to_string_lossy())
+                .collect::<Vec<_>>(),
+            [
+                "DIR2",
+                "DIR2/SUBDIR",
+                "DIR2/SUBDIR/f.txt",
+                "DIR2/e.txt",
+                "a.txt",
+                "b.txt",
+                "d.txt"
+            ]
+        );
+    });
+
+    project_b
+        .update(cx_b, |project, cx| {
+            project.delete_entry(entry.id, cx).unwrap()
+        })
+        .await
+        .unwrap();
+    worktree_a.read_with(cx_a, |worktree, _| {
+        assert_eq!(
+            worktree
+                .paths()
+                .map(|p| p.to_string_lossy())
+                .collect::<Vec<_>>(),
+            [
+                "DIR2",
+                "DIR2/SUBDIR",
+                "DIR2/SUBDIR/f.txt",
+                "DIR2/e.txt",
+                "a.txt",
+                "b.txt"
+            ]
+        );
+    });
+    worktree_b.read_with(cx_b, |worktree, _| {
+        assert_eq!(
+            worktree
+                .paths()
+                .map(|p| p.to_string_lossy())
+                .collect::<Vec<_>>(),
+            [
+                "DIR2",
+                "DIR2/SUBDIR",
+                "DIR2/SUBDIR/f.txt",
+                "DIR2/e.txt",
+                "a.txt",
+                "b.txt"
+            ]
+        );
+    });
+}
+
+#[gpui::test(iterations = 10)]
+async fn test_buffer_conflict_after_save(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
+    cx_a.foreground().forbid_parking();
+    let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
+    let mut client_a = server.create_client(cx_a, "user_a").await;
+    let mut client_b = server.create_client(cx_b, "user_b").await;
+    server
+        .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)])
+        .await;
+
+    let fs = FakeFs::new(cx_a.background());
+    fs.insert_tree(
+        "/dir",
+        json!({
+            "a.txt": "a-contents",
+        }),
+    )
+    .await;
+
+    let (project_a, worktree_id) = client_a.build_local_project(fs, "/dir", cx_a).await;
+    let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await;
+
+    // Open a buffer as client B
+    let buffer_b = project_b
+        .update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx))
+        .await
+        .unwrap();
+
+    buffer_b.update(cx_b, |buf, cx| buf.edit([(0..0, "world ")], cx));
+    buffer_b.read_with(cx_b, |buf, _| {
+        assert!(buf.is_dirty());
+        assert!(!buf.has_conflict());
+    });
+
+    buffer_b.update(cx_b, |buf, cx| buf.save(cx)).await.unwrap();
+    buffer_b
+        .condition(&cx_b, |buffer_b, _| !buffer_b.is_dirty())
+        .await;
+    buffer_b.read_with(cx_b, |buf, _| {
+        assert!(!buf.has_conflict());
+    });
+
+    buffer_b.update(cx_b, |buf, cx| buf.edit([(0..0, "hello ")], cx));
+    buffer_b.read_with(cx_b, |buf, _| {
+        assert!(buf.is_dirty());
+        assert!(!buf.has_conflict());
+    });
+}
+
+#[gpui::test(iterations = 10)]
+async fn test_buffer_reloading(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
+    cx_a.foreground().forbid_parking();
+    let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
+    let mut client_a = server.create_client(cx_a, "user_a").await;
+    let mut client_b = server.create_client(cx_b, "user_b").await;
+    server
+        .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)])
+        .await;
+
+    let fs = FakeFs::new(cx_a.background());
+    fs.insert_tree(
+        "/dir",
+        json!({
+            "a.txt": "a-contents",
+        }),
+    )
+    .await;
+
+    let (project_a, worktree_id) = client_a.build_local_project(fs.clone(), "/dir", cx_a).await;
+    let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await;
+
+    // Open a buffer as client B
+    let buffer_b = project_b
+        .update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx))
+        .await
+        .unwrap();
+    buffer_b.read_with(cx_b, |buf, _| {
+        assert!(!buf.is_dirty());
+        assert!(!buf.has_conflict());
+    });
+
+    fs.save(Path::new("/dir/a.txt"), &"new contents".into())
+        .await
+        .unwrap();
+    buffer_b
+        .condition(&cx_b, |buf, _| {
+            buf.text() == "new contents" && !buf.is_dirty()
+        })
+        .await;
+    buffer_b.read_with(cx_b, |buf, _| {
+        assert!(!buf.has_conflict());
+    });
+}
+
+#[gpui::test(iterations = 10)]
+async fn test_editing_while_guest_opens_buffer(
+    cx_a: &mut TestAppContext,
+    cx_b: &mut TestAppContext,
+) {
+    cx_a.foreground().forbid_parking();
+    let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
+    let mut client_a = server.create_client(cx_a, "user_a").await;
+    let mut client_b = server.create_client(cx_b, "user_b").await;
+    server
+        .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)])
+        .await;
+
+    let fs = FakeFs::new(cx_a.background());
+    fs.insert_tree(
+        "/dir",
+        json!({
+            "a.txt": "a-contents",
+        }),
+    )
+    .await;
+
+    let (project_a, worktree_id) = client_a.build_local_project(fs, "/dir", cx_a).await;
+    let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await;
+
+    // Open a buffer as client A
+    let buffer_a = project_a
+        .update(cx_a, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx))
+        .await
+        .unwrap();
+
+    // Start opening the same buffer as client B
+    let buffer_b = cx_b
+        .background()
+        .spawn(project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx)));
+
+    // Edit the buffer as client A while client B is still opening it.
+    cx_b.background().simulate_random_delay().await;
+    buffer_a.update(cx_a, |buf, cx| buf.edit([(0..0, "X")], cx));
+    cx_b.background().simulate_random_delay().await;
+    buffer_a.update(cx_a, |buf, cx| buf.edit([(1..1, "Y")], cx));
+
+    let text = buffer_a.read_with(cx_a, |buf, _| buf.text());
+    let buffer_b = buffer_b.await.unwrap();
+    buffer_b.condition(&cx_b, |buf, _| buf.text() == text).await;
+}
+
+#[gpui::test(iterations = 10)]
+async fn test_leaving_worktree_while_opening_buffer(
+    cx_a: &mut TestAppContext,
+    cx_b: &mut TestAppContext,
+) {
+    cx_a.foreground().forbid_parking();
+    let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
+    let mut client_a = server.create_client(cx_a, "user_a").await;
+    let mut client_b = server.create_client(cx_b, "user_b").await;
+    server
+        .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)])
+        .await;
+
+    let fs = FakeFs::new(cx_a.background());
+    fs.insert_tree(
+        "/dir",
+        json!({
+            "a.txt": "a-contents",
+        }),
+    )
+    .await;
+
+    let (project_a, worktree_id) = client_a.build_local_project(fs, "/dir", cx_a).await;
+    let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await;
+
+    // See that a guest has joined as client A.
+    project_a
+        .condition(&cx_a, |p, _| p.collaborators().len() == 1)
+        .await;
+
+    // Begin opening a buffer as client B, but leave the project before the open completes.
+    let buffer_b = cx_b
+        .background()
+        .spawn(project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx)));
+    cx_b.update(|_| {
+        drop(client_b.project.take());
+        drop(project_b);
+    });
+    drop(buffer_b);
+
+    // See that the guest has left.
+    project_a
+        .condition(&cx_a, |p, _| p.collaborators().len() == 0)
+        .await;
+}
+
+#[gpui::test(iterations = 10)]
+async fn test_leaving_project(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
+    cx_a.foreground().forbid_parking();
+    let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
+    let mut client_a = server.create_client(cx_a, "user_a").await;
+    let mut client_b = server.create_client(cx_b, "user_b").await;
+    server
+        .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)])
+        .await;
+
+    let fs = FakeFs::new(cx_a.background());
+    fs.insert_tree(
+        "/a",
+        json!({
+            "a.txt": "a-contents",
+            "b.txt": "b-contents",
+        }),
+    )
+    .await;
+
+    let (project_a, _) = client_a.build_local_project(fs, "/a", cx_a).await;
+    let _project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await;
+
+    // Client A sees that a guest has joined.
+    project_a
+        .condition(cx_a, |p, _| p.collaborators().len() == 1)
+        .await;
+
+    // Drop client B's connection and ensure client A observes client B leaving the project.
+    client_b.disconnect(&cx_b.to_async()).unwrap();
+    project_a
+        .condition(cx_a, |p, _| p.collaborators().len() == 0)
+        .await;
+
+    // Rejoin the project as client B
+    let _project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await;
+
+    // Client A sees that a guest has re-joined.
+    project_a
+        .condition(cx_a, |p, _| p.collaborators().len() == 1)
+        .await;
+
+    // Simulate connection loss for client B and ensure client A observes client B leaving the project.
+    client_b.wait_for_current_user(cx_b).await;
+    server.disconnect_client(client_b.current_user_id(cx_b));
+    cx_a.foreground().advance_clock(rpc::RECEIVE_TIMEOUT);
+    project_a
+        .condition(cx_a, |p, _| p.collaborators().len() == 0)
+        .await;
+}
+
+#[gpui::test(iterations = 10)]
+async fn test_collaborating_with_diagnostics(
+    deterministic: Arc<Deterministic>,
+    cx_a: &mut TestAppContext,
+    cx_b: &mut TestAppContext,
+    cx_c: &mut TestAppContext,
+) {
+    deterministic.forbid_parking();
+    let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
+    let mut client_a = server.create_client(cx_a, "user_a").await;
+    let mut client_b = server.create_client(cx_b, "user_b").await;
+    let mut client_c = server.create_client(cx_c, "user_c").await;
+    server
+        .make_contacts(vec![
+            (&client_a, cx_a),
+            (&client_b, cx_b),
+            (&client_c, cx_c),
+        ])
+        .await;
+
+    // Set up a fake language server.
+    let mut language = Language::new(
+        LanguageConfig {
+            name: "Rust".into(),
+            path_suffixes: vec!["rs".to_string()],
+            ..Default::default()
+        },
+        Some(tree_sitter_rust::language()),
+    );
+    let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default());
+    client_a.language_registry.add(Arc::new(language));
+
+    // Connect to a server as 2 clients.
+
+    // Share a project as client A
+    let fs = FakeFs::new(cx_a.background());
+    fs.insert_tree(
+        "/a",
+        json!({
+            "a.rs": "let one = two",
+            "other.rs": "",
+        }),
+    )
+    .await;
+    let (project_a, worktree_id) = client_a.build_local_project(fs, "/a", cx_a).await;
+    let project_id = project_a.update(cx_a, |p, _| p.next_remote_id()).await;
+
+    // Cause the language server to start.
+    let _buffer = cx_a
+        .background()
+        .spawn(project_a.update(cx_a, |project, cx| {
+            project.open_buffer(
+                ProjectPath {
+                    worktree_id,
+                    path: Path::new("other.rs").into(),
+                },
+                cx,
+            )
+        }))
+        .await
+        .unwrap();
+
+    // Join the worktree as client B.
+    let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await;
+
+    // Simulate a language server reporting errors for a file.
+    let mut fake_language_server = fake_language_servers.next().await.unwrap();
+    fake_language_server
+        .receive_notification::<lsp::notification::DidOpenTextDocument>()
+        .await;
+    fake_language_server.notify::<lsp::notification::PublishDiagnostics>(
+        lsp::PublishDiagnosticsParams {
+            uri: lsp::Url::from_file_path("/a/a.rs").unwrap(),
+            version: None,
+            diagnostics: vec![lsp::Diagnostic {
+                severity: Some(lsp::DiagnosticSeverity::ERROR),
+                range: lsp::Range::new(lsp::Position::new(0, 4), lsp::Position::new(0, 7)),
+                message: "message 1".to_string(),
+                ..Default::default()
+            }],
+        },
+    );
+
+    // Wait for server to see the diagnostics update.
+    deterministic.run_until_parked();
+    {
+        let store = server.store.read().await;
+        let project = store.project(project_id).unwrap();
+        let worktree = project.worktrees.get(&worktree_id.to_proto()).unwrap();
+        assert!(!worktree.diagnostic_summaries.is_empty());
+    }
+
+    // Ensure client B observes the new diagnostics.
+    project_b.read_with(cx_b, |project, cx| {
+        assert_eq!(
+            project.diagnostic_summaries(cx).collect::<Vec<_>>(),
+            &[(
+                ProjectPath {
+                    worktree_id,
+                    path: Arc::from(Path::new("a.rs")),
+                },
+                DiagnosticSummary {
+                    error_count: 1,
+                    warning_count: 0,
+                    ..Default::default()
+                },
+            )]
+        )
+    });
+
+    // Join project as client C and observe the diagnostics.
+    let project_c = client_c.build_remote_project(&project_a, cx_a, cx_c).await;
+    project_c.read_with(cx_c, |project, cx| {
+        assert_eq!(
+            project.diagnostic_summaries(cx).collect::<Vec<_>>(),
+            &[(
+                ProjectPath {
+                    worktree_id,
+                    path: Arc::from(Path::new("a.rs")),
+                },
+                DiagnosticSummary {
+                    error_count: 1,
+                    warning_count: 0,
+                    ..Default::default()
+                },
+            )]
+        )
+    });
+
+    // Simulate a language server reporting more errors for a file.
+    fake_language_server.notify::<lsp::notification::PublishDiagnostics>(
+        lsp::PublishDiagnosticsParams {
+            uri: lsp::Url::from_file_path("/a/a.rs").unwrap(),
+            version: None,
+            diagnostics: vec![
+                lsp::Diagnostic {
+                    severity: Some(lsp::DiagnosticSeverity::ERROR),
+                    range: lsp::Range::new(lsp::Position::new(0, 4), lsp::Position::new(0, 7)),
+                    message: "message 1".to_string(),
+                    ..Default::default()
+                },
+                lsp::Diagnostic {
+                    severity: Some(lsp::DiagnosticSeverity::WARNING),
+                    range: lsp::Range::new(lsp::Position::new(0, 10), lsp::Position::new(0, 13)),
+                    message: "message 2".to_string(),
+                    ..Default::default()
+                },
+            ],
+        },
+    );
+
+    // Clients B and C get the updated summaries
+    deterministic.run_until_parked();
+    project_b.read_with(cx_b, |project, cx| {
+        assert_eq!(
+            project.diagnostic_summaries(cx).collect::<Vec<_>>(),
+            [(
+                ProjectPath {
+                    worktree_id,
+                    path: Arc::from(Path::new("a.rs")),
+                },
+                DiagnosticSummary {
+                    error_count: 1,
+                    warning_count: 1,
+                    ..Default::default()
+                },
+            )]
+        );
+    });
+    project_c.read_with(cx_c, |project, cx| {
+        assert_eq!(
+            project.diagnostic_summaries(cx).collect::<Vec<_>>(),
+            [(
+                ProjectPath {
+                    worktree_id,
+                    path: Arc::from(Path::new("a.rs")),
+                },
+                DiagnosticSummary {
+                    error_count: 1,
+                    warning_count: 1,
+                    ..Default::default()
+                },
+            )]
+        );
+    });
+
+    // Open the file with the errors on client B. They should be present.
+    let buffer_b = cx_b
+        .background()
+        .spawn(project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx)))
+        .await
+        .unwrap();
+
+    buffer_b.read_with(cx_b, |buffer, _| {
+        assert_eq!(
+            buffer
+                .snapshot()
+                .diagnostics_in_range::<_, Point>(0..buffer.len(), false)
+                .map(|entry| entry)
+                .collect::<Vec<_>>(),
+            &[
+                DiagnosticEntry {
+                    range: Point::new(0, 4)..Point::new(0, 7),
+                    diagnostic: Diagnostic {
+                        group_id: 0,
+                        message: "message 1".to_string(),
+                        severity: lsp::DiagnosticSeverity::ERROR,
+                        is_primary: true,
+                        ..Default::default()
+                    }
+                },
+                DiagnosticEntry {
+                    range: Point::new(0, 10)..Point::new(0, 13),
+                    diagnostic: Diagnostic {
+                        group_id: 1,
+                        severity: lsp::DiagnosticSeverity::WARNING,
+                        message: "message 2".to_string(),
+                        is_primary: true,
+                        ..Default::default()
+                    }
+                }
+            ]
+        );
+    });
+
+    // Simulate a language server reporting no errors for a file.
+    fake_language_server.notify::<lsp::notification::PublishDiagnostics>(
+        lsp::PublishDiagnosticsParams {
+            uri: lsp::Url::from_file_path("/a/a.rs").unwrap(),
+            version: None,
+            diagnostics: vec![],
+        },
+    );
+    deterministic.run_until_parked();
+    project_a.read_with(cx_a, |project, cx| {
+        assert_eq!(project.diagnostic_summaries(cx).collect::<Vec<_>>(), [])
+    });
+    project_b.read_with(cx_b, |project, cx| {
+        assert_eq!(project.diagnostic_summaries(cx).collect::<Vec<_>>(), [])
+    });
+    project_c.read_with(cx_c, |project, cx| {
+        assert_eq!(project.diagnostic_summaries(cx).collect::<Vec<_>>(), [])
+    });
+}
+
+#[gpui::test(iterations = 10)]
+async fn test_collaborating_with_completion(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
+    cx_a.foreground().forbid_parking();
+    let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
+    let mut client_a = server.create_client(cx_a, "user_a").await;
+    let mut client_b = server.create_client(cx_b, "user_b").await;
+    server
+        .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)])
+        .await;
+
+    // Set up a fake language server.
+    let mut language = Language::new(
+        LanguageConfig {
+            name: "Rust".into(),
+            path_suffixes: vec!["rs".to_string()],
+            ..Default::default()
+        },
+        Some(tree_sitter_rust::language()),
+    );
+    let mut fake_language_servers = language.set_fake_lsp_adapter(FakeLspAdapter {
+        capabilities: lsp::ServerCapabilities {
+            completion_provider: Some(lsp::CompletionOptions {
+                trigger_characters: Some(vec![".".to_string()]),
+                ..Default::default()
+            }),
+            ..Default::default()
+        },
+        ..Default::default()
+    });
+    client_a.language_registry.add(Arc::new(language));
+
+    let fs = FakeFs::new(cx_a.background());
+    fs.insert_tree(
+        "/a",
+        json!({
+            "main.rs": "fn main() { a }",
+            "other.rs": "",
+        }),
+    )
+    .await;
+
+    let (project_a, worktree_id) = client_a.build_local_project(fs, "/a", cx_a).await;
+    let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await;
+
+    // Open a file in an editor as the guest.
+    let buffer_b = project_b
+        .update(cx_b, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx))
+        .await
+        .unwrap();
+    let (window_b, _) = cx_b.add_window(|_| EmptyView);
+    let editor_b = cx_b.add_view(window_b, |cx| {
+        Editor::for_buffer(buffer_b.clone(), Some(project_b.clone()), cx)
+    });
+
+    let fake_language_server = fake_language_servers.next().await.unwrap();
+    buffer_b
+        .condition(&cx_b, |buffer, _| !buffer.completion_triggers().is_empty())
+        .await;
+
+    // Type a completion trigger character as the guest.
+    editor_b.update(cx_b, |editor, cx| {
+        editor.change_selections(None, cx, |s| s.select_ranges([13..13]));
+        editor.handle_input(&Input(".".into()), cx);
+        cx.focus(&editor_b);
+    });
+
+    // Receive a completion request as the host's language server.
+    // Return some completions from the host's language server.
+    cx_a.foreground().start_waiting();
+    fake_language_server
+        .handle_request::<lsp::request::Completion, _, _>(|params, _| async move {
+            assert_eq!(
+                params.text_document_position.text_document.uri,
+                lsp::Url::from_file_path("/a/main.rs").unwrap(),
+            );
+            assert_eq!(
+                params.text_document_position.position,
+                lsp::Position::new(0, 14),
+            );
+
+            Ok(Some(lsp::CompletionResponse::Array(vec![
+                lsp::CompletionItem {
+                    label: "first_method(…)".into(),
+                    detail: Some("fn(&mut self, B) -> C".into()),
+                    text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
+                        new_text: "first_method($1)".to_string(),
+                        range: lsp::Range::new(
+                            lsp::Position::new(0, 14),
+                            lsp::Position::new(0, 14),
+                        ),
+                    })),
+                    insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
+                    ..Default::default()
+                },
+                lsp::CompletionItem {
+                    label: "second_method(…)".into(),
+                    detail: Some("fn(&mut self, C) -> D<E>".into()),
+                    text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
+                        new_text: "second_method()".to_string(),
+                        range: lsp::Range::new(
+                            lsp::Position::new(0, 14),
+                            lsp::Position::new(0, 14),
+                        ),
+                    })),
+                    insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
+                    ..Default::default()
+                },
+            ])))
+        })
+        .next()
+        .await
+        .unwrap();
+    cx_a.foreground().finish_waiting();
+
+    // Open the buffer on the host.
+    let buffer_a = project_a
+        .update(cx_a, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx))
+        .await
+        .unwrap();
+    buffer_a
+        .condition(&cx_a, |buffer, _| buffer.text() == "fn main() { a. }")
+        .await;
+
+    // Confirm a completion on the guest.
+    editor_b
+        .condition(&cx_b, |editor, _| editor.context_menu_visible())
+        .await;
+    editor_b.update(cx_b, |editor, cx| {
+        editor.confirm_completion(&ConfirmCompletion { item_ix: Some(0) }, cx);
+        assert_eq!(editor.text(cx), "fn main() { a.first_method() }");
+    });
+
+    // Return a resolved completion from the host's language server.
+    // The resolved completion has an additional text edit.
+    fake_language_server.handle_request::<lsp::request::ResolveCompletionItem, _, _>(
+        |params, _| async move {
+            assert_eq!(params.label, "first_method(…)");
+            Ok(lsp::CompletionItem {
+                label: "first_method(…)".into(),
+                detail: Some("fn(&mut self, B) -> C".into()),
+                text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
+                    new_text: "first_method($1)".to_string(),
+                    range: lsp::Range::new(lsp::Position::new(0, 14), lsp::Position::new(0, 14)),
+                })),
+                additional_text_edits: Some(vec![lsp::TextEdit {
+                    new_text: "use d::SomeTrait;\n".to_string(),
+                    range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
+                }]),
+                insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
+                ..Default::default()
+            })
+        },
+    );
+
+    // The additional edit is applied.
+    buffer_a
+        .condition(&cx_a, |buffer, _| {
+            buffer.text() == "use d::SomeTrait;\nfn main() { a.first_method() }"
+        })
+        .await;
+    buffer_b
+        .condition(&cx_b, |buffer, _| {
+            buffer.text() == "use d::SomeTrait;\nfn main() { a.first_method() }"
+        })
+        .await;
+}
+
+#[gpui::test(iterations = 10)]
+async fn test_reloading_buffer_manually(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
+    cx_a.foreground().forbid_parking();
+    let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
+    let mut client_a = server.create_client(cx_a, "user_a").await;
+    let mut client_b = server.create_client(cx_b, "user_b").await;
+    server
+        .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)])
+        .await;
+
+    let fs = FakeFs::new(cx_a.background());
+    fs.insert_tree(
+        "/a",
+        json!({
+            "a.rs": "let one = 1;",
+        }),
+    )
+    .await;
+
+    let (project_a, worktree_id) = client_a.build_local_project(fs.clone(), "/a", cx_a).await;
+    let buffer_a = project_a
+        .update(cx_a, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx))
+        .await
+        .unwrap();
+
+    let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await;
+
+    let buffer_b = cx_b
+        .background()
+        .spawn(project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx)))
+        .await
+        .unwrap();
+    buffer_b.update(cx_b, |buffer, cx| {
+        buffer.edit([(4..7, "six")], cx);
+        buffer.edit([(10..11, "6")], cx);
+        assert_eq!(buffer.text(), "let six = 6;");
+        assert!(buffer.is_dirty());
+        assert!(!buffer.has_conflict());
+    });
+    buffer_a
+        .condition(cx_a, |buffer, _| buffer.text() == "let six = 6;")
+        .await;
+
+    fs.save(Path::new("/a/a.rs"), &Rope::from("let seven = 7;"))
+        .await
+        .unwrap();
+    buffer_a
+        .condition(cx_a, |buffer, _| buffer.has_conflict())
+        .await;
+    buffer_b
+        .condition(cx_b, |buffer, _| buffer.has_conflict())
+        .await;
+
+    project_b
+        .update(cx_b, |project, cx| {
+            project.reload_buffers(HashSet::from_iter([buffer_b.clone()]), true, cx)
+        })
+        .await
+        .unwrap();
+    buffer_a.read_with(cx_a, |buffer, _| {
+        assert_eq!(buffer.text(), "let seven = 7;");
+        assert!(!buffer.is_dirty());
+        assert!(!buffer.has_conflict());
+    });
+    buffer_b.read_with(cx_b, |buffer, _| {
+        assert_eq!(buffer.text(), "let seven = 7;");
+        assert!(!buffer.is_dirty());
+        assert!(!buffer.has_conflict());
+    });
+
+    buffer_a.update(cx_a, |buffer, cx| {
+        // Undoing on the host is a no-op when the reload was initiated by the guest.
+        buffer.undo(cx);
+        assert_eq!(buffer.text(), "let seven = 7;");
+        assert!(!buffer.is_dirty());
+        assert!(!buffer.has_conflict());
+    });
+    buffer_b.update(cx_b, |buffer, cx| {
+        // Undoing on the guest rolls back the buffer to before it was reloaded but the conflict gets cleared.
+        buffer.undo(cx);
+        assert_eq!(buffer.text(), "let six = 6;");
+        assert!(buffer.is_dirty());
+        assert!(!buffer.has_conflict());
+    });
+}
+
+#[gpui::test(iterations = 10)]
+async fn test_formatting_buffer(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
+    cx_a.foreground().forbid_parking();
+    let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
+    let mut client_a = server.create_client(cx_a, "user_a").await;
+    let mut client_b = server.create_client(cx_b, "user_b").await;
+    server
+        .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)])
+        .await;
+
+    // Set up a fake language server.
+    let mut language = Language::new(
+        LanguageConfig {
+            name: "Rust".into(),
+            path_suffixes: vec!["rs".to_string()],
+            ..Default::default()
+        },
+        Some(tree_sitter_rust::language()),
+    );
+    let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default());
+    client_a.language_registry.add(Arc::new(language));
+
+    let fs = FakeFs::new(cx_a.background());
+    fs.insert_tree(
+        "/a",
+        json!({
+            "a.rs": "let one = two",
+        }),
+    )
+    .await;
+
+    let (project_a, worktree_id) = client_a.build_local_project(fs, "/a", cx_a).await;
+    let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await;
+
+    let buffer_b = cx_b
+        .background()
+        .spawn(project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx)))
+        .await
+        .unwrap();
+
+    let fake_language_server = fake_language_servers.next().await.unwrap();
+    fake_language_server.handle_request::<lsp::request::Formatting, _, _>(|_, _| async move {
+        Ok(Some(vec![
+            lsp::TextEdit {
+                range: lsp::Range::new(lsp::Position::new(0, 4), lsp::Position::new(0, 4)),
+                new_text: "h".to_string(),
+            },
+            lsp::TextEdit {
+                range: lsp::Range::new(lsp::Position::new(0, 7), lsp::Position::new(0, 7)),
+                new_text: "y".to_string(),
+            },
+        ]))
+    });
+
+    project_b
+        .update(cx_b, |project, cx| {
+            project.format(HashSet::from_iter([buffer_b.clone()]), true, cx)
+        })
+        .await
+        .unwrap();
+    assert_eq!(
+        buffer_b.read_with(cx_b, |buffer, _| buffer.text()),
+        "let honey = two"
+    );
+}
+
+#[gpui::test(iterations = 10)]
+async fn test_definition(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
+    cx_a.foreground().forbid_parking();
+    let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
+    let mut client_a = server.create_client(cx_a, "user_a").await;
+    let mut client_b = server.create_client(cx_b, "user_b").await;
+    server
+        .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)])
+        .await;
+
+    let fs = FakeFs::new(cx_a.background());
+    fs.insert_tree(
+        "/root-1",
+        json!({
+            "a.rs": "const ONE: usize = b::TWO + b::THREE;",
+        }),
+    )
+    .await;
+    fs.insert_tree(
+        "/root-2",
+        json!({
+            "b.rs": "const TWO: usize = 2;\nconst THREE: usize = 3;",
+        }),
+    )
+    .await;
+
+    // Set up a fake language server.
+    let mut language = Language::new(
+        LanguageConfig {
+            name: "Rust".into(),
+            path_suffixes: vec!["rs".to_string()],
+            ..Default::default()
+        },
+        Some(tree_sitter_rust::language()),
+    );
+    let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default());
+    client_a.language_registry.add(Arc::new(language));
+
+    let (project_a, worktree_id) = client_a.build_local_project(fs, "/root-1", cx_a).await;
+    let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await;
+
+    // Open the file on client B.
+    let buffer_b = cx_b
+        .background()
+        .spawn(project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx)))
+        .await
+        .unwrap();
+
+    // Request the definition of a symbol as the guest.
+    let fake_language_server = fake_language_servers.next().await.unwrap();
+    fake_language_server.handle_request::<lsp::request::GotoDefinition, _, _>(|_, _| async move {
+        Ok(Some(lsp::GotoDefinitionResponse::Scalar(
+            lsp::Location::new(
+                lsp::Url::from_file_path("/root-2/b.rs").unwrap(),
+                lsp::Range::new(lsp::Position::new(0, 6), lsp::Position::new(0, 9)),
+            ),
+        )))
+    });
+
+    let definitions_1 = project_b
+        .update(cx_b, |p, cx| p.definition(&buffer_b, 23, cx))
+        .await
+        .unwrap();
+    cx_b.read(|cx| {
+        assert_eq!(definitions_1.len(), 1);
+        assert_eq!(project_b.read(cx).worktrees(cx).count(), 2);
+        let target_buffer = definitions_1[0].buffer.read(cx);
+        assert_eq!(
+            target_buffer.text(),
+            "const TWO: usize = 2;\nconst THREE: usize = 3;"
+        );
+        assert_eq!(
+            definitions_1[0].range.to_point(target_buffer),
+            Point::new(0, 6)..Point::new(0, 9)
+        );
+    });
+
+    // Try getting more definitions for the same buffer, ensuring the buffer gets reused from
+    // the previous call to `definition`.
+    fake_language_server.handle_request::<lsp::request::GotoDefinition, _, _>(|_, _| async move {
+        Ok(Some(lsp::GotoDefinitionResponse::Scalar(
+            lsp::Location::new(
+                lsp::Url::from_file_path("/root-2/b.rs").unwrap(),
+                lsp::Range::new(lsp::Position::new(1, 6), lsp::Position::new(1, 11)),
+            ),
+        )))
+    });
+
+    let definitions_2 = project_b
+        .update(cx_b, |p, cx| p.definition(&buffer_b, 33, cx))
+        .await
+        .unwrap();
+    cx_b.read(|cx| {
+        assert_eq!(definitions_2.len(), 1);
+        assert_eq!(project_b.read(cx).worktrees(cx).count(), 2);
+        let target_buffer = definitions_2[0].buffer.read(cx);
+        assert_eq!(
+            target_buffer.text(),
+            "const TWO: usize = 2;\nconst THREE: usize = 3;"
+        );
+        assert_eq!(
+            definitions_2[0].range.to_point(target_buffer),
+            Point::new(1, 6)..Point::new(1, 11)
+        );
+    });
+    assert_eq!(definitions_1[0].buffer, definitions_2[0].buffer);
+}
+
+#[gpui::test(iterations = 10)]
+async fn test_references(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
+    cx_a.foreground().forbid_parking();
+    let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
+    let mut client_a = server.create_client(cx_a, "user_a").await;
+    let mut client_b = server.create_client(cx_b, "user_b").await;
+    server
+        .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)])
+        .await;
+
+    let fs = FakeFs::new(cx_a.background());
+    fs.insert_tree(
+        "/root-1",
+        json!({
+            "one.rs": "const ONE: usize = 1;",
+            "two.rs": "const TWO: usize = one::ONE + one::ONE;",
+        }),
+    )
+    .await;
+    fs.insert_tree(
+        "/root-2",
+        json!({
+            "three.rs": "const THREE: usize = two::TWO + one::ONE;",
+        }),
+    )
+    .await;
+
+    // Set up a fake language server.
+    let mut language = Language::new(
+        LanguageConfig {
+            name: "Rust".into(),
+            path_suffixes: vec!["rs".to_string()],
+            ..Default::default()
+        },
+        Some(tree_sitter_rust::language()),
+    );
+    let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default());
+    client_a.language_registry.add(Arc::new(language));
+
+    let (project_a, worktree_id) = client_a.build_local_project(fs, "/root-1", cx_a).await;
+    let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await;
+
+    // Open the file on client B.
+    let buffer_b = cx_b
+        .background()
+        .spawn(project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "one.rs"), cx)))
+        .await
+        .unwrap();
+
+    // Request references to a symbol as the guest.
+    let fake_language_server = fake_language_servers.next().await.unwrap();
+    fake_language_server.handle_request::<lsp::request::References, _, _>(|params, _| async move {
+        assert_eq!(
+            params.text_document_position.text_document.uri.as_str(),
+            "file:///root-1/one.rs"
+        );
+        Ok(Some(vec![
+            lsp::Location {
+                uri: lsp::Url::from_file_path("/root-1/two.rs").unwrap(),
+                range: lsp::Range::new(lsp::Position::new(0, 24), lsp::Position::new(0, 27)),
+            },
+            lsp::Location {
+                uri: lsp::Url::from_file_path("/root-1/two.rs").unwrap(),
+                range: lsp::Range::new(lsp::Position::new(0, 35), lsp::Position::new(0, 38)),
+            },
+            lsp::Location {
+                uri: lsp::Url::from_file_path("/root-2/three.rs").unwrap(),
+                range: lsp::Range::new(lsp::Position::new(0, 37), lsp::Position::new(0, 40)),
+            },
+        ]))
+    });
+
+    let references = project_b
+        .update(cx_b, |p, cx| p.references(&buffer_b, 7, cx))
+        .await
+        .unwrap();
+    cx_b.read(|cx| {
+        assert_eq!(references.len(), 3);
+        assert_eq!(project_b.read(cx).worktrees(cx).count(), 2);
+
+        let two_buffer = references[0].buffer.read(cx);
+        let three_buffer = references[2].buffer.read(cx);
+        assert_eq!(
+            two_buffer.file().unwrap().path().as_ref(),
+            Path::new("two.rs")
+        );
+        assert_eq!(references[1].buffer, references[0].buffer);
+        assert_eq!(
+            three_buffer.file().unwrap().full_path(cx),
+            Path::new("three.rs")
+        );
+
+        assert_eq!(references[0].range.to_offset(&two_buffer), 24..27);
+        assert_eq!(references[1].range.to_offset(&two_buffer), 35..38);
+        assert_eq!(references[2].range.to_offset(&three_buffer), 37..40);
+    });
+}
+
+#[gpui::test(iterations = 10)]
+async fn test_project_search(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
+    cx_a.foreground().forbid_parking();
+    let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
+    let mut client_a = server.create_client(cx_a, "user_a").await;
+    let mut client_b = server.create_client(cx_b, "user_b").await;
+    server
+        .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)])
+        .await;
+
+    let fs = FakeFs::new(cx_a.background());
+    fs.insert_tree(
+        "/root-1",
+        json!({
+            "a": "hello world",
+            "b": "goodnight moon",
+            "c": "a world of goo",
+            "d": "world champion of clown world",
+        }),
+    )
+    .await;
+    fs.insert_tree(
+        "/root-2",
+        json!({
+            "e": "disney world is fun",
+        }),
+    )
+    .await;
+
+    let (project_a, _) = client_a.build_local_project(fs, "/root-1", cx_a).await;
+    let (worktree_2, _) = project_a
+        .update(cx_a, |p, cx| {
+            p.find_or_create_local_worktree("/root-2", true, cx)
+        })
+        .await
+        .unwrap();
+    worktree_2
+        .read_with(cx_a, |tree, _| tree.as_local().unwrap().scan_complete())
+        .await;
+
+    let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await;
+
+    // Perform a search as the guest.
+    let results = project_b
+        .update(cx_b, |project, cx| {
+            project.search(SearchQuery::text("world", false, false), cx)
+        })
+        .await
+        .unwrap();
+
+    let mut ranges_by_path = results
+        .into_iter()
+        .map(|(buffer, ranges)| {
+            buffer.read_with(cx_b, |buffer, cx| {
+                let path = buffer.file().unwrap().full_path(cx);
+                let offset_ranges = ranges
+                    .into_iter()
+                    .map(|range| range.to_offset(buffer))
+                    .collect::<Vec<_>>();
+                (path, offset_ranges)
+            })
+        })
+        .collect::<Vec<_>>();
+    ranges_by_path.sort_by_key(|(path, _)| path.clone());
+
+    assert_eq!(
+        ranges_by_path,
+        &[
+            (PathBuf::from("root-1/a"), vec![6..11]),
+            (PathBuf::from("root-1/c"), vec![2..7]),
+            (PathBuf::from("root-1/d"), vec![0..5, 24..29]),
+            (PathBuf::from("root-2/e"), vec![7..12]),
+        ]
+    );
+}
+
+#[gpui::test(iterations = 10)]
+async fn test_document_highlights(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
+    cx_a.foreground().forbid_parking();
+    let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
+    let mut client_a = server.create_client(cx_a, "user_a").await;
+    let mut client_b = server.create_client(cx_b, "user_b").await;
+    server
+        .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)])
+        .await;
+
+    let fs = FakeFs::new(cx_a.background());
+    fs.insert_tree(
+        "/root-1",
+        json!({
+            "main.rs": "fn double(number: i32) -> i32 { number + number }",
+        }),
+    )
+    .await;
+
+    // Set up a fake language server.
+    let mut language = Language::new(
+        LanguageConfig {
+            name: "Rust".into(),
+            path_suffixes: vec!["rs".to_string()],
+            ..Default::default()
+        },
+        Some(tree_sitter_rust::language()),
+    );
+    let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default());
+    client_a.language_registry.add(Arc::new(language));
+
+    let (project_a, worktree_id) = client_a.build_local_project(fs, "/root-1", cx_a).await;
+    let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await;
+
+    // Open the file on client B.
+    let buffer_b = cx_b
+        .background()
+        .spawn(project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx)))
+        .await
+        .unwrap();
+
+    // Request document highlights as the guest.
+    let fake_language_server = fake_language_servers.next().await.unwrap();
+    fake_language_server.handle_request::<lsp::request::DocumentHighlightRequest, _, _>(
+        |params, _| async move {
+            assert_eq!(
+                params
+                    .text_document_position_params
+                    .text_document
+                    .uri
+                    .as_str(),
+                "file:///root-1/main.rs"
+            );
+            assert_eq!(
+                params.text_document_position_params.position,
+                lsp::Position::new(0, 34)
+            );
+            Ok(Some(vec![
+                lsp::DocumentHighlight {
+                    kind: Some(lsp::DocumentHighlightKind::WRITE),
+                    range: lsp::Range::new(lsp::Position::new(0, 10), lsp::Position::new(0, 16)),
+                },
+                lsp::DocumentHighlight {
+                    kind: Some(lsp::DocumentHighlightKind::READ),
+                    range: lsp::Range::new(lsp::Position::new(0, 32), lsp::Position::new(0, 38)),
+                },
+                lsp::DocumentHighlight {
+                    kind: Some(lsp::DocumentHighlightKind::READ),
+                    range: lsp::Range::new(lsp::Position::new(0, 41), lsp::Position::new(0, 47)),
+                },
+            ]))
+        },
+    );
+
+    let highlights = project_b
+        .update(cx_b, |p, cx| p.document_highlights(&buffer_b, 34, cx))
+        .await
+        .unwrap();
+    buffer_b.read_with(cx_b, |buffer, _| {
+        let snapshot = buffer.snapshot();
+
+        let highlights = highlights
+            .into_iter()
+            .map(|highlight| (highlight.kind, highlight.range.to_offset(&snapshot)))
+            .collect::<Vec<_>>();
+        assert_eq!(
+            highlights,
+            &[
+                (lsp::DocumentHighlightKind::WRITE, 10..16),
+                (lsp::DocumentHighlightKind::READ, 32..38),
+                (lsp::DocumentHighlightKind::READ, 41..47)
+            ]
+        )
+    });
+}
+
+#[gpui::test(iterations = 10)]
+async fn test_project_symbols(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
+    cx_a.foreground().forbid_parking();
+    let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
+    let mut client_a = server.create_client(cx_a, "user_a").await;
+    let mut client_b = server.create_client(cx_b, "user_b").await;
+    server
+        .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)])
+        .await;
+
+    // Set up a fake language server.
+    let mut language = Language::new(
+        LanguageConfig {
+            name: "Rust".into(),
+            path_suffixes: vec!["rs".to_string()],
+            ..Default::default()
+        },
+        Some(tree_sitter_rust::language()),
+    );
+    let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default());
+    client_a.language_registry.add(Arc::new(language));
+
+    let fs = FakeFs::new(cx_a.background());
+    fs.insert_tree(
+        "/code",
+        json!({
+            "crate-1": {
+                "one.rs": "const ONE: usize = 1;",
+            },
+            "crate-2": {
+                "two.rs": "const TWO: usize = 2; const THREE: usize = 3;",
+            },
+            "private": {
+                "passwords.txt": "the-password",
+            }
+        }),
+    )
+    .await;
+
+    let (project_a, worktree_id) = client_a
+        .build_local_project(fs, "/code/crate-1", cx_a)
+        .await;
+    let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await;
+
+    // Cause the language server to start.
+    let _buffer = cx_b
+        .background()
+        .spawn(project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "one.rs"), cx)))
+        .await
+        .unwrap();
+
+    let fake_language_server = fake_language_servers.next().await.unwrap();
+    fake_language_server.handle_request::<lsp::request::WorkspaceSymbol, _, _>(|_, _| async move {
+        #[allow(deprecated)]
+        Ok(Some(vec![lsp::SymbolInformation {
+            name: "TWO".into(),
+            location: lsp::Location {
+                uri: lsp::Url::from_file_path("/code/crate-2/two.rs").unwrap(),
+                range: lsp::Range::new(lsp::Position::new(0, 6), lsp::Position::new(0, 9)),
+            },
+            kind: lsp::SymbolKind::CONSTANT,
+            tags: None,
+            container_name: None,
+            deprecated: None,
+        }]))
+    });
+
+    // Request the definition of a symbol as the guest.
+    let symbols = project_b
+        .update(cx_b, |p, cx| p.symbols("two", cx))
+        .await
+        .unwrap();
+    assert_eq!(symbols.len(), 1);
+    assert_eq!(symbols[0].name, "TWO");
+
+    // Open one of the returned symbols.
+    let buffer_b_2 = project_b
+        .update(cx_b, |project, cx| {
+            project.open_buffer_for_symbol(&symbols[0], cx)
+        })
+        .await
+        .unwrap();
+    buffer_b_2.read_with(cx_b, |buffer, _| {
+        assert_eq!(
+            buffer.file().unwrap().path().as_ref(),
+            Path::new("../crate-2/two.rs")
+        );
+    });
+
+    // Attempt to craft a symbol and violate host's privacy by opening an arbitrary file.
+    let mut fake_symbol = symbols[0].clone();
+    fake_symbol.path = Path::new("/code/secrets").into();
+    let error = project_b
+        .update(cx_b, |project, cx| {
+            project.open_buffer_for_symbol(&fake_symbol, cx)
+        })
+        .await
+        .unwrap_err();
+    assert!(error.to_string().contains("invalid symbol signature"));
+}
+
+#[gpui::test(iterations = 10)]
+async fn test_open_buffer_while_getting_definition_pointing_to_it(
+    cx_a: &mut TestAppContext,
+    cx_b: &mut TestAppContext,
+    mut rng: StdRng,
+) {
+    cx_a.foreground().forbid_parking();
+    let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
+    let mut client_a = server.create_client(cx_a, "user_a").await;
+    let mut client_b = server.create_client(cx_b, "user_b").await;
+    server
+        .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)])
+        .await;
+
+    // Set up a fake language server.
+    let mut language = Language::new(
+        LanguageConfig {
+            name: "Rust".into(),
+            path_suffixes: vec!["rs".to_string()],
+            ..Default::default()
+        },
+        Some(tree_sitter_rust::language()),
+    );
+    let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default());
+    client_a.language_registry.add(Arc::new(language));
+
+    let fs = FakeFs::new(cx_a.background());
+    fs.insert_tree(
+        "/root",
+        json!({
+            "a.rs": "const ONE: usize = b::TWO;",
+            "b.rs": "const TWO: usize = 2",
+        }),
+    )
+    .await;
+
+    let (project_a, worktree_id) = client_a.build_local_project(fs, "/root", cx_a).await;
+    let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await;
+
+    let buffer_b1 = cx_b
+        .background()
+        .spawn(project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx)))
+        .await
+        .unwrap();
+
+    let fake_language_server = fake_language_servers.next().await.unwrap();
+    fake_language_server.handle_request::<lsp::request::GotoDefinition, _, _>(|_, _| async move {
+        Ok(Some(lsp::GotoDefinitionResponse::Scalar(
+            lsp::Location::new(
+                lsp::Url::from_file_path("/root/b.rs").unwrap(),
+                lsp::Range::new(lsp::Position::new(0, 6), lsp::Position::new(0, 9)),
+            ),
+        )))
+    });
+
+    let definitions;
+    let buffer_b2;
+    if rng.gen() {
+        definitions = project_b.update(cx_b, |p, cx| p.definition(&buffer_b1, 23, cx));
+        buffer_b2 = project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "b.rs"), cx));
+    } else {
+        buffer_b2 = project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "b.rs"), cx));
+        definitions = project_b.update(cx_b, |p, cx| p.definition(&buffer_b1, 23, cx));
+    }
+
+    let buffer_b2 = buffer_b2.await.unwrap();
+    let definitions = definitions.await.unwrap();
+    assert_eq!(definitions.len(), 1);
+    assert_eq!(definitions[0].buffer, buffer_b2);
+}
+
+#[gpui::test(iterations = 10)]
+async fn test_collaborating_with_code_actions(
+    cx_a: &mut TestAppContext,
+    cx_b: &mut TestAppContext,
+) {
+    cx_a.foreground().forbid_parking();
+    cx_b.update(|cx| editor::init(cx));
+    let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
+    let mut client_a = server.create_client(cx_a, "user_a").await;
+    let mut client_b = server.create_client(cx_b, "user_b").await;
+    server
+        .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)])
+        .await;
+
+    // Set up a fake language server.
+    let mut language = Language::new(
+        LanguageConfig {
+            name: "Rust".into(),
+            path_suffixes: vec!["rs".to_string()],
+            ..Default::default()
+        },
+        Some(tree_sitter_rust::language()),
+    );
+    let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default());
+    client_a.language_registry.add(Arc::new(language));
+
+    let fs = FakeFs::new(cx_a.background());
+    fs.insert_tree(
+        "/a",
+        json!({
+            "main.rs": "mod other;\nfn main() { let foo = other::foo(); }",
+            "other.rs": "pub fn foo() -> usize { 4 }",
+        }),
+    )
+    .await;
+    let (project_a, worktree_id) = client_a.build_local_project(fs, "/a", cx_a).await;
+
+    // Join the project as client B.
+    let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await;
+    let (_window_b, workspace_b) = cx_b.add_window(|cx| Workspace::new(project_b.clone(), cx));
+    let editor_b = workspace_b
+        .update(cx_b, |workspace, cx| {
+            workspace.open_path((worktree_id, "main.rs"), true, cx)
+        })
+        .await
+        .unwrap()
+        .downcast::<Editor>()
+        .unwrap();
+
+    let mut fake_language_server = fake_language_servers.next().await.unwrap();
+    fake_language_server
+        .handle_request::<lsp::request::CodeActionRequest, _, _>(|params, _| async move {
+            assert_eq!(
+                params.text_document.uri,
+                lsp::Url::from_file_path("/a/main.rs").unwrap(),
+            );
+            assert_eq!(params.range.start, lsp::Position::new(0, 0));
+            assert_eq!(params.range.end, lsp::Position::new(0, 0));
+            Ok(None)
+        })
+        .next()
+        .await;
+
+    // Move cursor to a location that contains code actions.
+    editor_b.update(cx_b, |editor, cx| {
+        editor.change_selections(None, cx, |s| {
+            s.select_ranges([Point::new(1, 31)..Point::new(1, 31)])
+        });
+        cx.focus(&editor_b);
+    });
+
+    fake_language_server
+        .handle_request::<lsp::request::CodeActionRequest, _, _>(|params, _| async move {
+            assert_eq!(
+                params.text_document.uri,
+                lsp::Url::from_file_path("/a/main.rs").unwrap(),
+            );
+            assert_eq!(params.range.start, lsp::Position::new(1, 31));
+            assert_eq!(params.range.end, lsp::Position::new(1, 31));
+
+            Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
+                lsp::CodeAction {
+                    title: "Inline into all callers".to_string(),
+                    edit: Some(lsp::WorkspaceEdit {
+                        changes: Some(
+                            [
+                                (
+                                    lsp::Url::from_file_path("/a/main.rs").unwrap(),
+                                    vec![lsp::TextEdit::new(
+                                        lsp::Range::new(
+                                            lsp::Position::new(1, 22),
+                                            lsp::Position::new(1, 34),
+                                        ),
+                                        "4".to_string(),
+                                    )],
+                                ),
+                                (
+                                    lsp::Url::from_file_path("/a/other.rs").unwrap(),
+                                    vec![lsp::TextEdit::new(
+                                        lsp::Range::new(
+                                            lsp::Position::new(0, 0),
+                                            lsp::Position::new(0, 27),
+                                        ),
+                                        "".to_string(),
+                                    )],
+                                ),
+                            ]
+                            .into_iter()
+                            .collect(),
+                        ),
+                        ..Default::default()
+                    }),
+                    data: Some(json!({
+                        "codeActionParams": {
+                            "range": {
+                                "start": {"line": 1, "column": 31},
+                                "end": {"line": 1, "column": 31},
+                            }
+                        }
+                    })),
+                    ..Default::default()
+                },
+            )]))
+        })
+        .next()
+        .await;
+
+    // Toggle code actions and wait for them to display.
+    editor_b.update(cx_b, |editor, cx| {
+        editor.toggle_code_actions(
+            &ToggleCodeActions {
+                deployed_from_indicator: false,
+            },
+            cx,
+        );
+    });
+    editor_b
+        .condition(&cx_b, |editor, _| editor.context_menu_visible())
+        .await;
+
+    fake_language_server.remove_request_handler::<lsp::request::CodeActionRequest>();
+
+    // Confirming the code action will trigger a resolve request.
+    let confirm_action = workspace_b
+        .update(cx_b, |workspace, cx| {
+            Editor::confirm_code_action(workspace, &ConfirmCodeAction { item_ix: Some(0) }, cx)
+        })
+        .unwrap();
+    fake_language_server.handle_request::<lsp::request::CodeActionResolveRequest, _, _>(
+        |_, _| async move {
+            Ok(lsp::CodeAction {
+                title: "Inline into all callers".to_string(),
+                edit: Some(lsp::WorkspaceEdit {
+                    changes: Some(
+                        [
+                            (
+                                lsp::Url::from_file_path("/a/main.rs").unwrap(),
+                                vec![lsp::TextEdit::new(
+                                    lsp::Range::new(
+                                        lsp::Position::new(1, 22),
+                                        lsp::Position::new(1, 34),
+                                    ),
+                                    "4".to_string(),
+                                )],
+                            ),
+                            (
+                                lsp::Url::from_file_path("/a/other.rs").unwrap(),
+                                vec![lsp::TextEdit::new(
+                                    lsp::Range::new(
+                                        lsp::Position::new(0, 0),
+                                        lsp::Position::new(0, 27),
+                                    ),
+                                    "".to_string(),
+                                )],
+                            ),
+                        ]
+                        .into_iter()
+                        .collect(),
+                    ),
+                    ..Default::default()
+                }),
+                ..Default::default()
+            })
+        },
+    );
+
+    // After the action is confirmed, an editor containing both modified files is opened.
+    confirm_action.await.unwrap();
+    let code_action_editor = workspace_b.read_with(cx_b, |workspace, cx| {
+        workspace
+            .active_item(cx)
+            .unwrap()
+            .downcast::<Editor>()
+            .unwrap()
+    });
+    code_action_editor.update(cx_b, |editor, cx| {
+        assert_eq!(editor.text(cx), "mod other;\nfn main() { let foo = 4; }\n");
+        editor.undo(&Undo, cx);
+        assert_eq!(
+            editor.text(cx),
+            "mod other;\nfn main() { let foo = other::foo(); }\npub fn foo() -> usize { 4 }"
+        );
+        editor.redo(&Redo, cx);
+        assert_eq!(editor.text(cx), "mod other;\nfn main() { let foo = 4; }\n");
+    });
+}
+
+#[gpui::test(iterations = 10)]
+async fn test_collaborating_with_renames(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
+    cx_a.foreground().forbid_parking();
+    cx_b.update(|cx| editor::init(cx));
+    let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
+    let mut client_a = server.create_client(cx_a, "user_a").await;
+    let mut client_b = server.create_client(cx_b, "user_b").await;
+    server
+        .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)])
+        .await;
+
+    // Set up a fake language server.
+    let mut language = Language::new(
+        LanguageConfig {
+            name: "Rust".into(),
+            path_suffixes: vec!["rs".to_string()],
+            ..Default::default()
+        },
+        Some(tree_sitter_rust::language()),
+    );
+    let mut fake_language_servers = language.set_fake_lsp_adapter(FakeLspAdapter {
+        capabilities: lsp::ServerCapabilities {
+            rename_provider: Some(lsp::OneOf::Right(lsp::RenameOptions {
+                prepare_provider: Some(true),
+                work_done_progress_options: Default::default(),
+            })),
+            ..Default::default()
+        },
+        ..Default::default()
+    });
+    client_a.language_registry.add(Arc::new(language));
+
+    let fs = FakeFs::new(cx_a.background());
+    fs.insert_tree(
+        "/dir",
+        json!({
+            "one.rs": "const ONE: usize = 1;",
+            "two.rs": "const TWO: usize = one::ONE + one::ONE;"
+        }),
+    )
+    .await;
+
+    let (project_a, worktree_id) = client_a.build_local_project(fs, "/dir", cx_a).await;
+    let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await;
+
+    let (_window_b, workspace_b) = cx_b.add_window(|cx| Workspace::new(project_b.clone(), cx));
+    let editor_b = workspace_b
+        .update(cx_b, |workspace, cx| {
+            workspace.open_path((worktree_id, "one.rs"), true, cx)
+        })
+        .await
+        .unwrap()
+        .downcast::<Editor>()
+        .unwrap();
+    let fake_language_server = fake_language_servers.next().await.unwrap();
+
+    // Move cursor to a location that can be renamed.
+    let prepare_rename = editor_b.update(cx_b, |editor, cx| {
+        editor.change_selections(None, cx, |s| s.select_ranges([7..7]));
+        editor.rename(&Rename, cx).unwrap()
+    });
+
+    fake_language_server
+        .handle_request::<lsp::request::PrepareRenameRequest, _, _>(|params, _| async move {
+            assert_eq!(params.text_document.uri.as_str(), "file:///dir/one.rs");
+            assert_eq!(params.position, lsp::Position::new(0, 7));
+            Ok(Some(lsp::PrepareRenameResponse::Range(lsp::Range::new(
+                lsp::Position::new(0, 6),
+                lsp::Position::new(0, 9),
+            ))))
+        })
+        .next()
+        .await
+        .unwrap();
+    prepare_rename.await.unwrap();
+    editor_b.update(cx_b, |editor, cx| {
+        let rename = editor.pending_rename().unwrap();
+        let buffer = editor.buffer().read(cx).snapshot(cx);
+        assert_eq!(
+            rename.range.start.to_offset(&buffer)..rename.range.end.to_offset(&buffer),
+            6..9
+        );
+        rename.editor.update(cx, |rename_editor, cx| {
+            rename_editor.buffer().update(cx, |rename_buffer, cx| {
+                rename_buffer.edit([(0..3, "THREE")], cx);
+            });
+        });
+    });
+
+    let confirm_rename = workspace_b.update(cx_b, |workspace, cx| {
+        Editor::confirm_rename(workspace, &ConfirmRename, cx).unwrap()
+    });
+    fake_language_server
+        .handle_request::<lsp::request::Rename, _, _>(|params, _| async move {
+            assert_eq!(
+                params.text_document_position.text_document.uri.as_str(),
+                "file:///dir/one.rs"
+            );
+            assert_eq!(
+                params.text_document_position.position,
+                lsp::Position::new(0, 6)
+            );
+            assert_eq!(params.new_name, "THREE");
+            Ok(Some(lsp::WorkspaceEdit {
+                changes: Some(
+                    [
+                        (
+                            lsp::Url::from_file_path("/dir/one.rs").unwrap(),
+                            vec![lsp::TextEdit::new(
+                                lsp::Range::new(lsp::Position::new(0, 6), lsp::Position::new(0, 9)),
+                                "THREE".to_string(),
+                            )],
+                        ),
+                        (
+                            lsp::Url::from_file_path("/dir/two.rs").unwrap(),
+                            vec![
+                                lsp::TextEdit::new(
+                                    lsp::Range::new(
+                                        lsp::Position::new(0, 24),
+                                        lsp::Position::new(0, 27),
+                                    ),
+                                    "THREE".to_string(),
+                                ),
+                                lsp::TextEdit::new(
+                                    lsp::Range::new(
+                                        lsp::Position::new(0, 35),
+                                        lsp::Position::new(0, 38),
+                                    ),
+                                    "THREE".to_string(),
+                                ),
+                            ],
+                        ),
+                    ]
+                    .into_iter()
+                    .collect(),
+                ),
+                ..Default::default()
+            }))
+        })
+        .next()
+        .await
+        .unwrap();
+    confirm_rename.await.unwrap();
+
+    let rename_editor = workspace_b.read_with(cx_b, |workspace, cx| {
+        workspace
+            .active_item(cx)
+            .unwrap()
+            .downcast::<Editor>()
+            .unwrap()
+    });
+    rename_editor.update(cx_b, |editor, cx| {
+        assert_eq!(
+            editor.text(cx),
+            "const THREE: usize = 1;\nconst TWO: usize = one::THREE + one::THREE;"
+        );
+        editor.undo(&Undo, cx);
+        assert_eq!(
+            editor.text(cx),
+            "const ONE: usize = 1;\nconst TWO: usize = one::ONE + one::ONE;"
+        );
+        editor.redo(&Redo, cx);
+        assert_eq!(
+            editor.text(cx),
+            "const THREE: usize = 1;\nconst TWO: usize = one::THREE + one::THREE;"
+        );
+    });
+
+    // Ensure temporary rename edits cannot be undone/redone.
+    editor_b.update(cx_b, |editor, cx| {
+        editor.undo(&Undo, cx);
+        assert_eq!(editor.text(cx), "const ONE: usize = 1;");
+        editor.undo(&Undo, cx);
+        assert_eq!(editor.text(cx), "const ONE: usize = 1;");
+        editor.redo(&Redo, cx);
+        assert_eq!(editor.text(cx), "const THREE: usize = 1;");
+    })
+}
+
+#[gpui::test(iterations = 10)]
+async fn test_basic_chat(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
+    cx_a.foreground().forbid_parking();
+    let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
+    let client_a = server.create_client(cx_a, "user_a").await;
+    let client_b = server.create_client(cx_b, "user_b").await;
+
+    // Create an org that includes these 2 users.
+    let db = &server.app_state.db;
+    let org_id = db.create_org("Test Org", "test-org").await.unwrap();
+    db.add_org_member(org_id, client_a.current_user_id(&cx_a), false)
+        .await
+        .unwrap();
+    db.add_org_member(org_id, client_b.current_user_id(&cx_b), false)
+        .await
+        .unwrap();
+
+    // Create a channel that includes all the users.
+    let channel_id = db.create_org_channel(org_id, "test-channel").await.unwrap();
+    db.add_channel_member(channel_id, client_a.current_user_id(&cx_a), false)
+        .await
+        .unwrap();
+    db.add_channel_member(channel_id, client_b.current_user_id(&cx_b), false)
+        .await
+        .unwrap();
+    db.create_channel_message(
+        channel_id,
+        client_b.current_user_id(&cx_b),
+        "hello A, it's B.",
+        OffsetDateTime::now_utc(),
+        1,
+    )
+    .await
+    .unwrap();
+
+    let channels_a =
+        cx_a.add_model(|cx| ChannelList::new(client_a.user_store.clone(), client_a.clone(), cx));
+    channels_a
+        .condition(cx_a, |list, _| list.available_channels().is_some())
+        .await;
+    channels_a.read_with(cx_a, |list, _| {
+        assert_eq!(
+            list.available_channels().unwrap(),
+            &[ChannelDetails {
+                id: channel_id.to_proto(),
+                name: "test-channel".to_string()
+            }]
+        )
+    });
+    let channel_a = channels_a.update(cx_a, |this, cx| {
+        this.get_channel(channel_id.to_proto(), cx).unwrap()
+    });
+    channel_a.read_with(cx_a, |channel, _| assert!(channel.messages().is_empty()));
+    channel_a
+        .condition(&cx_a, |channel, _| {
+            channel_messages(channel)
+                == [("user_b".to_string(), "hello A, it's B.".to_string(), false)]
+        })
+        .await;
+
+    let channels_b =
+        cx_b.add_model(|cx| ChannelList::new(client_b.user_store.clone(), client_b.clone(), cx));
+    channels_b
+        .condition(cx_b, |list, _| list.available_channels().is_some())
+        .await;
+    channels_b.read_with(cx_b, |list, _| {
+        assert_eq!(
+            list.available_channels().unwrap(),
+            &[ChannelDetails {
+                id: channel_id.to_proto(),
+                name: "test-channel".to_string()
+            }]
+        )
+    });
+
+    let channel_b = channels_b.update(cx_b, |this, cx| {
+        this.get_channel(channel_id.to_proto(), cx).unwrap()
+    });
+    channel_b.read_with(cx_b, |channel, _| assert!(channel.messages().is_empty()));
+    channel_b
+        .condition(&cx_b, |channel, _| {
+            channel_messages(channel)
+                == [("user_b".to_string(), "hello A, it's B.".to_string(), false)]
+        })
+        .await;
+
+    channel_a
+        .update(cx_a, |channel, cx| {
+            channel
+                .send_message("oh, hi B.".to_string(), cx)
+                .unwrap()
+                .detach();
+            let task = channel.send_message("sup".to_string(), cx).unwrap();
+            assert_eq!(
+                channel_messages(channel),
+                &[
+                    ("user_b".to_string(), "hello A, it's B.".to_string(), false),
+                    ("user_a".to_string(), "oh, hi B.".to_string(), true),
+                    ("user_a".to_string(), "sup".to_string(), true)
+                ]
+            );
+            task
+        })
+        .await
+        .unwrap();
+
+    channel_b
+        .condition(&cx_b, |channel, _| {
+            channel_messages(channel)
+                == [
+                    ("user_b".to_string(), "hello A, it's B.".to_string(), false),
+                    ("user_a".to_string(), "oh, hi B.".to_string(), false),
+                    ("user_a".to_string(), "sup".to_string(), false),
+                ]
+        })
+        .await;
+
+    assert_eq!(
+        server
+            .state()
+            .await
+            .channel(channel_id)
+            .unwrap()
+            .connection_ids
+            .len(),
+        2
+    );
+    cx_b.update(|_| drop(channel_b));
+    server
+        .condition(|state| state.channel(channel_id).unwrap().connection_ids.len() == 1)
+        .await;
+
+    cx_a.update(|_| drop(channel_a));
+    server
+        .condition(|state| state.channel(channel_id).is_none())
+        .await;
+}
+
+#[gpui::test(iterations = 10)]
+async fn test_chat_message_validation(cx_a: &mut TestAppContext) {
+    cx_a.foreground().forbid_parking();
+    let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
+    let client_a = server.create_client(cx_a, "user_a").await;
+
+    let db = &server.app_state.db;
+    let org_id = db.create_org("Test Org", "test-org").await.unwrap();
+    let channel_id = db.create_org_channel(org_id, "test-channel").await.unwrap();
+    db.add_org_member(org_id, client_a.current_user_id(&cx_a), false)
+        .await
+        .unwrap();
+    db.add_channel_member(channel_id, client_a.current_user_id(&cx_a), false)
+        .await
+        .unwrap();
+
+    let channels_a =
+        cx_a.add_model(|cx| ChannelList::new(client_a.user_store.clone(), client_a.clone(), cx));
+    channels_a
+        .condition(cx_a, |list, _| list.available_channels().is_some())
+        .await;
+    let channel_a = channels_a.update(cx_a, |this, cx| {
+        this.get_channel(channel_id.to_proto(), cx).unwrap()
+    });
+
+    // Messages aren't allowed to be too long.
+    channel_a
+        .update(cx_a, |channel, cx| {
+            let long_body = "this is long.\n".repeat(1024);
+            channel.send_message(long_body, cx).unwrap()
+        })
+        .await
+        .unwrap_err();
+
+    // Messages aren't allowed to be blank.
+    channel_a.update(cx_a, |channel, cx| {
+        channel.send_message(String::new(), cx).unwrap_err()
+    });
+
+    // Leading and trailing whitespace are trimmed.
+    channel_a
+        .update(cx_a, |channel, cx| {
+            channel
+                .send_message("\n surrounded by whitespace  \n".to_string(), cx)
+                .unwrap()
+        })
+        .await
+        .unwrap();
+    assert_eq!(
+        db.get_channel_messages(channel_id, 10, None)
+            .await
+            .unwrap()
+            .iter()
+            .map(|m| &m.body)
+            .collect::<Vec<_>>(),
+        &["surrounded by whitespace"]
+    );
+}
+
+#[gpui::test(iterations = 10)]
+async fn test_chat_reconnection(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
+    cx_a.foreground().forbid_parking();
+    let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
+    let client_a = server.create_client(cx_a, "user_a").await;
+    let client_b = server.create_client(cx_b, "user_b").await;
+
+    let mut status_b = client_b.status();
+
+    // Create an org that includes these 2 users.
+    let db = &server.app_state.db;
+    let org_id = db.create_org("Test Org", "test-org").await.unwrap();
+    db.add_org_member(org_id, client_a.current_user_id(&cx_a), false)
+        .await
+        .unwrap();
+    db.add_org_member(org_id, client_b.current_user_id(&cx_b), false)
+        .await
+        .unwrap();
+
+    // Create a channel that includes all the users.
+    let channel_id = db.create_org_channel(org_id, "test-channel").await.unwrap();
+    db.add_channel_member(channel_id, client_a.current_user_id(&cx_a), false)
+        .await
+        .unwrap();
+    db.add_channel_member(channel_id, client_b.current_user_id(&cx_b), false)
+        .await
+        .unwrap();
+    db.create_channel_message(
+        channel_id,
+        client_b.current_user_id(&cx_b),
+        "hello A, it's B.",
+        OffsetDateTime::now_utc(),
+        2,
+    )
+    .await
+    .unwrap();
+
+    let channels_a =
+        cx_a.add_model(|cx| ChannelList::new(client_a.user_store.clone(), client_a.clone(), cx));
+    channels_a
+        .condition(cx_a, |list, _| list.available_channels().is_some())
+        .await;
+
+    channels_a.read_with(cx_a, |list, _| {
+        assert_eq!(
+            list.available_channels().unwrap(),
+            &[ChannelDetails {
+                id: channel_id.to_proto(),
+                name: "test-channel".to_string()
+            }]
+        )
+    });
+    let channel_a = channels_a.update(cx_a, |this, cx| {
+        this.get_channel(channel_id.to_proto(), cx).unwrap()
+    });
+    channel_a.read_with(cx_a, |channel, _| assert!(channel.messages().is_empty()));
+    channel_a
+        .condition(&cx_a, |channel, _| {
+            channel_messages(channel)
+                == [("user_b".to_string(), "hello A, it's B.".to_string(), false)]
+        })
+        .await;
+
+    let channels_b =
+        cx_b.add_model(|cx| ChannelList::new(client_b.user_store.clone(), client_b.clone(), cx));
+    channels_b
+        .condition(cx_b, |list, _| list.available_channels().is_some())
+        .await;
+    channels_b.read_with(cx_b, |list, _| {
+        assert_eq!(
+            list.available_channels().unwrap(),
+            &[ChannelDetails {
+                id: channel_id.to_proto(),
+                name: "test-channel".to_string()
+            }]
+        )
+    });
+
+    let channel_b = channels_b.update(cx_b, |this, cx| {
+        this.get_channel(channel_id.to_proto(), cx).unwrap()
+    });
+    channel_b.read_with(cx_b, |channel, _| assert!(channel.messages().is_empty()));
+    channel_b
+        .condition(&cx_b, |channel, _| {
+            channel_messages(channel)
+                == [("user_b".to_string(), "hello A, it's B.".to_string(), false)]
+        })
+        .await;
+
+    // Disconnect client B, ensuring we can still access its cached channel data.
+    server.forbid_connections();
+    server.disconnect_client(client_b.current_user_id(&cx_b));
+    cx_b.foreground().advance_clock(rpc::RECEIVE_TIMEOUT);
+    while !matches!(
+        status_b.next().await,
+        Some(client::Status::ReconnectionError { .. })
+    ) {}
+
+    channels_b.read_with(cx_b, |channels, _| {
+        assert_eq!(
+            channels.available_channels().unwrap(),
+            [ChannelDetails {
+                id: channel_id.to_proto(),
+                name: "test-channel".to_string()
+            }]
+        )
+    });
+    channel_b.read_with(cx_b, |channel, _| {
+        assert_eq!(
+            channel_messages(channel),
+            [("user_b".to_string(), "hello A, it's B.".to_string(), false)]
+        )
+    });
+
+    // Send a message from client B while it is disconnected.
+    channel_b
+        .update(cx_b, |channel, cx| {
+            let task = channel
+                .send_message("can you see this?".to_string(), cx)
+                .unwrap();
+            assert_eq!(
+                channel_messages(channel),
+                &[
+                    ("user_b".to_string(), "hello A, it's B.".to_string(), false),
+                    ("user_b".to_string(), "can you see this?".to_string(), true)
+                ]
+            );
+            task
+        })
+        .await
+        .unwrap_err();
+
+    // Send a message from client A while B is disconnected.
+    channel_a
+        .update(cx_a, |channel, cx| {
+            channel
+                .send_message("oh, hi B.".to_string(), cx)
+                .unwrap()
+                .detach();
+            let task = channel.send_message("sup".to_string(), cx).unwrap();
+            assert_eq!(
+                channel_messages(channel),
+                &[
+                    ("user_b".to_string(), "hello A, it's B.".to_string(), false),
+                    ("user_a".to_string(), "oh, hi B.".to_string(), true),
+                    ("user_a".to_string(), "sup".to_string(), true)
+                ]
+            );
+            task
+        })
+        .await
+        .unwrap();
+
+    // Give client B a chance to reconnect.
+    server.allow_connections();
+    cx_b.foreground().advance_clock(Duration::from_secs(10));
+
+    // Verify that B sees the new messages upon reconnection, as well as the message client B
+    // sent while offline.
+    channel_b
+        .condition(&cx_b, |channel, _| {
+            channel_messages(channel)
+                == [
+                    ("user_b".to_string(), "hello A, it's B.".to_string(), false),
+                    ("user_a".to_string(), "oh, hi B.".to_string(), false),
+                    ("user_a".to_string(), "sup".to_string(), false),
+                    ("user_b".to_string(), "can you see this?".to_string(), false),
+                ]
+        })
+        .await;
+
+    // Ensure client A and B can communicate normally after reconnection.
+    channel_a
+        .update(cx_a, |channel, cx| {
+            channel.send_message("you online?".to_string(), cx).unwrap()
+        })
+        .await
+        .unwrap();
+    channel_b
+        .condition(&cx_b, |channel, _| {
+            channel_messages(channel)
+                == [
+                    ("user_b".to_string(), "hello A, it's B.".to_string(), false),
+                    ("user_a".to_string(), "oh, hi B.".to_string(), false),
+                    ("user_a".to_string(), "sup".to_string(), false),
+                    ("user_b".to_string(), "can you see this?".to_string(), false),
+                    ("user_a".to_string(), "you online?".to_string(), false),
+                ]
+        })
+        .await;
+
+    channel_b
+        .update(cx_b, |channel, cx| {
+            channel.send_message("yep".to_string(), cx).unwrap()
+        })
+        .await
+        .unwrap();
+    channel_a
+        .condition(&cx_a, |channel, _| {
+            channel_messages(channel)
+                == [
+                    ("user_b".to_string(), "hello A, it's B.".to_string(), false),
+                    ("user_a".to_string(), "oh, hi B.".to_string(), false),
+                    ("user_a".to_string(), "sup".to_string(), false),
+                    ("user_b".to_string(), "can you see this?".to_string(), false),
+                    ("user_a".to_string(), "you online?".to_string(), false),
+                    ("user_b".to_string(), "yep".to_string(), false),
+                ]
+        })
+        .await;
+}
+
+#[gpui::test(iterations = 10)]
+async fn test_contacts(
+    deterministic: Arc<Deterministic>,
+    cx_a: &mut TestAppContext,
+    cx_b: &mut TestAppContext,
+    cx_c: &mut TestAppContext,
+) {
+    cx_a.foreground().forbid_parking();
+    let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
+    let mut client_a = server.create_client(cx_a, "user_a").await;
+    let mut client_b = server.create_client(cx_b, "user_b").await;
+    let client_c = server.create_client(cx_c, "user_c").await;
+    server
+        .make_contacts(vec![
+            (&client_a, cx_a),
+            (&client_b, cx_b),
+            (&client_c, cx_c),
+        ])
+        .await;
+
+    deterministic.run_until_parked();
+    for (client, cx) in [(&client_a, &cx_a), (&client_b, &cx_b), (&client_c, &cx_c)] {
+        client.user_store.read_with(*cx, |store, _| {
+            assert_eq!(
+                contacts(store),
+                [
+                    ("user_a", true, vec![]),
+                    ("user_b", true, vec![]),
+                    ("user_c", true, vec![])
+                ],
+                "{} has the wrong contacts",
+                client.username
+            )
+        });
+    }
+
+    // Share a project as client A.
+    let fs = FakeFs::new(cx_a.background());
+    fs.create_dir(Path::new("/a")).await.unwrap();
+    let (project_a, _) = client_a.build_local_project(fs, "/a", cx_a).await;
+
+    deterministic.run_until_parked();
+    for (client, cx) in [(&client_a, &cx_a), (&client_b, &cx_b), (&client_c, &cx_c)] {
+        client.user_store.read_with(*cx, |store, _| {
+            assert_eq!(
+                contacts(store),
+                [
+                    ("user_a", true, vec![("a", vec![])]),
+                    ("user_b", true, vec![]),
+                    ("user_c", true, vec![])
+                ],
+                "{} has the wrong contacts",
+                client.username
+            )
+        });
+    }
+
+    let _project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await;
+
+    deterministic.run_until_parked();
+    for (client, cx) in [(&client_a, &cx_a), (&client_b, &cx_b), (&client_c, &cx_c)] {
+        client.user_store.read_with(*cx, |store, _| {
+            assert_eq!(
+                contacts(store),
+                [
+                    ("user_a", true, vec![("a", vec!["user_b"])]),
+                    ("user_b", true, vec![]),
+                    ("user_c", true, vec![])
+                ],
+                "{} has the wrong contacts",
+                client.username
+            )
+        });
+    }
+
+    // Add a local project as client B
+    let fs = FakeFs::new(cx_b.background());
+    fs.create_dir(Path::new("/b")).await.unwrap();
+    let (_project_b, _) = client_b.build_local_project(fs, "/b", cx_a).await;
+
+    deterministic.run_until_parked();
+    for (client, cx) in [(&client_a, &cx_a), (&client_b, &cx_b), (&client_c, &cx_c)] {
+        client.user_store.read_with(*cx, |store, _| {
+            assert_eq!(
+                contacts(store),
+                [
+                    ("user_a", true, vec![("a", vec!["user_b"])]),
+                    ("user_b", true, vec![("b", vec![])]),
+                    ("user_c", true, vec![])
+                ],
+                "{} has the wrong contacts",
+                client.username
+            )
+        });
+    }
+
+    project_a
+        .condition(&cx_a, |project, _| {
+            project.collaborators().contains_key(&client_b.peer_id)
+        })
+        .await;
+
+    client_a.project.take();
+    cx_a.update(move |_| drop(project_a));
+    deterministic.run_until_parked();
+    for (client, cx) in [(&client_a, &cx_a), (&client_b, &cx_b), (&client_c, &cx_c)] {
+        client.user_store.read_with(*cx, |store, _| {
+            assert_eq!(
+                contacts(store),
+                [
+                    ("user_a", true, vec![]),
+                    ("user_b", true, vec![("b", vec![])]),
+                    ("user_c", true, vec![])
+                ],
+                "{} has the wrong contacts",
+                client.username
+            )
+        });
+    }
+
+    server.disconnect_client(client_c.current_user_id(cx_c));
+    server.forbid_connections();
+    deterministic.advance_clock(rpc::RECEIVE_TIMEOUT);
+    for (client, cx) in [(&client_a, &cx_a), (&client_b, &cx_b)] {
+        client.user_store.read_with(*cx, |store, _| {
+            assert_eq!(
+                contacts(store),
+                [
+                    ("user_a", true, vec![]),
+                    ("user_b", true, vec![("b", vec![])]),
+                    ("user_c", false, vec![])
+                ],
+                "{} has the wrong contacts",
+                client.username
+            )
+        });
+    }
+    client_c
+        .user_store
+        .read_with(cx_c, |store, _| assert_eq!(contacts(store), []));
+
+    server.allow_connections();
+    client_c
+        .authenticate_and_connect(false, &cx_c.to_async())
+        .await
+        .unwrap();
+
+    deterministic.run_until_parked();
+    for (client, cx) in [(&client_a, &cx_a), (&client_b, &cx_b), (&client_c, &cx_c)] {
+        client.user_store.read_with(*cx, |store, _| {
+            assert_eq!(
+                contacts(store),
+                [
+                    ("user_a", true, vec![]),
+                    ("user_b", true, vec![("b", vec![])]),
+                    ("user_c", true, vec![])
+                ],
+                "{} has the wrong contacts",
+                client.username
+            )
+        });
+    }
+
+    fn contacts(user_store: &UserStore) -> Vec<(&str, bool, Vec<(&str, Vec<&str>)>)> {
+        user_store
+            .contacts()
+            .iter()
+            .map(|contact| {
+                let projects = contact
+                    .projects
+                    .iter()
+                    .map(|p| {
+                        (
+                            p.worktree_root_names[0].as_str(),
+                            p.guests.iter().map(|p| p.github_login.as_str()).collect(),
+                        )
+                    })
+                    .collect();
+                (contact.user.github_login.as_str(), contact.online, projects)
+            })
+            .collect()
+    }
+}
+
+#[gpui::test(iterations = 10)]
+async fn test_contact_requests(
+    executor: Arc<Deterministic>,
+    cx_a: &mut TestAppContext,
+    cx_a2: &mut TestAppContext,
+    cx_b: &mut TestAppContext,
+    cx_b2: &mut TestAppContext,
+    cx_c: &mut TestAppContext,
+    cx_c2: &mut TestAppContext,
+) {
+    cx_a.foreground().forbid_parking();
+
+    // Connect to a server as 3 clients.
+    let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
+    let client_a = server.create_client(cx_a, "user_a").await;
+    let client_a2 = server.create_client(cx_a2, "user_a").await;
+    let client_b = server.create_client(cx_b, "user_b").await;
+    let client_b2 = server.create_client(cx_b2, "user_b").await;
+    let client_c = server.create_client(cx_c, "user_c").await;
+    let client_c2 = server.create_client(cx_c2, "user_c").await;
+
+    assert_eq!(client_a.user_id().unwrap(), client_a2.user_id().unwrap());
+    assert_eq!(client_b.user_id().unwrap(), client_b2.user_id().unwrap());
+    assert_eq!(client_c.user_id().unwrap(), client_c2.user_id().unwrap());
+
+    // User A and User C request that user B become their contact.
+    client_a
+        .user_store
+        .update(cx_a, |store, cx| {
+            store.request_contact(client_b.user_id().unwrap(), cx)
+        })
+        .await
+        .unwrap();
+    client_c
+        .user_store
+        .update(cx_c, |store, cx| {
+            store.request_contact(client_b.user_id().unwrap(), cx)
+        })
+        .await
+        .unwrap();
+    executor.run_until_parked();
+
+    // All users see the pending request appear in all their clients.
+    assert_eq!(
+        client_a.summarize_contacts(&cx_a).outgoing_requests,
+        &["user_b"]
+    );
+    assert_eq!(
+        client_a2.summarize_contacts(&cx_a2).outgoing_requests,
+        &["user_b"]
+    );
+    assert_eq!(
+        client_b.summarize_contacts(&cx_b).incoming_requests,
+        &["user_a", "user_c"]
+    );
+    assert_eq!(
+        client_b2.summarize_contacts(&cx_b2).incoming_requests,
+        &["user_a", "user_c"]
+    );
+    assert_eq!(
+        client_c.summarize_contacts(&cx_c).outgoing_requests,
+        &["user_b"]
+    );
+    assert_eq!(
+        client_c2.summarize_contacts(&cx_c2).outgoing_requests,
+        &["user_b"]
+    );
+
+    // Contact requests are present upon connecting (tested here via disconnect/reconnect)
+    disconnect_and_reconnect(&client_a, cx_a).await;
+    disconnect_and_reconnect(&client_b, cx_b).await;
+    disconnect_and_reconnect(&client_c, cx_c).await;
+    executor.run_until_parked();
+    assert_eq!(
+        client_a.summarize_contacts(&cx_a).outgoing_requests,
+        &["user_b"]
+    );
+    assert_eq!(
+        client_b.summarize_contacts(&cx_b).incoming_requests,
+        &["user_a", "user_c"]
+    );
+    assert_eq!(
+        client_c.summarize_contacts(&cx_c).outgoing_requests,
+        &["user_b"]
+    );
+
+    // User B accepts the request from user A.
+    client_b
+        .user_store
+        .update(cx_b, |store, cx| {
+            store.respond_to_contact_request(client_a.user_id().unwrap(), true, cx)
+        })
+        .await
+        .unwrap();
+
+    executor.run_until_parked();
+
+    // User B sees user A as their contact now in all client, and the incoming request from them is removed.
+    let contacts_b = client_b.summarize_contacts(&cx_b);
+    assert_eq!(contacts_b.current, &["user_a", "user_b"]);
+    assert_eq!(contacts_b.incoming_requests, &["user_c"]);
+    let contacts_b2 = client_b2.summarize_contacts(&cx_b2);
+    assert_eq!(contacts_b2.current, &["user_a", "user_b"]);
+    assert_eq!(contacts_b2.incoming_requests, &["user_c"]);
+
+    // User A sees user B as their contact now in all clients, and the outgoing request to them is removed.
+    let contacts_a = client_a.summarize_contacts(&cx_a);
+    assert_eq!(contacts_a.current, &["user_a", "user_b"]);
+    assert!(contacts_a.outgoing_requests.is_empty());
+    let contacts_a2 = client_a2.summarize_contacts(&cx_a2);
+    assert_eq!(contacts_a2.current, &["user_a", "user_b"]);
+    assert!(contacts_a2.outgoing_requests.is_empty());
+
+    // Contacts are present upon connecting (tested here via disconnect/reconnect)
+    disconnect_and_reconnect(&client_a, cx_a).await;
+    disconnect_and_reconnect(&client_b, cx_b).await;
+    disconnect_and_reconnect(&client_c, cx_c).await;
+    executor.run_until_parked();
+    assert_eq!(
+        client_a.summarize_contacts(&cx_a).current,
+        &["user_a", "user_b"]
+    );
+    assert_eq!(
+        client_b.summarize_contacts(&cx_b).current,
+        &["user_a", "user_b"]
+    );
+    assert_eq!(
+        client_b.summarize_contacts(&cx_b).incoming_requests,
+        &["user_c"]
+    );
+    assert_eq!(client_c.summarize_contacts(&cx_c).current, &["user_c"]);
+    assert_eq!(
+        client_c.summarize_contacts(&cx_c).outgoing_requests,
+        &["user_b"]
+    );
+
+    // User B rejects the request from user C.
+    client_b
+        .user_store
+        .update(cx_b, |store, cx| {
+            store.respond_to_contact_request(client_c.user_id().unwrap(), false, cx)
+        })
+        .await
+        .unwrap();
+
+    executor.run_until_parked();
+
+    // User B doesn't see user C as their contact, and the incoming request from them is removed.
+    let contacts_b = client_b.summarize_contacts(&cx_b);
+    assert_eq!(contacts_b.current, &["user_a", "user_b"]);
+    assert!(contacts_b.incoming_requests.is_empty());
+    let contacts_b2 = client_b2.summarize_contacts(&cx_b2);
+    assert_eq!(contacts_b2.current, &["user_a", "user_b"]);
+    assert!(contacts_b2.incoming_requests.is_empty());
+
+    // User C doesn't see user B as their contact, and the outgoing request to them is removed.
+    let contacts_c = client_c.summarize_contacts(&cx_c);
+    assert_eq!(contacts_c.current, &["user_c"]);
+    assert!(contacts_c.outgoing_requests.is_empty());
+    let contacts_c2 = client_c2.summarize_contacts(&cx_c2);
+    assert_eq!(contacts_c2.current, &["user_c"]);
+    assert!(contacts_c2.outgoing_requests.is_empty());
+
+    // Incoming/outgoing requests are not present upon connecting (tested here via disconnect/reconnect)
+    disconnect_and_reconnect(&client_a, cx_a).await;
+    disconnect_and_reconnect(&client_b, cx_b).await;
+    disconnect_and_reconnect(&client_c, cx_c).await;
+    executor.run_until_parked();
+    assert_eq!(
+        client_a.summarize_contacts(&cx_a).current,
+        &["user_a", "user_b"]
+    );
+    assert_eq!(
+        client_b.summarize_contacts(&cx_b).current,
+        &["user_a", "user_b"]
+    );
+    assert!(client_b
+        .summarize_contacts(&cx_b)
+        .incoming_requests
+        .is_empty());
+    assert_eq!(client_c.summarize_contacts(&cx_c).current, &["user_c"]);
+    assert!(client_c
+        .summarize_contacts(&cx_c)
+        .outgoing_requests
+        .is_empty());
+
+    async fn disconnect_and_reconnect(client: &TestClient, cx: &mut TestAppContext) {
+        client.disconnect(&cx.to_async()).unwrap();
+        client.clear_contacts(cx).await;
+        client
+            .authenticate_and_connect(false, &cx.to_async())
+            .await
+            .unwrap();
+    }
+}
+
+#[gpui::test(iterations = 10)]
+async fn test_following(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
+    cx_a.foreground().forbid_parking();
+    let fs = FakeFs::new(cx_a.background());
+
+    // 2 clients connect to a server.
+    let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
+    let mut client_a = server.create_client(cx_a, "user_a").await;
+    let mut client_b = server.create_client(cx_b, "user_b").await;
+    server
+        .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)])
+        .await;
+    cx_a.update(editor::init);
+    cx_b.update(editor::init);
+
+    // Client A shares a project.
+    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(fs.clone(), "/a", cx_a).await;
+
+    // Client B joins the project.
+    let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await;
+
+    // Client A opens some editors.
+    let workspace_a = client_a.build_workspace(&project_a, cx_a);
+    let pane_a = workspace_a.read_with(cx_a, |workspace, _| workspace.active_pane().clone());
+    let editor_a1 = workspace_a
+        .update(cx_a, |workspace, cx| {
+            workspace.open_path((worktree_id, "1.txt"), true, cx)
+        })
+        .await
+        .unwrap()
+        .downcast::<Editor>()
+        .unwrap();
+    let editor_a2 = workspace_a
+        .update(cx_a, |workspace, cx| {
+            workspace.open_path((worktree_id, "2.txt"), true, cx)
+        })
+        .await
+        .unwrap()
+        .downcast::<Editor>()
+        .unwrap();
+
+    // Client B opens an editor.
+    let workspace_b = client_b.build_workspace(&project_b, cx_b);
+    let editor_b1 = workspace_b
+        .update(cx_b, |workspace, cx| {
+            workspace.open_path((worktree_id, "1.txt"), true, cx)
+        })
+        .await
+        .unwrap()
+        .downcast::<Editor>()
+        .unwrap();
+
+    let client_a_id = project_b.read_with(cx_b, |project, _| {
+        project.collaborators().values().next().unwrap().peer_id
+    });
+    let client_b_id = project_a.read_with(cx_a, |project, _| {
+        project.collaborators().values().next().unwrap().peer_id
+    });
+
+    // When client B starts following client A, all visible view states are replicated to client B.
+    editor_a1.update(cx_a, |editor, cx| {
+        editor.change_selections(None, cx, |s| s.select_ranges([0..1]))
+    });
+    editor_a2.update(cx_a, |editor, cx| {
+        editor.change_selections(None, cx, |s| s.select_ranges([2..3]))
+    });
+    workspace_b
+        .update(cx_b, |workspace, cx| {
+            workspace
+                .toggle_follow(&ToggleFollow(client_a_id), cx)
+                .unwrap()
+        })
+        .await
+        .unwrap();
+
+    let editor_b2 = workspace_b.read_with(cx_b, |workspace, cx| {
+        workspace
+            .active_item(cx)
+            .unwrap()
+            .downcast::<Editor>()
+            .unwrap()
+    });
+    assert!(cx_b.read(|cx| editor_b2.is_focused(cx)));
+    assert_eq!(
+        editor_b2.read_with(cx_b, |editor, cx| editor.project_path(cx)),
+        Some((worktree_id, "2.txt").into())
+    );
+    assert_eq!(
+        editor_b2.read_with(cx_b, |editor, cx| editor.selections.ranges(cx)),
+        vec![2..3]
+    );
+    assert_eq!(
+        editor_b1.read_with(cx_b, |editor, cx| editor.selections.ranges(cx)),
+        vec![0..1]
+    );
+
+    // 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)
+    });
+    workspace_b
+        .condition(cx_b, |workspace, cx| {
+            workspace.active_item(cx).unwrap().id() == editor_b1.id()
+        })
+        .await;
+
+    // When client A navigates back and forth, client B does so as well.
+    workspace_a
+        .update(cx_a, |workspace, cx| {
+            workspace::Pane::go_back(workspace, None, cx)
+        })
+        .await;
+    workspace_b
+        .condition(cx_b, |workspace, cx| {
+            workspace.active_item(cx).unwrap().id() == editor_b2.id()
+        })
+        .await;
+
+    workspace_a
+        .update(cx_a, |workspace, cx| {
+            workspace::Pane::go_forward(workspace, None, cx)
+        })
+        .await;
+    workspace_b
+        .condition(cx_b, |workspace, cx| {
+            workspace.active_item(cx).unwrap().id() == editor_b1.id()
+        })
+        .await;
+
+    // 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]));
+    });
+    editor_b1
+        .condition(cx_b, |editor, cx| {
+            editor.selections.ranges(cx) == vec![1..1, 2..2]
+        })
+        .await;
+
+    editor_a1.update(cx_a, |editor, cx| editor.set_text("TWO", cx));
+    editor_b1
+        .condition(cx_b, |editor, cx| editor.text(cx) == "TWO")
+        .await;
+
+    editor_a1.update(cx_a, |editor, cx| {
+        editor.change_selections(None, cx, |s| s.select_ranges([3..3]));
+        editor.set_scroll_position(vec2f(0., 100.), cx);
+    });
+    editor_b1
+        .condition(cx_b, |editor, cx| {
+            editor.selections.ranges(cx) == vec![3..3]
+        })
+        .await;
+
+    // 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)
+    });
+    cx_a.foreground().run_until_parked();
+    assert_eq!(
+        workspace_b.read_with(cx_b, |workspace, cx| workspace
+            .active_item(cx)
+            .unwrap()
+            .id()),
+        editor_b1.id()
+    );
+
+    // Client A starts following client B.
+    workspace_a
+        .update(cx_a, |workspace, cx| {
+            workspace
+                .toggle_follow(&ToggleFollow(client_b_id), cx)
+                .unwrap()
+        })
+        .await
+        .unwrap();
+    assert_eq!(
+        workspace_a.read_with(cx_a, |workspace, _| workspace.leader_for_pane(&pane_a)),
+        Some(client_b_id)
+    );
+    assert_eq!(
+        workspace_a.read_with(cx_a, |workspace, cx| workspace
+            .active_item(cx)
+            .unwrap()
+            .id()),
+        editor_a1.id()
+    );
+
+    // Following interrupts when client B disconnects.
+    client_b.disconnect(&cx_b.to_async()).unwrap();
+    cx_a.foreground().run_until_parked();
+    assert_eq!(
+        workspace_a.read_with(cx_a, |workspace, _| workspace.leader_for_pane(&pane_a)),
+        None
+    );
+}
+
+#[gpui::test(iterations = 10)]
+async fn test_peers_following_each_other(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
+    cx_a.foreground().forbid_parking();
+    let fs = FakeFs::new(cx_a.background());
+
+    // 2 clients connect to a server.
+    let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
+    let mut client_a = server.create_client(cx_a, "user_a").await;
+    let mut client_b = server.create_client(cx_b, "user_b").await;
+    server
+        .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)])
+        .await;
+    cx_a.update(editor::init);
+    cx_b.update(editor::init);
+
+    // Client A shares a project.
+    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(fs.clone(), "/a", cx_a).await;
+
+    // Client B joins the project.
+    let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await;
+
+    // Client A opens some editors.
+    let workspace_a = client_a.build_workspace(&project_a, cx_a);
+    let pane_a1 = workspace_a.read_with(cx_a, |workspace, _| workspace.active_pane().clone());
+    let _editor_a1 = workspace_a
+        .update(cx_a, |workspace, cx| {
+            workspace.open_path((worktree_id, "1.txt"), true, cx)
+        })
+        .await
+        .unwrap()
+        .downcast::<Editor>()
+        .unwrap();
+
+    // Client B opens an editor.
+    let workspace_b = client_b.build_workspace(&project_b, cx_b);
+    let pane_b1 = workspace_b.read_with(cx_b, |workspace, _| workspace.active_pane().clone());
+    let _editor_b1 = workspace_b
+        .update(cx_b, |workspace, cx| {
+            workspace.open_path((worktree_id, "2.txt"), 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_pane(workspace.active_pane().clone(), SplitDirection::Right, cx);
+        assert_ne!(*workspace.active_pane(), pane_a1);
+    });
+    workspace_a
+        .update(cx_a, |workspace, cx| {
+            let leader_id = *project_a.read(cx).collaborators().keys().next().unwrap();
+            workspace
+                .toggle_follow(&workspace::ToggleFollow(leader_id), cx)
+                .unwrap()
+        })
+        .await
+        .unwrap();
+    workspace_b.update(cx_b, |workspace, cx| {
+        workspace.split_pane(workspace.active_pane().clone(), SplitDirection::Right, cx);
+        assert_ne!(*workspace.active_pane(), pane_b1);
+    });
+    workspace_b
+        .update(cx_b, |workspace, cx| {
+            let leader_id = *project_b.read(cx).collaborators().keys().next().unwrap();
+            workspace
+                .toggle_follow(&workspace::ToggleFollow(leader_id), cx)
+                .unwrap()
+        })
+        .await
+        .unwrap();
+
+    workspace_a
+        .update(cx_a, |workspace, cx| {
+            workspace.activate_next_pane(cx);
+            assert_eq!(*workspace.active_pane(), pane_a1);
+            workspace.open_path((worktree_id, "3.txt"), true, cx)
+        })
+        .await
+        .unwrap();
+    workspace_b
+        .update(cx_b, |workspace, cx| {
+            workspace.activate_next_pane(cx);
+            assert_eq!(*workspace.active_pane(), pane_b1);
+            workspace.open_path((worktree_id, "4.txt"), true, cx)
+        })
+        .await
+        .unwrap();
+    cx_a.foreground().run_until_parked();
+
+    // Ensure leader updates don't change the active pane of followers
+    workspace_a.read_with(cx_a, |workspace, _| {
+        assert_eq!(*workspace.active_pane(), pane_a1);
+    });
+    workspace_b.read_with(cx_b, |workspace, _| {
+        assert_eq!(*workspace.active_pane(), pane_b1);
+    });
+
+    // Ensure peers following each other doesn't cause an infinite loop.
+    assert_eq!(
+        workspace_a.read_with(cx_a, |workspace, cx| workspace
+            .active_item(cx)
+            .unwrap()
+            .project_path(cx)),
+        Some((worktree_id, "3.txt").into())
+    );
+    workspace_a.update(cx_a, |workspace, cx| {
+        assert_eq!(
+            workspace.active_item(cx).unwrap().project_path(cx),
+            Some((worktree_id, "3.txt").into())
+        );
+        workspace.activate_next_pane(cx);
+        assert_eq!(
+            workspace.active_item(cx).unwrap().project_path(cx),
+            Some((worktree_id, "4.txt").into())
+        );
+    });
+    workspace_b.update(cx_b, |workspace, cx| {
+        assert_eq!(
+            workspace.active_item(cx).unwrap().project_path(cx),
+            Some((worktree_id, "4.txt").into())
+        );
+        workspace.activate_next_pane(cx);
+        assert_eq!(
+            workspace.active_item(cx).unwrap().project_path(cx),
+            Some((worktree_id, "3.txt").into())
+        );
+    });
+}
+
+#[gpui::test(iterations = 10)]
+async fn test_auto_unfollowing(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
+    cx_a.foreground().forbid_parking();
+
+    // 2 clients connect to a server.
+    let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
+    let mut client_a = server.create_client(cx_a, "user_a").await;
+    let mut client_b = server.create_client(cx_b, "user_b").await;
+    server
+        .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)])
+        .await;
+    cx_a.update(editor::init);
+    cx_b.update(editor::init);
+
+    // Client A shares a project.
+    let fs = FakeFs::new(cx_a.background());
+    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(fs.clone(), "/a", cx_a).await;
+    let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await;
+
+    // Client A opens some editors.
+    let workspace_a = client_a.build_workspace(&project_a, cx_a);
+    let _editor_a1 = workspace_a
+        .update(cx_a, |workspace, cx| {
+            workspace.open_path((worktree_id, "1.txt"), true, cx)
+        })
+        .await
+        .unwrap()
+        .downcast::<Editor>()
+        .unwrap();
+
+    // Client B starts following client A.
+    let workspace_b = client_b.build_workspace(&project_b, cx_b);
+    let pane_b = workspace_b.read_with(cx_b, |workspace, _| workspace.active_pane().clone());
+    let leader_id = project_b.read_with(cx_b, |project, _| {
+        project.collaborators().values().next().unwrap().peer_id
+    });
+    workspace_b
+        .update(cx_b, |workspace, cx| {
+            workspace
+                .toggle_follow(&ToggleFollow(leader_id), cx)
+                .unwrap()
+        })
+        .await
+        .unwrap();
+    assert_eq!(
+        workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
+        Some(leader_id)
+    );
+    let editor_b2 = workspace_b.read_with(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.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
+        None
+    );
+
+    workspace_b
+        .update(cx_b, |workspace, cx| {
+            workspace
+                .toggle_follow(&ToggleFollow(leader_id), cx)
+                .unwrap()
+        })
+        .await
+        .unwrap();
+    assert_eq!(
+        workspace_b.read_with(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.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
+        None
+    );
+
+    workspace_b
+        .update(cx_b, |workspace, cx| {
+            workspace
+                .toggle_follow(&ToggleFollow(leader_id), cx)
+                .unwrap()
+        })
+        .await
+        .unwrap();
+    assert_eq!(
+        workspace_b.read_with(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(vec2f(0., 3.), cx)
+    });
+    assert_eq!(
+        workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
+        None
+    );
+
+    workspace_b
+        .update(cx_b, |workspace, cx| {
+            workspace
+                .toggle_follow(&ToggleFollow(leader_id), cx)
+                .unwrap()
+        })
+        .await
+        .unwrap();
+    assert_eq!(
+        workspace_b.read_with(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_pane(pane_b.clone(), SplitDirection::Right, cx)
+    });
+    assert_eq!(
+        workspace_b.read_with(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.read_with(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"), true, cx)
+        })
+        .await
+        .unwrap();
+    assert_eq!(
+        workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
+        None
+    );
+}
+
+#[gpui::test(iterations = 100)]
+async fn test_random_collaboration(
+    cx: &mut TestAppContext,
+    deterministic: Arc<Deterministic>,
+    rng: StdRng,
+) {
+    cx.foreground().forbid_parking();
+    let max_peers = env::var("MAX_PEERS")
+        .map(|i| i.parse().expect("invalid `MAX_PEERS` variable"))
+        .unwrap_or(5);
+    assert!(max_peers <= 5);
+
+    let max_operations = env::var("OPERATIONS")
+        .map(|i| i.parse().expect("invalid `OPERATIONS` variable"))
+        .unwrap_or(10);
+
+    let rng = Arc::new(Mutex::new(rng));
+
+    let guest_lang_registry = Arc::new(LanguageRegistry::test());
+    let host_language_registry = Arc::new(LanguageRegistry::test());
+
+    let fs = FakeFs::new(cx.background());
+    fs.insert_tree("/_collab", json!({"init": ""})).await;
+
+    let mut server = TestServer::start(cx.foreground(), cx.background()).await;
+    let db = server.app_state.db.clone();
+    let host_user_id = db.create_user("host", None, false).await.unwrap();
+    for username in ["guest-1", "guest-2", "guest-3", "guest-4"] {
+        let guest_user_id = db.create_user(username, None, false).await.unwrap();
+        server
+            .app_state
+            .db
+            .send_contact_request(guest_user_id, host_user_id)
+            .await
+            .unwrap();
+        server
+            .app_state
+            .db
+            .respond_to_contact_request(host_user_id, guest_user_id, true)
+            .await
+            .unwrap();
+    }
+
+    let mut clients = Vec::new();
+    let mut user_ids = Vec::new();
+    let mut op_start_signals = Vec::new();
+
+    let mut next_entity_id = 100000;
+    let mut host_cx = TestAppContext::new(
+        cx.foreground_platform(),
+        cx.platform(),
+        deterministic.build_foreground(next_entity_id),
+        deterministic.build_background(),
+        cx.font_cache(),
+        cx.leak_detector(),
+        next_entity_id,
+    );
+    let host = server.create_client(&mut host_cx, "host").await;
+    let host_project = host_cx.update(|cx| {
+        Project::local(
+            host.client.clone(),
+            host.user_store.clone(),
+            host_language_registry.clone(),
+            fs.clone(),
+            cx,
+        )
+    });
+    let host_project_id = host_project
+        .update(&mut host_cx, |p, _| p.next_remote_id())
+        .await;
+
+    let (collab_worktree, _) = host_project
+        .update(&mut host_cx, |project, cx| {
+            project.find_or_create_local_worktree("/_collab", true, cx)
+        })
+        .await
+        .unwrap();
+    collab_worktree
+        .read_with(&host_cx, |tree, _| tree.as_local().unwrap().scan_complete())
+        .await;
+
+    // Set up fake language servers.
+    let mut language = Language::new(
+        LanguageConfig {
+            name: "Rust".into(),
+            path_suffixes: vec!["rs".to_string()],
+            ..Default::default()
+        },
+        None,
+    );
+    let _fake_servers = language.set_fake_lsp_adapter(FakeLspAdapter {
+        name: "the-fake-language-server",
+        capabilities: lsp::LanguageServer::full_capabilities(),
+        initializer: Some(Box::new({
+            let rng = rng.clone();
+            let fs = fs.clone();
+            let project = host_project.downgrade();
+            move |fake_server: &mut FakeLanguageServer| {
+                fake_server.handle_request::<lsp::request::Completion, _, _>(|_, _| async move {
+                    Ok(Some(lsp::CompletionResponse::Array(vec![
+                        lsp::CompletionItem {
+                            text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
+                                range: lsp::Range::new(
+                                    lsp::Position::new(0, 0),
+                                    lsp::Position::new(0, 0),
+                                ),
+                                new_text: "the-new-text".to_string(),
+                            })),
+                            ..Default::default()
+                        },
+                    ])))
+                });
+
+                fake_server.handle_request::<lsp::request::CodeActionRequest, _, _>(
+                    |_, _| async move {
+                        Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
+                            lsp::CodeAction {
+                                title: "the-code-action".to_string(),
+                                ..Default::default()
+                            },
+                        )]))
+                    },
+                );
+
+                fake_server.handle_request::<lsp::request::PrepareRenameRequest, _, _>(
+                    |params, _| async move {
+                        Ok(Some(lsp::PrepareRenameResponse::Range(lsp::Range::new(
+                            params.position,
+                            params.position,
+                        ))))
+                    },
+                );
+
+                fake_server.handle_request::<lsp::request::GotoDefinition, _, _>({
+                    let fs = fs.clone();
+                    let rng = rng.clone();
+                    move |_, _| {
+                        let fs = fs.clone();
+                        let rng = rng.clone();
+                        async move {
+                            let files = fs.files().await;
+                            let mut rng = rng.lock();
+                            let count = rng.gen_range::<usize, _>(1..3);
+                            let files = (0..count)
+                                .map(|_| files.choose(&mut *rng).unwrap())
+                                .collect::<Vec<_>>();
+                            log::info!("LSP: Returning definitions in files {:?}", &files);
+                            Ok(Some(lsp::GotoDefinitionResponse::Array(
+                                files
+                                    .into_iter()
+                                    .map(|file| lsp::Location {
+                                        uri: lsp::Url::from_file_path(file).unwrap(),
+                                        range: Default::default(),
+                                    })
+                                    .collect(),
+                            )))
+                        }
+                    }
+                });
+
+                fake_server.handle_request::<lsp::request::DocumentHighlightRequest, _, _>({
+                    let rng = rng.clone();
+                    let project = project.clone();
+                    move |params, mut cx| {
+                        let highlights = if let Some(project) = project.upgrade(&cx) {
+                            project.update(&mut cx, |project, cx| {
+                                let path = params
+                                    .text_document_position_params
+                                    .text_document
+                                    .uri
+                                    .to_file_path()
+                                    .unwrap();
+                                let (worktree, relative_path) =
+                                    project.find_local_worktree(&path, cx)?;
+                                let project_path =
+                                    ProjectPath::from((worktree.read(cx).id(), relative_path));
+                                let buffer = project.get_open_buffer(&project_path, cx)?.read(cx);
+
+                                let mut highlights = Vec::new();
+                                let highlight_count = rng.lock().gen_range(1..=5);
+                                let mut prev_end = 0;
+                                for _ in 0..highlight_count {
+                                    let range =
+                                        buffer.random_byte_range(prev_end, &mut *rng.lock());
+
+                                    highlights.push(lsp::DocumentHighlight {
+                                        range: range_to_lsp(range.to_point_utf16(buffer)),
+                                        kind: Some(lsp::DocumentHighlightKind::READ),
+                                    });
+                                    prev_end = range.end;
+                                }
+                                Some(highlights)
+                            })
+                        } else {
+                            None
+                        };
+                        async move { Ok(highlights) }
+                    }
+                });
+            }
+        })),
+        ..Default::default()
+    });
+    host_language_registry.add(Arc::new(language));
+
+    let op_start_signal = futures::channel::mpsc::unbounded();
+    user_ids.push(host.current_user_id(&host_cx));
+    op_start_signals.push(op_start_signal.0);
+    clients.push(host_cx.foreground().spawn(host.simulate_host(
+        host_project,
+        op_start_signal.1,
+        rng.clone(),
+        host_cx,
+    )));
+
+    let disconnect_host_at = if rng.lock().gen_bool(0.2) {
+        rng.lock().gen_range(0..max_operations)
+    } else {
+        max_operations
+    };
+    let mut available_guests = vec![
+        "guest-1".to_string(),
+        "guest-2".to_string(),
+        "guest-3".to_string(),
+        "guest-4".to_string(),
+    ];
+    let mut operations = 0;
+    while operations < max_operations {
+        if operations == disconnect_host_at {
+            server.disconnect_client(user_ids[0]);
+            cx.foreground().advance_clock(RECEIVE_TIMEOUT);
+            drop(op_start_signals);
+            let mut clients = futures::future::join_all(clients).await;
+            cx.foreground().run_until_parked();
+
+            let (host, mut host_cx, host_err) = clients.remove(0);
+            if let Some(host_err) = host_err {
+                log::error!("host error - {:?}", host_err);
+            }
+            host.project
+                .as_ref()
+                .unwrap()
+                .read_with(&host_cx, |project, _| assert!(!project.is_shared()));
+            for (guest, mut guest_cx, guest_err) in clients {
+                if let Some(guest_err) = guest_err {
+                    log::error!("{} error - {:?}", guest.username, guest_err);
+                }
+
+                let contacts = server
+                    .app_state
+                    .db
+                    .get_contacts(guest.current_user_id(&guest_cx))
+                    .await
+                    .unwrap();
+                let contacts = server
+                    .store
+                    .read()
+                    .await
+                    .build_initial_contacts_update(contacts)
+                    .contacts;
+                assert!(!contacts
+                    .iter()
+                    .flat_map(|contact| &contact.projects)
+                    .any(|project| project.id == host_project_id));
+                guest
+                    .project
+                    .as_ref()
+                    .unwrap()
+                    .read_with(&guest_cx, |project, _| assert!(project.is_read_only()));
+                guest_cx.update(|_| drop(guest));
+            }
+            host_cx.update(|_| drop(host));
+
+            return;
+        }
+
+        let distribution = rng.lock().gen_range(0..100);
+        match distribution {
+            0..=19 if !available_guests.is_empty() => {
+                let guest_ix = rng.lock().gen_range(0..available_guests.len());
+                let guest_username = available_guests.remove(guest_ix);
+                log::info!("Adding new connection for {}", guest_username);
+                next_entity_id += 100000;
+                let mut guest_cx = TestAppContext::new(
+                    cx.foreground_platform(),
+                    cx.platform(),
+                    deterministic.build_foreground(next_entity_id),
+                    deterministic.build_background(),
+                    cx.font_cache(),
+                    cx.leak_detector(),
+                    next_entity_id,
+                );
+                let guest = server.create_client(&mut guest_cx, &guest_username).await;
+                let guest_project = Project::remote(
+                    host_project_id,
+                    guest.client.clone(),
+                    guest.user_store.clone(),
+                    guest_lang_registry.clone(),
+                    FakeFs::new(cx.background()),
+                    &mut guest_cx.to_async(),
+                )
+                .await
+                .unwrap();
+                let op_start_signal = futures::channel::mpsc::unbounded();
+                user_ids.push(guest.current_user_id(&guest_cx));
+                op_start_signals.push(op_start_signal.0);
+                clients.push(guest_cx.foreground().spawn(guest.simulate_guest(
+                    guest_username.clone(),
+                    guest_project,
+                    op_start_signal.1,
+                    rng.clone(),
+                    guest_cx,
+                )));
+
+                log::info!("Added connection for {}", guest_username);
+                operations += 1;
+            }
+            20..=29 if clients.len() > 1 => {
+                let guest_ix = rng.lock().gen_range(1..clients.len());
+                log::info!("Removing guest {}", user_ids[guest_ix]);
+                let removed_guest_id = user_ids.remove(guest_ix);
+                let guest = clients.remove(guest_ix);
+                op_start_signals.remove(guest_ix);
+                server.forbid_connections();
+                server.disconnect_client(removed_guest_id);
+                cx.foreground().advance_clock(RECEIVE_TIMEOUT);
+                let (guest, mut guest_cx, guest_err) = guest.await;
+                server.allow_connections();
+
+                if let Some(guest_err) = guest_err {
+                    log::error!("{} error - {:?}", guest.username, guest_err);
+                }
+                guest
+                    .project
+                    .as_ref()
+                    .unwrap()
+                    .read_with(&guest_cx, |project, _| assert!(project.is_read_only()));
+                for user_id in &user_ids {
+                    let contacts = server.app_state.db.get_contacts(*user_id).await.unwrap();
+                    let contacts = server
+                        .store
+                        .read()
+                        .await
+                        .build_initial_contacts_update(contacts)
+                        .contacts;
+                    for contact in contacts {
+                        if contact.online {
+                            assert_ne!(
+                                contact.user_id, removed_guest_id.0 as u64,
+                                "removed guest is still a contact of another peer"
+                            );
+                        }
+                        for project in contact.projects {
+                            for project_guest_id in project.guests {
+                                assert_ne!(
+                                    project_guest_id, removed_guest_id.0 as u64,
+                                    "removed guest appears as still participating on a project"
+                                );
+                            }
+                        }
+                    }
+                }
+
+                log::info!("{} removed", guest.username);
+                available_guests.push(guest.username.clone());
+                guest_cx.update(|_| drop(guest));
+
+                operations += 1;
+            }
+            _ => {
+                while operations < max_operations && rng.lock().gen_bool(0.7) {
+                    op_start_signals
+                        .choose(&mut *rng.lock())
+                        .unwrap()
+                        .unbounded_send(())
+                        .unwrap();
+                    operations += 1;
+                }
+
+                if rng.lock().gen_bool(0.8) {
+                    cx.foreground().run_until_parked();
+                }
+            }
+        }
+    }
+
+    drop(op_start_signals);
+    let mut clients = futures::future::join_all(clients).await;
+    cx.foreground().run_until_parked();
+
+    let (host_client, mut host_cx, host_err) = clients.remove(0);
+    if let Some(host_err) = host_err {
+        panic!("host error - {:?}", host_err);
+    }
+    let host_project = host_client.project.as_ref().unwrap();
+    let host_worktree_snapshots = host_project.read_with(&host_cx, |project, cx| {
+        project
+            .worktrees(cx)
+            .map(|worktree| {
+                let snapshot = worktree.read(cx).snapshot();
+                (snapshot.id(), snapshot)
+            })
+            .collect::<BTreeMap<_, _>>()
+    });
+
+    host_client
+        .project
+        .as_ref()
+        .unwrap()
+        .read_with(&host_cx, |project, cx| project.check_invariants(cx));
+
+    for (guest_client, mut guest_cx, guest_err) in clients.into_iter() {
+        if let Some(guest_err) = guest_err {
+            panic!("{} error - {:?}", guest_client.username, guest_err);
+        }
+        let worktree_snapshots =
+            guest_client
+                .project
+                .as_ref()
+                .unwrap()
+                .read_with(&guest_cx, |project, cx| {
+                    project
+                        .worktrees(cx)
+                        .map(|worktree| {
+                            let worktree = worktree.read(cx);
+                            (worktree.id(), worktree.snapshot())
+                        })
+                        .collect::<BTreeMap<_, _>>()
+                });
+
+        assert_eq!(
+            worktree_snapshots.keys().collect::<Vec<_>>(),
+            host_worktree_snapshots.keys().collect::<Vec<_>>(),
+            "{} has different worktrees than the host",
+            guest_client.username
+        );
+        for (id, host_snapshot) in &host_worktree_snapshots {
+            let guest_snapshot = &worktree_snapshots[id];
+            assert_eq!(
+                guest_snapshot.root_name(),
+                host_snapshot.root_name(),
+                "{} has different root name than the host for worktree {}",
+                guest_client.username,
+                id
+            );
+            assert_eq!(
+                guest_snapshot.entries(false).collect::<Vec<_>>(),
+                host_snapshot.entries(false).collect::<Vec<_>>(),
+                "{} has different snapshot than the host for worktree {}",
+                guest_client.username,
+                id
+            );
+            assert_eq!(guest_snapshot.scan_id(), host_snapshot.scan_id());
+        }
+
+        guest_client
+            .project
+            .as_ref()
+            .unwrap()
+            .read_with(&guest_cx, |project, cx| project.check_invariants(cx));
+
+        for guest_buffer in &guest_client.buffers {
+            let buffer_id = guest_buffer.read_with(&guest_cx, |buffer, _| buffer.remote_id());
+            let host_buffer = host_project.read_with(&host_cx, |project, cx| {
+                project.buffer_for_id(buffer_id, cx).expect(&format!(
+                    "host does not have buffer for guest:{}, peer:{}, id:{}",
+                    guest_client.username, guest_client.peer_id, buffer_id
+                ))
+            });
+            let path =
+                host_buffer.read_with(&host_cx, |buffer, cx| buffer.file().unwrap().full_path(cx));
+
+            assert_eq!(
+                guest_buffer.read_with(&guest_cx, |buffer, _| buffer.deferred_ops_len()),
+                0,
+                "{}, buffer {}, path {:?} has deferred operations",
+                guest_client.username,
+                buffer_id,
+                path,
+            );
+            assert_eq!(
+                guest_buffer.read_with(&guest_cx, |buffer, _| buffer.text()),
+                host_buffer.read_with(&host_cx, |buffer, _| buffer.text()),
+                "{}, buffer {}, path {:?}, differs from the host's buffer",
+                guest_client.username,
+                buffer_id,
+                path
+            );
+        }
+
+        guest_cx.update(|_| drop(guest_client));
+    }
+
+    host_cx.update(|_| drop(host_client));
+}
+
+struct TestServer {
+    peer: Arc<Peer>,
+    app_state: Arc<AppState>,
+    server: Arc<Server>,
+    foreground: Rc<executor::Foreground>,
+    notifications: mpsc::UnboundedReceiver<()>,
+    connection_killers: Arc<Mutex<HashMap<UserId, Arc<AtomicBool>>>>,
+    forbid_connections: Arc<AtomicBool>,
+    _test_db: TestDb,
+}
+
+impl TestServer {
+    async fn start(
+        foreground: Rc<executor::Foreground>,
+        background: Arc<executor::Background>,
+    ) -> Self {
+        let test_db = TestDb::fake(background);
+        let app_state = Self::build_app_state(&test_db).await;
+        let peer = Peer::new();
+        let notifications = mpsc::unbounded();
+        let server = Server::new(app_state.clone(), Some(notifications.0));
+        Self {
+            peer,
+            app_state,
+            server,
+            foreground,
+            notifications: notifications.1,
+            connection_killers: Default::default(),
+            forbid_connections: Default::default(),
+            _test_db: test_db,
+        }
+    }
+
+    async fn create_client(&mut self, cx: &mut TestAppContext, name: &str) -> TestClient {
+        cx.update(|cx| {
+            let settings = Settings::test(cx);
+            cx.set_global(settings);
+        });
+
+        let http = FakeHttpClient::with_404_response();
+        let user_id = if let Ok(Some(user)) = self.app_state.db.get_user_by_github_login(name).await
+        {
+            user.id
+        } else {
+            self.app_state
+                .db
+                .create_user(name, None, false)
+                .await
+                .unwrap()
+        };
+        let client_name = name.to_string();
+        let mut client = Client::new(http.clone());
+        let server = self.server.clone();
+        let db = self.app_state.db.clone();
+        let connection_killers = self.connection_killers.clone();
+        let forbid_connections = self.forbid_connections.clone();
+        let (connection_id_tx, mut connection_id_rx) = mpsc::channel(16);
+
+        Arc::get_mut(&mut client)
+            .unwrap()
+            .override_authenticate(move |cx| {
+                cx.spawn(|_| async move {
+                    let access_token = "the-token".to_string();
+                    Ok(Credentials {
+                        user_id: user_id.0 as u64,
+                        access_token,
+                    })
+                })
+            })
+            .override_establish_connection(move |credentials, cx| {
+                assert_eq!(credentials.user_id, user_id.0 as u64);
+                assert_eq!(credentials.access_token, "the-token");
+
+                let server = server.clone();
+                let db = db.clone();
+                let connection_killers = connection_killers.clone();
+                let forbid_connections = forbid_connections.clone();
+                let client_name = client_name.clone();
+                let connection_id_tx = connection_id_tx.clone();
+                cx.spawn(move |cx| async move {
+                    if forbid_connections.load(SeqCst) {
+                        Err(EstablishConnectionError::other(anyhow!(
+                            "server is forbidding connections"
+                        )))
+                    } else {
+                        let (client_conn, server_conn, killed) =
+                            Connection::in_memory(cx.background());
+                        connection_killers.lock().insert(user_id, killed);
+                        let user = db.get_user_by_id(user_id).await.unwrap().unwrap();
+                        cx.background()
+                            .spawn(server.handle_connection(
+                                server_conn,
+                                client_name,
+                                user,
+                                Some(connection_id_tx),
+                                cx.background(),
+                            ))
+                            .detach();
+                        Ok(client_conn)
+                    }
+                })
+            });
+
+        let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http, cx));
+        let app_state = Arc::new(workspace::AppState {
+            client: client.clone(),
+            user_store: user_store.clone(),
+            languages: Arc::new(LanguageRegistry::new(Task::ready(()))),
+            themes: ThemeRegistry::new((), cx.font_cache()),
+            fs: FakeFs::new(cx.background()),
+            build_window_options: || Default::default(),
+            initialize_workspace: |_, _, _| unimplemented!(),
+        });
+
+        Channel::init(&client);
+        Project::init(&client);
+        cx.update(|cx| workspace::init(app_state.clone(), cx));
+
+        client
+            .authenticate_and_connect(false, &cx.to_async())
+            .await
+            .unwrap();
+        let peer_id = PeerId(connection_id_rx.next().await.unwrap().0);
+
+        let client = TestClient {
+            client,
+            peer_id,
+            username: name.to_string(),
+            user_store,
+            language_registry: Arc::new(LanguageRegistry::test()),
+            project: Default::default(),
+            buffers: Default::default(),
+        };
+        client.wait_for_current_user(cx).await;
+        client
+    }
+
+    fn disconnect_client(&self, user_id: UserId) {
+        self.connection_killers
+            .lock()
+            .remove(&user_id)
+            .unwrap()
+            .store(true, SeqCst);
+    }
+
+    fn forbid_connections(&self) {
+        self.forbid_connections.store(true, SeqCst);
+    }
+
+    fn allow_connections(&self) {
+        self.forbid_connections.store(false, SeqCst);
+    }
+
+    async fn make_contacts(&self, mut clients: Vec<(&TestClient, &mut TestAppContext)>) {
+        while let Some((client_a, cx_a)) = clients.pop() {
+            for (client_b, cx_b) in &mut clients {
+                client_a
+                    .user_store
+                    .update(cx_a, |store, cx| {
+                        store.request_contact(client_b.user_id().unwrap(), cx)
+                    })
+                    .await
+                    .unwrap();
+                cx_a.foreground().run_until_parked();
+                client_b
+                    .user_store
+                    .update(*cx_b, |store, cx| {
+                        store.respond_to_contact_request(client_a.user_id().unwrap(), true, cx)
+                    })
+                    .await
+                    .unwrap();
+            }
+        }
+    }
+
+    async fn build_app_state(test_db: &TestDb) -> Arc<AppState> {
+        Arc::new(AppState {
+            db: test_db.db().clone(),
+            api_token: Default::default(),
+            invite_link_prefix: Default::default(),
+        })
+    }
+
+    async fn state<'a>(&'a self) -> RwLockReadGuard<'a, Store> {
+        self.server.store.read().await
+    }
+
+    async fn condition<F>(&mut self, mut predicate: F)
+    where
+        F: FnMut(&Store) -> bool,
+    {
+        assert!(
+            self.foreground.parking_forbidden(),
+            "you must call forbid_parking to use server conditions so we don't block indefinitely"
+        );
+        while !(predicate)(&*self.server.store.read().await) {
+            self.foreground.start_waiting();
+            self.notifications.next().await;
+            self.foreground.finish_waiting();
+        }
+    }
+}
+
+impl Deref for TestServer {
+    type Target = Server;
+
+    fn deref(&self) -> &Self::Target {
+        &self.server
+    }
+}
+
+impl Drop for TestServer {
+    fn drop(&mut self) {
+        self.peer.reset();
+    }
+}
+
+struct TestClient {
+    client: Arc<Client>,
+    username: String,
+    pub peer_id: PeerId,
+    pub user_store: ModelHandle<UserStore>,
+    language_registry: Arc<LanguageRegistry>,
+    project: Option<ModelHandle<Project>>,
+    buffers: HashSet<ModelHandle<language::Buffer>>,
+}
+
+impl Deref for TestClient {
+    type Target = Arc<Client>;
+
+    fn deref(&self) -> &Self::Target {
+        &self.client
+    }
+}
+
+struct ContactsSummary {
+    pub current: Vec<String>,
+    pub outgoing_requests: Vec<String>,
+    pub incoming_requests: Vec<String>,
+}
+
+impl TestClient {
+    pub fn current_user_id(&self, cx: &TestAppContext) -> UserId {
+        UserId::from_proto(
+            self.user_store
+                .read_with(cx, |user_store, _| user_store.current_user().unwrap().id),
+        )
+    }
+
+    async fn wait_for_current_user(&self, cx: &TestAppContext) {
+        let mut authed_user = self
+            .user_store
+            .read_with(cx, |user_store, _| user_store.watch_current_user());
+        while authed_user.next().await.unwrap().is_none() {}
+    }
+
+    async fn clear_contacts(&self, cx: &mut TestAppContext) {
+        self.user_store
+            .update(cx, |store, _| store.clear_contacts())
+            .await;
+    }
+
+    fn summarize_contacts(&self, cx: &TestAppContext) -> ContactsSummary {
+        self.user_store.read_with(cx, |store, _| ContactsSummary {
+            current: store
+                .contacts()
+                .iter()
+                .map(|contact| contact.user.github_login.clone())
+                .collect(),
+            outgoing_requests: store
+                .outgoing_contact_requests()
+                .iter()
+                .map(|user| user.github_login.clone())
+                .collect(),
+            incoming_requests: store
+                .incoming_contact_requests()
+                .iter()
+                .map(|user| user.github_login.clone())
+                .collect(),
+        })
+    }
+
+    async fn build_local_project(
+        &mut self,
+        fs: Arc<FakeFs>,
+        root_path: impl AsRef<Path>,
+        cx: &mut TestAppContext,
+    ) -> (ModelHandle<Project>, WorktreeId) {
+        let project = cx.update(|cx| {
+            Project::local(
+                self.client.clone(),
+                self.user_store.clone(),
+                self.language_registry.clone(),
+                fs,
+                cx,
+            )
+        });
+        self.project = Some(project.clone());
+        let (worktree, _) = project
+            .update(cx, |p, cx| {
+                p.find_or_create_local_worktree(root_path, true, cx)
+            })
+            .await
+            .unwrap();
+        worktree
+            .read_with(cx, |tree, _| tree.as_local().unwrap().scan_complete())
+            .await;
+        project
+            .update(cx, |project, _| project.next_remote_id())
+            .await;
+        (project, worktree.read_with(cx, |tree, _| tree.id()))
+    }
+
+    async fn build_remote_project(
+        &mut self,
+        host_project: &ModelHandle<Project>,
+        host_cx: &mut TestAppContext,
+        guest_cx: &mut TestAppContext,
+    ) -> ModelHandle<Project> {
+        let host_project_id = host_project
+            .read_with(host_cx, |project, _| project.next_remote_id())
+            .await;
+        let guest_user_id = self.user_id().unwrap();
+        let languages = host_project.read_with(host_cx, |project, _| project.languages().clone());
+        let project_b = guest_cx.spawn(|mut cx| {
+            let user_store = self.user_store.clone();
+            let guest_client = self.client.clone();
+            async move {
+                Project::remote(
+                    host_project_id,
+                    guest_client,
+                    user_store.clone(),
+                    languages,
+                    FakeFs::new(cx.background()),
+                    &mut cx,
+                )
+                .await
+                .unwrap()
+            }
+        });
+        host_cx.foreground().run_until_parked();
+        host_project.update(host_cx, |project, cx| {
+            project.respond_to_join_request(guest_user_id, true, cx)
+        });
+        let project = project_b.await;
+        self.project = Some(project.clone());
+        project
+    }
+
+    fn build_workspace(
+        &self,
+        project: &ModelHandle<Project>,
+        cx: &mut TestAppContext,
+    ) -> ViewHandle<Workspace> {
+        let (window_id, _) = cx.add_window(|_| EmptyView);
+        cx.add_view(window_id, |cx| Workspace::new(project.clone(), cx))
+    }
+
+    async fn simulate_host(
+        mut self,
+        project: ModelHandle<Project>,
+        op_start_signal: futures::channel::mpsc::UnboundedReceiver<()>,
+        rng: Arc<Mutex<StdRng>>,
+        mut cx: TestAppContext,
+    ) -> (Self, TestAppContext, Option<anyhow::Error>) {
+        async fn simulate_host_internal(
+            client: &mut TestClient,
+            project: ModelHandle<Project>,
+            mut op_start_signal: futures::channel::mpsc::UnboundedReceiver<()>,
+            rng: Arc<Mutex<StdRng>>,
+            cx: &mut TestAppContext,
+        ) -> anyhow::Result<()> {
+            let fs = project.read_with(cx, |project, _| project.fs().clone());
+
+            cx.update(|cx| {
+                cx.subscribe(&project, move |project, event, cx| {
+                    if let project::Event::ContactRequestedJoin(user) = event {
+                        log::info!("Host: accepting join request from {}", user.github_login);
+                        project.update(cx, |project, cx| {
+                            project.respond_to_join_request(user.id, true, cx)
+                        });
+                    }
+                })
+                .detach();
+            });
+
+            while op_start_signal.next().await.is_some() {
+                let distribution = rng.lock().gen_range::<usize, _>(0..100);
+                let files = fs.as_fake().files().await;
+                match distribution {
+                    0..=19 if !files.is_empty() => {
+                        let path = files.choose(&mut *rng.lock()).unwrap();
+                        let mut path = path.as_path();
+                        while let Some(parent_path) = path.parent() {
+                            path = parent_path;
+                            if rng.lock().gen() {
+                                break;
+                            }
+                        }
+
+                        log::info!("Host: find/create local worktree {:?}", path);
+                        let find_or_create_worktree = project.update(cx, |project, cx| {
+                            project.find_or_create_local_worktree(path, true, cx)
+                        });
+                        if rng.lock().gen() {
+                            cx.background().spawn(find_or_create_worktree).detach();
+                        } else {
+                            find_or_create_worktree.await?;
+                        }
+                    }
+                    20..=79 if !files.is_empty() => {
+                        let buffer = if client.buffers.is_empty() || rng.lock().gen() {
+                            let file = files.choose(&mut *rng.lock()).unwrap();
+                            let (worktree, path) = project
+                                .update(cx, |project, cx| {
+                                    project.find_or_create_local_worktree(file.clone(), true, cx)
+                                })
+                                .await?;
+                            let project_path =
+                                worktree.read_with(cx, |worktree, _| (worktree.id(), path));
+                            log::info!(
+                                "Host: opening path {:?}, worktree {}, relative_path {:?}",
+                                file,
+                                project_path.0,
+                                project_path.1
+                            );
+                            let buffer = project
+                                .update(cx, |project, cx| project.open_buffer(project_path, cx))
+                                .await
+                                .unwrap();
+                            client.buffers.insert(buffer.clone());
+                            buffer
+                        } else {
+                            client
+                                .buffers
+                                .iter()
+                                .choose(&mut *rng.lock())
+                                .unwrap()
+                                .clone()
+                        };
+
+                        if rng.lock().gen_bool(0.1) {
+                            cx.update(|cx| {
+                                log::info!(
+                                    "Host: dropping buffer {:?}",
+                                    buffer.read(cx).file().unwrap().full_path(cx)
+                                );
+                                client.buffers.remove(&buffer);
+                                drop(buffer);
+                            });
+                        } else {
+                            buffer.update(cx, |buffer, cx| {
+                                log::info!(
+                                    "Host: updating buffer {:?} ({})",
+                                    buffer.file().unwrap().full_path(cx),
+                                    buffer.remote_id()
+                                );
+
+                                if rng.lock().gen_bool(0.7) {
+                                    buffer.randomly_edit(&mut *rng.lock(), 5, cx);
+                                } else {
+                                    buffer.randomly_undo_redo(&mut *rng.lock(), cx);
+                                }
+                            });
+                        }
+                    }
+                    _ => loop {
+                        let path_component_count = rng.lock().gen_range::<usize, _>(1..=5);
+                        let mut path = PathBuf::new();
+                        path.push("/");
+                        for _ in 0..path_component_count {
+                            let letter = rng.lock().gen_range(b'a'..=b'z');
+                            path.push(std::str::from_utf8(&[letter]).unwrap());
+                        }
+                        path.set_extension("rs");
+                        let parent_path = path.parent().unwrap();
+
+                        log::info!("Host: creating file {:?}", path,);
+
+                        if fs.create_dir(&parent_path).await.is_ok()
+                            && fs.create_file(&path, Default::default()).await.is_ok()
+                        {
+                            break;
+                        } else {
+                            log::info!("Host: cannot create file");
+                        }
+                    },
+                }
+
+                cx.background().simulate_random_delay().await;
+            }
+
+            Ok(())
+        }
+
+        let result =
+            simulate_host_internal(&mut self, project.clone(), op_start_signal, rng, &mut cx).await;
+        log::info!("Host done");
+        self.project = Some(project);
+        (self, cx, result.err())
+    }
+
+    pub async fn simulate_guest(
+        mut self,
+        guest_username: String,
+        project: ModelHandle<Project>,
+        op_start_signal: futures::channel::mpsc::UnboundedReceiver<()>,
+        rng: Arc<Mutex<StdRng>>,
+        mut cx: TestAppContext,
+    ) -> (Self, TestAppContext, Option<anyhow::Error>) {
+        async fn simulate_guest_internal(
+            client: &mut TestClient,
+            guest_username: &str,
+            project: ModelHandle<Project>,
+            mut op_start_signal: futures::channel::mpsc::UnboundedReceiver<()>,
+            rng: Arc<Mutex<StdRng>>,
+            cx: &mut TestAppContext,
+        ) -> anyhow::Result<()> {
+            while op_start_signal.next().await.is_some() {
+                let buffer = if client.buffers.is_empty() || rng.lock().gen() {
+                    let worktree = if let Some(worktree) = project.read_with(cx, |project, cx| {
+                        project
+                            .worktrees(&cx)
+                            .filter(|worktree| {
+                                let worktree = worktree.read(cx);
+                                worktree.is_visible()
+                                    && worktree.entries(false).any(|e| e.is_file())
+                            })
+                            .choose(&mut *rng.lock())
+                    }) {
+                        worktree
+                    } else {
+                        cx.background().simulate_random_delay().await;
+                        continue;
+                    };
+
+                    let (worktree_root_name, project_path) =
+                        worktree.read_with(cx, |worktree, _| {
+                            let entry = worktree
+                                .entries(false)
+                                .filter(|e| e.is_file())
+                                .choose(&mut *rng.lock())
+                                .unwrap();
+                            (
+                                worktree.root_name().to_string(),
+                                (worktree.id(), entry.path.clone()),
+                            )
+                        });
+                    log::info!(
+                        "{}: opening path {:?} in worktree {} ({})",
+                        guest_username,
+                        project_path.1,
+                        project_path.0,
+                        worktree_root_name,
+                    );
+                    let buffer = project
+                        .update(cx, |project, cx| {
+                            project.open_buffer(project_path.clone(), cx)
+                        })
+                        .await?;
+                    log::info!(
+                        "{}: opened path {:?} in worktree {} ({}) with buffer id {}",
+                        guest_username,
+                        project_path.1,
+                        project_path.0,
+                        worktree_root_name,
+                        buffer.read_with(cx, |buffer, _| buffer.remote_id())
+                    );
+                    client.buffers.insert(buffer.clone());
+                    buffer
+                } else {
+                    client
+                        .buffers
+                        .iter()
+                        .choose(&mut *rng.lock())
+                        .unwrap()
+                        .clone()
+                };
+
+                let choice = rng.lock().gen_range(0..100);
+                match choice {
+                    0..=9 => {
+                        cx.update(|cx| {
+                            log::info!(
+                                "{}: dropping buffer {:?}",
+                                guest_username,
+                                buffer.read(cx).file().unwrap().full_path(cx)
+                            );
+                            client.buffers.remove(&buffer);
+                            drop(buffer);
+                        });
+                    }
+                    10..=19 => {
+                        let completions = project.update(cx, |project, cx| {
+                            log::info!(
+                                "{}: requesting completions for buffer {} ({:?})",
+                                guest_username,
+                                buffer.read(cx).remote_id(),
+                                buffer.read(cx).file().unwrap().full_path(cx)
+                            );
+                            let offset = rng.lock().gen_range(0..=buffer.read(cx).len());
+                            project.completions(&buffer, offset, cx)
+                        });
+                        let completions = cx.background().spawn(async move {
+                            completions
+                                .await
+                                .map_err(|err| anyhow!("completions request failed: {:?}", err))
+                        });
+                        if rng.lock().gen_bool(0.3) {
+                            log::info!("{}: detaching completions request", guest_username);
+                            cx.update(|cx| completions.detach_and_log_err(cx));
+                        } else {
+                            completions.await?;
+                        }
+                    }
+                    20..=29 => {
+                        let code_actions = project.update(cx, |project, cx| {
+                            log::info!(
+                                "{}: requesting code actions for buffer {} ({:?})",
+                                guest_username,
+                                buffer.read(cx).remote_id(),
+                                buffer.read(cx).file().unwrap().full_path(cx)
+                            );
+                            let range = buffer.read(cx).random_byte_range(0, &mut *rng.lock());
+                            project.code_actions(&buffer, range, cx)
+                        });
+                        let code_actions = cx.background().spawn(async move {
+                            code_actions
+                                .await
+                                .map_err(|err| anyhow!("code actions request failed: {:?}", err))
+                        });
+                        if rng.lock().gen_bool(0.3) {
+                            log::info!("{}: detaching code actions request", guest_username);
+                            cx.update(|cx| code_actions.detach_and_log_err(cx));
+                        } else {
+                            code_actions.await?;
+                        }
+                    }
+                    30..=39 if buffer.read_with(cx, |buffer, _| buffer.is_dirty()) => {
+                        let (requested_version, save) = buffer.update(cx, |buffer, cx| {
+                            log::info!(
+                                "{}: saving buffer {} ({:?})",
+                                guest_username,
+                                buffer.remote_id(),
+                                buffer.file().unwrap().full_path(cx)
+                            );
+                            (buffer.version(), buffer.save(cx))
+                        });
+                        let save = cx.background().spawn(async move {
+                            let (saved_version, _) = save
+                                .await
+                                .map_err(|err| anyhow!("save request failed: {:?}", err))?;
+                            assert!(saved_version.observed_all(&requested_version));
+                            Ok::<_, anyhow::Error>(())
+                        });
+                        if rng.lock().gen_bool(0.3) {
+                            log::info!("{}: detaching save request", guest_username);
+                            cx.update(|cx| save.detach_and_log_err(cx));
+                        } else {
+                            save.await?;
+                        }
+                    }
+                    40..=44 => {
+                        let prepare_rename = project.update(cx, |project, cx| {
+                            log::info!(
+                                "{}: preparing rename for buffer {} ({:?})",
+                                guest_username,
+                                buffer.read(cx).remote_id(),
+                                buffer.read(cx).file().unwrap().full_path(cx)
+                            );
+                            let offset = rng.lock().gen_range(0..=buffer.read(cx).len());
+                            project.prepare_rename(buffer, offset, cx)
+                        });
+                        let prepare_rename = cx.background().spawn(async move {
+                            prepare_rename
+                                .await
+                                .map_err(|err| anyhow!("prepare rename request failed: {:?}", err))
+                        });
+                        if rng.lock().gen_bool(0.3) {
+                            log::info!("{}: detaching prepare rename request", guest_username);
+                            cx.update(|cx| prepare_rename.detach_and_log_err(cx));
+                        } else {
+                            prepare_rename.await?;
+                        }
+                    }
+                    45..=49 => {
+                        let definitions = project.update(cx, |project, cx| {
+                            log::info!(
+                                "{}: requesting definitions for buffer {} ({:?})",
+                                guest_username,
+                                buffer.read(cx).remote_id(),
+                                buffer.read(cx).file().unwrap().full_path(cx)
+                            );
+                            let offset = rng.lock().gen_range(0..=buffer.read(cx).len());
+                            project.definition(&buffer, offset, cx)
+                        });
+                        let definitions = cx.background().spawn(async move {
+                            definitions
+                                .await
+                                .map_err(|err| anyhow!("definitions request failed: {:?}", err))
+                        });
+                        if rng.lock().gen_bool(0.3) {
+                            log::info!("{}: detaching definitions request", guest_username);
+                            cx.update(|cx| definitions.detach_and_log_err(cx));
+                        } else {
+                            client
+                                .buffers
+                                .extend(definitions.await?.into_iter().map(|loc| loc.buffer));
+                        }
+                    }
+                    50..=54 => {
+                        let highlights = project.update(cx, |project, cx| {
+                            log::info!(
+                                "{}: requesting highlights for buffer {} ({:?})",
+                                guest_username,
+                                buffer.read(cx).remote_id(),
+                                buffer.read(cx).file().unwrap().full_path(cx)
+                            );
+                            let offset = rng.lock().gen_range(0..=buffer.read(cx).len());
+                            project.document_highlights(&buffer, offset, cx)
+                        });
+                        let highlights = cx.background().spawn(async move {
+                            highlights
+                                .await
+                                .map_err(|err| anyhow!("highlights request failed: {:?}", err))
+                        });
+                        if rng.lock().gen_bool(0.3) {
+                            log::info!("{}: detaching highlights request", guest_username);
+                            cx.update(|cx| highlights.detach_and_log_err(cx));
+                        } else {
+                            highlights.await?;
+                        }
+                    }
+                    55..=59 => {
+                        let search = project.update(cx, |project, cx| {
+                            let query = rng.lock().gen_range('a'..='z');
+                            log::info!("{}: project-wide search {:?}", guest_username, query);
+                            project.search(SearchQuery::text(query, false, false), cx)
+                        });
+                        let search = cx.background().spawn(async move {
+                            search
+                                .await
+                                .map_err(|err| anyhow!("search request failed: {:?}", err))
+                        });
+                        if rng.lock().gen_bool(0.3) {
+                            log::info!("{}: detaching search request", guest_username);
+                            cx.update(|cx| search.detach_and_log_err(cx));
+                        } else {
+                            client.buffers.extend(search.await?.into_keys());
+                        }
+                    }
+                    60..=69 => {
+                        let worktree = project
+                            .read_with(cx, |project, cx| {
+                                project
+                                    .worktrees(&cx)
+                                    .filter(|worktree| {
+                                        let worktree = worktree.read(cx);
+                                        worktree.is_visible()
+                                            && worktree.entries(false).any(|e| e.is_file())
+                                            && worktree.root_entry().map_or(false, |e| e.is_dir())
+                                    })
+                                    .choose(&mut *rng.lock())
+                            })
+                            .unwrap();
+                        let (worktree_id, worktree_root_name) = worktree
+                            .read_with(cx, |worktree, _| {
+                                (worktree.id(), worktree.root_name().to_string())
+                            });
+
+                        let mut new_name = String::new();
+                        for _ in 0..10 {
+                            let letter = rng.lock().gen_range('a'..='z');
+                            new_name.push(letter);
+                        }
+                        let mut new_path = PathBuf::new();
+                        new_path.push(new_name);
+                        new_path.set_extension("rs");
+                        log::info!(
+                            "{}: creating {:?} in worktree {} ({})",
+                            guest_username,
+                            new_path,
+                            worktree_id,
+                            worktree_root_name,
+                        );
+                        project
+                            .update(cx, |project, cx| {
+                                project.create_entry((worktree_id, new_path), false, cx)
+                            })
+                            .unwrap()
+                            .await?;
+                    }
+                    _ => {
+                        buffer.update(cx, |buffer, cx| {
+                            log::info!(
+                                "{}: updating buffer {} ({:?})",
+                                guest_username,
+                                buffer.remote_id(),
+                                buffer.file().unwrap().full_path(cx)
+                            );
+                            if rng.lock().gen_bool(0.7) {
+                                buffer.randomly_edit(&mut *rng.lock(), 5, cx);
+                            } else {
+                                buffer.randomly_undo_redo(&mut *rng.lock(), cx);
+                            }
+                        });
+                    }
+                }
+                cx.background().simulate_random_delay().await;
+            }
+            Ok(())
+        }
+
+        let result = simulate_guest_internal(
+            &mut self,
+            &guest_username,
+            project.clone(),
+            op_start_signal,
+            rng,
+            &mut cx,
+        )
+        .await;
+        log::info!("{}: done", guest_username);
+
+        self.project = Some(project);
+        (self, cx, result.err())
+    }
+}
+
+impl Drop for TestClient {
+    fn drop(&mut self) {
+        self.client.tear_down();
+    }
+}
+
+impl Executor for Arc<gpui::executor::Background> {
+    type Sleep = gpui::executor::Timer;
+
+    fn spawn_detached<F: 'static + Send + Future<Output = ()>>(&self, future: F) {
+        self.spawn(future).detach();
+    }
+
+    fn sleep(&self, duration: Duration) -> Self::Sleep {
+        self.as_ref().timer(duration)
+    }
+}
+
+fn channel_messages(channel: &Channel) -> Vec<(String, String, bool)> {
+    channel
+        .messages()
+        .cursor::<()>()
+        .map(|m| {
+            (
+                m.sender.github_login.clone(),
+                m.body.clone(),
+                m.is_pending(),
+            )
+        })
+        .collect()
+}
+
+struct EmptyView;
+
+impl gpui::Entity for EmptyView {
+    type Event = ();
+}
+
+impl gpui::View for EmptyView {
+    fn ui_name() -> &'static str {
+        "empty view"
+    }
+
+    fn render(&mut self, _: &mut gpui::RenderContext<Self>) -> gpui::ElementBox {
+        gpui::Element::boxed(gpui::elements::Empty::new())
+    }
+}

crates/collab/src/lib.rs 🔗

@@ -0,0 +1,69 @@
+use axum::{http::StatusCode, response::IntoResponse};
+
+pub type Result<T, E = Error> = std::result::Result<T, E>;
+
+pub enum Error {
+    Http(StatusCode, String),
+    Internal(anyhow::Error),
+}
+
+impl From<anyhow::Error> for Error {
+    fn from(error: anyhow::Error) -> Self {
+        Self::Internal(error)
+    }
+}
+
+impl From<sqlx::Error> for Error {
+    fn from(error: sqlx::Error) -> Self {
+        Self::Internal(error.into())
+    }
+}
+
+impl From<axum::Error> for Error {
+    fn from(error: axum::Error) -> Self {
+        Self::Internal(error.into())
+    }
+}
+
+impl From<hyper::Error> for Error {
+    fn from(error: hyper::Error) -> Self {
+        Self::Internal(error.into())
+    }
+}
+
+impl From<serde_json::Error> for Error {
+    fn from(error: serde_json::Error) -> Self {
+        Self::Internal(error.into())
+    }
+}
+
+impl IntoResponse for Error {
+    fn into_response(self) -> axum::response::Response {
+        match self {
+            Error::Http(code, message) => (code, message).into_response(),
+            Error::Internal(error) => {
+                (StatusCode::INTERNAL_SERVER_ERROR, format!("{}", &error)).into_response()
+            }
+        }
+    }
+}
+
+impl std::fmt::Debug for Error {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        match self {
+            Error::Http(code, message) => (code, message).fmt(f),
+            Error::Internal(error) => error.fmt(f),
+        }
+    }
+}
+
+impl std::fmt::Display for Error {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        match self {
+            Error::Http(code, message) => write!(f, "{code}: {message}"),
+            Error::Internal(error) => error.fmt(f),
+        }
+    }
+}
+
+impl std::error::Error for Error {}

crates/collab/src/main.rs 🔗

@@ -4,7 +4,11 @@ mod db;
 mod env;
 mod rpc;
 
-use axum::{body::Body, http::StatusCode, response::IntoResponse, Router};
+#[cfg(test)]
+mod integration_tests;
+
+use axum::{body::Body, Router};
+use collab::{Error, Result};
 use db::{Db, PostgresDb};
 use serde::Deserialize;
 use std::{
@@ -73,74 +77,6 @@ async fn main() -> Result<()> {
     Ok(())
 }
 
-pub type Result<T, E = Error> = std::result::Result<T, E>;
-
-pub enum Error {
-    Http(StatusCode, String),
-    Internal(anyhow::Error),
-}
-
-impl From<anyhow::Error> for Error {
-    fn from(error: anyhow::Error) -> Self {
-        Self::Internal(error)
-    }
-}
-
-impl From<sqlx::Error> for Error {
-    fn from(error: sqlx::Error) -> Self {
-        Self::Internal(error.into())
-    }
-}
-
-impl From<axum::Error> for Error {
-    fn from(error: axum::Error) -> Self {
-        Self::Internal(error.into())
-    }
-}
-
-impl From<hyper::Error> for Error {
-    fn from(error: hyper::Error) -> Self {
-        Self::Internal(error.into())
-    }
-}
-
-impl From<serde_json::Error> for Error {
-    fn from(error: serde_json::Error) -> Self {
-        Self::Internal(error.into())
-    }
-}
-
-impl IntoResponse for Error {
-    fn into_response(self) -> axum::response::Response {
-        match self {
-            Error::Http(code, message) => (code, message).into_response(),
-            Error::Internal(error) => {
-                (StatusCode::INTERNAL_SERVER_ERROR, format!("{}", &error)).into_response()
-            }
-        }
-    }
-}
-
-impl std::fmt::Debug for Error {
-    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
-        match self {
-            Error::Http(code, message) => (code, message).fmt(f),
-            Error::Internal(error) => error.fmt(f),
-        }
-    }
-}
-
-impl std::fmt::Display for Error {
-    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
-        match self {
-            Error::Http(code, message) => write!(f, "{code}: {message}"),
-            Error::Internal(error) => error.fmt(f),
-        }
-    }
-}
-
-impl std::error::Error for Error {}
-
 pub fn init_tracing(config: &Config) -> Option<()> {
     use opentelemetry::KeyValue;
     use opentelemetry_otlp::WithExportConfig;

crates/collab/src/rpc.rs 🔗

@@ -47,7 +47,6 @@ use std::{
     },
     time::Duration,
 };
-use store::{Store, Worktree};
 use time::OffsetDateTime;
 use tokio::{
     sync::{RwLock, RwLockReadGuard, RwLockWriteGuard},
@@ -56,6 +55,8 @@ use tokio::{
 use tower::ServiceBuilder;
 use tracing::{info_span, instrument, Instrument};
 
+pub use store::{Store, Worktree};
+
 type MessageHandler =
     Box<dyn Send + Sync + Fn(Arc<Server>, Box<dyn AnyTypedEnvelope>) -> BoxFuture<'static, ()>>;
 
@@ -80,13 +81,12 @@ impl<R: RequestMessage> Response<R> {
 
 pub struct Server {
     peer: Arc<Peer>,
-    store: RwLock<Store>,
+    pub(crate) store: RwLock<Store>,
     app_state: Arc<AppState>,
     handlers: HashMap<TypeId, MessageHandler>,
     notifications: Option<mpsc::UnboundedSender<()>>,
 }
 
-
 pub trait Executor: Send + Clone {
     type Sleep: Send + Future;
     fn spawn_detached<F: 'static + Send + Future<Output = ()>>(&self, future: F);
@@ -120,11 +120,10 @@ pub fn serialize_deref<S, T, U>(value: &T, serializer: S) -> Result<S::Ok, S::Er
 where
     S: Serializer,
     T: Deref<Target = U>,
-    U: Serialize
+    U: Serialize,
 {
     Serialize::serialize(value.deref(), serializer)
 }
-  
 
 impl Server {
     pub fn new(
@@ -314,7 +313,7 @@ impl Server {
                 let mut store = this.store_mut().await;
                 store.add_connection(connection_id, user_id);
                 this.peer.send(connection_id, store.build_initial_contacts_update(contacts))?;
-                
+
                 if let Some((code, count)) = invite_code {
                     this.peer.send(connection_id, proto::UpdateInviteInfo {
                         url: format!("{}{}", this.app_state.invite_link_prefix, code),
@@ -437,19 +436,29 @@ impl Server {
         Ok(())
     }
 
-    pub async fn invite_code_redeemed(self: &Arc<Self>, code: &str, invitee_id: UserId) -> Result<()> {
+    pub async fn invite_code_redeemed(
+        self: &Arc<Self>,
+        code: &str,
+        invitee_id: UserId,
+    ) -> Result<()> {
         let user = self.app_state.db.get_user_for_invite_code(code).await?;
         let store = self.store().await;
         let invitee_contact = store.contact_for_user(invitee_id, true);
         for connection_id in store.connection_ids_for_user(user.id) {
-            self.peer.send(connection_id, proto::UpdateContacts {
-                contacts: vec![invitee_contact.clone()],
-                ..Default::default()
-            })?;
-            self.peer.send(connection_id, proto::UpdateInviteInfo {
-                url: format!("{}{}", self.app_state.invite_link_prefix, code),
-                count: user.invite_count as u32,
-            })?;
+            self.peer.send(
+                connection_id,
+                proto::UpdateContacts {
+                    contacts: vec![invitee_contact.clone()],
+                    ..Default::default()
+                },
+            )?;
+            self.peer.send(
+                connection_id,
+                proto::UpdateInviteInfo {
+                    url: format!("{}{}", self.app_state.invite_link_prefix, code),
+                    count: user.invite_count as u32,
+                },
+            )?;
         }
         Ok(())
     }
@@ -1489,11 +1498,11 @@ impl Server {
             _not_send: PhantomData,
         }
     }
-    
+
     pub async fn snapshot<'a>(self: &'a Arc<Self>) -> ServerSnapshot<'a> {
         ServerSnapshot {
             store: self.store.read().await,
-            peer: &self.peer
+            peer: &self.peer,
         }
     }
 }
@@ -1684,5860 +1693,3 @@ where
         }
     }
 }
-
-#[cfg(test)]
-mod tests {
-    use super::*;
-    use crate::{
-        db::{tests::TestDb, UserId},
-        AppState,
-    };
-    use ::rpc::Peer;
-    use client::{
-        self, test::FakeHttpClient, Channel, ChannelDetails, ChannelList, Client, Credentials,
-        EstablishConnectionError, UserStore, RECEIVE_TIMEOUT,
-    };
-    use collections::{BTreeMap, HashSet};
-    use editor::{
-        self, ConfirmCodeAction, ConfirmCompletion, ConfirmRename, Editor, Input, Redo, Rename,
-        ToOffset, ToggleCodeActions, Undo,
-    };
-    use gpui::{
-        executor::{self, Deterministic},
-        geometry::vector::vec2f,
-        ModelHandle, Task, TestAppContext, ViewHandle,
-    };
-    use language::{
-        range_to_lsp, tree_sitter_rust, Diagnostic, DiagnosticEntry, FakeLspAdapter, Language,
-        LanguageConfig, LanguageRegistry, OffsetRangeExt, Point, Rope,
-    };
-    use lsp::{self, FakeLanguageServer};
-    use parking_lot::Mutex;
-    use project::{
-        fs::{FakeFs, Fs as _},
-        search::SearchQuery,
-        worktree::WorktreeHandle,
-        DiagnosticSummary, Project, ProjectPath, WorktreeId,
-    };
-    use rand::prelude::*;
-    use rpc::PeerId;
-    use serde_json::json;
-    use settings::Settings;
-    use sqlx::types::time::OffsetDateTime;
-    use std::{
-        cell::RefCell,
-        env,
-        ops::Deref,
-        path::{Path, PathBuf},
-        rc::Rc,
-        sync::{
-            atomic::{AtomicBool, Ordering::SeqCst},
-            Arc,
-        },
-        time::Duration,
-    };
-    use theme::ThemeRegistry;
-    use workspace::{Item, SplitDirection, ToggleFollow, Workspace};
-
-    #[cfg(test)]
-    #[ctor::ctor]
-    fn init_logger() {
-        if std::env::var("RUST_LOG").is_ok() {
-            env_logger::init();
-        }
-    }
-
-    #[gpui::test(iterations = 10)]
-    async fn test_share_project(
-        deterministic: Arc<Deterministic>,
-        cx_a: &mut TestAppContext,
-        cx_b: &mut TestAppContext,
-        cx_b2: &mut TestAppContext,
-    ) {
-        let (window_b, _) = cx_b.add_window(|_| EmptyView);
-        let lang_registry = Arc::new(LanguageRegistry::test());
-        let fs = FakeFs::new(cx_a.background());
-        cx_a.foreground().forbid_parking();
-
-        // Connect to a server as 2 clients.
-        let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
-        let client_a = server.create_client(cx_a, "user_a").await;
-        let mut client_b = server.create_client(cx_b, "user_b").await;
-        server
-            .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)])
-            .await;
-
-        // Share a project as client A
-        fs.insert_tree(
-            "/a",
-            json!({
-                ".gitignore": "ignored-dir",
-                "a.txt": "a-contents",
-                "b.txt": "b-contents",
-                "ignored-dir": {
-                    "c.txt": "",
-                    "d.txt": "",
-                }
-            }),
-        )
-        .await;
-        let project_a = cx_a.update(|cx| {
-            Project::local(
-                client_a.clone(),
-                client_a.user_store.clone(),
-                lang_registry.clone(),
-                fs.clone(),
-                cx,
-            )
-        });
-        let project_id = project_a
-            .read_with(cx_a, |project, _| project.next_remote_id())
-            .await;
-        let (worktree_a, _) = project_a
-            .update(cx_a, |p, cx| {
-                p.find_or_create_local_worktree("/a", true, cx)
-            })
-            .await
-            .unwrap();
-        let worktree_id = worktree_a.read_with(cx_a, |tree, _| tree.id());
-        worktree_a
-            .read_with(cx_a, |tree, _| tree.as_local().unwrap().scan_complete())
-            .await;
-
-        // Join that project as client B
-        let client_b_peer_id = client_b.peer_id;
-        let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await;
-        let replica_id_b = project_b.read_with(cx_b, |project, _| {
-            assert_eq!(
-                project
-                    .collaborators()
-                    .get(&client_a.peer_id)
-                    .unwrap()
-                    .user
-                    .github_login,
-                "user_a"
-            );
-            project.replica_id()
-        });
-
-        deterministic.run_until_parked();
-        project_a.read_with(cx_a, |project, _| {
-            let client_b_collaborator = project.collaborators().get(&client_b_peer_id).unwrap();
-            assert_eq!(client_b_collaborator.replica_id, replica_id_b);
-            assert_eq!(client_b_collaborator.user.github_login, "user_b");
-        });
-        project_b.read_with(cx_b, |project, cx| {
-            let worktree = project.worktrees(cx).next().unwrap().read(cx);
-            assert_eq!(
-                worktree.paths().map(AsRef::as_ref).collect::<Vec<_>>(),
-                [
-                    Path::new(".gitignore"),
-                    Path::new("a.txt"),
-                    Path::new("b.txt"),
-                    Path::new("ignored-dir"),
-                    Path::new("ignored-dir/c.txt"),
-                    Path::new("ignored-dir/d.txt"),
-                ]
-            );
-        });
-
-        // Open the same file as client B and client A.
-        let buffer_b = project_b
-            .update(cx_b, |p, cx| p.open_buffer((worktree_id, "b.txt"), cx))
-            .await
-            .unwrap();
-        buffer_b.read_with(cx_b, |buf, _| assert_eq!(buf.text(), "b-contents"));
-        project_a.read_with(cx_a, |project, cx| {
-            assert!(project.has_open_buffer((worktree_id, "b.txt"), cx))
-        });
-        let buffer_a = project_a
-            .update(cx_a, |p, cx| p.open_buffer((worktree_id, "b.txt"), cx))
-            .await
-            .unwrap();
-
-        let editor_b = cx_b.add_view(window_b, |cx| Editor::for_buffer(buffer_b, None, cx));
-
-        // TODO
-        // // Create a selection set as client B and see that selection set as client A.
-        // buffer_a
-        //     .condition(&cx_a, |buffer, _| buffer.selection_sets().count() == 1)
-        //     .await;
-
-        // Edit the buffer as client B and see that edit as client A.
-        editor_b.update(cx_b, |editor, cx| {
-            editor.handle_input(&Input("ok, ".into()), cx)
-        });
-        buffer_a
-            .condition(&cx_a, |buffer, _| buffer.text() == "ok, b-contents")
-            .await;
-
-        // TODO
-        // // Remove the selection set as client B, see those selections disappear as client A.
-        cx_b.update(move |_| drop(editor_b));
-        // buffer_a
-        //     .condition(&cx_a, |buffer, _| buffer.selection_sets().count() == 0)
-        //     .await;
-
-        // Client B can join again on a different window because they are already a participant.
-        let client_b2 = server.create_client(cx_b2, "user_b").await;
-        let project_b2 = Project::remote(
-            project_id,
-            client_b2.client.clone(),
-            client_b2.user_store.clone(),
-            lang_registry.clone(),
-            FakeFs::new(cx_b2.background()),
-            &mut cx_b2.to_async(),
-        )
-        .await
-        .unwrap();
-        deterministic.run_until_parked();
-        project_a.read_with(cx_a, |project, _| {
-            assert_eq!(project.collaborators().len(), 2);
-        });
-        project_b.read_with(cx_b, |project, _| {
-            assert_eq!(project.collaborators().len(), 2);
-        });
-        project_b2.read_with(cx_b2, |project, _| {
-            assert_eq!(project.collaborators().len(), 2);
-        });
-
-        // Dropping client B's first project removes only that from client A's collaborators.
-        cx_b.update(move |_| {
-            drop(client_b.project.take());
-            drop(project_b);
-        });
-        deterministic.run_until_parked();
-        project_a.read_with(cx_a, |project, _| {
-            assert_eq!(project.collaborators().len(), 1);
-        });
-        project_b2.read_with(cx_b2, |project, _| {
-            assert_eq!(project.collaborators().len(), 1);
-        });
-    }
-
-    #[gpui::test(iterations = 10)]
-    async fn test_unshare_project(
-        deterministic: Arc<Deterministic>,
-        cx_a: &mut TestAppContext,
-        cx_b: &mut TestAppContext,
-    ) {
-        let lang_registry = Arc::new(LanguageRegistry::test());
-        let fs = FakeFs::new(cx_a.background());
-        cx_a.foreground().forbid_parking();
-
-        // Connect to a server as 2 clients.
-        let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
-        let client_a = server.create_client(cx_a, "user_a").await;
-        let mut client_b = server.create_client(cx_b, "user_b").await;
-        server
-            .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)])
-            .await;
-
-        // Share a project as client A
-        fs.insert_tree(
-            "/a",
-            json!({
-                "a.txt": "a-contents",
-                "b.txt": "b-contents",
-            }),
-        )
-        .await;
-        let project_a = cx_a.update(|cx| {
-            Project::local(
-                client_a.clone(),
-                client_a.user_store.clone(),
-                lang_registry.clone(),
-                fs.clone(),
-                cx,
-            )
-        });
-        let (worktree_a, _) = project_a
-            .update(cx_a, |p, cx| {
-                p.find_or_create_local_worktree("/a", true, cx)
-            })
-            .await
-            .unwrap();
-        worktree_a
-            .read_with(cx_a, |tree, _| tree.as_local().unwrap().scan_complete())
-            .await;
-        let worktree_id = worktree_a.read_with(cx_a, |tree, _| tree.id());
-
-        // Join that project as client B
-        let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await;
-        assert!(worktree_a.read_with(cx_a, |tree, _| tree.as_local().unwrap().is_shared()));
-        project_b
-            .update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx))
-            .await
-            .unwrap();
-
-        // When client B leaves the project, it gets automatically unshared.
-        cx_b.update(|_| {
-            drop(client_b.project.take());
-            drop(project_b);
-        });
-        deterministic.run_until_parked();
-        assert!(worktree_a.read_with(cx_a, |tree, _| !tree.as_local().unwrap().is_shared()));
-
-        // When client B joins again, the project gets re-shared.
-        let project_b2 = client_b.build_remote_project(&project_a, cx_a, cx_b).await;
-        assert!(worktree_a.read_with(cx_a, |tree, _| tree.as_local().unwrap().is_shared()));
-        project_b2
-            .update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx))
-            .await
-            .unwrap();
-
-        // When client A (the host) leaves, the project gets unshared and guests are notified.
-        cx_a.update(|_| drop(project_a));
-        deterministic.run_until_parked();
-        project_b2.read_with(cx_b, |project, _| {
-            assert!(project.is_read_only());
-            assert!(project.collaborators().is_empty());
-        });
-    }
-
-    #[gpui::test(iterations = 10)]
-    async fn test_host_disconnect(
-        deterministic: Arc<Deterministic>,
-        cx_a: &mut TestAppContext,
-        cx_b: &mut TestAppContext,
-        cx_c: &mut TestAppContext,
-    ) {
-        let lang_registry = Arc::new(LanguageRegistry::test());
-        let fs = FakeFs::new(cx_a.background());
-        cx_a.foreground().forbid_parking();
-
-        // Connect to a server as 3 clients.
-        let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
-        let client_a = server.create_client(cx_a, "user_a").await;
-        let mut client_b = server.create_client(cx_b, "user_b").await;
-        let client_c = server.create_client(cx_c, "user_c").await;
-        server
-            .make_contacts(vec![
-                (&client_a, cx_a),
-                (&client_b, cx_b),
-                (&client_c, cx_c),
-            ])
-            .await;
-
-        // Share a project as client A
-        fs.insert_tree(
-            "/a",
-            json!({
-                "a.txt": "a-contents",
-                "b.txt": "b-contents",
-            }),
-        )
-        .await;
-        let project_a = cx_a.update(|cx| {
-            Project::local(
-                client_a.clone(),
-                client_a.user_store.clone(),
-                lang_registry.clone(),
-                fs.clone(),
-                cx,
-            )
-        });
-        let project_id = project_a
-            .read_with(cx_a, |project, _| project.next_remote_id())
-            .await;
-        let (worktree_a, _) = project_a
-            .update(cx_a, |p, cx| {
-                p.find_or_create_local_worktree("/a", true, cx)
-            })
-            .await
-            .unwrap();
-        worktree_a
-            .read_with(cx_a, |tree, _| tree.as_local().unwrap().scan_complete())
-            .await;
-        let worktree_id = worktree_a.read_with(cx_a, |tree, _| tree.id());
-
-        // Join that project as client B
-        let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await;
-        assert!(worktree_a.read_with(cx_a, |tree, _| tree.as_local().unwrap().is_shared()));
-        project_b
-            .update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx))
-            .await
-            .unwrap();
-
-        // Request to join that project as client C
-        let project_c = cx_c.spawn(|mut cx| {
-            let client = client_c.client.clone();
-            let user_store = client_c.user_store.clone();
-            let lang_registry = lang_registry.clone();
-            async move {
-                Project::remote(
-                    project_id,
-                    client,
-                    user_store,
-                    lang_registry.clone(),
-                    FakeFs::new(cx.background()),
-                    &mut cx,
-                )
-                .await
-            }
-        });
-        deterministic.run_until_parked();
-
-        // Drop client A's connection. Collaborators should disappear and the project should not be shown as shared.
-        server.disconnect_client(client_a.current_user_id(cx_a));
-        cx_a.foreground().advance_clock(rpc::RECEIVE_TIMEOUT);
-        project_a
-            .condition(cx_a, |project, _| project.collaborators().is_empty())
-            .await;
-        project_a.read_with(cx_a, |project, _| assert!(!project.is_shared()));
-        project_b
-            .condition(cx_b, |project, _| project.is_read_only())
-            .await;
-        assert!(worktree_a.read_with(cx_a, |tree, _| !tree.as_local().unwrap().is_shared()));
-        cx_b.update(|_| {
-            drop(project_b);
-        });
-        assert!(matches!(
-            project_c.await.unwrap_err(),
-            project::JoinProjectError::HostWentOffline
-        ));
-
-        // Ensure guests can still join.
-        let project_b2 = client_b.build_remote_project(&project_a, cx_a, cx_b).await;
-        assert!(worktree_a.read_with(cx_a, |tree, _| tree.as_local().unwrap().is_shared()));
-        project_b2
-            .update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx))
-            .await
-            .unwrap();
-    }
-
-    #[gpui::test(iterations = 10)]
-    async fn test_decline_join_request(
-        deterministic: Arc<Deterministic>,
-        cx_a: &mut TestAppContext,
-        cx_b: &mut TestAppContext,
-    ) {
-        let lang_registry = Arc::new(LanguageRegistry::test());
-        let fs = FakeFs::new(cx_a.background());
-        cx_a.foreground().forbid_parking();
-
-        // Connect to a server as 2 clients.
-        let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
-        let client_a = server.create_client(cx_a, "user_a").await;
-        let client_b = server.create_client(cx_b, "user_b").await;
-        server
-            .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)])
-            .await;
-
-        // Share a project as client A
-        fs.insert_tree("/a", json!({})).await;
-        let project_a = cx_a.update(|cx| {
-            Project::local(
-                client_a.clone(),
-                client_a.user_store.clone(),
-                lang_registry.clone(),
-                fs.clone(),
-                cx,
-            )
-        });
-        let project_id = project_a
-            .read_with(cx_a, |project, _| project.next_remote_id())
-            .await;
-        let (worktree_a, _) = project_a
-            .update(cx_a, |p, cx| {
-                p.find_or_create_local_worktree("/a", true, cx)
-            })
-            .await
-            .unwrap();
-        worktree_a
-            .read_with(cx_a, |tree, _| tree.as_local().unwrap().scan_complete())
-            .await;
-
-        // Request to join that project as client B
-        let project_b = cx_b.spawn(|mut cx| {
-            let client = client_b.client.clone();
-            let user_store = client_b.user_store.clone();
-            let lang_registry = lang_registry.clone();
-            async move {
-                Project::remote(
-                    project_id,
-                    client,
-                    user_store,
-                    lang_registry.clone(),
-                    FakeFs::new(cx.background()),
-                    &mut cx,
-                )
-                .await
-            }
-        });
-        deterministic.run_until_parked();
-        project_a.update(cx_a, |project, cx| {
-            project.respond_to_join_request(client_b.user_id().unwrap(), false, cx)
-        });
-        assert!(matches!(
-            project_b.await.unwrap_err(),
-            project::JoinProjectError::HostDeclined
-        ));
-
-        // Request to join the project again as client B
-        let project_b = cx_b.spawn(|mut cx| {
-            let client = client_b.client.clone();
-            let user_store = client_b.user_store.clone();
-            let lang_registry = lang_registry.clone();
-            async move {
-                Project::remote(
-                    project_id,
-                    client,
-                    user_store,
-                    lang_registry.clone(),
-                    FakeFs::new(cx.background()),
-                    &mut cx,
-                )
-                .await
-            }
-        });
-
-        // Close the project on the host
-        deterministic.run_until_parked();
-        cx_a.update(|_| drop(project_a));
-        deterministic.run_until_parked();
-        assert!(matches!(
-            project_b.await.unwrap_err(),
-            project::JoinProjectError::HostClosedProject
-        ));
-    }
-
-    #[gpui::test(iterations = 10)]
-    async fn test_cancel_join_request(
-        deterministic: Arc<Deterministic>,
-        cx_a: &mut TestAppContext,
-        cx_b: &mut TestAppContext,
-    ) {
-        let lang_registry = Arc::new(LanguageRegistry::test());
-        let fs = FakeFs::new(cx_a.background());
-        cx_a.foreground().forbid_parking();
-
-        // Connect to a server as 2 clients.
-        let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
-        let client_a = server.create_client(cx_a, "user_a").await;
-        let client_b = server.create_client(cx_b, "user_b").await;
-        server
-            .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)])
-            .await;
-
-        // Share a project as client A
-        fs.insert_tree("/a", json!({})).await;
-        let project_a = cx_a.update(|cx| {
-            Project::local(
-                client_a.clone(),
-                client_a.user_store.clone(),
-                lang_registry.clone(),
-                fs.clone(),
-                cx,
-            )
-        });
-        let project_id = project_a
-            .read_with(cx_a, |project, _| project.next_remote_id())
-            .await;
-
-        let project_a_events = Rc::new(RefCell::new(Vec::new()));
-        let user_b = client_a
-            .user_store
-            .update(cx_a, |store, cx| {
-                store.fetch_user(client_b.user_id().unwrap(), cx)
-            })
-            .await
-            .unwrap();
-        project_a.update(cx_a, {
-            let project_a_events = project_a_events.clone();
-            move |_, cx| {
-                cx.subscribe(&cx.handle(), move |_, _, event, _| {
-                    project_a_events.borrow_mut().push(event.clone());
-                })
-                .detach();
-            }
-        });
-
-        let (worktree_a, _) = project_a
-            .update(cx_a, |p, cx| {
-                p.find_or_create_local_worktree("/a", true, cx)
-            })
-            .await
-            .unwrap();
-        worktree_a
-            .read_with(cx_a, |tree, _| tree.as_local().unwrap().scan_complete())
-            .await;
-
-        // Request to join that project as client B
-        let project_b = cx_b.spawn(|mut cx| {
-            let client = client_b.client.clone();
-            let user_store = client_b.user_store.clone();
-            let lang_registry = lang_registry.clone();
-            async move {
-                Project::remote(
-                    project_id,
-                    client,
-                    user_store,
-                    lang_registry.clone(),
-                    FakeFs::new(cx.background()),
-                    &mut cx,
-                )
-                .await
-            }
-        });
-        deterministic.run_until_parked();
-        assert_eq!(
-            &*project_a_events.borrow(),
-            &[project::Event::ContactRequestedJoin(user_b.clone())]
-        );
-        project_a_events.borrow_mut().clear();
-
-        // Cancel the join request by leaving the project
-        client_b
-            .client
-            .send(proto::LeaveProject { project_id })
-            .unwrap();
-        drop(project_b);
-
-        deterministic.run_until_parked();
-        assert_eq!(
-            &*project_a_events.borrow(),
-            &[project::Event::ContactCancelledJoinRequest(user_b.clone())]
-        );
-    }
-
-    #[gpui::test(iterations = 10)]
-    async fn test_propagate_saves_and_fs_changes(
-        cx_a: &mut TestAppContext,
-        cx_b: &mut TestAppContext,
-        cx_c: &mut TestAppContext,
-    ) {
-        let lang_registry = Arc::new(LanguageRegistry::test());
-        let fs = FakeFs::new(cx_a.background());
-        cx_a.foreground().forbid_parking();
-
-        // Connect to a server as 3 clients.
-        let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
-        let client_a = server.create_client(cx_a, "user_a").await;
-        let mut client_b = server.create_client(cx_b, "user_b").await;
-        let mut client_c = server.create_client(cx_c, "user_c").await;
-        server
-            .make_contacts(vec![
-                (&client_a, cx_a),
-                (&client_b, cx_b),
-                (&client_c, cx_c),
-            ])
-            .await;
-
-        // Share a worktree as client A.
-        fs.insert_tree(
-            "/a",
-            json!({
-                "file1": "",
-                "file2": ""
-            }),
-        )
-        .await;
-        let project_a = cx_a.update(|cx| {
-            Project::local(
-                client_a.clone(),
-                client_a.user_store.clone(),
-                lang_registry.clone(),
-                fs.clone(),
-                cx,
-            )
-        });
-        let (worktree_a, _) = project_a
-            .update(cx_a, |p, cx| {
-                p.find_or_create_local_worktree("/a", true, cx)
-            })
-            .await
-            .unwrap();
-        worktree_a
-            .read_with(cx_a, |tree, _| tree.as_local().unwrap().scan_complete())
-            .await;
-        let worktree_id = worktree_a.read_with(cx_a, |tree, _| tree.id());
-
-        // Join that worktree as clients B and C.
-        let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await;
-        let project_c = client_c.build_remote_project(&project_a, cx_a, cx_c).await;
-        let worktree_b = project_b.read_with(cx_b, |p, cx| p.worktrees(cx).next().unwrap());
-        let worktree_c = project_c.read_with(cx_c, |p, cx| p.worktrees(cx).next().unwrap());
-
-        // Open and edit a buffer as both guests B and C.
-        let buffer_b = project_b
-            .update(cx_b, |p, cx| p.open_buffer((worktree_id, "file1"), cx))
-            .await
-            .unwrap();
-        let buffer_c = project_c
-            .update(cx_c, |p, cx| p.open_buffer((worktree_id, "file1"), cx))
-            .await
-            .unwrap();
-        buffer_b.update(cx_b, |buf, cx| buf.edit([(0..0, "i-am-b, ")], cx));
-        buffer_c.update(cx_c, |buf, cx| buf.edit([(0..0, "i-am-c, ")], cx));
-
-        // Open and edit that buffer as the host.
-        let buffer_a = project_a
-            .update(cx_a, |p, cx| p.open_buffer((worktree_id, "file1"), cx))
-            .await
-            .unwrap();
-
-        buffer_a
-            .condition(cx_a, |buf, _| buf.text() == "i-am-c, i-am-b, ")
-            .await;
-        buffer_a.update(cx_a, |buf, cx| {
-            buf.edit([(buf.len()..buf.len(), "i-am-a")], cx)
-        });
-
-        // Wait for edits to propagate
-        buffer_a
-            .condition(cx_a, |buf, _| buf.text() == "i-am-c, i-am-b, i-am-a")
-            .await;
-        buffer_b
-            .condition(cx_b, |buf, _| buf.text() == "i-am-c, i-am-b, i-am-a")
-            .await;
-        buffer_c
-            .condition(cx_c, |buf, _| buf.text() == "i-am-c, i-am-b, i-am-a")
-            .await;
-
-        // Edit the buffer as the host and concurrently save as guest B.
-        let save_b = buffer_b.update(cx_b, |buf, cx| buf.save(cx));
-        buffer_a.update(cx_a, |buf, cx| buf.edit([(0..0, "hi-a, ")], cx));
-        save_b.await.unwrap();
-        assert_eq!(
-            fs.load("/a/file1".as_ref()).await.unwrap(),
-            "hi-a, i-am-c, i-am-b, i-am-a"
-        );
-        buffer_a.read_with(cx_a, |buf, _| assert!(!buf.is_dirty()));
-        buffer_b.read_with(cx_b, |buf, _| assert!(!buf.is_dirty()));
-        buffer_c.condition(cx_c, |buf, _| !buf.is_dirty()).await;
-
-        worktree_a.flush_fs_events(cx_a).await;
-
-        // Make changes on host's file system, see those changes on guest worktrees.
-        fs.rename(
-            "/a/file1".as_ref(),
-            "/a/file1-renamed".as_ref(),
-            Default::default(),
-        )
-        .await
-        .unwrap();
-
-        fs.rename("/a/file2".as_ref(), "/a/file3".as_ref(), Default::default())
-            .await
-            .unwrap();
-        fs.insert_file(Path::new("/a/file4"), "4".into()).await;
-
-        worktree_a
-            .condition(&cx_a, |tree, _| {
-                tree.paths()
-                    .map(|p| p.to_string_lossy())
-                    .collect::<Vec<_>>()
-                    == ["file1-renamed", "file3", "file4"]
-            })
-            .await;
-        worktree_b
-            .condition(&cx_b, |tree, _| {
-                tree.paths()
-                    .map(|p| p.to_string_lossy())
-                    .collect::<Vec<_>>()
-                    == ["file1-renamed", "file3", "file4"]
-            })
-            .await;
-        worktree_c
-            .condition(&cx_c, |tree, _| {
-                tree.paths()
-                    .map(|p| p.to_string_lossy())
-                    .collect::<Vec<_>>()
-                    == ["file1-renamed", "file3", "file4"]
-            })
-            .await;
-
-        // Ensure buffer files are updated as well.
-        buffer_a
-            .condition(&cx_a, |buf, _| {
-                buf.file().unwrap().path().to_str() == Some("file1-renamed")
-            })
-            .await;
-        buffer_b
-            .condition(&cx_b, |buf, _| {
-                buf.file().unwrap().path().to_str() == Some("file1-renamed")
-            })
-            .await;
-        buffer_c
-            .condition(&cx_c, |buf, _| {
-                buf.file().unwrap().path().to_str() == Some("file1-renamed")
-            })
-            .await;
-    }
-
-    #[gpui::test(iterations = 10)]
-    async fn test_fs_operations(
-        executor: Arc<Deterministic>,
-        cx_a: &mut TestAppContext,
-        cx_b: &mut TestAppContext,
-    ) {
-        executor.forbid_parking();
-        let fs = FakeFs::new(cx_a.background());
-
-        // Connect to a server as 2 clients.
-        let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
-        let mut client_a = server.create_client(cx_a, "user_a").await;
-        let mut client_b = server.create_client(cx_b, "user_b").await;
-        server
-            .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)])
-            .await;
-
-        // Share a project as client A
-        fs.insert_tree(
-            "/dir",
-            json!({
-                "a.txt": "a-contents",
-                "b.txt": "b-contents",
-            }),
-        )
-        .await;
-
-        let (project_a, worktree_id) = client_a.build_local_project(fs, "/dir", cx_a).await;
-        let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await;
-
-        let worktree_a =
-            project_a.read_with(cx_a, |project, cx| project.worktrees(cx).next().unwrap());
-        let worktree_b =
-            project_b.read_with(cx_b, |project, cx| project.worktrees(cx).next().unwrap());
-
-        let entry = project_b
-            .update(cx_b, |project, cx| {
-                project
-                    .create_entry((worktree_id, "c.txt"), false, cx)
-                    .unwrap()
-            })
-            .await
-            .unwrap();
-        worktree_a.read_with(cx_a, |worktree, _| {
-            assert_eq!(
-                worktree
-                    .paths()
-                    .map(|p| p.to_string_lossy())
-                    .collect::<Vec<_>>(),
-                ["a.txt", "b.txt", "c.txt"]
-            );
-        });
-        worktree_b.read_with(cx_b, |worktree, _| {
-            assert_eq!(
-                worktree
-                    .paths()
-                    .map(|p| p.to_string_lossy())
-                    .collect::<Vec<_>>(),
-                ["a.txt", "b.txt", "c.txt"]
-            );
-        });
-
-        project_b
-            .update(cx_b, |project, cx| {
-                project.rename_entry(entry.id, Path::new("d.txt"), cx)
-            })
-            .unwrap()
-            .await
-            .unwrap();
-        worktree_a.read_with(cx_a, |worktree, _| {
-            assert_eq!(
-                worktree
-                    .paths()
-                    .map(|p| p.to_string_lossy())
-                    .collect::<Vec<_>>(),
-                ["a.txt", "b.txt", "d.txt"]
-            );
-        });
-        worktree_b.read_with(cx_b, |worktree, _| {
-            assert_eq!(
-                worktree
-                    .paths()
-                    .map(|p| p.to_string_lossy())
-                    .collect::<Vec<_>>(),
-                ["a.txt", "b.txt", "d.txt"]
-            );
-        });
-
-        let dir_entry = project_b
-            .update(cx_b, |project, cx| {
-                project
-                    .create_entry((worktree_id, "DIR"), true, cx)
-                    .unwrap()
-            })
-            .await
-            .unwrap();
-        worktree_a.read_with(cx_a, |worktree, _| {
-            assert_eq!(
-                worktree
-                    .paths()
-                    .map(|p| p.to_string_lossy())
-                    .collect::<Vec<_>>(),
-                ["DIR", "a.txt", "b.txt", "d.txt"]
-            );
-        });
-        worktree_b.read_with(cx_b, |worktree, _| {
-            assert_eq!(
-                worktree
-                    .paths()
-                    .map(|p| p.to_string_lossy())
-                    .collect::<Vec<_>>(),
-                ["DIR", "a.txt", "b.txt", "d.txt"]
-            );
-        });
-
-        project_b
-            .update(cx_b, |project, cx| {
-                project
-                    .create_entry((worktree_id, "DIR/e.txt"), false, cx)
-                    .unwrap()
-            })
-            .await
-            .unwrap();
-        project_b
-            .update(cx_b, |project, cx| {
-                project
-                    .create_entry((worktree_id, "DIR/SUBDIR"), true, cx)
-                    .unwrap()
-            })
-            .await
-            .unwrap();
-        project_b
-            .update(cx_b, |project, cx| {
-                project
-                    .create_entry((worktree_id, "DIR/SUBDIR/f.txt"), false, cx)
-                    .unwrap()
-            })
-            .await
-            .unwrap();
-        worktree_a.read_with(cx_a, |worktree, _| {
-            assert_eq!(
-                worktree
-                .paths()
-                .map(|p| p.to_string_lossy())
-                .collect::<Vec<_>>(),
-                ["DIR", "DIR/SUBDIR", "DIR/SUBDIR/f.txt", "DIR/e.txt", "a.txt", "b.txt", "d.txt"]
-            );
-        });
-        worktree_b.read_with(cx_b, |worktree, _| {
-            assert_eq!(
-                worktree
-                .paths()
-                .map(|p| p.to_string_lossy())
-                .collect::<Vec<_>>(),
-                ["DIR", "DIR/SUBDIR", "DIR/SUBDIR/f.txt", "DIR/e.txt", "a.txt", "b.txt", "d.txt"]
-            );
-        });
-
-        project_b
-            .update(cx_b, |project, cx| {
-                project
-                    .copy_entry(dir_entry.id, Path::new("DIR2"), cx)
-                    .unwrap()
-            })
-            .await
-            .unwrap();
-        worktree_a.read_with(cx_a, |worktree, _| {
-            assert_eq!(
-                worktree
-                .paths()
-                .map(|p| p.to_string_lossy())
-                .collect::<Vec<_>>(),
-                ["DIR", "DIR/SUBDIR", "DIR/SUBDIR/f.txt", "DIR/e.txt", "DIR2", "DIR2/SUBDIR", "DIR2/SUBDIR/f.txt", "DIR2/e.txt", "a.txt", "b.txt", "d.txt"]
-            );
-        });
-        worktree_b.read_with(cx_b, |worktree, _| {
-            assert_eq!(
-                worktree
-                .paths()
-                .map(|p| p.to_string_lossy())
-                .collect::<Vec<_>>(),
-                ["DIR", "DIR/SUBDIR", "DIR/SUBDIR/f.txt", "DIR/e.txt", "DIR2", "DIR2/SUBDIR", "DIR2/SUBDIR/f.txt", "DIR2/e.txt", "a.txt", "b.txt", "d.txt"]
-            );
-        });
-
-        project_b
-            .update(cx_b, |project, cx| {
-                project.delete_entry(dir_entry.id, cx).unwrap()
-            })
-            .await
-            .unwrap();
-        worktree_a.read_with(cx_a, |worktree, _| {
-            assert_eq!(
-                worktree
-                    .paths()
-                    .map(|p| p.to_string_lossy())
-                    .collect::<Vec<_>>(),
-                ["DIR2", "DIR2/SUBDIR", "DIR2/SUBDIR/f.txt", "DIR2/e.txt", "a.txt", "b.txt", "d.txt"]
-            );
-        });
-        worktree_b.read_with(cx_b, |worktree, _| {
-            assert_eq!(
-                worktree
-                    .paths()
-                    .map(|p| p.to_string_lossy())
-                    .collect::<Vec<_>>(),
-                ["DIR2", "DIR2/SUBDIR", "DIR2/SUBDIR/f.txt", "DIR2/e.txt", "a.txt", "b.txt", "d.txt"]
-            );
-        });
-
-        project_b
-            .update(cx_b, |project, cx| {
-                project.delete_entry(entry.id, cx).unwrap()
-            })
-            .await
-            .unwrap();
-        worktree_a.read_with(cx_a, |worktree, _| {
-            assert_eq!(
-                worktree
-                    .paths()
-                    .map(|p| p.to_string_lossy())
-                    .collect::<Vec<_>>(),
-                ["DIR2", "DIR2/SUBDIR", "DIR2/SUBDIR/f.txt", "DIR2/e.txt", "a.txt", "b.txt"]
-            );
-        });
-        worktree_b.read_with(cx_b, |worktree, _| {
-            assert_eq!(
-                worktree
-                    .paths()
-                    .map(|p| p.to_string_lossy())
-                    .collect::<Vec<_>>(),
-                ["DIR2", "DIR2/SUBDIR", "DIR2/SUBDIR/f.txt", "DIR2/e.txt", "a.txt", "b.txt"]
-            );
-        });
-    }
-
-    #[gpui::test(iterations = 10)]
-    async fn test_buffer_conflict_after_save(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
-        cx_a.foreground().forbid_parking();
-        let lang_registry = Arc::new(LanguageRegistry::test());
-        let fs = FakeFs::new(cx_a.background());
-
-        // Connect to a server as 2 clients.
-        let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
-        let client_a = server.create_client(cx_a, "user_a").await;
-        let mut client_b = server.create_client(cx_b, "user_b").await;
-        server
-            .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)])
-            .await;
-
-        // Share a project as client A
-        fs.insert_tree(
-            "/dir",
-            json!({
-                "a.txt": "a-contents",
-            }),
-        )
-        .await;
-
-        let project_a = cx_a.update(|cx| {
-            Project::local(
-                client_a.clone(),
-                client_a.user_store.clone(),
-                lang_registry.clone(),
-                fs.clone(),
-                cx,
-            )
-        });
-        let (worktree_a, _) = project_a
-            .update(cx_a, |p, cx| {
-                p.find_or_create_local_worktree("/dir", true, cx)
-            })
-            .await
-            .unwrap();
-        worktree_a
-            .read_with(cx_a, |tree, _| tree.as_local().unwrap().scan_complete())
-            .await;
-        let worktree_id = worktree_a.read_with(cx_a, |tree, _| tree.id());
-
-        // Join that project as client B
-        let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await;
-
-        // Open a buffer as client B
-        let buffer_b = project_b
-            .update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx))
-            .await
-            .unwrap();
-
-        buffer_b.update(cx_b, |buf, cx| buf.edit([(0..0, "world ")], cx));
-        buffer_b.read_with(cx_b, |buf, _| {
-            assert!(buf.is_dirty());
-            assert!(!buf.has_conflict());
-        });
-
-        buffer_b.update(cx_b, |buf, cx| buf.save(cx)).await.unwrap();
-        buffer_b
-            .condition(&cx_b, |buffer_b, _| !buffer_b.is_dirty())
-            .await;
-        buffer_b.read_with(cx_b, |buf, _| {
-            assert!(!buf.has_conflict());
-        });
-
-        buffer_b.update(cx_b, |buf, cx| buf.edit([(0..0, "hello ")], cx));
-        buffer_b.read_with(cx_b, |buf, _| {
-            assert!(buf.is_dirty());
-            assert!(!buf.has_conflict());
-        });
-    }
-
-    #[gpui::test(iterations = 10)]
-    async fn test_buffer_reloading(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
-        cx_a.foreground().forbid_parking();
-        let lang_registry = Arc::new(LanguageRegistry::test());
-        let fs = FakeFs::new(cx_a.background());
-
-        // Connect to a server as 2 clients.
-        let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
-        let client_a = server.create_client(cx_a, "user_a").await;
-        let mut client_b = server.create_client(cx_b, "user_b").await;
-        server
-            .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)])
-            .await;
-
-        // Share a project as client A
-        fs.insert_tree(
-            "/dir",
-            json!({
-                "a.txt": "a-contents",
-            }),
-        )
-        .await;
-
-        let project_a = cx_a.update(|cx| {
-            Project::local(
-                client_a.clone(),
-                client_a.user_store.clone(),
-                lang_registry.clone(),
-                fs.clone(),
-                cx,
-            )
-        });
-        let (worktree_a, _) = project_a
-            .update(cx_a, |p, cx| {
-                p.find_or_create_local_worktree("/dir", true, cx)
-            })
-            .await
-            .unwrap();
-        worktree_a
-            .read_with(cx_a, |tree, _| tree.as_local().unwrap().scan_complete())
-            .await;
-        let worktree_id = worktree_a.read_with(cx_a, |tree, _| tree.id());
-
-        // Join that project as client B
-        let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await;
-        let _worktree_b = project_b.update(cx_b, |p, cx| p.worktrees(cx).next().unwrap());
-
-        // Open a buffer as client B
-        let buffer_b = project_b
-            .update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx))
-            .await
-            .unwrap();
-        buffer_b.read_with(cx_b, |buf, _| {
-            assert!(!buf.is_dirty());
-            assert!(!buf.has_conflict());
-        });
-
-        fs.save(Path::new("/dir/a.txt"), &"new contents".into())
-            .await
-            .unwrap();
-        buffer_b
-            .condition(&cx_b, |buf, _| {
-                buf.text() == "new contents" && !buf.is_dirty()
-            })
-            .await;
-        buffer_b.read_with(cx_b, |buf, _| {
-            assert!(!buf.has_conflict());
-        });
-    }
-
-    #[gpui::test(iterations = 10)]
-    async fn test_editing_while_guest_opens_buffer(
-        cx_a: &mut TestAppContext,
-        cx_b: &mut TestAppContext,
-    ) {
-        cx_a.foreground().forbid_parking();
-        let lang_registry = Arc::new(LanguageRegistry::test());
-        let fs = FakeFs::new(cx_a.background());
-
-        // Connect to a server as 2 clients.
-        let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
-        let client_a = server.create_client(cx_a, "user_a").await;
-        let mut client_b = server.create_client(cx_b, "user_b").await;
-        server
-            .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)])
-            .await;
-
-        // Share a project as client A
-        fs.insert_tree(
-            "/dir",
-            json!({
-                "a.txt": "a-contents",
-            }),
-        )
-        .await;
-        let project_a = cx_a.update(|cx| {
-            Project::local(
-                client_a.clone(),
-                client_a.user_store.clone(),
-                lang_registry.clone(),
-                fs.clone(),
-                cx,
-            )
-        });
-        let (worktree_a, _) = project_a
-            .update(cx_a, |p, cx| {
-                p.find_or_create_local_worktree("/dir", true, cx)
-            })
-            .await
-            .unwrap();
-        worktree_a
-            .read_with(cx_a, |tree, _| tree.as_local().unwrap().scan_complete())
-            .await;
-        let worktree_id = worktree_a.read_with(cx_a, |tree, _| tree.id());
-
-        // Join that project as client B
-        let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await;
-
-        // Open a buffer as client A
-        let buffer_a = project_a
-            .update(cx_a, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx))
-            .await
-            .unwrap();
-
-        // Start opening the same buffer as client B
-        let buffer_b = cx_b
-            .background()
-            .spawn(project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx)));
-
-        // Edit the buffer as client A while client B is still opening it.
-        cx_b.background().simulate_random_delay().await;
-        buffer_a.update(cx_a, |buf, cx| buf.edit([(0..0, "X")], cx));
-        cx_b.background().simulate_random_delay().await;
-        buffer_a.update(cx_a, |buf, cx| buf.edit([(1..1, "Y")], cx));
-
-        let text = buffer_a.read_with(cx_a, |buf, _| buf.text());
-        let buffer_b = buffer_b.await.unwrap();
-        buffer_b.condition(&cx_b, |buf, _| buf.text() == text).await;
-    }
-
-    #[gpui::test(iterations = 10)]
-    async fn test_leaving_worktree_while_opening_buffer(
-        cx_a: &mut TestAppContext,
-        cx_b: &mut TestAppContext,
-    ) {
-        cx_a.foreground().forbid_parking();
-        let lang_registry = Arc::new(LanguageRegistry::test());
-        let fs = FakeFs::new(cx_a.background());
-
-        // Connect to a server as 2 clients.
-        let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
-        let client_a = server.create_client(cx_a, "user_a").await;
-        let mut client_b = server.create_client(cx_b, "user_b").await;
-        server
-            .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)])
-            .await;
-
-        // Share a project as client A
-        fs.insert_tree(
-            "/dir",
-            json!({
-                "a.txt": "a-contents",
-            }),
-        )
-        .await;
-        let project_a = cx_a.update(|cx| {
-            Project::local(
-                client_a.clone(),
-                client_a.user_store.clone(),
-                lang_registry.clone(),
-                fs.clone(),
-                cx,
-            )
-        });
-        let (worktree_a, _) = project_a
-            .update(cx_a, |p, cx| {
-                p.find_or_create_local_worktree("/dir", true, cx)
-            })
-            .await
-            .unwrap();
-        worktree_a
-            .read_with(cx_a, |tree, _| tree.as_local().unwrap().scan_complete())
-            .await;
-        let worktree_id = worktree_a.read_with(cx_a, |tree, _| tree.id());
-
-        // Join that project as client B
-        let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await;
-
-        // See that a guest has joined as client A.
-        project_a
-            .condition(&cx_a, |p, _| p.collaborators().len() == 1)
-            .await;
-
-        // Begin opening a buffer as client B, but leave the project before the open completes.
-        let buffer_b = cx_b
-            .background()
-            .spawn(project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx)));
-        cx_b.update(|_| {
-            drop(client_b.project.take());
-            drop(project_b);
-        });
-        drop(buffer_b);
-
-        // See that the guest has left.
-        project_a
-            .condition(&cx_a, |p, _| p.collaborators().len() == 0)
-            .await;
-    }
-
-    #[gpui::test(iterations = 10)]
-    async fn test_leaving_project(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
-        cx_a.foreground().forbid_parking();
-        let lang_registry = Arc::new(LanguageRegistry::test());
-        let fs = FakeFs::new(cx_a.background());
-
-        // Connect to a server as 2 clients.
-        let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
-        let client_a = server.create_client(cx_a, "user_a").await;
-        let mut client_b = server.create_client(cx_b, "user_b").await;
-        server
-            .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)])
-            .await;
-
-        // Share a project as client A
-        fs.insert_tree(
-            "/a",
-            json!({
-                "a.txt": "a-contents",
-                "b.txt": "b-contents",
-            }),
-        )
-        .await;
-        let project_a = cx_a.update(|cx| {
-            Project::local(
-                client_a.clone(),
-                client_a.user_store.clone(),
-                lang_registry.clone(),
-                fs.clone(),
-                cx,
-            )
-        });
-        let (worktree_a, _) = project_a
-            .update(cx_a, |p, cx| {
-                p.find_or_create_local_worktree("/a", true, cx)
-            })
-            .await
-            .unwrap();
-        worktree_a
-            .read_with(cx_a, |tree, _| tree.as_local().unwrap().scan_complete())
-            .await;
-
-        // Join that project as client B
-        let _project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await;
-
-        // Client A sees that a guest has joined.
-        project_a
-            .condition(cx_a, |p, _| p.collaborators().len() == 1)
-            .await;
-
-        // Drop client B's connection and ensure client A observes client B leaving the project.
-        client_b.disconnect(&cx_b.to_async()).unwrap();
-        project_a
-            .condition(cx_a, |p, _| p.collaborators().len() == 0)
-            .await;
-
-        // Rejoin the project as client B
-        let _project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await;
-
-        // Client A sees that a guest has re-joined.
-        project_a
-            .condition(cx_a, |p, _| p.collaborators().len() == 1)
-            .await;
-
-        // Simulate connection loss for client B and ensure client A observes client B leaving the project.
-        client_b.wait_for_current_user(cx_b).await;
-        server.disconnect_client(client_b.current_user_id(cx_b));
-        cx_a.foreground().advance_clock(rpc::RECEIVE_TIMEOUT);
-        project_a
-            .condition(cx_a, |p, _| p.collaborators().len() == 0)
-            .await;
-    }
-
-    #[gpui::test(iterations = 10)]
-    async fn test_collaborating_with_diagnostics(
-        deterministic: Arc<Deterministic>,
-        cx_a: &mut TestAppContext,
-        cx_b: &mut TestAppContext,
-        cx_c: &mut TestAppContext,
-    ) {
-        deterministic.forbid_parking();
-        let lang_registry = Arc::new(LanguageRegistry::test());
-        let fs = FakeFs::new(cx_a.background());
-
-        // Set up a fake language server.
-        let mut language = Language::new(
-            LanguageConfig {
-                name: "Rust".into(),
-                path_suffixes: vec!["rs".to_string()],
-                ..Default::default()
-            },
-            Some(tree_sitter_rust::language()),
-        );
-        let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default());
-        lang_registry.add(Arc::new(language));
-
-        // Connect to a server as 2 clients.
-        let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
-        let client_a = server.create_client(cx_a, "user_a").await;
-        let mut client_b = server.create_client(cx_b, "user_b").await;
-        let mut client_c = server.create_client(cx_c, "user_c").await;
-        server
-            .make_contacts(vec![
-                (&client_a, cx_a),
-                (&client_b, cx_b),
-                (&client_c, cx_c),
-            ])
-            .await;
-
-        // Share a project as client A
-        fs.insert_tree(
-            "/a",
-            json!({
-                "a.rs": "let one = two",
-                "other.rs": "",
-            }),
-        )
-        .await;
-        let project_a = cx_a.update(|cx| {
-            Project::local(
-                client_a.clone(),
-                client_a.user_store.clone(),
-                lang_registry.clone(),
-                fs.clone(),
-                cx,
-            )
-        });
-        let (worktree_a, _) = project_a
-            .update(cx_a, |p, cx| {
-                p.find_or_create_local_worktree("/a", true, cx)
-            })
-            .await
-            .unwrap();
-        worktree_a
-            .read_with(cx_a, |tree, _| tree.as_local().unwrap().scan_complete())
-            .await;
-        let project_id = project_a.update(cx_a, |p, _| p.next_remote_id()).await;
-        let worktree_id = worktree_a.read_with(cx_a, |tree, _| tree.id());
-
-        // Cause the language server to start.
-        let _buffer = cx_a
-            .background()
-            .spawn(project_a.update(cx_a, |project, cx| {
-                project.open_buffer(
-                    ProjectPath {
-                        worktree_id,
-                        path: Path::new("other.rs").into(),
-                    },
-                    cx,
-                )
-            }))
-            .await
-            .unwrap();
-
-        // Join the worktree as client B.
-        let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await;
-
-        // Simulate a language server reporting errors for a file.
-        let mut fake_language_server = fake_language_servers.next().await.unwrap();
-        fake_language_server
-            .receive_notification::<lsp::notification::DidOpenTextDocument>()
-            .await;
-        fake_language_server.notify::<lsp::notification::PublishDiagnostics>(
-            lsp::PublishDiagnosticsParams {
-                uri: lsp::Url::from_file_path("/a/a.rs").unwrap(),
-                version: None,
-                diagnostics: vec![lsp::Diagnostic {
-                    severity: Some(lsp::DiagnosticSeverity::ERROR),
-                    range: lsp::Range::new(lsp::Position::new(0, 4), lsp::Position::new(0, 7)),
-                    message: "message 1".to_string(),
-                    ..Default::default()
-                }],
-            },
-        );
-
-        // Wait for server to see the diagnostics update.
-        deterministic.run_until_parked();
-        {
-            let store = server.store.read().await;
-            let project = store.project(project_id).unwrap();
-            let worktree = project.worktrees.get(&worktree_id.to_proto()).unwrap();
-            assert!(!worktree.diagnostic_summaries.is_empty());
-        }
-
-        // Ensure client B observes the new diagnostics.
-        project_b.read_with(cx_b, |project, cx| {
-            assert_eq!(
-                project.diagnostic_summaries(cx).collect::<Vec<_>>(),
-                &[(
-                    ProjectPath {
-                        worktree_id,
-                        path: Arc::from(Path::new("a.rs")),
-                    },
-                    DiagnosticSummary {
-                        error_count: 1,
-                        warning_count: 0,
-                        ..Default::default()
-                    },
-                )]
-            )
-        });
-
-        // Join project as client C and observe the diagnostics.
-        let project_c = client_c.build_remote_project(&project_a, cx_a, cx_c).await;
-        project_c.read_with(cx_c, |project, cx| {
-            assert_eq!(
-                project.diagnostic_summaries(cx).collect::<Vec<_>>(),
-                &[(
-                    ProjectPath {
-                        worktree_id,
-                        path: Arc::from(Path::new("a.rs")),
-                    },
-                    DiagnosticSummary {
-                        error_count: 1,
-                        warning_count: 0,
-                        ..Default::default()
-                    },
-                )]
-            )
-        });
-
-        // Simulate a language server reporting more errors for a file.
-        fake_language_server.notify::<lsp::notification::PublishDiagnostics>(
-            lsp::PublishDiagnosticsParams {
-                uri: lsp::Url::from_file_path("/a/a.rs").unwrap(),
-                version: None,
-                diagnostics: vec![
-                    lsp::Diagnostic {
-                        severity: Some(lsp::DiagnosticSeverity::ERROR),
-                        range: lsp::Range::new(lsp::Position::new(0, 4), lsp::Position::new(0, 7)),
-                        message: "message 1".to_string(),
-                        ..Default::default()
-                    },
-                    lsp::Diagnostic {
-                        severity: Some(lsp::DiagnosticSeverity::WARNING),
-                        range: lsp::Range::new(
-                            lsp::Position::new(0, 10),
-                            lsp::Position::new(0, 13),
-                        ),
-                        message: "message 2".to_string(),
-                        ..Default::default()
-                    },
-                ],
-            },
-        );
-
-        // Clients B and C get the updated summaries
-        deterministic.run_until_parked();
-        project_b.read_with(cx_b, |project, cx| {
-            assert_eq!(
-                project.diagnostic_summaries(cx).collect::<Vec<_>>(),
-                [(
-                    ProjectPath {
-                        worktree_id,
-                        path: Arc::from(Path::new("a.rs")),
-                    },
-                    DiagnosticSummary {
-                        error_count: 1,
-                        warning_count: 1,
-                        ..Default::default()
-                    },
-                )]
-            );
-        });
-        project_c.read_with(cx_c, |project, cx| {
-            assert_eq!(
-                project.diagnostic_summaries(cx).collect::<Vec<_>>(),
-                [(
-                    ProjectPath {
-                        worktree_id,
-                        path: Arc::from(Path::new("a.rs")),
-                    },
-                    DiagnosticSummary {
-                        error_count: 1,
-                        warning_count: 1,
-                        ..Default::default()
-                    },
-                )]
-            );
-        });
-
-        // Open the file with the errors on client B. They should be present.
-        let buffer_b = cx_b
-            .background()
-            .spawn(project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx)))
-            .await
-            .unwrap();
-
-        buffer_b.read_with(cx_b, |buffer, _| {
-            assert_eq!(
-                buffer
-                    .snapshot()
-                    .diagnostics_in_range::<_, Point>(0..buffer.len(), false)
-                    .map(|entry| entry)
-                    .collect::<Vec<_>>(),
-                &[
-                    DiagnosticEntry {
-                        range: Point::new(0, 4)..Point::new(0, 7),
-                        diagnostic: Diagnostic {
-                            group_id: 0,
-                            message: "message 1".to_string(),
-                            severity: lsp::DiagnosticSeverity::ERROR,
-                            is_primary: true,
-                            ..Default::default()
-                        }
-                    },
-                    DiagnosticEntry {
-                        range: Point::new(0, 10)..Point::new(0, 13),
-                        diagnostic: Diagnostic {
-                            group_id: 1,
-                            severity: lsp::DiagnosticSeverity::WARNING,
-                            message: "message 2".to_string(),
-                            is_primary: true,
-                            ..Default::default()
-                        }
-                    }
-                ]
-            );
-        });
-
-        // Simulate a language server reporting no errors for a file.
-        fake_language_server.notify::<lsp::notification::PublishDiagnostics>(
-            lsp::PublishDiagnosticsParams {
-                uri: lsp::Url::from_file_path("/a/a.rs").unwrap(),
-                version: None,
-                diagnostics: vec![],
-            },
-        );
-        deterministic.run_until_parked();
-        project_a.read_with(cx_a, |project, cx| {
-            assert_eq!(project.diagnostic_summaries(cx).collect::<Vec<_>>(), [])
-        });
-        project_b.read_with(cx_b, |project, cx| {
-            assert_eq!(project.diagnostic_summaries(cx).collect::<Vec<_>>(), [])
-        });
-        project_c.read_with(cx_c, |project, cx| {
-            assert_eq!(project.diagnostic_summaries(cx).collect::<Vec<_>>(), [])
-        });
-    }
-
-    #[gpui::test(iterations = 10)]
-    async fn test_collaborating_with_completion(
-        cx_a: &mut TestAppContext,
-        cx_b: &mut TestAppContext,
-    ) {
-        cx_a.foreground().forbid_parking();
-        let lang_registry = Arc::new(LanguageRegistry::test());
-        let fs = FakeFs::new(cx_a.background());
-
-        // Set up a fake language server.
-        let mut language = Language::new(
-            LanguageConfig {
-                name: "Rust".into(),
-                path_suffixes: vec!["rs".to_string()],
-                ..Default::default()
-            },
-            Some(tree_sitter_rust::language()),
-        );
-        let mut fake_language_servers = language.set_fake_lsp_adapter(FakeLspAdapter {
-            capabilities: lsp::ServerCapabilities {
-                completion_provider: Some(lsp::CompletionOptions {
-                    trigger_characters: Some(vec![".".to_string()]),
-                    ..Default::default()
-                }),
-                ..Default::default()
-            },
-            ..Default::default()
-        });
-        lang_registry.add(Arc::new(language));
-
-        // Connect to a server as 2 clients.
-        let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
-        let client_a = server.create_client(cx_a, "user_a").await;
-        let mut client_b = server.create_client(cx_b, "user_b").await;
-        server
-            .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)])
-            .await;
-
-        // Share a project as client A
-        fs.insert_tree(
-            "/a",
-            json!({
-                "main.rs": "fn main() { a }",
-                "other.rs": "",
-            }),
-        )
-        .await;
-        let project_a = cx_a.update(|cx| {
-            Project::local(
-                client_a.clone(),
-                client_a.user_store.clone(),
-                lang_registry.clone(),
-                fs.clone(),
-                cx,
-            )
-        });
-        let (worktree_a, _) = project_a
-            .update(cx_a, |p, cx| {
-                p.find_or_create_local_worktree("/a", true, cx)
-            })
-            .await
-            .unwrap();
-        worktree_a
-            .read_with(cx_a, |tree, _| tree.as_local().unwrap().scan_complete())
-            .await;
-        let worktree_id = worktree_a.read_with(cx_a, |tree, _| tree.id());
-
-        // Join the worktree as client B.
-        let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await;
-
-        // Open a file in an editor as the guest.
-        let buffer_b = project_b
-            .update(cx_b, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx))
-            .await
-            .unwrap();
-        let (window_b, _) = cx_b.add_window(|_| EmptyView);
-        let editor_b = cx_b.add_view(window_b, |cx| {
-            Editor::for_buffer(buffer_b.clone(), Some(project_b.clone()), cx)
-        });
-
-        let fake_language_server = fake_language_servers.next().await.unwrap();
-        buffer_b
-            .condition(&cx_b, |buffer, _| !buffer.completion_triggers().is_empty())
-            .await;
-
-        // Type a completion trigger character as the guest.
-        editor_b.update(cx_b, |editor, cx| {
-            editor.change_selections(None, cx, |s| s.select_ranges([13..13]));
-            editor.handle_input(&Input(".".into()), cx);
-            cx.focus(&editor_b);
-        });
-
-        // Receive a completion request as the host's language server.
-        // Return some completions from the host's language server.
-        cx_a.foreground().start_waiting();
-        fake_language_server
-            .handle_request::<lsp::request::Completion, _, _>(|params, _| async move {
-                assert_eq!(
-                    params.text_document_position.text_document.uri,
-                    lsp::Url::from_file_path("/a/main.rs").unwrap(),
-                );
-                assert_eq!(
-                    params.text_document_position.position,
-                    lsp::Position::new(0, 14),
-                );
-
-                Ok(Some(lsp::CompletionResponse::Array(vec![
-                    lsp::CompletionItem {
-                        label: "first_method(…)".into(),
-                        detail: Some("fn(&mut self, B) -> C".into()),
-                        text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
-                            new_text: "first_method($1)".to_string(),
-                            range: lsp::Range::new(
-                                lsp::Position::new(0, 14),
-                                lsp::Position::new(0, 14),
-                            ),
-                        })),
-                        insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
-                        ..Default::default()
-                    },
-                    lsp::CompletionItem {
-                        label: "second_method(…)".into(),
-                        detail: Some("fn(&mut self, C) -> D<E>".into()),
-                        text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
-                            new_text: "second_method()".to_string(),
-                            range: lsp::Range::new(
-                                lsp::Position::new(0, 14),
-                                lsp::Position::new(0, 14),
-                            ),
-                        })),
-                        insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
-                        ..Default::default()
-                    },
-                ])))
-            })
-            .next()
-            .await
-            .unwrap();
-        cx_a.foreground().finish_waiting();
-
-        // Open the buffer on the host.
-        let buffer_a = project_a
-            .update(cx_a, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx))
-            .await
-            .unwrap();
-        buffer_a
-            .condition(&cx_a, |buffer, _| buffer.text() == "fn main() { a. }")
-            .await;
-
-        // Confirm a completion on the guest.
-        editor_b
-            .condition(&cx_b, |editor, _| editor.context_menu_visible())
-            .await;
-        editor_b.update(cx_b, |editor, cx| {
-            editor.confirm_completion(&ConfirmCompletion { item_ix: Some(0) }, cx);
-            assert_eq!(editor.text(cx), "fn main() { a.first_method() }");
-        });
-
-        // Return a resolved completion from the host's language server.
-        // The resolved completion has an additional text edit.
-        fake_language_server.handle_request::<lsp::request::ResolveCompletionItem, _, _>(
-            |params, _| async move {
-                assert_eq!(params.label, "first_method(…)");
-                Ok(lsp::CompletionItem {
-                    label: "first_method(…)".into(),
-                    detail: Some("fn(&mut self, B) -> C".into()),
-                    text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
-                        new_text: "first_method($1)".to_string(),
-                        range: lsp::Range::new(
-                            lsp::Position::new(0, 14),
-                            lsp::Position::new(0, 14),
-                        ),
-                    })),
-                    additional_text_edits: Some(vec![lsp::TextEdit {
-                        new_text: "use d::SomeTrait;\n".to_string(),
-                        range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
-                    }]),
-                    insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
-                    ..Default::default()
-                })
-            },
-        );
-
-        // The additional edit is applied.
-        buffer_a
-            .condition(&cx_a, |buffer, _| {
-                buffer.text() == "use d::SomeTrait;\nfn main() { a.first_method() }"
-            })
-            .await;
-        buffer_b
-            .condition(&cx_b, |buffer, _| {
-                buffer.text() == "use d::SomeTrait;\nfn main() { a.first_method() }"
-            })
-            .await;
-    }
-
-    #[gpui::test(iterations = 10)]
-    async fn test_reloading_buffer_manually(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
-        cx_a.foreground().forbid_parking();
-        let lang_registry = Arc::new(LanguageRegistry::test());
-        let fs = FakeFs::new(cx_a.background());
-
-        // Connect to a server as 2 clients.
-        let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
-        let client_a = server.create_client(cx_a, "user_a").await;
-        let mut client_b = server.create_client(cx_b, "user_b").await;
-        server
-            .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)])
-            .await;
-
-        // Share a project as client A
-        fs.insert_tree(
-            "/a",
-            json!({
-                "a.rs": "let one = 1;",
-            }),
-        )
-        .await;
-        let project_a = cx_a.update(|cx| {
-            Project::local(
-                client_a.clone(),
-                client_a.user_store.clone(),
-                lang_registry.clone(),
-                fs.clone(),
-                cx,
-            )
-        });
-        let (worktree_a, _) = project_a
-            .update(cx_a, |p, cx| {
-                p.find_or_create_local_worktree("/a", true, cx)
-            })
-            .await
-            .unwrap();
-        worktree_a
-            .read_with(cx_a, |tree, _| tree.as_local().unwrap().scan_complete())
-            .await;
-        let worktree_id = worktree_a.read_with(cx_a, |tree, _| tree.id());
-        let buffer_a = project_a
-            .update(cx_a, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx))
-            .await
-            .unwrap();
-
-        // Join the worktree as client B.
-        let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await;
-
-        let buffer_b = cx_b
-            .background()
-            .spawn(project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx)))
-            .await
-            .unwrap();
-        buffer_b.update(cx_b, |buffer, cx| {
-            buffer.edit([(4..7, "six")], cx);
-            buffer.edit([(10..11, "6")], cx);
-            assert_eq!(buffer.text(), "let six = 6;");
-            assert!(buffer.is_dirty());
-            assert!(!buffer.has_conflict());
-        });
-        buffer_a
-            .condition(cx_a, |buffer, _| buffer.text() == "let six = 6;")
-            .await;
-
-        fs.save(Path::new("/a/a.rs"), &Rope::from("let seven = 7;"))
-            .await
-            .unwrap();
-        buffer_a
-            .condition(cx_a, |buffer, _| buffer.has_conflict())
-            .await;
-        buffer_b
-            .condition(cx_b, |buffer, _| buffer.has_conflict())
-            .await;
-
-        project_b
-            .update(cx_b, |project, cx| {
-                project.reload_buffers(HashSet::from_iter([buffer_b.clone()]), true, cx)
-            })
-            .await
-            .unwrap();
-        buffer_a.read_with(cx_a, |buffer, _| {
-            assert_eq!(buffer.text(), "let seven = 7;");
-            assert!(!buffer.is_dirty());
-            assert!(!buffer.has_conflict());
-        });
-        buffer_b.read_with(cx_b, |buffer, _| {
-            assert_eq!(buffer.text(), "let seven = 7;");
-            assert!(!buffer.is_dirty());
-            assert!(!buffer.has_conflict());
-        });
-
-        buffer_a.update(cx_a, |buffer, cx| {
-            // Undoing on the host is a no-op when the reload was initiated by the guest.
-            buffer.undo(cx);
-            assert_eq!(buffer.text(), "let seven = 7;");
-            assert!(!buffer.is_dirty());
-            assert!(!buffer.has_conflict());
-        });
-        buffer_b.update(cx_b, |buffer, cx| {
-            // Undoing on the guest rolls back the buffer to before it was reloaded but the conflict gets cleared.
-            buffer.undo(cx);
-            assert_eq!(buffer.text(), "let six = 6;");
-            assert!(buffer.is_dirty());
-            assert!(!buffer.has_conflict());
-        });
-    }
-
-    #[gpui::test(iterations = 10)]
-    async fn test_formatting_buffer(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
-        cx_a.foreground().forbid_parking();
-        let lang_registry = Arc::new(LanguageRegistry::test());
-        let fs = FakeFs::new(cx_a.background());
-
-        // Set up a fake language server.
-        let mut language = Language::new(
-            LanguageConfig {
-                name: "Rust".into(),
-                path_suffixes: vec!["rs".to_string()],
-                ..Default::default()
-            },
-            Some(tree_sitter_rust::language()),
-        );
-        let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default());
-        lang_registry.add(Arc::new(language));
-
-        // Connect to a server as 2 clients.
-        let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
-        let client_a = server.create_client(cx_a, "user_a").await;
-        let mut client_b = server.create_client(cx_b, "user_b").await;
-        server
-            .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)])
-            .await;
-
-        // Share a project as client A
-        fs.insert_tree(
-            "/a",
-            json!({
-                "a.rs": "let one = two",
-            }),
-        )
-        .await;
-        let project_a = cx_a.update(|cx| {
-            Project::local(
-                client_a.clone(),
-                client_a.user_store.clone(),
-                lang_registry.clone(),
-                fs.clone(),
-                cx,
-            )
-        });
-        let (worktree_a, _) = project_a
-            .update(cx_a, |p, cx| {
-                p.find_or_create_local_worktree("/a", true, cx)
-            })
-            .await
-            .unwrap();
-        worktree_a
-            .read_with(cx_a, |tree, _| tree.as_local().unwrap().scan_complete())
-            .await;
-        let worktree_id = worktree_a.read_with(cx_a, |tree, _| tree.id());
-
-        // Join the project as client B.
-        let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await;
-
-        let buffer_b = cx_b
-            .background()
-            .spawn(project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx)))
-            .await
-            .unwrap();
-
-        let fake_language_server = fake_language_servers.next().await.unwrap();
-        fake_language_server.handle_request::<lsp::request::Formatting, _, _>(|_, _| async move {
-            Ok(Some(vec![
-                lsp::TextEdit {
-                    range: lsp::Range::new(lsp::Position::new(0, 4), lsp::Position::new(0, 4)),
-                    new_text: "h".to_string(),
-                },
-                lsp::TextEdit {
-                    range: lsp::Range::new(lsp::Position::new(0, 7), lsp::Position::new(0, 7)),
-                    new_text: "y".to_string(),
-                },
-            ]))
-        });
-
-        project_b
-            .update(cx_b, |project, cx| {
-                project.format(HashSet::from_iter([buffer_b.clone()]), true, cx)
-            })
-            .await
-            .unwrap();
-        assert_eq!(
-            buffer_b.read_with(cx_b, |buffer, _| buffer.text()),
-            "let honey = two"
-        );
-    }
-
-    #[gpui::test(iterations = 10)]
-    async fn test_definition(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
-        cx_a.foreground().forbid_parking();
-        let lang_registry = Arc::new(LanguageRegistry::test());
-        let fs = FakeFs::new(cx_a.background());
-        fs.insert_tree(
-            "/root-1",
-            json!({
-                "a.rs": "const ONE: usize = b::TWO + b::THREE;",
-            }),
-        )
-        .await;
-        fs.insert_tree(
-            "/root-2",
-            json!({
-                "b.rs": "const TWO: usize = 2;\nconst THREE: usize = 3;",
-            }),
-        )
-        .await;
-
-        // Set up a fake language server.
-        let mut language = Language::new(
-            LanguageConfig {
-                name: "Rust".into(),
-                path_suffixes: vec!["rs".to_string()],
-                ..Default::default()
-            },
-            Some(tree_sitter_rust::language()),
-        );
-        let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default());
-        lang_registry.add(Arc::new(language));
-
-        // Connect to a server as 2 clients.
-        let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
-        let client_a = server.create_client(cx_a, "user_a").await;
-        let mut client_b = server.create_client(cx_b, "user_b").await;
-        server
-            .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)])
-            .await;
-
-        // Share a project as client A
-        let project_a = cx_a.update(|cx| {
-            Project::local(
-                client_a.clone(),
-                client_a.user_store.clone(),
-                lang_registry.clone(),
-                fs.clone(),
-                cx,
-            )
-        });
-        let (worktree_a, _) = project_a
-            .update(cx_a, |p, cx| {
-                p.find_or_create_local_worktree("/root-1", true, cx)
-            })
-            .await
-            .unwrap();
-        worktree_a
-            .read_with(cx_a, |tree, _| tree.as_local().unwrap().scan_complete())
-            .await;
-        let worktree_id = worktree_a.read_with(cx_a, |tree, _| tree.id());
-
-        // Join the project as client B.
-        let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await;
-
-        // Open the file on client B.
-        let buffer_b = cx_b
-            .background()
-            .spawn(project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx)))
-            .await
-            .unwrap();
-
-        // Request the definition of a symbol as the guest.
-        let fake_language_server = fake_language_servers.next().await.unwrap();
-        fake_language_server.handle_request::<lsp::request::GotoDefinition, _, _>(
-            |_, _| async move {
-                Ok(Some(lsp::GotoDefinitionResponse::Scalar(
-                    lsp::Location::new(
-                        lsp::Url::from_file_path("/root-2/b.rs").unwrap(),
-                        lsp::Range::new(lsp::Position::new(0, 6), lsp::Position::new(0, 9)),
-                    ),
-                )))
-            },
-        );
-
-        let definitions_1 = project_b
-            .update(cx_b, |p, cx| p.definition(&buffer_b, 23, cx))
-            .await
-            .unwrap();
-        cx_b.read(|cx| {
-            assert_eq!(definitions_1.len(), 1);
-            assert_eq!(project_b.read(cx).worktrees(cx).count(), 2);
-            let target_buffer = definitions_1[0].buffer.read(cx);
-            assert_eq!(
-                target_buffer.text(),
-                "const TWO: usize = 2;\nconst THREE: usize = 3;"
-            );
-            assert_eq!(
-                definitions_1[0].range.to_point(target_buffer),
-                Point::new(0, 6)..Point::new(0, 9)
-            );
-        });
-
-        // Try getting more definitions for the same buffer, ensuring the buffer gets reused from
-        // the previous call to `definition`.
-        fake_language_server.handle_request::<lsp::request::GotoDefinition, _, _>(
-            |_, _| async move {
-                Ok(Some(lsp::GotoDefinitionResponse::Scalar(
-                    lsp::Location::new(
-                        lsp::Url::from_file_path("/root-2/b.rs").unwrap(),
-                        lsp::Range::new(lsp::Position::new(1, 6), lsp::Position::new(1, 11)),
-                    ),
-                )))
-            },
-        );
-
-        let definitions_2 = project_b
-            .update(cx_b, |p, cx| p.definition(&buffer_b, 33, cx))
-            .await
-            .unwrap();
-        cx_b.read(|cx| {
-            assert_eq!(definitions_2.len(), 1);
-            assert_eq!(project_b.read(cx).worktrees(cx).count(), 2);
-            let target_buffer = definitions_2[0].buffer.read(cx);
-            assert_eq!(
-                target_buffer.text(),
-                "const TWO: usize = 2;\nconst THREE: usize = 3;"
-            );
-            assert_eq!(
-                definitions_2[0].range.to_point(target_buffer),
-                Point::new(1, 6)..Point::new(1, 11)
-            );
-        });
-        assert_eq!(definitions_1[0].buffer, definitions_2[0].buffer);
-    }
-
-    #[gpui::test(iterations = 10)]
-    async fn test_references(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
-        cx_a.foreground().forbid_parking();
-        let lang_registry = Arc::new(LanguageRegistry::test());
-        let fs = FakeFs::new(cx_a.background());
-        fs.insert_tree(
-            "/root-1",
-            json!({
-                "one.rs": "const ONE: usize = 1;",
-                "two.rs": "const TWO: usize = one::ONE + one::ONE;",
-            }),
-        )
-        .await;
-        fs.insert_tree(
-            "/root-2",
-            json!({
-                "three.rs": "const THREE: usize = two::TWO + one::ONE;",
-            }),
-        )
-        .await;
-
-        // Set up a fake language server.
-        let mut language = Language::new(
-            LanguageConfig {
-                name: "Rust".into(),
-                path_suffixes: vec!["rs".to_string()],
-                ..Default::default()
-            },
-            Some(tree_sitter_rust::language()),
-        );
-        let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default());
-        lang_registry.add(Arc::new(language));
-
-        // Connect to a server as 2 clients.
-        let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
-        let client_a = server.create_client(cx_a, "user_a").await;
-        let mut client_b = server.create_client(cx_b, "user_b").await;
-        server
-            .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)])
-            .await;
-
-        // Share a project as client A
-        let project_a = cx_a.update(|cx| {
-            Project::local(
-                client_a.clone(),
-                client_a.user_store.clone(),
-                lang_registry.clone(),
-                fs.clone(),
-                cx,
-            )
-        });
-        let (worktree_a, _) = project_a
-            .update(cx_a, |p, cx| {
-                p.find_or_create_local_worktree("/root-1", true, cx)
-            })
-            .await
-            .unwrap();
-        worktree_a
-            .read_with(cx_a, |tree, _| tree.as_local().unwrap().scan_complete())
-            .await;
-        let worktree_id = worktree_a.read_with(cx_a, |tree, _| tree.id());
-
-        // Join the worktree as client B.
-        let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await;
-
-        // Open the file on client B.
-        let buffer_b = cx_b
-            .background()
-            .spawn(project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "one.rs"), cx)))
-            .await
-            .unwrap();
-
-        // Request references to a symbol as the guest.
-        let fake_language_server = fake_language_servers.next().await.unwrap();
-        fake_language_server.handle_request::<lsp::request::References, _, _>(
-            |params, _| async move {
-                assert_eq!(
-                    params.text_document_position.text_document.uri.as_str(),
-                    "file:///root-1/one.rs"
-                );
-                Ok(Some(vec![
-                    lsp::Location {
-                        uri: lsp::Url::from_file_path("/root-1/two.rs").unwrap(),
-                        range: lsp::Range::new(
-                            lsp::Position::new(0, 24),
-                            lsp::Position::new(0, 27),
-                        ),
-                    },
-                    lsp::Location {
-                        uri: lsp::Url::from_file_path("/root-1/two.rs").unwrap(),
-                        range: lsp::Range::new(
-                            lsp::Position::new(0, 35),
-                            lsp::Position::new(0, 38),
-                        ),
-                    },
-                    lsp::Location {
-                        uri: lsp::Url::from_file_path("/root-2/three.rs").unwrap(),
-                        range: lsp::Range::new(
-                            lsp::Position::new(0, 37),
-                            lsp::Position::new(0, 40),
-                        ),
-                    },
-                ]))
-            },
-        );
-
-        let references = project_b
-            .update(cx_b, |p, cx| p.references(&buffer_b, 7, cx))
-            .await
-            .unwrap();
-        cx_b.read(|cx| {
-            assert_eq!(references.len(), 3);
-            assert_eq!(project_b.read(cx).worktrees(cx).count(), 2);
-
-            let two_buffer = references[0].buffer.read(cx);
-            let three_buffer = references[2].buffer.read(cx);
-            assert_eq!(
-                two_buffer.file().unwrap().path().as_ref(),
-                Path::new("two.rs")
-            );
-            assert_eq!(references[1].buffer, references[0].buffer);
-            assert_eq!(
-                three_buffer.file().unwrap().full_path(cx),
-                Path::new("three.rs")
-            );
-
-            assert_eq!(references[0].range.to_offset(&two_buffer), 24..27);
-            assert_eq!(references[1].range.to_offset(&two_buffer), 35..38);
-            assert_eq!(references[2].range.to_offset(&three_buffer), 37..40);
-        });
-    }
-
-    #[gpui::test(iterations = 10)]
-    async fn test_project_search(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
-        cx_a.foreground().forbid_parking();
-        let lang_registry = Arc::new(LanguageRegistry::test());
-        let fs = FakeFs::new(cx_a.background());
-        fs.insert_tree(
-            "/root-1",
-            json!({
-                "a": "hello world",
-                "b": "goodnight moon",
-                "c": "a world of goo",
-                "d": "world champion of clown world",
-            }),
-        )
-        .await;
-        fs.insert_tree(
-            "/root-2",
-            json!({
-                "e": "disney world is fun",
-            }),
-        )
-        .await;
-
-        // Connect to a server as 2 clients.
-        let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
-        let client_a = server.create_client(cx_a, "user_a").await;
-        let mut client_b = server.create_client(cx_b, "user_b").await;
-        server
-            .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)])
-            .await;
-
-        // Share a project as client A
-        let project_a = cx_a.update(|cx| {
-            Project::local(
-                client_a.clone(),
-                client_a.user_store.clone(),
-                lang_registry.clone(),
-                fs.clone(),
-                cx,
-            )
-        });
-
-        let (worktree_1, _) = project_a
-            .update(cx_a, |p, cx| {
-                p.find_or_create_local_worktree("/root-1", true, cx)
-            })
-            .await
-            .unwrap();
-        worktree_1
-            .read_with(cx_a, |tree, _| tree.as_local().unwrap().scan_complete())
-            .await;
-        let (worktree_2, _) = project_a
-            .update(cx_a, |p, cx| {
-                p.find_or_create_local_worktree("/root-2", true, cx)
-            })
-            .await
-            .unwrap();
-        worktree_2
-            .read_with(cx_a, |tree, _| tree.as_local().unwrap().scan_complete())
-            .await;
-
-        // Join the worktree as client B.
-        let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await;
-        let results = project_b
-            .update(cx_b, |project, cx| {
-                project.search(SearchQuery::text("world", false, false), cx)
-            })
-            .await
-            .unwrap();
-
-        let mut ranges_by_path = results
-            .into_iter()
-            .map(|(buffer, ranges)| {
-                buffer.read_with(cx_b, |buffer, cx| {
-                    let path = buffer.file().unwrap().full_path(cx);
-                    let offset_ranges = ranges
-                        .into_iter()
-                        .map(|range| range.to_offset(buffer))
-                        .collect::<Vec<_>>();
-                    (path, offset_ranges)
-                })
-            })
-            .collect::<Vec<_>>();
-        ranges_by_path.sort_by_key(|(path, _)| path.clone());
-
-        assert_eq!(
-            ranges_by_path,
-            &[
-                (PathBuf::from("root-1/a"), vec![6..11]),
-                (PathBuf::from("root-1/c"), vec![2..7]),
-                (PathBuf::from("root-1/d"), vec![0..5, 24..29]),
-                (PathBuf::from("root-2/e"), vec![7..12]),
-            ]
-        );
-    }
-
-    #[gpui::test(iterations = 10)]
-    async fn test_document_highlights(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
-        cx_a.foreground().forbid_parking();
-        let lang_registry = Arc::new(LanguageRegistry::test());
-        let fs = FakeFs::new(cx_a.background());
-        fs.insert_tree(
-            "/root-1",
-            json!({
-                "main.rs": "fn double(number: i32) -> i32 { number + number }",
-            }),
-        )
-        .await;
-
-        // Set up a fake language server.
-        let mut language = Language::new(
-            LanguageConfig {
-                name: "Rust".into(),
-                path_suffixes: vec!["rs".to_string()],
-                ..Default::default()
-            },
-            Some(tree_sitter_rust::language()),
-        );
-        let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default());
-        lang_registry.add(Arc::new(language));
-
-        // Connect to a server as 2 clients.
-        let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
-        let client_a = server.create_client(cx_a, "user_a").await;
-        let mut client_b = server.create_client(cx_b, "user_b").await;
-        server
-            .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)])
-            .await;
-
-        // Share a project as client A
-        let project_a = cx_a.update(|cx| {
-            Project::local(
-                client_a.clone(),
-                client_a.user_store.clone(),
-                lang_registry.clone(),
-                fs.clone(),
-                cx,
-            )
-        });
-        let (worktree_a, _) = project_a
-            .update(cx_a, |p, cx| {
-                p.find_or_create_local_worktree("/root-1", true, cx)
-            })
-            .await
-            .unwrap();
-        worktree_a
-            .read_with(cx_a, |tree, _| tree.as_local().unwrap().scan_complete())
-            .await;
-        let worktree_id = worktree_a.read_with(cx_a, |tree, _| tree.id());
-
-        // Join the worktree as client B.
-        let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await;
-
-        // Open the file on client B.
-        let buffer_b = cx_b
-            .background()
-            .spawn(project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx)))
-            .await
-            .unwrap();
-
-        // Request document highlights as the guest.
-        let fake_language_server = fake_language_servers.next().await.unwrap();
-        fake_language_server.handle_request::<lsp::request::DocumentHighlightRequest, _, _>(
-            |params, _| async move {
-                assert_eq!(
-                    params
-                        .text_document_position_params
-                        .text_document
-                        .uri
-                        .as_str(),
-                    "file:///root-1/main.rs"
-                );
-                assert_eq!(
-                    params.text_document_position_params.position,
-                    lsp::Position::new(0, 34)
-                );
-                Ok(Some(vec![
-                    lsp::DocumentHighlight {
-                        kind: Some(lsp::DocumentHighlightKind::WRITE),
-                        range: lsp::Range::new(
-                            lsp::Position::new(0, 10),
-                            lsp::Position::new(0, 16),
-                        ),
-                    },
-                    lsp::DocumentHighlight {
-                        kind: Some(lsp::DocumentHighlightKind::READ),
-                        range: lsp::Range::new(
-                            lsp::Position::new(0, 32),
-                            lsp::Position::new(0, 38),
-                        ),
-                    },
-                    lsp::DocumentHighlight {
-                        kind: Some(lsp::DocumentHighlightKind::READ),
-                        range: lsp::Range::new(
-                            lsp::Position::new(0, 41),
-                            lsp::Position::new(0, 47),
-                        ),
-                    },
-                ]))
-            },
-        );
-
-        let highlights = project_b
-            .update(cx_b, |p, cx| p.document_highlights(&buffer_b, 34, cx))
-            .await
-            .unwrap();
-        buffer_b.read_with(cx_b, |buffer, _| {
-            let snapshot = buffer.snapshot();
-
-            let highlights = highlights
-                .into_iter()
-                .map(|highlight| (highlight.kind, highlight.range.to_offset(&snapshot)))
-                .collect::<Vec<_>>();
-            assert_eq!(
-                highlights,
-                &[
-                    (lsp::DocumentHighlightKind::WRITE, 10..16),
-                    (lsp::DocumentHighlightKind::READ, 32..38),
-                    (lsp::DocumentHighlightKind::READ, 41..47)
-                ]
-            )
-        });
-    }
-
-    #[gpui::test(iterations = 10)]
-    async fn test_project_symbols(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
-        cx_a.foreground().forbid_parking();
-        let lang_registry = Arc::new(LanguageRegistry::test());
-        let fs = FakeFs::new(cx_a.background());
-        fs.insert_tree(
-            "/code",
-            json!({
-                "crate-1": {
-                    "one.rs": "const ONE: usize = 1;",
-                },
-                "crate-2": {
-                    "two.rs": "const TWO: usize = 2; const THREE: usize = 3;",
-                },
-                "private": {
-                    "passwords.txt": "the-password",
-                }
-            }),
-        )
-        .await;
-
-        // Set up a fake language server.
-        let mut language = Language::new(
-            LanguageConfig {
-                name: "Rust".into(),
-                path_suffixes: vec!["rs".to_string()],
-                ..Default::default()
-            },
-            Some(tree_sitter_rust::language()),
-        );
-        let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default());
-        lang_registry.add(Arc::new(language));
-
-        // Connect to a server as 2 clients.
-        let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
-        let client_a = server.create_client(cx_a, "user_a").await;
-        let mut client_b = server.create_client(cx_b, "user_b").await;
-        server
-            .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)])
-            .await;
-
-        // Share a project as client A
-        let project_a = cx_a.update(|cx| {
-            Project::local(
-                client_a.clone(),
-                client_a.user_store.clone(),
-                lang_registry.clone(),
-                fs.clone(),
-                cx,
-            )
-        });
-        let (worktree_a, _) = project_a
-            .update(cx_a, |p, cx| {
-                p.find_or_create_local_worktree("/code/crate-1", true, cx)
-            })
-            .await
-            .unwrap();
-        worktree_a
-            .read_with(cx_a, |tree, _| tree.as_local().unwrap().scan_complete())
-            .await;
-        let worktree_id = worktree_a.read_with(cx_a, |tree, _| tree.id());
-
-        // Join the worktree as client B.
-        let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await;
-
-        // Cause the language server to start.
-        let _buffer = cx_b
-            .background()
-            .spawn(project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "one.rs"), cx)))
-            .await
-            .unwrap();
-
-        let fake_language_server = fake_language_servers.next().await.unwrap();
-        fake_language_server.handle_request::<lsp::request::WorkspaceSymbol, _, _>(
-            |_, _| async move {
-                #[allow(deprecated)]
-                Ok(Some(vec![lsp::SymbolInformation {
-                    name: "TWO".into(),
-                    location: lsp::Location {
-                        uri: lsp::Url::from_file_path("/code/crate-2/two.rs").unwrap(),
-                        range: lsp::Range::new(lsp::Position::new(0, 6), lsp::Position::new(0, 9)),
-                    },
-                    kind: lsp::SymbolKind::CONSTANT,
-                    tags: None,
-                    container_name: None,
-                    deprecated: None,
-                }]))
-            },
-        );
-
-        // Request the definition of a symbol as the guest.
-        let symbols = project_b
-            .update(cx_b, |p, cx| p.symbols("two", cx))
-            .await
-            .unwrap();
-        assert_eq!(symbols.len(), 1);
-        assert_eq!(symbols[0].name, "TWO");
-
-        // Open one of the returned symbols.
-        let buffer_b_2 = project_b
-            .update(cx_b, |project, cx| {
-                project.open_buffer_for_symbol(&symbols[0], cx)
-            })
-            .await
-            .unwrap();
-        buffer_b_2.read_with(cx_b, |buffer, _| {
-            assert_eq!(
-                buffer.file().unwrap().path().as_ref(),
-                Path::new("../crate-2/two.rs")
-            );
-        });
-
-        // Attempt to craft a symbol and violate host's privacy by opening an arbitrary file.
-        let mut fake_symbol = symbols[0].clone();
-        fake_symbol.path = Path::new("/code/secrets").into();
-        let error = project_b
-            .update(cx_b, |project, cx| {
-                project.open_buffer_for_symbol(&fake_symbol, cx)
-            })
-            .await
-            .unwrap_err();
-        assert!(error.to_string().contains("invalid symbol signature"));
-    }
-
-    #[gpui::test(iterations = 10)]
-    async fn test_open_buffer_while_getting_definition_pointing_to_it(
-        cx_a: &mut TestAppContext,
-        cx_b: &mut TestAppContext,
-        mut rng: StdRng,
-    ) {
-        cx_a.foreground().forbid_parking();
-        let lang_registry = Arc::new(LanguageRegistry::test());
-        let fs = FakeFs::new(cx_a.background());
-        fs.insert_tree(
-            "/root",
-            json!({
-                "a.rs": "const ONE: usize = b::TWO;",
-                "b.rs": "const TWO: usize = 2",
-            }),
-        )
-        .await;
-
-        // Set up a fake language server.
-        let mut language = Language::new(
-            LanguageConfig {
-                name: "Rust".into(),
-                path_suffixes: vec!["rs".to_string()],
-                ..Default::default()
-            },
-            Some(tree_sitter_rust::language()),
-        );
-        let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default());
-        lang_registry.add(Arc::new(language));
-
-        // Connect to a server as 2 clients.
-        let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
-        let client_a = server.create_client(cx_a, "user_a").await;
-        let mut client_b = server.create_client(cx_b, "user_b").await;
-        server
-            .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)])
-            .await;
-
-        // Share a project as client A
-        let project_a = cx_a.update(|cx| {
-            Project::local(
-                client_a.clone(),
-                client_a.user_store.clone(),
-                lang_registry.clone(),
-                fs.clone(),
-                cx,
-            )
-        });
-
-        let (worktree_a, _) = project_a
-            .update(cx_a, |p, cx| {
-                p.find_or_create_local_worktree("/root", true, cx)
-            })
-            .await
-            .unwrap();
-        worktree_a
-            .read_with(cx_a, |tree, _| tree.as_local().unwrap().scan_complete())
-            .await;
-        let worktree_id = worktree_a.read_with(cx_a, |tree, _| tree.id());
-
-        // Join the project as client B.
-        let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await;
-
-        let buffer_b1 = cx_b
-            .background()
-            .spawn(project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx)))
-            .await
-            .unwrap();
-
-        let fake_language_server = fake_language_servers.next().await.unwrap();
-        fake_language_server.handle_request::<lsp::request::GotoDefinition, _, _>(
-            |_, _| async move {
-                Ok(Some(lsp::GotoDefinitionResponse::Scalar(
-                    lsp::Location::new(
-                        lsp::Url::from_file_path("/root/b.rs").unwrap(),
-                        lsp::Range::new(lsp::Position::new(0, 6), lsp::Position::new(0, 9)),
-                    ),
-                )))
-            },
-        );
-
-        let definitions;
-        let buffer_b2;
-        if rng.gen() {
-            definitions = project_b.update(cx_b, |p, cx| p.definition(&buffer_b1, 23, cx));
-            buffer_b2 = project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "b.rs"), cx));
-        } else {
-            buffer_b2 = project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "b.rs"), cx));
-            definitions = project_b.update(cx_b, |p, cx| p.definition(&buffer_b1, 23, cx));
-        }
-
-        let buffer_b2 = buffer_b2.await.unwrap();
-        let definitions = definitions.await.unwrap();
-        assert_eq!(definitions.len(), 1);
-        assert_eq!(definitions[0].buffer, buffer_b2);
-    }
-
-    #[gpui::test(iterations = 10)]
-    async fn test_collaborating_with_code_actions(
-        cx_a: &mut TestAppContext,
-        cx_b: &mut TestAppContext,
-    ) {
-        cx_a.foreground().forbid_parking();
-        let lang_registry = Arc::new(LanguageRegistry::test());
-        let fs = FakeFs::new(cx_a.background());
-        cx_b.update(|cx| editor::init(cx));
-
-        // Set up a fake language server.
-        let mut language = Language::new(
-            LanguageConfig {
-                name: "Rust".into(),
-                path_suffixes: vec!["rs".to_string()],
-                ..Default::default()
-            },
-            Some(tree_sitter_rust::language()),
-        );
-        let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default());
-        lang_registry.add(Arc::new(language));
-
-        // Connect to a server as 2 clients.
-        let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
-        let client_a = server.create_client(cx_a, "user_a").await;
-        let mut client_b = server.create_client(cx_b, "user_b").await;
-        server
-            .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)])
-            .await;
-
-        // Share a project as client A
-        fs.insert_tree(
-            "/a",
-            json!({
-                "main.rs": "mod other;\nfn main() { let foo = other::foo(); }",
-                "other.rs": "pub fn foo() -> usize { 4 }",
-            }),
-        )
-        .await;
-        let project_a = cx_a.update(|cx| {
-            Project::local(
-                client_a.clone(),
-                client_a.user_store.clone(),
-                lang_registry.clone(),
-                fs.clone(),
-                cx,
-            )
-        });
-        let (worktree_a, _) = project_a
-            .update(cx_a, |p, cx| {
-                p.find_or_create_local_worktree("/a", true, cx)
-            })
-            .await
-            .unwrap();
-        worktree_a
-            .read_with(cx_a, |tree, _| tree.as_local().unwrap().scan_complete())
-            .await;
-        let worktree_id = worktree_a.read_with(cx_a, |tree, _| tree.id());
-
-        // Join the project as client B.
-        let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await;
-        let (_window_b, workspace_b) = cx_b.add_window(|cx| Workspace::new(project_b.clone(), cx));
-        let editor_b = workspace_b
-            .update(cx_b, |workspace, cx| {
-                workspace.open_path((worktree_id, "main.rs"), true, cx)
-            })
-            .await
-            .unwrap()
-            .downcast::<Editor>()
-            .unwrap();
-
-        let mut fake_language_server = fake_language_servers.next().await.unwrap();
-        fake_language_server
-            .handle_request::<lsp::request::CodeActionRequest, _, _>(|params, _| async move {
-                assert_eq!(
-                    params.text_document.uri,
-                    lsp::Url::from_file_path("/a/main.rs").unwrap(),
-                );
-                assert_eq!(params.range.start, lsp::Position::new(0, 0));
-                assert_eq!(params.range.end, lsp::Position::new(0, 0));
-                Ok(None)
-            })
-            .next()
-            .await;
-
-        // Move cursor to a location that contains code actions.
-        editor_b.update(cx_b, |editor, cx| {
-            editor.change_selections(None, cx, |s| {
-                s.select_ranges([Point::new(1, 31)..Point::new(1, 31)])
-            });
-            cx.focus(&editor_b);
-        });
-
-        fake_language_server
-            .handle_request::<lsp::request::CodeActionRequest, _, _>(|params, _| async move {
-                assert_eq!(
-                    params.text_document.uri,
-                    lsp::Url::from_file_path("/a/main.rs").unwrap(),
-                );
-                assert_eq!(params.range.start, lsp::Position::new(1, 31));
-                assert_eq!(params.range.end, lsp::Position::new(1, 31));
-
-                Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
-                    lsp::CodeAction {
-                        title: "Inline into all callers".to_string(),
-                        edit: Some(lsp::WorkspaceEdit {
-                            changes: Some(
-                                [
-                                    (
-                                        lsp::Url::from_file_path("/a/main.rs").unwrap(),
-                                        vec![lsp::TextEdit::new(
-                                            lsp::Range::new(
-                                                lsp::Position::new(1, 22),
-                                                lsp::Position::new(1, 34),
-                                            ),
-                                            "4".to_string(),
-                                        )],
-                                    ),
-                                    (
-                                        lsp::Url::from_file_path("/a/other.rs").unwrap(),
-                                        vec![lsp::TextEdit::new(
-                                            lsp::Range::new(
-                                                lsp::Position::new(0, 0),
-                                                lsp::Position::new(0, 27),
-                                            ),
-                                            "".to_string(),
-                                        )],
-                                    ),
-                                ]
-                                .into_iter()
-                                .collect(),
-                            ),
-                            ..Default::default()
-                        }),
-                        data: Some(json!({
-                            "codeActionParams": {
-                                "range": {
-                                    "start": {"line": 1, "column": 31},
-                                    "end": {"line": 1, "column": 31},
-                                }
-                            }
-                        })),
-                        ..Default::default()
-                    },
-                )]))
-            })
-            .next()
-            .await;
-
-        // Toggle code actions and wait for them to display.
-        editor_b.update(cx_b, |editor, cx| {
-            editor.toggle_code_actions(
-                &ToggleCodeActions {
-                    deployed_from_indicator: false,
-                },
-                cx,
-            );
-        });
-        editor_b
-            .condition(&cx_b, |editor, _| editor.context_menu_visible())
-            .await;
-
-        fake_language_server.remove_request_handler::<lsp::request::CodeActionRequest>();
-
-        // Confirming the code action will trigger a resolve request.
-        let confirm_action = workspace_b
-            .update(cx_b, |workspace, cx| {
-                Editor::confirm_code_action(workspace, &ConfirmCodeAction { item_ix: Some(0) }, cx)
-            })
-            .unwrap();
-        fake_language_server.handle_request::<lsp::request::CodeActionResolveRequest, _, _>(
-            |_, _| async move {
-                Ok(lsp::CodeAction {
-                    title: "Inline into all callers".to_string(),
-                    edit: Some(lsp::WorkspaceEdit {
-                        changes: Some(
-                            [
-                                (
-                                    lsp::Url::from_file_path("/a/main.rs").unwrap(),
-                                    vec![lsp::TextEdit::new(
-                                        lsp::Range::new(
-                                            lsp::Position::new(1, 22),
-                                            lsp::Position::new(1, 34),
-                                        ),
-                                        "4".to_string(),
-                                    )],
-                                ),
-                                (
-                                    lsp::Url::from_file_path("/a/other.rs").unwrap(),
-                                    vec![lsp::TextEdit::new(
-                                        lsp::Range::new(
-                                            lsp::Position::new(0, 0),
-                                            lsp::Position::new(0, 27),
-                                        ),
-                                        "".to_string(),
-                                    )],
-                                ),
-                            ]
-                            .into_iter()
-                            .collect(),
-                        ),
-                        ..Default::default()
-                    }),
-                    ..Default::default()
-                })
-            },
-        );
-
-        // After the action is confirmed, an editor containing both modified files is opened.
-        confirm_action.await.unwrap();
-        let code_action_editor = workspace_b.read_with(cx_b, |workspace, cx| {
-            workspace
-                .active_item(cx)
-                .unwrap()
-                .downcast::<Editor>()
-                .unwrap()
-        });
-        code_action_editor.update(cx_b, |editor, cx| {
-            assert_eq!(editor.text(cx), "mod other;\nfn main() { let foo = 4; }\n");
-            editor.undo(&Undo, cx);
-            assert_eq!(
-                editor.text(cx),
-                "mod other;\nfn main() { let foo = other::foo(); }\npub fn foo() -> usize { 4 }"
-            );
-            editor.redo(&Redo, cx);
-            assert_eq!(editor.text(cx), "mod other;\nfn main() { let foo = 4; }\n");
-        });
-    }
-
-    #[gpui::test(iterations = 10)]
-    async fn test_collaborating_with_renames(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
-        cx_a.foreground().forbid_parking();
-        let lang_registry = Arc::new(LanguageRegistry::test());
-        let fs = FakeFs::new(cx_a.background());
-        cx_b.update(|cx| editor::init(cx));
-
-        // Set up a fake language server.
-        let mut language = Language::new(
-            LanguageConfig {
-                name: "Rust".into(),
-                path_suffixes: vec!["rs".to_string()],
-                ..Default::default()
-            },
-            Some(tree_sitter_rust::language()),
-        );
-        let mut fake_language_servers = language.set_fake_lsp_adapter(FakeLspAdapter {
-            capabilities: lsp::ServerCapabilities {
-                rename_provider: Some(lsp::OneOf::Right(lsp::RenameOptions {
-                    prepare_provider: Some(true),
-                    work_done_progress_options: Default::default(),
-                })),
-                ..Default::default()
-            },
-            ..Default::default()
-        });
-        lang_registry.add(Arc::new(language));
-
-        // Connect to a server as 2 clients.
-        let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
-        let client_a = server.create_client(cx_a, "user_a").await;
-        let mut client_b = server.create_client(cx_b, "user_b").await;
-        server
-            .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)])
-            .await;
-
-        // Share a project as client A
-        fs.insert_tree(
-            "/dir",
-            json!({
-                "one.rs": "const ONE: usize = 1;",
-                "two.rs": "const TWO: usize = one::ONE + one::ONE;"
-            }),
-        )
-        .await;
-        let project_a = cx_a.update(|cx| {
-            Project::local(
-                client_a.clone(),
-                client_a.user_store.clone(),
-                lang_registry.clone(),
-                fs.clone(),
-                cx,
-            )
-        });
-        let (worktree_a, _) = project_a
-            .update(cx_a, |p, cx| {
-                p.find_or_create_local_worktree("/dir", true, cx)
-            })
-            .await
-            .unwrap();
-        worktree_a
-            .read_with(cx_a, |tree, _| tree.as_local().unwrap().scan_complete())
-            .await;
-        let worktree_id = worktree_a.read_with(cx_a, |tree, _| tree.id());
-
-        // Join the worktree as client B.
-        let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await;
-        let (_window_b, workspace_b) = cx_b.add_window(|cx| Workspace::new(project_b.clone(), cx));
-        let editor_b = workspace_b
-            .update(cx_b, |workspace, cx| {
-                workspace.open_path((worktree_id, "one.rs"), true, cx)
-            })
-            .await
-            .unwrap()
-            .downcast::<Editor>()
-            .unwrap();
-        let fake_language_server = fake_language_servers.next().await.unwrap();
-
-        // Move cursor to a location that can be renamed.
-        let prepare_rename = editor_b.update(cx_b, |editor, cx| {
-            editor.change_selections(None, cx, |s| s.select_ranges([7..7]));
-            editor.rename(&Rename, cx).unwrap()
-        });
-
-        fake_language_server
-            .handle_request::<lsp::request::PrepareRenameRequest, _, _>(|params, _| async move {
-                assert_eq!(params.text_document.uri.as_str(), "file:///dir/one.rs");
-                assert_eq!(params.position, lsp::Position::new(0, 7));
-                Ok(Some(lsp::PrepareRenameResponse::Range(lsp::Range::new(
-                    lsp::Position::new(0, 6),
-                    lsp::Position::new(0, 9),
-                ))))
-            })
-            .next()
-            .await
-            .unwrap();
-        prepare_rename.await.unwrap();
-        editor_b.update(cx_b, |editor, cx| {
-            let rename = editor.pending_rename().unwrap();
-            let buffer = editor.buffer().read(cx).snapshot(cx);
-            assert_eq!(
-                rename.range.start.to_offset(&buffer)..rename.range.end.to_offset(&buffer),
-                6..9
-            );
-            rename.editor.update(cx, |rename_editor, cx| {
-                rename_editor.buffer().update(cx, |rename_buffer, cx| {
-                    rename_buffer.edit([(0..3, "THREE")], cx);
-                });
-            });
-        });
-
-        let confirm_rename = workspace_b.update(cx_b, |workspace, cx| {
-            Editor::confirm_rename(workspace, &ConfirmRename, cx).unwrap()
-        });
-        fake_language_server
-            .handle_request::<lsp::request::Rename, _, _>(|params, _| async move {
-                assert_eq!(
-                    params.text_document_position.text_document.uri.as_str(),
-                    "file:///dir/one.rs"
-                );
-                assert_eq!(
-                    params.text_document_position.position,
-                    lsp::Position::new(0, 6)
-                );
-                assert_eq!(params.new_name, "THREE");
-                Ok(Some(lsp::WorkspaceEdit {
-                    changes: Some(
-                        [
-                            (
-                                lsp::Url::from_file_path("/dir/one.rs").unwrap(),
-                                vec![lsp::TextEdit::new(
-                                    lsp::Range::new(
-                                        lsp::Position::new(0, 6),
-                                        lsp::Position::new(0, 9),
-                                    ),
-                                    "THREE".to_string(),
-                                )],
-                            ),
-                            (
-                                lsp::Url::from_file_path("/dir/two.rs").unwrap(),
-                                vec![
-                                    lsp::TextEdit::new(
-                                        lsp::Range::new(
-                                            lsp::Position::new(0, 24),
-                                            lsp::Position::new(0, 27),
-                                        ),
-                                        "THREE".to_string(),
-                                    ),
-                                    lsp::TextEdit::new(
-                                        lsp::Range::new(
-                                            lsp::Position::new(0, 35),
-                                            lsp::Position::new(0, 38),
-                                        ),
-                                        "THREE".to_string(),
-                                    ),
-                                ],
-                            ),
-                        ]
-                        .into_iter()
-                        .collect(),
-                    ),
-                    ..Default::default()
-                }))
-            })
-            .next()
-            .await
-            .unwrap();
-        confirm_rename.await.unwrap();
-
-        let rename_editor = workspace_b.read_with(cx_b, |workspace, cx| {
-            workspace
-                .active_item(cx)
-                .unwrap()
-                .downcast::<Editor>()
-                .unwrap()
-        });
-        rename_editor.update(cx_b, |editor, cx| {
-            assert_eq!(
-                editor.text(cx),
-                "const THREE: usize = 1;\nconst TWO: usize = one::THREE + one::THREE;"
-            );
-            editor.undo(&Undo, cx);
-            assert_eq!(
-                editor.text(cx),
-                "const ONE: usize = 1;\nconst TWO: usize = one::ONE + one::ONE;"
-            );
-            editor.redo(&Redo, cx);
-            assert_eq!(
-                editor.text(cx),
-                "const THREE: usize = 1;\nconst TWO: usize = one::THREE + one::THREE;"
-            );
-        });
-
-        // Ensure temporary rename edits cannot be undone/redone.
-        editor_b.update(cx_b, |editor, cx| {
-            editor.undo(&Undo, cx);
-            assert_eq!(editor.text(cx), "const ONE: usize = 1;");
-            editor.undo(&Undo, cx);
-            assert_eq!(editor.text(cx), "const ONE: usize = 1;");
-            editor.redo(&Redo, cx);
-            assert_eq!(editor.text(cx), "const THREE: usize = 1;");
-        })
-    }
-
-    #[gpui::test(iterations = 10)]
-    async fn test_basic_chat(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
-        cx_a.foreground().forbid_parking();
-
-        // Connect to a server as 2 clients.
-        let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
-        let client_a = server.create_client(cx_a, "user_a").await;
-        let client_b = server.create_client(cx_b, "user_b").await;
-
-        // Create an org that includes these 2 users.
-        let db = &server.app_state.db;
-        let org_id = db.create_org("Test Org", "test-org").await.unwrap();
-        db.add_org_member(org_id, client_a.current_user_id(&cx_a), false)
-            .await
-            .unwrap();
-        db.add_org_member(org_id, client_b.current_user_id(&cx_b), false)
-            .await
-            .unwrap();
-
-        // Create a channel that includes all the users.
-        let channel_id = db.create_org_channel(org_id, "test-channel").await.unwrap();
-        db.add_channel_member(channel_id, client_a.current_user_id(&cx_a), false)
-            .await
-            .unwrap();
-        db.add_channel_member(channel_id, client_b.current_user_id(&cx_b), false)
-            .await
-            .unwrap();
-        db.create_channel_message(
-            channel_id,
-            client_b.current_user_id(&cx_b),
-            "hello A, it's B.",
-            OffsetDateTime::now_utc(),
-            1,
-        )
-        .await
-        .unwrap();
-
-        let channels_a = cx_a
-            .add_model(|cx| ChannelList::new(client_a.user_store.clone(), client_a.clone(), cx));
-        channels_a
-            .condition(cx_a, |list, _| list.available_channels().is_some())
-            .await;
-        channels_a.read_with(cx_a, |list, _| {
-            assert_eq!(
-                list.available_channels().unwrap(),
-                &[ChannelDetails {
-                    id: channel_id.to_proto(),
-                    name: "test-channel".to_string()
-                }]
-            )
-        });
-        let channel_a = channels_a.update(cx_a, |this, cx| {
-            this.get_channel(channel_id.to_proto(), cx).unwrap()
-        });
-        channel_a.read_with(cx_a, |channel, _| assert!(channel.messages().is_empty()));
-        channel_a
-            .condition(&cx_a, |channel, _| {
-                channel_messages(channel)
-                    == [("user_b".to_string(), "hello A, it's B.".to_string(), false)]
-            })
-            .await;
-
-        let channels_b = cx_b
-            .add_model(|cx| ChannelList::new(client_b.user_store.clone(), client_b.clone(), cx));
-        channels_b
-            .condition(cx_b, |list, _| list.available_channels().is_some())
-            .await;
-        channels_b.read_with(cx_b, |list, _| {
-            assert_eq!(
-                list.available_channels().unwrap(),
-                &[ChannelDetails {
-                    id: channel_id.to_proto(),
-                    name: "test-channel".to_string()
-                }]
-            )
-        });
-
-        let channel_b = channels_b.update(cx_b, |this, cx| {
-            this.get_channel(channel_id.to_proto(), cx).unwrap()
-        });
-        channel_b.read_with(cx_b, |channel, _| assert!(channel.messages().is_empty()));
-        channel_b
-            .condition(&cx_b, |channel, _| {
-                channel_messages(channel)
-                    == [("user_b".to_string(), "hello A, it's B.".to_string(), false)]
-            })
-            .await;
-
-        channel_a
-            .update(cx_a, |channel, cx| {
-                channel
-                    .send_message("oh, hi B.".to_string(), cx)
-                    .unwrap()
-                    .detach();
-                let task = channel.send_message("sup".to_string(), cx).unwrap();
-                assert_eq!(
-                    channel_messages(channel),
-                    &[
-                        ("user_b".to_string(), "hello A, it's B.".to_string(), false),
-                        ("user_a".to_string(), "oh, hi B.".to_string(), true),
-                        ("user_a".to_string(), "sup".to_string(), true)
-                    ]
-                );
-                task
-            })
-            .await
-            .unwrap();
-
-        channel_b
-            .condition(&cx_b, |channel, _| {
-                channel_messages(channel)
-                    == [
-                        ("user_b".to_string(), "hello A, it's B.".to_string(), false),
-                        ("user_a".to_string(), "oh, hi B.".to_string(), false),
-                        ("user_a".to_string(), "sup".to_string(), false),
-                    ]
-            })
-            .await;
-
-        assert_eq!(
-            server
-                .state()
-                .await
-                .channel(channel_id)
-                .unwrap()
-                .connection_ids
-                .len(),
-            2
-        );
-        cx_b.update(|_| drop(channel_b));
-        server
-            .condition(|state| state.channel(channel_id).unwrap().connection_ids.len() == 1)
-            .await;
-
-        cx_a.update(|_| drop(channel_a));
-        server
-            .condition(|state| state.channel(channel_id).is_none())
-            .await;
-    }
-
-    #[gpui::test(iterations = 10)]
-    async fn test_chat_message_validation(cx_a: &mut TestAppContext) {
-        cx_a.foreground().forbid_parking();
-
-        let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
-        let client_a = server.create_client(cx_a, "user_a").await;
-
-        let db = &server.app_state.db;
-        let org_id = db.create_org("Test Org", "test-org").await.unwrap();
-        let channel_id = db.create_org_channel(org_id, "test-channel").await.unwrap();
-        db.add_org_member(org_id, client_a.current_user_id(&cx_a), false)
-            .await
-            .unwrap();
-        db.add_channel_member(channel_id, client_a.current_user_id(&cx_a), false)
-            .await
-            .unwrap();
-
-        let channels_a = cx_a
-            .add_model(|cx| ChannelList::new(client_a.user_store.clone(), client_a.clone(), cx));
-        channels_a
-            .condition(cx_a, |list, _| list.available_channels().is_some())
-            .await;
-        let channel_a = channels_a.update(cx_a, |this, cx| {
-            this.get_channel(channel_id.to_proto(), cx).unwrap()
-        });
-
-        // Messages aren't allowed to be too long.
-        channel_a
-            .update(cx_a, |channel, cx| {
-                let long_body = "this is long.\n".repeat(1024);
-                channel.send_message(long_body, cx).unwrap()
-            })
-            .await
-            .unwrap_err();
-
-        // Messages aren't allowed to be blank.
-        channel_a.update(cx_a, |channel, cx| {
-            channel.send_message(String::new(), cx).unwrap_err()
-        });
-
-        // Leading and trailing whitespace are trimmed.
-        channel_a
-            .update(cx_a, |channel, cx| {
-                channel
-                    .send_message("\n surrounded by whitespace  \n".to_string(), cx)
-                    .unwrap()
-            })
-            .await
-            .unwrap();
-        assert_eq!(
-            db.get_channel_messages(channel_id, 10, None)
-                .await
-                .unwrap()
-                .iter()
-                .map(|m| &m.body)
-                .collect::<Vec<_>>(),
-            &["surrounded by whitespace"]
-        );
-    }
-
-    #[gpui::test(iterations = 10)]
-    async fn test_chat_reconnection(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
-        cx_a.foreground().forbid_parking();
-
-        // Connect to a server as 2 clients.
-        let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
-        let client_a = server.create_client(cx_a, "user_a").await;
-        let client_b = server.create_client(cx_b, "user_b").await;
-        let mut status_b = client_b.status();
-
-        // Create an org that includes these 2 users.
-        let db = &server.app_state.db;
-        let org_id = db.create_org("Test Org", "test-org").await.unwrap();
-        db.add_org_member(org_id, client_a.current_user_id(&cx_a), false)
-            .await
-            .unwrap();
-        db.add_org_member(org_id, client_b.current_user_id(&cx_b), false)
-            .await
-            .unwrap();
-
-        // Create a channel that includes all the users.
-        let channel_id = db.create_org_channel(org_id, "test-channel").await.unwrap();
-        db.add_channel_member(channel_id, client_a.current_user_id(&cx_a), false)
-            .await
-            .unwrap();
-        db.add_channel_member(channel_id, client_b.current_user_id(&cx_b), false)
-            .await
-            .unwrap();
-        db.create_channel_message(
-            channel_id,
-            client_b.current_user_id(&cx_b),
-            "hello A, it's B.",
-            OffsetDateTime::now_utc(),
-            2,
-        )
-        .await
-        .unwrap();
-
-        let channels_a = cx_a
-            .add_model(|cx| ChannelList::new(client_a.user_store.clone(), client_a.clone(), cx));
-        channels_a
-            .condition(cx_a, |list, _| list.available_channels().is_some())
-            .await;
-
-        channels_a.read_with(cx_a, |list, _| {
-            assert_eq!(
-                list.available_channels().unwrap(),
-                &[ChannelDetails {
-                    id: channel_id.to_proto(),
-                    name: "test-channel".to_string()
-                }]
-            )
-        });
-        let channel_a = channels_a.update(cx_a, |this, cx| {
-            this.get_channel(channel_id.to_proto(), cx).unwrap()
-        });
-        channel_a.read_with(cx_a, |channel, _| assert!(channel.messages().is_empty()));
-        channel_a
-            .condition(&cx_a, |channel, _| {
-                channel_messages(channel)
-                    == [("user_b".to_string(), "hello A, it's B.".to_string(), false)]
-            })
-            .await;
-
-        let channels_b = cx_b
-            .add_model(|cx| ChannelList::new(client_b.user_store.clone(), client_b.clone(), cx));
-        channels_b
-            .condition(cx_b, |list, _| list.available_channels().is_some())
-            .await;
-        channels_b.read_with(cx_b, |list, _| {
-            assert_eq!(
-                list.available_channels().unwrap(),
-                &[ChannelDetails {
-                    id: channel_id.to_proto(),
-                    name: "test-channel".to_string()
-                }]
-            )
-        });
-
-        let channel_b = channels_b.update(cx_b, |this, cx| {
-            this.get_channel(channel_id.to_proto(), cx).unwrap()
-        });
-        channel_b.read_with(cx_b, |channel, _| assert!(channel.messages().is_empty()));
-        channel_b
-            .condition(&cx_b, |channel, _| {
-                channel_messages(channel)
-                    == [("user_b".to_string(), "hello A, it's B.".to_string(), false)]
-            })
-            .await;
-
-        // Disconnect client B, ensuring we can still access its cached channel data.
-        server.forbid_connections();
-        server.disconnect_client(client_b.current_user_id(&cx_b));
-        cx_b.foreground().advance_clock(rpc::RECEIVE_TIMEOUT);
-        while !matches!(
-            status_b.next().await,
-            Some(client::Status::ReconnectionError { .. })
-        ) {}
-
-        channels_b.read_with(cx_b, |channels, _| {
-            assert_eq!(
-                channels.available_channels().unwrap(),
-                [ChannelDetails {
-                    id: channel_id.to_proto(),
-                    name: "test-channel".to_string()
-                }]
-            )
-        });
-        channel_b.read_with(cx_b, |channel, _| {
-            assert_eq!(
-                channel_messages(channel),
-                [("user_b".to_string(), "hello A, it's B.".to_string(), false)]
-            )
-        });
-
-        // Send a message from client B while it is disconnected.
-        channel_b
-            .update(cx_b, |channel, cx| {
-                let task = channel
-                    .send_message("can you see this?".to_string(), cx)
-                    .unwrap();
-                assert_eq!(
-                    channel_messages(channel),
-                    &[
-                        ("user_b".to_string(), "hello A, it's B.".to_string(), false),
-                        ("user_b".to_string(), "can you see this?".to_string(), true)
-                    ]
-                );
-                task
-            })
-            .await
-            .unwrap_err();
-
-        // Send a message from client A while B is disconnected.
-        channel_a
-            .update(cx_a, |channel, cx| {
-                channel
-                    .send_message("oh, hi B.".to_string(), cx)
-                    .unwrap()
-                    .detach();
-                let task = channel.send_message("sup".to_string(), cx).unwrap();
-                assert_eq!(
-                    channel_messages(channel),
-                    &[
-                        ("user_b".to_string(), "hello A, it's B.".to_string(), false),
-                        ("user_a".to_string(), "oh, hi B.".to_string(), true),
-                        ("user_a".to_string(), "sup".to_string(), true)
-                    ]
-                );
-                task
-            })
-            .await
-            .unwrap();
-
-        // Give client B a chance to reconnect.
-        server.allow_connections();
-        cx_b.foreground().advance_clock(Duration::from_secs(10));
-
-        // Verify that B sees the new messages upon reconnection, as well as the message client B
-        // sent while offline.
-        channel_b
-            .condition(&cx_b, |channel, _| {
-                channel_messages(channel)
-                    == [
-                        ("user_b".to_string(), "hello A, it's B.".to_string(), false),
-                        ("user_a".to_string(), "oh, hi B.".to_string(), false),
-                        ("user_a".to_string(), "sup".to_string(), false),
-                        ("user_b".to_string(), "can you see this?".to_string(), false),
-                    ]
-            })
-            .await;
-
-        // Ensure client A and B can communicate normally after reconnection.
-        channel_a
-            .update(cx_a, |channel, cx| {
-                channel.send_message("you online?".to_string(), cx).unwrap()
-            })
-            .await
-            .unwrap();
-        channel_b
-            .condition(&cx_b, |channel, _| {
-                channel_messages(channel)
-                    == [
-                        ("user_b".to_string(), "hello A, it's B.".to_string(), false),
-                        ("user_a".to_string(), "oh, hi B.".to_string(), false),
-                        ("user_a".to_string(), "sup".to_string(), false),
-                        ("user_b".to_string(), "can you see this?".to_string(), false),
-                        ("user_a".to_string(), "you online?".to_string(), false),
-                    ]
-            })
-            .await;
-
-        channel_b
-            .update(cx_b, |channel, cx| {
-                channel.send_message("yep".to_string(), cx).unwrap()
-            })
-            .await
-            .unwrap();
-        channel_a
-            .condition(&cx_a, |channel, _| {
-                channel_messages(channel)
-                    == [
-                        ("user_b".to_string(), "hello A, it's B.".to_string(), false),
-                        ("user_a".to_string(), "oh, hi B.".to_string(), false),
-                        ("user_a".to_string(), "sup".to_string(), false),
-                        ("user_b".to_string(), "can you see this?".to_string(), false),
-                        ("user_a".to_string(), "you online?".to_string(), false),
-                        ("user_b".to_string(), "yep".to_string(), false),
-                    ]
-            })
-            .await;
-    }
-
-    #[gpui::test(iterations = 10)]
-    async fn test_contacts(
-        deterministic: Arc<Deterministic>,
-        cx_a: &mut TestAppContext,
-        cx_b: &mut TestAppContext,
-        cx_c: &mut TestAppContext,
-    ) {
-        cx_a.foreground().forbid_parking();
-
-        // Connect to a server as 3 clients.
-        let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
-        let mut client_a = server.create_client(cx_a, "user_a").await;
-        let mut client_b = server.create_client(cx_b, "user_b").await;
-        let client_c = server.create_client(cx_c, "user_c").await;
-        server
-            .make_contacts(vec![
-                (&client_a, cx_a),
-                (&client_b, cx_b),
-                (&client_c, cx_c),
-            ])
-            .await;
-
-        deterministic.run_until_parked();
-        for (client, cx) in [(&client_a, &cx_a), (&client_b, &cx_b), (&client_c, &cx_c)] {
-            client.user_store.read_with(*cx, |store, _| {
-                assert_eq!(
-                    contacts(store),
-                    [
-                        ("user_a", true, vec![]),
-                        ("user_b", true, vec![]),
-                        ("user_c", true, vec![])
-                    ],
-                    "{} has the wrong contacts",
-                    client.username
-                )
-            });
-        }
-
-        // Share a project as client A.
-        let fs = FakeFs::new(cx_a.background());
-        fs.create_dir(Path::new("/a")).await.unwrap();
-        let (project_a, _) = client_a.build_local_project(fs, "/a", cx_a).await;
-
-        deterministic.run_until_parked();
-        for (client, cx) in [(&client_a, &cx_a), (&client_b, &cx_b), (&client_c, &cx_c)] {
-            client.user_store.read_with(*cx, |store, _| {
-                assert_eq!(
-                    contacts(store),
-                    [
-                        ("user_a", true, vec![("a", vec![])]),
-                        ("user_b", true, vec![]),
-                        ("user_c", true, vec![])
-                    ],
-                    "{} has the wrong contacts",
-                    client.username
-                )
-            });
-        }
-
-        let _project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await;
-
-        deterministic.run_until_parked();
-        for (client, cx) in [(&client_a, &cx_a), (&client_b, &cx_b), (&client_c, &cx_c)] {
-            client.user_store.read_with(*cx, |store, _| {
-                assert_eq!(
-                    contacts(store),
-                    [
-                        ("user_a", true, vec![("a", vec!["user_b"])]),
-                        ("user_b", true, vec![]),
-                        ("user_c", true, vec![])
-                    ],
-                    "{} has the wrong contacts",
-                    client.username
-                )
-            });
-        }
-
-        // Add a local project as client B
-        let fs = FakeFs::new(cx_b.background());
-        fs.create_dir(Path::new("/b")).await.unwrap();
-        let (_project_b, _) = client_b.build_local_project(fs, "/b", cx_a).await;
-
-        deterministic.run_until_parked();
-        for (client, cx) in [(&client_a, &cx_a), (&client_b, &cx_b), (&client_c, &cx_c)] {
-            client.user_store.read_with(*cx, |store, _| {
-                assert_eq!(
-                    contacts(store),
-                    [
-                        ("user_a", true, vec![("a", vec!["user_b"])]),
-                        ("user_b", true, vec![("b", vec![])]),
-                        ("user_c", true, vec![])
-                    ],
-                    "{} has the wrong contacts",
-                    client.username
-                )
-            });
-        }
-
-        project_a
-            .condition(&cx_a, |project, _| {
-                project.collaborators().contains_key(&client_b.peer_id)
-            })
-            .await;
-
-        client_a.project.take();
-        cx_a.update(move |_| drop(project_a));
-        deterministic.run_until_parked();
-        for (client, cx) in [(&client_a, &cx_a), (&client_b, &cx_b), (&client_c, &cx_c)] {
-            client.user_store.read_with(*cx, |store, _| {
-                assert_eq!(
-                    contacts(store),
-                    [
-                        ("user_a", true, vec![]),
-                        ("user_b", true, vec![("b", vec![])]),
-                        ("user_c", true, vec![])
-                    ],
-                    "{} has the wrong contacts",
-                    client.username
-                )
-            });
-        }
-
-        server.disconnect_client(client_c.current_user_id(cx_c));
-        server.forbid_connections();
-        deterministic.advance_clock(rpc::RECEIVE_TIMEOUT);
-        for (client, cx) in [(&client_a, &cx_a), (&client_b, &cx_b)] {
-            client.user_store.read_with(*cx, |store, _| {
-                assert_eq!(
-                    contacts(store),
-                    [
-                        ("user_a", true, vec![]),
-                        ("user_b", true, vec![("b", vec![])]),
-                        ("user_c", false, vec![])
-                    ],
-                    "{} has the wrong contacts",
-                    client.username
-                )
-            });
-        }
-        client_c
-            .user_store
-            .read_with(cx_c, |store, _| assert_eq!(contacts(store), []));
-
-        server.allow_connections();
-        client_c
-            .authenticate_and_connect(false, &cx_c.to_async())
-            .await
-            .unwrap();
-
-        deterministic.run_until_parked();
-        for (client, cx) in [(&client_a, &cx_a), (&client_b, &cx_b), (&client_c, &cx_c)] {
-            client.user_store.read_with(*cx, |store, _| {
-                assert_eq!(
-                    contacts(store),
-                    [
-                        ("user_a", true, vec![]),
-                        ("user_b", true, vec![("b", vec![])]),
-                        ("user_c", true, vec![])
-                    ],
-                    "{} has the wrong contacts",
-                    client.username
-                )
-            });
-        }
-
-        fn contacts(user_store: &UserStore) -> Vec<(&str, bool, Vec<(&str, Vec<&str>)>)> {
-            user_store
-                .contacts()
-                .iter()
-                .map(|contact| {
-                    let projects = contact
-                        .projects
-                        .iter()
-                        .map(|p| {
-                            (
-                                p.worktree_root_names[0].as_str(),
-                                p.guests.iter().map(|p| p.github_login.as_str()).collect(),
-                            )
-                        })
-                        .collect();
-                    (contact.user.github_login.as_str(), contact.online, projects)
-                })
-                .collect()
-        }
-    }
-
-    #[gpui::test(iterations = 10)]
-    async fn test_contact_requests(
-        executor: Arc<Deterministic>,
-        cx_a: &mut TestAppContext,
-        cx_a2: &mut TestAppContext,
-        cx_b: &mut TestAppContext,
-        cx_b2: &mut TestAppContext,
-        cx_c: &mut TestAppContext,
-        cx_c2: &mut TestAppContext,
-    ) {
-        cx_a.foreground().forbid_parking();
-
-        // Connect to a server as 3 clients.
-        let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
-        let client_a = server.create_client(cx_a, "user_a").await;
-        let client_a2 = server.create_client(cx_a2, "user_a").await;
-        let client_b = server.create_client(cx_b, "user_b").await;
-        let client_b2 = server.create_client(cx_b2, "user_b").await;
-        let client_c = server.create_client(cx_c, "user_c").await;
-        let client_c2 = server.create_client(cx_c2, "user_c").await;
-
-        assert_eq!(client_a.user_id().unwrap(), client_a2.user_id().unwrap());
-        assert_eq!(client_b.user_id().unwrap(), client_b2.user_id().unwrap());
-        assert_eq!(client_c.user_id().unwrap(), client_c2.user_id().unwrap());
-
-        // User A and User C request that user B become their contact.
-        client_a
-            .user_store
-            .update(cx_a, |store, cx| {
-                store.request_contact(client_b.user_id().unwrap(), cx)
-            })
-            .await
-            .unwrap();
-        client_c
-            .user_store
-            .update(cx_c, |store, cx| {
-                store.request_contact(client_b.user_id().unwrap(), cx)
-            })
-            .await
-            .unwrap();
-        executor.run_until_parked();
-
-        // All users see the pending request appear in all their clients.
-        assert_eq!(
-            client_a.summarize_contacts(&cx_a).outgoing_requests,
-            &["user_b"]
-        );
-        assert_eq!(
-            client_a2.summarize_contacts(&cx_a2).outgoing_requests,
-            &["user_b"]
-        );
-        assert_eq!(
-            client_b.summarize_contacts(&cx_b).incoming_requests,
-            &["user_a", "user_c"]
-        );
-        assert_eq!(
-            client_b2.summarize_contacts(&cx_b2).incoming_requests,
-            &["user_a", "user_c"]
-        );
-        assert_eq!(
-            client_c.summarize_contacts(&cx_c).outgoing_requests,
-            &["user_b"]
-        );
-        assert_eq!(
-            client_c2.summarize_contacts(&cx_c2).outgoing_requests,
-            &["user_b"]
-        );
-
-        // Contact requests are present upon connecting (tested here via disconnect/reconnect)
-        disconnect_and_reconnect(&client_a, cx_a).await;
-        disconnect_and_reconnect(&client_b, cx_b).await;
-        disconnect_and_reconnect(&client_c, cx_c).await;
-        executor.run_until_parked();
-        assert_eq!(
-            client_a.summarize_contacts(&cx_a).outgoing_requests,
-            &["user_b"]
-        );
-        assert_eq!(
-            client_b.summarize_contacts(&cx_b).incoming_requests,
-            &["user_a", "user_c"]
-        );
-        assert_eq!(
-            client_c.summarize_contacts(&cx_c).outgoing_requests,
-            &["user_b"]
-        );
-
-        // User B accepts the request from user A.
-        client_b
-            .user_store
-            .update(cx_b, |store, cx| {
-                store.respond_to_contact_request(client_a.user_id().unwrap(), true, cx)
-            })
-            .await
-            .unwrap();
-
-        executor.run_until_parked();
-
-        // User B sees user A as their contact now in all client, and the incoming request from them is removed.
-        let contacts_b = client_b.summarize_contacts(&cx_b);
-        assert_eq!(contacts_b.current, &["user_a", "user_b"]);
-        assert_eq!(contacts_b.incoming_requests, &["user_c"]);
-        let contacts_b2 = client_b2.summarize_contacts(&cx_b2);
-        assert_eq!(contacts_b2.current, &["user_a", "user_b"]);
-        assert_eq!(contacts_b2.incoming_requests, &["user_c"]);
-
-        // User A sees user B as their contact now in all clients, and the outgoing request to them is removed.
-        let contacts_a = client_a.summarize_contacts(&cx_a);
-        assert_eq!(contacts_a.current, &["user_a", "user_b"]);
-        assert!(contacts_a.outgoing_requests.is_empty());
-        let contacts_a2 = client_a2.summarize_contacts(&cx_a2);
-        assert_eq!(contacts_a2.current, &["user_a", "user_b"]);
-        assert!(contacts_a2.outgoing_requests.is_empty());
-
-        // Contacts are present upon connecting (tested here via disconnect/reconnect)
-        disconnect_and_reconnect(&client_a, cx_a).await;
-        disconnect_and_reconnect(&client_b, cx_b).await;
-        disconnect_and_reconnect(&client_c, cx_c).await;
-        executor.run_until_parked();
-        assert_eq!(
-            client_a.summarize_contacts(&cx_a).current,
-            &["user_a", "user_b"]
-        );
-        assert_eq!(
-            client_b.summarize_contacts(&cx_b).current,
-            &["user_a", "user_b"]
-        );
-        assert_eq!(
-            client_b.summarize_contacts(&cx_b).incoming_requests,
-            &["user_c"]
-        );
-        assert_eq!(client_c.summarize_contacts(&cx_c).current, &["user_c"]);
-        assert_eq!(
-            client_c.summarize_contacts(&cx_c).outgoing_requests,
-            &["user_b"]
-        );
-
-        // User B rejects the request from user C.
-        client_b
-            .user_store
-            .update(cx_b, |store, cx| {
-                store.respond_to_contact_request(client_c.user_id().unwrap(), false, cx)
-            })
-            .await
-            .unwrap();
-
-        executor.run_until_parked();
-
-        // User B doesn't see user C as their contact, and the incoming request from them is removed.
-        let contacts_b = client_b.summarize_contacts(&cx_b);
-        assert_eq!(contacts_b.current, &["user_a", "user_b"]);
-        assert!(contacts_b.incoming_requests.is_empty());
-        let contacts_b2 = client_b2.summarize_contacts(&cx_b2);
-        assert_eq!(contacts_b2.current, &["user_a", "user_b"]);
-        assert!(contacts_b2.incoming_requests.is_empty());
-
-        // User C doesn't see user B as their contact, and the outgoing request to them is removed.
-        let contacts_c = client_c.summarize_contacts(&cx_c);
-        assert_eq!(contacts_c.current, &["user_c"]);
-        assert!(contacts_c.outgoing_requests.is_empty());
-        let contacts_c2 = client_c2.summarize_contacts(&cx_c2);
-        assert_eq!(contacts_c2.current, &["user_c"]);
-        assert!(contacts_c2.outgoing_requests.is_empty());
-
-        // Incoming/outgoing requests are not present upon connecting (tested here via disconnect/reconnect)
-        disconnect_and_reconnect(&client_a, cx_a).await;
-        disconnect_and_reconnect(&client_b, cx_b).await;
-        disconnect_and_reconnect(&client_c, cx_c).await;
-        executor.run_until_parked();
-        assert_eq!(
-            client_a.summarize_contacts(&cx_a).current,
-            &["user_a", "user_b"]
-        );
-        assert_eq!(
-            client_b.summarize_contacts(&cx_b).current,
-            &["user_a", "user_b"]
-        );
-        assert!(client_b
-            .summarize_contacts(&cx_b)
-            .incoming_requests
-            .is_empty());
-        assert_eq!(client_c.summarize_contacts(&cx_c).current, &["user_c"]);
-        assert!(client_c
-            .summarize_contacts(&cx_c)
-            .outgoing_requests
-            .is_empty());
-
-        async fn disconnect_and_reconnect(client: &TestClient, cx: &mut TestAppContext) {
-            client.disconnect(&cx.to_async()).unwrap();
-            client.clear_contacts(cx).await;
-            client
-                .authenticate_and_connect(false, &cx.to_async())
-                .await
-                .unwrap();
-        }
-    }
-
-    #[gpui::test(iterations = 10)]
-    async fn test_following(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
-        cx_a.foreground().forbid_parking();
-        let fs = FakeFs::new(cx_a.background());
-
-        // 2 clients connect to a server.
-        let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
-        let mut client_a = server.create_client(cx_a, "user_a").await;
-        let mut client_b = server.create_client(cx_b, "user_b").await;
-        server
-            .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)])
-            .await;
-        cx_a.update(editor::init);
-        cx_b.update(editor::init);
-
-        // Client A shares a project.
-        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(fs.clone(), "/a", cx_a).await;
-
-        // Client B joins the project.
-        let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await;
-
-        // Client A opens some editors.
-        let workspace_a = client_a.build_workspace(&project_a, cx_a);
-        let pane_a = workspace_a.read_with(cx_a, |workspace, _| workspace.active_pane().clone());
-        let editor_a1 = workspace_a
-            .update(cx_a, |workspace, cx| {
-                workspace.open_path((worktree_id, "1.txt"), true, cx)
-            })
-            .await
-            .unwrap()
-            .downcast::<Editor>()
-            .unwrap();
-        let editor_a2 = workspace_a
-            .update(cx_a, |workspace, cx| {
-                workspace.open_path((worktree_id, "2.txt"), true, cx)
-            })
-            .await
-            .unwrap()
-            .downcast::<Editor>()
-            .unwrap();
-
-        // Client B opens an editor.
-        let workspace_b = client_b.build_workspace(&project_b, cx_b);
-        let editor_b1 = workspace_b
-            .update(cx_b, |workspace, cx| {
-                workspace.open_path((worktree_id, "1.txt"), true, cx)
-            })
-            .await
-            .unwrap()
-            .downcast::<Editor>()
-            .unwrap();
-
-        let client_a_id = project_b.read_with(cx_b, |project, _| {
-            project.collaborators().values().next().unwrap().peer_id
-        });
-        let client_b_id = project_a.read_with(cx_a, |project, _| {
-            project.collaborators().values().next().unwrap().peer_id
-        });
-
-        // When client B starts following client A, all visible view states are replicated to client B.
-        editor_a1.update(cx_a, |editor, cx| {
-            editor.change_selections(None, cx, |s| s.select_ranges([0..1]))
-        });
-        editor_a2.update(cx_a, |editor, cx| {
-            editor.change_selections(None, cx, |s| s.select_ranges([2..3]))
-        });
-        workspace_b
-            .update(cx_b, |workspace, cx| {
-                workspace
-                    .toggle_follow(&ToggleFollow(client_a_id), cx)
-                    .unwrap()
-            })
-            .await
-            .unwrap();
-
-        let editor_b2 = workspace_b.read_with(cx_b, |workspace, cx| {
-            workspace
-                .active_item(cx)
-                .unwrap()
-                .downcast::<Editor>()
-                .unwrap()
-        });
-        assert!(cx_b.read(|cx| editor_b2.is_focused(cx)));
-        assert_eq!(
-            editor_b2.read_with(cx_b, |editor, cx| editor.project_path(cx)),
-            Some((worktree_id, "2.txt").into())
-        );
-        assert_eq!(
-            editor_b2.read_with(cx_b, |editor, cx| editor.selections.ranges(cx)),
-            vec![2..3]
-        );
-        assert_eq!(
-            editor_b1.read_with(cx_b, |editor, cx| editor.selections.ranges(cx)),
-            vec![0..1]
-        );
-
-        // 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)
-        });
-        workspace_b
-            .condition(cx_b, |workspace, cx| {
-                workspace.active_item(cx).unwrap().id() == editor_b1.id()
-            })
-            .await;
-
-        // When client A navigates back and forth, client B does so as well.
-        workspace_a
-            .update(cx_a, |workspace, cx| {
-                workspace::Pane::go_back(workspace, None, cx)
-            })
-            .await;
-        workspace_b
-            .condition(cx_b, |workspace, cx| {
-                workspace.active_item(cx).unwrap().id() == editor_b2.id()
-            })
-            .await;
-
-        workspace_a
-            .update(cx_a, |workspace, cx| {
-                workspace::Pane::go_forward(workspace, None, cx)
-            })
-            .await;
-        workspace_b
-            .condition(cx_b, |workspace, cx| {
-                workspace.active_item(cx).unwrap().id() == editor_b1.id()
-            })
-            .await;
-
-        // 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]));
-        });
-        editor_b1
-            .condition(cx_b, |editor, cx| {
-                editor.selections.ranges(cx) == vec![1..1, 2..2]
-            })
-            .await;
-
-        editor_a1.update(cx_a, |editor, cx| editor.set_text("TWO", cx));
-        editor_b1
-            .condition(cx_b, |editor, cx| editor.text(cx) == "TWO")
-            .await;
-
-        editor_a1.update(cx_a, |editor, cx| {
-            editor.change_selections(None, cx, |s| s.select_ranges([3..3]));
-            editor.set_scroll_position(vec2f(0., 100.), cx);
-        });
-        editor_b1
-            .condition(cx_b, |editor, cx| {
-                editor.selections.ranges(cx) == vec![3..3]
-            })
-            .await;
-
-        // 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)
-        });
-        cx_a.foreground().run_until_parked();
-        assert_eq!(
-            workspace_b.read_with(cx_b, |workspace, cx| workspace
-                .active_item(cx)
-                .unwrap()
-                .id()),
-            editor_b1.id()
-        );
-
-        // Client A starts following client B.
-        workspace_a
-            .update(cx_a, |workspace, cx| {
-                workspace
-                    .toggle_follow(&ToggleFollow(client_b_id), cx)
-                    .unwrap()
-            })
-            .await
-            .unwrap();
-        assert_eq!(
-            workspace_a.read_with(cx_a, |workspace, _| workspace.leader_for_pane(&pane_a)),
-            Some(client_b_id)
-        );
-        assert_eq!(
-            workspace_a.read_with(cx_a, |workspace, cx| workspace
-                .active_item(cx)
-                .unwrap()
-                .id()),
-            editor_a1.id()
-        );
-
-        // Following interrupts when client B disconnects.
-        client_b.disconnect(&cx_b.to_async()).unwrap();
-        cx_a.foreground().run_until_parked();
-        assert_eq!(
-            workspace_a.read_with(cx_a, |workspace, _| workspace.leader_for_pane(&pane_a)),
-            None
-        );
-    }
-
-    #[gpui::test(iterations = 10)]
-    async fn test_peers_following_each_other(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
-        cx_a.foreground().forbid_parking();
-        let fs = FakeFs::new(cx_a.background());
-
-        // 2 clients connect to a server.
-        let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
-        let mut client_a = server.create_client(cx_a, "user_a").await;
-        let mut client_b = server.create_client(cx_b, "user_b").await;
-        server
-            .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)])
-            .await;
-        cx_a.update(editor::init);
-        cx_b.update(editor::init);
-
-        // Client A shares a project.
-        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(fs.clone(), "/a", cx_a).await;
-
-        // Client B joins the project.
-        let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await;
-
-        // Client A opens some editors.
-        let workspace_a = client_a.build_workspace(&project_a, cx_a);
-        let pane_a1 = workspace_a.read_with(cx_a, |workspace, _| workspace.active_pane().clone());
-        let _editor_a1 = workspace_a
-            .update(cx_a, |workspace, cx| {
-                workspace.open_path((worktree_id, "1.txt"), true, cx)
-            })
-            .await
-            .unwrap()
-            .downcast::<Editor>()
-            .unwrap();
-
-        // Client B opens an editor.
-        let workspace_b = client_b.build_workspace(&project_b, cx_b);
-        let pane_b1 = workspace_b.read_with(cx_b, |workspace, _| workspace.active_pane().clone());
-        let _editor_b1 = workspace_b
-            .update(cx_b, |workspace, cx| {
-                workspace.open_path((worktree_id, "2.txt"), 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_pane(workspace.active_pane().clone(), SplitDirection::Right, cx);
-                assert_ne!(*workspace.active_pane(), pane_a1);
-                let leader_id = *project_a.read(cx).collaborators().keys().next().unwrap();
-                workspace
-                    .toggle_follow(&workspace::ToggleFollow(leader_id), cx)
-                    .unwrap()
-            })
-            .await
-            .unwrap();
-        workspace_b
-            .update(cx_b, |workspace, cx| {
-                workspace.split_pane(workspace.active_pane().clone(), SplitDirection::Right, cx);
-                assert_ne!(*workspace.active_pane(), pane_b1);
-                let leader_id = *project_b.read(cx).collaborators().keys().next().unwrap();
-                workspace
-                    .toggle_follow(&workspace::ToggleFollow(leader_id), cx)
-                    .unwrap()
-            })
-            .await
-            .unwrap();
-
-        workspace_a
-            .update(cx_a, |workspace, cx| {
-                workspace.activate_next_pane(cx);
-                assert_eq!(*workspace.active_pane(), pane_a1);
-                workspace.open_path((worktree_id, "3.txt"), true, cx)
-            })
-            .await
-            .unwrap();
-        workspace_b
-            .update(cx_b, |workspace, cx| {
-                workspace.activate_next_pane(cx);
-                assert_eq!(*workspace.active_pane(), pane_b1);
-                workspace.open_path((worktree_id, "4.txt"), true, cx)
-            })
-            .await
-            .unwrap();
-        cx_a.foreground().run_until_parked();
-
-        // Ensure leader updates don't change the active pane of followers
-        workspace_a.read_with(cx_a, |workspace, _| {
-            assert_eq!(*workspace.active_pane(), pane_a1);
-        });
-        workspace_b.read_with(cx_b, |workspace, _| {
-            assert_eq!(*workspace.active_pane(), pane_b1);
-        });
-
-        // Ensure peers following each other doesn't cause an infinite loop.
-        assert_eq!(
-            workspace_a.read_with(cx_a, |workspace, cx| workspace
-                .active_item(cx)
-                .unwrap()
-                .project_path(cx)),
-            Some((worktree_id, "3.txt").into())
-        );
-        workspace_a.update(cx_a, |workspace, cx| {
-            assert_eq!(
-                workspace.active_item(cx).unwrap().project_path(cx),
-                Some((worktree_id, "3.txt").into())
-            );
-            workspace.activate_next_pane(cx);
-            assert_eq!(
-                workspace.active_item(cx).unwrap().project_path(cx),
-                Some((worktree_id, "4.txt").into())
-            );
-        });
-        workspace_b.update(cx_b, |workspace, cx| {
-            assert_eq!(
-                workspace.active_item(cx).unwrap().project_path(cx),
-                Some((worktree_id, "4.txt").into())
-            );
-            workspace.activate_next_pane(cx);
-            assert_eq!(
-                workspace.active_item(cx).unwrap().project_path(cx),
-                Some((worktree_id, "3.txt").into())
-            );
-        });
-    }
-
-    #[gpui::test(iterations = 10)]
-    async fn test_auto_unfollowing(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
-        cx_a.foreground().forbid_parking();
-        let fs = FakeFs::new(cx_a.background());
-
-        // 2 clients connect to a server.
-        let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
-        let mut client_a = server.create_client(cx_a, "user_a").await;
-        let mut client_b = server.create_client(cx_b, "user_b").await;
-        server
-            .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)])
-            .await;
-        cx_a.update(editor::init);
-        cx_b.update(editor::init);
-
-        // Client A shares a project.
-        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(fs.clone(), "/a", cx_a).await;
-
-        // Client B joins the project.
-        let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await;
-
-        // Client A opens some editors.
-        let workspace_a = client_a.build_workspace(&project_a, cx_a);
-        let _editor_a1 = workspace_a
-            .update(cx_a, |workspace, cx| {
-                workspace.open_path((worktree_id, "1.txt"), true, cx)
-            })
-            .await
-            .unwrap()
-            .downcast::<Editor>()
-            .unwrap();
-
-        // Client B starts following client A.
-        let workspace_b = client_b.build_workspace(&project_b, cx_b);
-        let pane_b = workspace_b.read_with(cx_b, |workspace, _| workspace.active_pane().clone());
-        let leader_id = project_b.read_with(cx_b, |project, _| {
-            project.collaborators().values().next().unwrap().peer_id
-        });
-        workspace_b
-            .update(cx_b, |workspace, cx| {
-                workspace
-                    .toggle_follow(&ToggleFollow(leader_id), cx)
-                    .unwrap()
-            })
-            .await
-            .unwrap();
-        assert_eq!(
-            workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
-            Some(leader_id)
-        );
-        let editor_b2 = workspace_b.read_with(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.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
-            None
-        );
-
-        workspace_b
-            .update(cx_b, |workspace, cx| {
-                workspace
-                    .toggle_follow(&ToggleFollow(leader_id), cx)
-                    .unwrap()
-            })
-            .await
-            .unwrap();
-        assert_eq!(
-            workspace_b.read_with(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.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
-            None
-        );
-
-        workspace_b
-            .update(cx_b, |workspace, cx| {
-                workspace
-                    .toggle_follow(&ToggleFollow(leader_id), cx)
-                    .unwrap()
-            })
-            .await
-            .unwrap();
-        assert_eq!(
-            workspace_b.read_with(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(vec2f(0., 3.), cx)
-        });
-        assert_eq!(
-            workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
-            None
-        );
-
-        workspace_b
-            .update(cx_b, |workspace, cx| {
-                workspace
-                    .toggle_follow(&ToggleFollow(leader_id), cx)
-                    .unwrap()
-            })
-            .await
-            .unwrap();
-        assert_eq!(
-            workspace_b.read_with(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_pane(pane_b.clone(), SplitDirection::Right, cx)
-        });
-        assert_eq!(
-            workspace_b.read_with(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.read_with(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"), true, cx)
-            })
-            .await
-            .unwrap();
-        assert_eq!(
-            workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
-            None
-        );
-    }
-
-    #[gpui::test(iterations = 100)]
-    async fn test_random_collaboration(
-        cx: &mut TestAppContext,
-        deterministic: Arc<Deterministic>,
-        rng: StdRng,
-    ) {
-        cx.foreground().forbid_parking();
-        let max_peers = env::var("MAX_PEERS")
-            .map(|i| i.parse().expect("invalid `MAX_PEERS` variable"))
-            .unwrap_or(5);
-        assert!(max_peers <= 5);
-
-        let max_operations = env::var("OPERATIONS")
-            .map(|i| i.parse().expect("invalid `OPERATIONS` variable"))
-            .unwrap_or(10);
-
-        let rng = Arc::new(Mutex::new(rng));
-
-        let guest_lang_registry = Arc::new(LanguageRegistry::test());
-        let host_language_registry = Arc::new(LanguageRegistry::test());
-
-        let fs = FakeFs::new(cx.background());
-        fs.insert_tree("/_collab", json!({"init": ""})).await;
-
-        let mut server = TestServer::start(cx.foreground(), cx.background()).await;
-        let db = server.app_state.db.clone();
-        let host_user_id = db.create_user("host", None, false).await.unwrap();
-        for username in ["guest-1", "guest-2", "guest-3", "guest-4"] {
-            let guest_user_id = db.create_user(username, None, false).await.unwrap();
-            server
-                .app_state
-                .db
-                .send_contact_request(guest_user_id, host_user_id)
-                .await
-                .unwrap();
-            server
-                .app_state
-                .db
-                .respond_to_contact_request(host_user_id, guest_user_id, true)
-                .await
-                .unwrap();
-        }
-
-        let mut clients = Vec::new();
-        let mut user_ids = Vec::new();
-        let mut op_start_signals = Vec::new();
-
-        let mut next_entity_id = 100000;
-        let mut host_cx = TestAppContext::new(
-            cx.foreground_platform(),
-            cx.platform(),
-            deterministic.build_foreground(next_entity_id),
-            deterministic.build_background(),
-            cx.font_cache(),
-            cx.leak_detector(),
-            next_entity_id,
-        );
-        let host = server.create_client(&mut host_cx, "host").await;
-        let host_project = host_cx.update(|cx| {
-            Project::local(
-                host.client.clone(),
-                host.user_store.clone(),
-                host_language_registry.clone(),
-                fs.clone(),
-                cx,
-            )
-        });
-        let host_project_id = host_project
-            .update(&mut host_cx, |p, _| p.next_remote_id())
-            .await;
-
-        let (collab_worktree, _) = host_project
-            .update(&mut host_cx, |project, cx| {
-                project.find_or_create_local_worktree("/_collab", true, cx)
-            })
-            .await
-            .unwrap();
-        collab_worktree
-            .read_with(&host_cx, |tree, _| tree.as_local().unwrap().scan_complete())
-            .await;
-
-        // Set up fake language servers.
-        let mut language = Language::new(
-            LanguageConfig {
-                name: "Rust".into(),
-                path_suffixes: vec!["rs".to_string()],
-                ..Default::default()
-            },
-            None,
-        );
-        let _fake_servers = language.set_fake_lsp_adapter(FakeLspAdapter {
-            name: "the-fake-language-server",
-            capabilities: lsp::LanguageServer::full_capabilities(),
-            initializer: Some(Box::new({
-                let rng = rng.clone();
-                let fs = fs.clone();
-                let project = host_project.downgrade();
-                move |fake_server: &mut FakeLanguageServer| {
-                    fake_server.handle_request::<lsp::request::Completion, _, _>(
-                        |_, _| async move {
-                            Ok(Some(lsp::CompletionResponse::Array(vec![
-                                lsp::CompletionItem {
-                                    text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
-                                        range: lsp::Range::new(
-                                            lsp::Position::new(0, 0),
-                                            lsp::Position::new(0, 0),
-                                        ),
-                                        new_text: "the-new-text".to_string(),
-                                    })),
-                                    ..Default::default()
-                                },
-                            ])))
-                        },
-                    );
-
-                    fake_server.handle_request::<lsp::request::CodeActionRequest, _, _>(
-                        |_, _| async move {
-                            Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
-                                lsp::CodeAction {
-                                    title: "the-code-action".to_string(),
-                                    ..Default::default()
-                                },
-                            )]))
-                        },
-                    );
-
-                    fake_server.handle_request::<lsp::request::PrepareRenameRequest, _, _>(
-                        |params, _| async move {
-                            Ok(Some(lsp::PrepareRenameResponse::Range(lsp::Range::new(
-                                params.position,
-                                params.position,
-                            ))))
-                        },
-                    );
-
-                    fake_server.handle_request::<lsp::request::GotoDefinition, _, _>({
-                        let fs = fs.clone();
-                        let rng = rng.clone();
-                        move |_, _| {
-                            let fs = fs.clone();
-                            let rng = rng.clone();
-                            async move {
-                                let files = fs.files().await;
-                                let mut rng = rng.lock();
-                                let count = rng.gen_range::<usize, _>(1..3);
-                                let files = (0..count)
-                                    .map(|_| files.choose(&mut *rng).unwrap())
-                                    .collect::<Vec<_>>();
-                                log::info!("LSP: Returning definitions in files {:?}", &files);
-                                Ok(Some(lsp::GotoDefinitionResponse::Array(
-                                    files
-                                        .into_iter()
-                                        .map(|file| lsp::Location {
-                                            uri: lsp::Url::from_file_path(file).unwrap(),
-                                            range: Default::default(),
-                                        })
-                                        .collect(),
-                                )))
-                            }
-                        }
-                    });
-
-                    fake_server.handle_request::<lsp::request::DocumentHighlightRequest, _, _>({
-                        let rng = rng.clone();
-                        let project = project.clone();
-                        move |params, mut cx| {
-                            let highlights = if let Some(project) = project.upgrade(&cx) {
-                                project.update(&mut cx, |project, cx| {
-                                    let path = params
-                                        .text_document_position_params
-                                        .text_document
-                                        .uri
-                                        .to_file_path()
-                                        .unwrap();
-                                    let (worktree, relative_path) =
-                                        project.find_local_worktree(&path, cx)?;
-                                    let project_path =
-                                        ProjectPath::from((worktree.read(cx).id(), relative_path));
-                                    let buffer =
-                                        project.get_open_buffer(&project_path, cx)?.read(cx);
-
-                                    let mut highlights = Vec::new();
-                                    let highlight_count = rng.lock().gen_range(1..=5);
-                                    let mut prev_end = 0;
-                                    for _ in 0..highlight_count {
-                                        let range =
-                                            buffer.random_byte_range(prev_end, &mut *rng.lock());
-
-                                        highlights.push(lsp::DocumentHighlight {
-                                            range: range_to_lsp(range.to_point_utf16(buffer)),
-                                            kind: Some(lsp::DocumentHighlightKind::READ),
-                                        });
-                                        prev_end = range.end;
-                                    }
-                                    Some(highlights)
-                                })
-                            } else {
-                                None
-                            };
-                            async move { Ok(highlights) }
-                        }
-                    });
-                }
-            })),
-            ..Default::default()
-        });
-        host_language_registry.add(Arc::new(language));
-
-        let op_start_signal = futures::channel::mpsc::unbounded();
-        user_ids.push(host.current_user_id(&host_cx));
-        op_start_signals.push(op_start_signal.0);
-        clients.push(host_cx.foreground().spawn(host.simulate_host(
-            host_project,
-            op_start_signal.1,
-            rng.clone(),
-            host_cx,
-        )));
-
-        let disconnect_host_at = if rng.lock().gen_bool(0.2) {
-            rng.lock().gen_range(0..max_operations)
-        } else {
-            max_operations
-        };
-        let mut available_guests = vec![
-            "guest-1".to_string(),
-            "guest-2".to_string(),
-            "guest-3".to_string(),
-            "guest-4".to_string(),
-        ];
-        let mut operations = 0;
-        while operations < max_operations {
-            if operations == disconnect_host_at {
-                server.disconnect_client(user_ids[0]);
-                cx.foreground().advance_clock(RECEIVE_TIMEOUT);
-                drop(op_start_signals);
-                let mut clients = futures::future::join_all(clients).await;
-                cx.foreground().run_until_parked();
-
-                let (host, mut host_cx, host_err) = clients.remove(0);
-                if let Some(host_err) = host_err {
-                    log::error!("host error - {:?}", host_err);
-                }
-                host.project
-                    .as_ref()
-                    .unwrap()
-                    .read_with(&host_cx, |project, _| assert!(!project.is_shared()));
-                for (guest, mut guest_cx, guest_err) in clients {
-                    if let Some(guest_err) = guest_err {
-                        log::error!("{} error - {:?}", guest.username, guest_err);
-                    }
-
-                    let contacts = server
-                        .app_state
-                        .db
-                        .get_contacts(guest.current_user_id(&guest_cx))
-                        .await
-                        .unwrap();
-                    let contacts = server
-                        .store
-                        .read()
-                        .await
-                        .build_initial_contacts_update(contacts)
-                        .contacts;
-                    assert!(!contacts
-                        .iter()
-                        .flat_map(|contact| &contact.projects)
-                        .any(|project| project.id == host_project_id));
-                    guest
-                        .project
-                        .as_ref()
-                        .unwrap()
-                        .read_with(&guest_cx, |project, _| assert!(project.is_read_only()));
-                    guest_cx.update(|_| drop(guest));
-                }
-                host_cx.update(|_| drop(host));
-
-                return;
-            }
-
-            let distribution = rng.lock().gen_range(0..100);
-            match distribution {
-                0..=19 if !available_guests.is_empty() => {
-                    let guest_ix = rng.lock().gen_range(0..available_guests.len());
-                    let guest_username = available_guests.remove(guest_ix);
-                    log::info!("Adding new connection for {}", guest_username);
-                    next_entity_id += 100000;
-                    let mut guest_cx = TestAppContext::new(
-                        cx.foreground_platform(),
-                        cx.platform(),
-                        deterministic.build_foreground(next_entity_id),
-                        deterministic.build_background(),
-                        cx.font_cache(),
-                        cx.leak_detector(),
-                        next_entity_id,
-                    );
-                    let guest = server.create_client(&mut guest_cx, &guest_username).await;
-                    let guest_project = Project::remote(
-                        host_project_id,
-                        guest.client.clone(),
-                        guest.user_store.clone(),
-                        guest_lang_registry.clone(),
-                        FakeFs::new(cx.background()),
-                        &mut guest_cx.to_async(),
-                    )
-                    .await
-                    .unwrap();
-                    let op_start_signal = futures::channel::mpsc::unbounded();
-                    user_ids.push(guest.current_user_id(&guest_cx));
-                    op_start_signals.push(op_start_signal.0);
-                    clients.push(guest_cx.foreground().spawn(guest.simulate_guest(
-                        guest_username.clone(),
-                        guest_project,
-                        op_start_signal.1,
-                        rng.clone(),
-                        guest_cx,
-                    )));
-
-                    log::info!("Added connection for {}", guest_username);
-                    operations += 1;
-                }
-                20..=29 if clients.len() > 1 => {
-                    let guest_ix = rng.lock().gen_range(1..clients.len());
-                    log::info!("Removing guest {}", user_ids[guest_ix]);
-                    let removed_guest_id = user_ids.remove(guest_ix);
-                    let guest = clients.remove(guest_ix);
-                    op_start_signals.remove(guest_ix);
-                    server.forbid_connections();
-                    server.disconnect_client(removed_guest_id);
-                    cx.foreground().advance_clock(RECEIVE_TIMEOUT);
-                    let (guest, mut guest_cx, guest_err) = guest.await;
-                    server.allow_connections();
-
-                    if let Some(guest_err) = guest_err {
-                        log::error!("{} error - {:?}", guest.username, guest_err);
-                    }
-                    guest
-                        .project
-                        .as_ref()
-                        .unwrap()
-                        .read_with(&guest_cx, |project, _| assert!(project.is_read_only()));
-                    for user_id in &user_ids {
-                        let contacts = server.app_state.db.get_contacts(*user_id).await.unwrap();
-                        let contacts = server
-                            .store
-                            .read()
-                            .await
-                            .build_initial_contacts_update(contacts)
-                            .contacts;
-                        for contact in contacts {
-                            if contact.online {
-                                assert_ne!(
-                                    contact.user_id, removed_guest_id.0 as u64,
-                                    "removed guest is still a contact of another peer"
-                                );
-                            }
-                            for project in contact.projects {
-                                for project_guest_id in project.guests {
-                                    assert_ne!(
-                                        project_guest_id, removed_guest_id.0 as u64,
-                                        "removed guest appears as still participating on a project"
-                                    );
-                                }
-                            }
-                        }
-                    }
-
-                    log::info!("{} removed", guest.username);
-                    available_guests.push(guest.username.clone());
-                    guest_cx.update(|_| drop(guest));
-
-                    operations += 1;
-                }
-                _ => {
-                    while operations < max_operations && rng.lock().gen_bool(0.7) {
-                        op_start_signals
-                            .choose(&mut *rng.lock())
-                            .unwrap()
-                            .unbounded_send(())
-                            .unwrap();
-                        operations += 1;
-                    }
-
-                    if rng.lock().gen_bool(0.8) {
-                        cx.foreground().run_until_parked();
-                    }
-                }
-            }
-        }
-
-        drop(op_start_signals);
-        let mut clients = futures::future::join_all(clients).await;
-        cx.foreground().run_until_parked();
-
-        let (host_client, mut host_cx, host_err) = clients.remove(0);
-        if let Some(host_err) = host_err {
-            panic!("host error - {:?}", host_err);
-        }
-        let host_project = host_client.project.as_ref().unwrap();
-        let host_worktree_snapshots = host_project.read_with(&host_cx, |project, cx| {
-            project
-                .worktrees(cx)
-                .map(|worktree| {
-                    let snapshot = worktree.read(cx).snapshot();
-                    (snapshot.id(), snapshot)
-                })
-                .collect::<BTreeMap<_, _>>()
-        });
-
-        host_client
-            .project
-            .as_ref()
-            .unwrap()
-            .read_with(&host_cx, |project, cx| project.check_invariants(cx));
-
-        for (guest_client, mut guest_cx, guest_err) in clients.into_iter() {
-            if let Some(guest_err) = guest_err {
-                panic!("{} error - {:?}", guest_client.username, guest_err);
-            }
-            let worktree_snapshots =
-                guest_client
-                    .project
-                    .as_ref()
-                    .unwrap()
-                    .read_with(&guest_cx, |project, cx| {
-                        project
-                            .worktrees(cx)
-                            .map(|worktree| {
-                                let worktree = worktree.read(cx);
-                                (worktree.id(), worktree.snapshot())
-                            })
-                            .collect::<BTreeMap<_, _>>()
-                    });
-
-            assert_eq!(
-                worktree_snapshots.keys().collect::<Vec<_>>(),
-                host_worktree_snapshots.keys().collect::<Vec<_>>(),
-                "{} has different worktrees than the host",
-                guest_client.username
-            );
-            for (id, host_snapshot) in &host_worktree_snapshots {
-                let guest_snapshot = &worktree_snapshots[id];
-                assert_eq!(
-                    guest_snapshot.root_name(),
-                    host_snapshot.root_name(),
-                    "{} has different root name than the host for worktree {}",
-                    guest_client.username,
-                    id
-                );
-                assert_eq!(
-                    guest_snapshot.entries(false).collect::<Vec<_>>(),
-                    host_snapshot.entries(false).collect::<Vec<_>>(),
-                    "{} has different snapshot than the host for worktree {}",
-                    guest_client.username,
-                    id
-                );
-                assert_eq!(guest_snapshot.scan_id(), host_snapshot.scan_id());
-            }
-
-            guest_client
-                .project
-                .as_ref()
-                .unwrap()
-                .read_with(&guest_cx, |project, cx| project.check_invariants(cx));
-
-            for guest_buffer in &guest_client.buffers {
-                let buffer_id = guest_buffer.read_with(&guest_cx, |buffer, _| buffer.remote_id());
-                let host_buffer = host_project.read_with(&host_cx, |project, cx| {
-                    project.buffer_for_id(buffer_id, cx).expect(&format!(
-                        "host does not have buffer for guest:{}, peer:{}, id:{}",
-                        guest_client.username, guest_client.peer_id, buffer_id
-                    ))
-                });
-                let path = host_buffer
-                    .read_with(&host_cx, |buffer, cx| buffer.file().unwrap().full_path(cx));
-
-                assert_eq!(
-                    guest_buffer.read_with(&guest_cx, |buffer, _| buffer.deferred_ops_len()),
-                    0,
-                    "{}, buffer {}, path {:?} has deferred operations",
-                    guest_client.username,
-                    buffer_id,
-                    path,
-                );
-                assert_eq!(
-                    guest_buffer.read_with(&guest_cx, |buffer, _| buffer.text()),
-                    host_buffer.read_with(&host_cx, |buffer, _| buffer.text()),
-                    "{}, buffer {}, path {:?}, differs from the host's buffer",
-                    guest_client.username,
-                    buffer_id,
-                    path
-                );
-            }
-
-            guest_cx.update(|_| drop(guest_client));
-        }
-
-        host_cx.update(|_| drop(host_client));
-    }
-
-    struct TestServer {
-        peer: Arc<Peer>,
-        app_state: Arc<AppState>,
-        server: Arc<Server>,
-        foreground: Rc<executor::Foreground>,
-        notifications: mpsc::UnboundedReceiver<()>,
-        connection_killers: Arc<Mutex<HashMap<UserId, Arc<AtomicBool>>>>,
-        forbid_connections: Arc<AtomicBool>,
-        _test_db: TestDb,
-    }
-
-    impl TestServer {
-        async fn start(
-            foreground: Rc<executor::Foreground>,
-            background: Arc<executor::Background>,
-        ) -> Self {
-            let test_db = TestDb::fake(background);
-            let app_state = Self::build_app_state(&test_db).await;
-            let peer = Peer::new();
-            let notifications = mpsc::unbounded();
-            let server = Server::new(app_state.clone(), Some(notifications.0));
-            Self {
-                peer,
-                app_state,
-                server,
-                foreground,
-                notifications: notifications.1,
-                connection_killers: Default::default(),
-                forbid_connections: Default::default(),
-                _test_db: test_db,
-            }
-        }
-
-        async fn create_client(&mut self, cx: &mut TestAppContext, name: &str) -> TestClient {
-            cx.update(|cx| {
-                let settings = Settings::test(cx);
-                cx.set_global(settings);
-            });
-
-            let http = FakeHttpClient::with_404_response();
-            let user_id =
-                if let Ok(Some(user)) = self.app_state.db.get_user_by_github_login(name).await {
-                    user.id
-                } else {
-                    self.app_state.db.create_user(name, None, false).await.unwrap()
-                };
-            let client_name = name.to_string();
-            let mut client = Client::new(http.clone());
-            let server = self.server.clone();
-            let db = self.app_state.db.clone();
-            let connection_killers = self.connection_killers.clone();
-            let forbid_connections = self.forbid_connections.clone();
-            let (connection_id_tx, mut connection_id_rx) = mpsc::channel(16);
-
-            Arc::get_mut(&mut client)
-                .unwrap()
-                .override_authenticate(move |cx| {
-                    cx.spawn(|_| async move {
-                        let access_token = "the-token".to_string();
-                        Ok(Credentials {
-                            user_id: user_id.0 as u64,
-                            access_token,
-                        })
-                    })
-                })
-                .override_establish_connection(move |credentials, cx| {
-                    assert_eq!(credentials.user_id, user_id.0 as u64);
-                    assert_eq!(credentials.access_token, "the-token");
-
-                    let server = server.clone();
-                    let db = db.clone();
-                    let connection_killers = connection_killers.clone();
-                    let forbid_connections = forbid_connections.clone();
-                    let client_name = client_name.clone();
-                    let connection_id_tx = connection_id_tx.clone();
-                    cx.spawn(move |cx| async move {
-                        if forbid_connections.load(SeqCst) {
-                            Err(EstablishConnectionError::other(anyhow!(
-                                "server is forbidding connections"
-                            )))
-                        } else {
-                            let (client_conn, server_conn, killed) =
-                                Connection::in_memory(cx.background());
-                            connection_killers.lock().insert(user_id, killed);
-                            let user = db.get_user_by_id(user_id).await.unwrap().unwrap();
-                            cx.background()
-                                .spawn(server.handle_connection(
-                                    server_conn,
-                                    client_name,
-                                    user,
-                                    Some(connection_id_tx),
-                                    cx.background(),
-                                ))
-                                .detach();
-                            Ok(client_conn)
-                        }
-                    })
-                });
-
-            let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http, cx));
-            let app_state = Arc::new(workspace::AppState {
-                client: client.clone(),
-                user_store: user_store.clone(),
-                languages: Arc::new(LanguageRegistry::new(Task::ready(()))),
-                themes: ThemeRegistry::new((), cx.font_cache()),
-                fs: FakeFs::new(cx.background()),
-                build_window_options: || Default::default(),
-                initialize_workspace: |_, _, _| unimplemented!(),
-            });
-
-            Channel::init(&client);
-            Project::init(&client);
-            cx.update(|cx| workspace::init(app_state.clone(), cx));
-
-            client
-                .authenticate_and_connect(false, &cx.to_async())
-                .await
-                .unwrap();
-            let peer_id = PeerId(connection_id_rx.next().await.unwrap().0);
-
-            let client = TestClient {
-                client,
-                peer_id,
-                username: name.to_string(),
-                user_store,
-                language_registry: Arc::new(LanguageRegistry::test()),
-                project: Default::default(),
-                buffers: Default::default(),
-            };
-            client.wait_for_current_user(cx).await;
-            client
-        }
-
-        fn disconnect_client(&self, user_id: UserId) {
-            self.connection_killers
-                .lock()
-                .remove(&user_id)
-                .unwrap()
-                .store(true, SeqCst);
-        }
-
-        fn forbid_connections(&self) {
-            self.forbid_connections.store(true, SeqCst);
-        }
-
-        fn allow_connections(&self) {
-            self.forbid_connections.store(false, SeqCst);
-        }
-
-        async fn make_contacts(&self, mut clients: Vec<(&TestClient, &mut TestAppContext)>) {
-            while let Some((client_a, cx_a)) = clients.pop() {
-                for (client_b, cx_b) in &mut clients {
-                    client_a
-                        .user_store
-                        .update(cx_a, |store, cx| {
-                            store.request_contact(client_b.user_id().unwrap(), cx)
-                        })
-                        .await
-                        .unwrap();
-                    cx_a.foreground().run_until_parked();
-                    client_b
-                        .user_store
-                        .update(*cx_b, |store, cx| {
-                            store.respond_to_contact_request(client_a.user_id().unwrap(), true, cx)
-                        })
-                        .await
-                        .unwrap();
-                }
-            }
-        }
-
-        async fn build_app_state(test_db: &TestDb) -> Arc<AppState> {
-            Arc::new(AppState {
-                db: test_db.db().clone(),
-                api_token: Default::default(),
-                invite_link_prefix: Default::default(),
-            })
-        }
-
-        async fn state<'a>(&'a self) -> RwLockReadGuard<'a, Store> {
-            self.server.store.read().await
-        }
-
-        async fn condition<F>(&mut self, mut predicate: F)
-        where
-            F: FnMut(&Store) -> bool,
-        {
-            assert!(
-                self.foreground.parking_forbidden(),
-                "you must call forbid_parking to use server conditions so we don't block indefinitely"
-            );
-            while !(predicate)(&*self.server.store.read().await) {
-                self.foreground.start_waiting();
-                self.notifications.next().await;
-                self.foreground.finish_waiting();
-            }
-        }
-    }
-
-    impl Deref for TestServer {
-        type Target = Server;
-
-        fn deref(&self) -> &Self::Target {
-            &self.server
-        }
-    }
-
-    impl Drop for TestServer {
-        fn drop(&mut self) {
-            self.peer.reset();
-        }
-    }
-
-    struct TestClient {
-        client: Arc<Client>,
-        username: String,
-        pub peer_id: PeerId,
-        pub user_store: ModelHandle<UserStore>,
-        language_registry: Arc<LanguageRegistry>,
-        project: Option<ModelHandle<Project>>,
-        buffers: HashSet<ModelHandle<language::Buffer>>,
-    }
-
-    impl Deref for TestClient {
-        type Target = Arc<Client>;
-
-        fn deref(&self) -> &Self::Target {
-            &self.client
-        }
-    }
-
-    struct ContactsSummary {
-        pub current: Vec<String>,
-        pub outgoing_requests: Vec<String>,
-        pub incoming_requests: Vec<String>,
-    }
-
-    impl TestClient {
-        pub fn current_user_id(&self, cx: &TestAppContext) -> UserId {
-            UserId::from_proto(
-                self.user_store
-                    .read_with(cx, |user_store, _| user_store.current_user().unwrap().id),
-            )
-        }
-
-        async fn wait_for_current_user(&self, cx: &TestAppContext) {
-            let mut authed_user = self
-                .user_store
-                .read_with(cx, |user_store, _| user_store.watch_current_user());
-            while authed_user.next().await.unwrap().is_none() {}
-        }
-
-        async fn clear_contacts(&self, cx: &mut TestAppContext) {
-            self.user_store
-                .update(cx, |store, _| store.clear_contacts())
-                .await;
-        }
-
-        fn summarize_contacts(&self, cx: &TestAppContext) -> ContactsSummary {
-            self.user_store.read_with(cx, |store, _| ContactsSummary {
-                current: store
-                    .contacts()
-                    .iter()
-                    .map(|contact| contact.user.github_login.clone())
-                    .collect(),
-                outgoing_requests: store
-                    .outgoing_contact_requests()
-                    .iter()
-                    .map(|user| user.github_login.clone())
-                    .collect(),
-                incoming_requests: store
-                    .incoming_contact_requests()
-                    .iter()
-                    .map(|user| user.github_login.clone())
-                    .collect(),
-            })
-        }
-
-        async fn build_local_project(
-            &mut self,
-            fs: Arc<FakeFs>,
-            root_path: impl AsRef<Path>,
-            cx: &mut TestAppContext,
-        ) -> (ModelHandle<Project>, WorktreeId) {
-            let project = cx.update(|cx| {
-                Project::local(
-                    self.client.clone(),
-                    self.user_store.clone(),
-                    self.language_registry.clone(),
-                    fs,
-                    cx,
-                )
-            });
-            self.project = Some(project.clone());
-            let (worktree, _) = project
-                .update(cx, |p, cx| {
-                    p.find_or_create_local_worktree(root_path, true, cx)
-                })
-                .await
-                .unwrap();
-            worktree
-                .read_with(cx, |tree, _| tree.as_local().unwrap().scan_complete())
-                .await;
-            project
-                .update(cx, |project, _| project.next_remote_id())
-                .await;
-            (project, worktree.read_with(cx, |tree, _| tree.id()))
-        }
-
-        async fn build_remote_project(
-            &mut self,
-            host_project: &ModelHandle<Project>,
-            host_cx: &mut TestAppContext,
-            guest_cx: &mut TestAppContext,
-        ) -> ModelHandle<Project> {
-            let host_project_id = host_project
-                .read_with(host_cx, |project, _| project.next_remote_id())
-                .await;
-            let guest_user_id = self.user_id().unwrap();
-            let languages =
-                host_project.read_with(host_cx, |project, _| project.languages().clone());
-            let project_b = guest_cx.spawn(|mut cx| {
-                let user_store = self.user_store.clone();
-                let guest_client = self.client.clone();
-                async move {
-                    Project::remote(
-                        host_project_id,
-                        guest_client,
-                        user_store.clone(),
-                        languages,
-                        FakeFs::new(cx.background()),
-                        &mut cx,
-                    )
-                    .await
-                    .unwrap()
-                }
-            });
-            host_cx.foreground().run_until_parked();
-            host_project.update(host_cx, |project, cx| {
-                project.respond_to_join_request(guest_user_id, true, cx)
-            });
-            let project = project_b.await;
-            self.project = Some(project.clone());
-            project
-        }
-
-        fn build_workspace(
-            &self,
-            project: &ModelHandle<Project>,
-            cx: &mut TestAppContext,
-        ) -> ViewHandle<Workspace> {
-            let (window_id, _) = cx.add_window(|_| EmptyView);
-            cx.add_view(window_id, |cx| Workspace::new(project.clone(), cx))
-        }
-
-        async fn simulate_host(
-            mut self,
-            project: ModelHandle<Project>,
-            op_start_signal: futures::channel::mpsc::UnboundedReceiver<()>,
-            rng: Arc<Mutex<StdRng>>,
-            mut cx: TestAppContext,
-        ) -> (Self, TestAppContext, Option<anyhow::Error>) {
-            async fn simulate_host_internal(
-                client: &mut TestClient,
-                project: ModelHandle<Project>,
-                mut op_start_signal: futures::channel::mpsc::UnboundedReceiver<()>,
-                rng: Arc<Mutex<StdRng>>,
-                cx: &mut TestAppContext,
-            ) -> anyhow::Result<()> {
-                let fs = project.read_with(cx, |project, _| project.fs().clone());
-
-                cx.update(|cx| {
-                    cx.subscribe(&project, move |project, event, cx| {
-                        if let project::Event::ContactRequestedJoin(user) = event {
-                            log::info!("Host: accepting join request from {}", user.github_login);
-                            project.update(cx, |project, cx| {
-                                project.respond_to_join_request(user.id, true, cx)
-                            });
-                        }
-                    })
-                    .detach();
-                });
-
-                while op_start_signal.next().await.is_some() {
-                    let distribution = rng.lock().gen_range::<usize, _>(0..100);
-                    let files = fs.as_fake().files().await;
-                    match distribution {
-                        0..=19 if !files.is_empty() => {
-                            let path = files.choose(&mut *rng.lock()).unwrap();
-                            let mut path = path.as_path();
-                            while let Some(parent_path) = path.parent() {
-                                path = parent_path;
-                                if rng.lock().gen() {
-                                    break;
-                                }
-                            }
-
-                            log::info!("Host: find/create local worktree {:?}", path);
-                            let find_or_create_worktree = project.update(cx, |project, cx| {
-                                project.find_or_create_local_worktree(path, true, cx)
-                            });
-                            if rng.lock().gen() {
-                                cx.background().spawn(find_or_create_worktree).detach();
-                            } else {
-                                find_or_create_worktree.await?;
-                            }
-                        }
-                        20..=79 if !files.is_empty() => {
-                            let buffer = if client.buffers.is_empty() || rng.lock().gen() {
-                                let file = files.choose(&mut *rng.lock()).unwrap();
-                                let (worktree, path) = project
-                                    .update(cx, |project, cx| {
-                                        project.find_or_create_local_worktree(
-                                            file.clone(),
-                                            true,
-                                            cx,
-                                        )
-                                    })
-                                    .await?;
-                                let project_path =
-                                    worktree.read_with(cx, |worktree, _| (worktree.id(), path));
-                                log::info!(
-                                    "Host: opening path {:?}, worktree {}, relative_path {:?}",
-                                    file,
-                                    project_path.0,
-                                    project_path.1
-                                );
-                                let buffer = project
-                                    .update(cx, |project, cx| project.open_buffer(project_path, cx))
-                                    .await
-                                    .unwrap();
-                                client.buffers.insert(buffer.clone());
-                                buffer
-                            } else {
-                                client
-                                    .buffers
-                                    .iter()
-                                    .choose(&mut *rng.lock())
-                                    .unwrap()
-                                    .clone()
-                            };
-
-                            if rng.lock().gen_bool(0.1) {
-                                cx.update(|cx| {
-                                    log::info!(
-                                        "Host: dropping buffer {:?}",
-                                        buffer.read(cx).file().unwrap().full_path(cx)
-                                    );
-                                    client.buffers.remove(&buffer);
-                                    drop(buffer);
-                                });
-                            } else {
-                                buffer.update(cx, |buffer, cx| {
-                                    log::info!(
-                                        "Host: updating buffer {:?} ({})",
-                                        buffer.file().unwrap().full_path(cx),
-                                        buffer.remote_id()
-                                    );
-
-                                    if rng.lock().gen_bool(0.7) {
-                                        buffer.randomly_edit(&mut *rng.lock(), 5, cx);
-                                    } else {
-                                        buffer.randomly_undo_redo(&mut *rng.lock(), cx);
-                                    }
-                                });
-                            }
-                        }
-                        _ => loop {
-                            let path_component_count = rng.lock().gen_range::<usize, _>(1..=5);
-                            let mut path = PathBuf::new();
-                            path.push("/");
-                            for _ in 0..path_component_count {
-                                let letter = rng.lock().gen_range(b'a'..=b'z');
-                                path.push(std::str::from_utf8(&[letter]).unwrap());
-                            }
-                            path.set_extension("rs");
-                            let parent_path = path.parent().unwrap();
-
-                            log::info!("Host: creating file {:?}", path,);
-
-                            if fs.create_dir(&parent_path).await.is_ok()
-                                && fs.create_file(&path, Default::default()).await.is_ok()
-                            {
-                                break;
-                            } else {
-                                log::info!("Host: cannot create file");
-                            }
-                        },
-                    }
-
-                    cx.background().simulate_random_delay().await;
-                }
-
-                Ok(())
-            }
-
-            let result =
-                simulate_host_internal(&mut self, project.clone(), op_start_signal, rng, &mut cx)
-                    .await;
-            log::info!("Host done");
-            self.project = Some(project);
-            (self, cx, result.err())
-        }
-
-        pub async fn simulate_guest(
-            mut self,
-            guest_username: String,
-            project: ModelHandle<Project>,
-            op_start_signal: futures::channel::mpsc::UnboundedReceiver<()>,
-            rng: Arc<Mutex<StdRng>>,
-            mut cx: TestAppContext,
-        ) -> (Self, TestAppContext, Option<anyhow::Error>) {
-            async fn simulate_guest_internal(
-                client: &mut TestClient,
-                guest_username: &str,
-                project: ModelHandle<Project>,
-                mut op_start_signal: futures::channel::mpsc::UnboundedReceiver<()>,
-                rng: Arc<Mutex<StdRng>>,
-                cx: &mut TestAppContext,
-            ) -> anyhow::Result<()> {
-                while op_start_signal.next().await.is_some() {
-                    let buffer = if client.buffers.is_empty() || rng.lock().gen() {
-                        let worktree = if let Some(worktree) =
-                            project.read_with(cx, |project, cx| {
-                                project
-                                    .worktrees(&cx)
-                                    .filter(|worktree| {
-                                        let worktree = worktree.read(cx);
-                                        worktree.is_visible()
-                                            && worktree.entries(false).any(|e| e.is_file())
-                                    })
-                                    .choose(&mut *rng.lock())
-                            }) {
-                            worktree
-                        } else {
-                            cx.background().simulate_random_delay().await;
-                            continue;
-                        };
-
-                        let (worktree_root_name, project_path) =
-                            worktree.read_with(cx, |worktree, _| {
-                                let entry = worktree
-                                    .entries(false)
-                                    .filter(|e| e.is_file())
-                                    .choose(&mut *rng.lock())
-                                    .unwrap();
-                                (
-                                    worktree.root_name().to_string(),
-                                    (worktree.id(), entry.path.clone()),
-                                )
-                            });
-                        log::info!(
-                            "{}: opening path {:?} in worktree {} ({})",
-                            guest_username,
-                            project_path.1,
-                            project_path.0,
-                            worktree_root_name,
-                        );
-                        let buffer = project
-                            .update(cx, |project, cx| {
-                                project.open_buffer(project_path.clone(), cx)
-                            })
-                            .await?;
-                        log::info!(
-                            "{}: opened path {:?} in worktree {} ({}) with buffer id {}",
-                            guest_username,
-                            project_path.1,
-                            project_path.0,
-                            worktree_root_name,
-                            buffer.read_with(cx, |buffer, _| buffer.remote_id())
-                        );
-                        client.buffers.insert(buffer.clone());
-                        buffer
-                    } else {
-                        client
-                            .buffers
-                            .iter()
-                            .choose(&mut *rng.lock())
-                            .unwrap()
-                            .clone()
-                    };
-
-                    let choice = rng.lock().gen_range(0..100);
-                    match choice {
-                        0..=9 => {
-                            cx.update(|cx| {
-                                log::info!(
-                                    "{}: dropping buffer {:?}",
-                                    guest_username,
-                                    buffer.read(cx).file().unwrap().full_path(cx)
-                                );
-                                client.buffers.remove(&buffer);
-                                drop(buffer);
-                            });
-                        }
-                        10..=19 => {
-                            let completions = project.update(cx, |project, cx| {
-                                log::info!(
-                                    "{}: requesting completions for buffer {} ({:?})",
-                                    guest_username,
-                                    buffer.read(cx).remote_id(),
-                                    buffer.read(cx).file().unwrap().full_path(cx)
-                                );
-                                let offset = rng.lock().gen_range(0..=buffer.read(cx).len());
-                                project.completions(&buffer, offset, cx)
-                            });
-                            let completions = cx.background().spawn(async move {
-                                completions
-                                    .await
-                                    .map_err(|err| anyhow!("completions request failed: {:?}", err))
-                            });
-                            if rng.lock().gen_bool(0.3) {
-                                log::info!("{}: detaching completions request", guest_username);
-                                cx.update(|cx| completions.detach_and_log_err(cx));
-                            } else {
-                                completions.await?;
-                            }
-                        }
-                        20..=29 => {
-                            let code_actions = project.update(cx, |project, cx| {
-                                log::info!(
-                                    "{}: requesting code actions for buffer {} ({:?})",
-                                    guest_username,
-                                    buffer.read(cx).remote_id(),
-                                    buffer.read(cx).file().unwrap().full_path(cx)
-                                );
-                                let range = buffer.read(cx).random_byte_range(0, &mut *rng.lock());
-                                project.code_actions(&buffer, range, cx)
-                            });
-                            let code_actions = cx.background().spawn(async move {
-                                code_actions.await.map_err(|err| {
-                                    anyhow!("code actions request failed: {:?}", err)
-                                })
-                            });
-                            if rng.lock().gen_bool(0.3) {
-                                log::info!("{}: detaching code actions request", guest_username);
-                                cx.update(|cx| code_actions.detach_and_log_err(cx));
-                            } else {
-                                code_actions.await?;
-                            }
-                        }
-                        30..=39 if buffer.read_with(cx, |buffer, _| buffer.is_dirty()) => {
-                            let (requested_version, save) = buffer.update(cx, |buffer, cx| {
-                                log::info!(
-                                    "{}: saving buffer {} ({:?})",
-                                    guest_username,
-                                    buffer.remote_id(),
-                                    buffer.file().unwrap().full_path(cx)
-                                );
-                                (buffer.version(), buffer.save(cx))
-                            });
-                            let save = cx.background().spawn(async move {
-                                let (saved_version, _) = save
-                                    .await
-                                    .map_err(|err| anyhow!("save request failed: {:?}", err))?;
-                                assert!(saved_version.observed_all(&requested_version));
-                                Ok::<_, anyhow::Error>(())
-                            });
-                            if rng.lock().gen_bool(0.3) {
-                                log::info!("{}: detaching save request", guest_username);
-                                cx.update(|cx| save.detach_and_log_err(cx));
-                            } else {
-                                save.await?;
-                            }
-                        }
-                        40..=44 => {
-                            let prepare_rename = project.update(cx, |project, cx| {
-                                log::info!(
-                                    "{}: preparing rename for buffer {} ({:?})",
-                                    guest_username,
-                                    buffer.read(cx).remote_id(),
-                                    buffer.read(cx).file().unwrap().full_path(cx)
-                                );
-                                let offset = rng.lock().gen_range(0..=buffer.read(cx).len());
-                                project.prepare_rename(buffer, offset, cx)
-                            });
-                            let prepare_rename = cx.background().spawn(async move {
-                                prepare_rename.await.map_err(|err| {
-                                    anyhow!("prepare rename request failed: {:?}", err)
-                                })
-                            });
-                            if rng.lock().gen_bool(0.3) {
-                                log::info!("{}: detaching prepare rename request", guest_username);
-                                cx.update(|cx| prepare_rename.detach_and_log_err(cx));
-                            } else {
-                                prepare_rename.await?;
-                            }
-                        }
-                        45..=49 => {
-                            let definitions = project.update(cx, |project, cx| {
-                                log::info!(
-                                    "{}: requesting definitions for buffer {} ({:?})",
-                                    guest_username,
-                                    buffer.read(cx).remote_id(),
-                                    buffer.read(cx).file().unwrap().full_path(cx)
-                                );
-                                let offset = rng.lock().gen_range(0..=buffer.read(cx).len());
-                                project.definition(&buffer, offset, cx)
-                            });
-                            let definitions = cx.background().spawn(async move {
-                                definitions
-                                    .await
-                                    .map_err(|err| anyhow!("definitions request failed: {:?}", err))
-                            });
-                            if rng.lock().gen_bool(0.3) {
-                                log::info!("{}: detaching definitions request", guest_username);
-                                cx.update(|cx| definitions.detach_and_log_err(cx));
-                            } else {
-                                client
-                                    .buffers
-                                    .extend(definitions.await?.into_iter().map(|loc| loc.buffer));
-                            }
-                        }
-                        50..=54 => {
-                            let highlights = project.update(cx, |project, cx| {
-                                log::info!(
-                                    "{}: requesting highlights for buffer {} ({:?})",
-                                    guest_username,
-                                    buffer.read(cx).remote_id(),
-                                    buffer.read(cx).file().unwrap().full_path(cx)
-                                );
-                                let offset = rng.lock().gen_range(0..=buffer.read(cx).len());
-                                project.document_highlights(&buffer, offset, cx)
-                            });
-                            let highlights = cx.background().spawn(async move {
-                                highlights
-                                    .await
-                                    .map_err(|err| anyhow!("highlights request failed: {:?}", err))
-                            });
-                            if rng.lock().gen_bool(0.3) {
-                                log::info!("{}: detaching highlights request", guest_username);
-                                cx.update(|cx| highlights.detach_and_log_err(cx));
-                            } else {
-                                highlights.await?;
-                            }
-                        }
-                        55..=59 => {
-                            let search = project.update(cx, |project, cx| {
-                                let query = rng.lock().gen_range('a'..='z');
-                                log::info!("{}: project-wide search {:?}", guest_username, query);
-                                project.search(SearchQuery::text(query, false, false), cx)
-                            });
-                            let search = cx.background().spawn(async move {
-                                search
-                                    .await
-                                    .map_err(|err| anyhow!("search request failed: {:?}", err))
-                            });
-                            if rng.lock().gen_bool(0.3) {
-                                log::info!("{}: detaching search request", guest_username);
-                                cx.update(|cx| search.detach_and_log_err(cx));
-                            } else {
-                                client.buffers.extend(search.await?.into_keys());
-                            }
-                        }
-                        60..=69 => {
-                            let worktree = project
-                                .read_with(cx, |project, cx| {
-                                    project
-                                        .worktrees(&cx)
-                                        .filter(|worktree| {
-                                            let worktree = worktree.read(cx);
-                                            worktree.is_visible()
-                                                && worktree.entries(false).any(|e| e.is_file())
-                                                && worktree
-                                                    .root_entry()
-                                                    .map_or(false, |e| e.is_dir())
-                                        })
-                                        .choose(&mut *rng.lock())
-                                })
-                                .unwrap();
-                            let (worktree_id, worktree_root_name) = worktree
-                                .read_with(cx, |worktree, _| {
-                                    (worktree.id(), worktree.root_name().to_string())
-                                });
-
-                            let mut new_name = String::new();
-                            for _ in 0..10 {
-                                let letter = rng.lock().gen_range('a'..='z');
-                                new_name.push(letter);
-                            }
-                            let mut new_path = PathBuf::new();
-                            new_path.push(new_name);
-                            new_path.set_extension("rs");
-                            log::info!(
-                                "{}: creating {:?} in worktree {} ({})",
-                                guest_username,
-                                new_path,
-                                worktree_id,
-                                worktree_root_name,
-                            );
-                            project
-                                .update(cx, |project, cx| {
-                                    project.create_entry((worktree_id, new_path), false, cx)
-                                })
-                                .unwrap()
-                                .await?;
-                        }
-                        _ => {
-                            buffer.update(cx, |buffer, cx| {
-                                log::info!(
-                                    "{}: updating buffer {} ({:?})",
-                                    guest_username,
-                                    buffer.remote_id(),
-                                    buffer.file().unwrap().full_path(cx)
-                                );
-                                if rng.lock().gen_bool(0.7) {
-                                    buffer.randomly_edit(&mut *rng.lock(), 5, cx);
-                                } else {
-                                    buffer.randomly_undo_redo(&mut *rng.lock(), cx);
-                                }
-                            });
-                        }
-                    }
-                    cx.background().simulate_random_delay().await;
-                }
-                Ok(())
-            }
-
-            let result = simulate_guest_internal(
-                &mut self,
-                &guest_username,
-                project.clone(),
-                op_start_signal,
-                rng,
-                &mut cx,
-            )
-            .await;
-            log::info!("{}: done", guest_username);
-
-            self.project = Some(project);
-            (self, cx, result.err())
-        }
-    }
-
-    impl Drop for TestClient {
-        fn drop(&mut self) {
-            self.client.tear_down();
-        }
-    }
-
-    impl Executor for Arc<gpui::executor::Background> {
-        type Sleep = gpui::executor::Timer;
-
-        fn spawn_detached<F: 'static + Send + Future<Output = ()>>(&self, future: F) {
-            self.spawn(future).detach();
-        }
-
-        fn sleep(&self, duration: Duration) -> Self::Sleep {
-            self.as_ref().timer(duration)
-        }
-    }
-
-    fn channel_messages(channel: &Channel) -> Vec<(String, String, bool)> {
-        channel
-            .messages()
-            .cursor::<()>()
-            .map(|m| {
-                (
-                    m.sender.github_login.clone(),
-                    m.body.clone(),
-                    m.is_pending(),
-                )
-            })
-            .collect()
-    }
-
-    struct EmptyView;
-
-    impl gpui::Entity for EmptyView {
-        type Event = ();
-    }
-
-    impl gpui::View for EmptyView {
-        fn ui_name() -> &'static str {
-            "empty view"
-        }
-
-        fn render(&mut self, _: &mut gpui::RenderContext<Self>) -> gpui::ElementBox {
-            gpui::Element::boxed(gpui::elements::Empty::new())
-        }
-    }
-}

crates/editor/src/context_menu.rs 🔗

@@ -1,272 +0,0 @@
-pub enum ContextMenu {
-    Completions(CompletionsMenu),
-    CodeActions(CodeActionsMenu),
-}
-
-impl ContextMenu {
-    pub fn select_prev(&mut self, cx: &mut ViewContext<Editor>) -> bool {
-        if self.visible() {
-            match self {
-                ContextMenu::Completions(menu) => menu.select_prev(cx),
-                ContextMenu::CodeActions(menu) => menu.select_prev(cx),
-            }
-            true
-        } else {
-            false
-        }
-    }
-
-    pub fn select_next(&mut self, cx: &mut ViewContext<Editor>) -> bool {
-        if self.visible() {
-            match self {
-                ContextMenu::Completions(menu) => menu.select_next(cx),
-                ContextMenu::CodeActions(menu) => menu.select_next(cx),
-            }
-            true
-        } else {
-            false
-        }
-    }
-
-    pub fn visible(&self) -> bool {
-        match self {
-            ContextMenu::Completions(menu) => menu.visible(),
-            ContextMenu::CodeActions(menu) => menu.visible(),
-        }
-    }
-
-    pub fn render(
-        &self,
-        cursor_position: DisplayPoint,
-        style: EditorStyle,
-        cx: &AppContext,
-    ) -> (DisplayPoint, ElementBox) {
-        match self {
-            ContextMenu::Completions(menu) => (cursor_position, menu.render(style, cx)),
-            ContextMenu::CodeActions(menu) => menu.render(cursor_position, style),
-        }
-    }
-}
-
-struct CompletionsMenu {
-    id: CompletionId,
-    initial_position: Anchor,
-    buffer: ModelHandle<Buffer>,
-    completions: Arc<[Completion]>,
-    match_candidates: Vec<StringMatchCandidate>,
-    matches: Arc<[StringMatch]>,
-    selected_item: usize,
-    list: UniformListState,
-}
-
-impl CompletionsMenu {
-    fn select_prev(&mut self, cx: &mut ViewContext<Editor>) {
-        if self.selected_item > 0 {
-            self.selected_item -= 1;
-            self.list.scroll_to(ScrollTarget::Show(self.selected_item));
-        }
-        cx.notify();
-    }
-
-    fn select_next(&mut self, cx: &mut ViewContext<Editor>) {
-        if self.selected_item + 1 < self.matches.len() {
-            self.selected_item += 1;
-            self.list.scroll_to(ScrollTarget::Show(self.selected_item));
-        }
-        cx.notify();
-    }
-
-    fn visible(&self) -> bool {
-        !self.matches.is_empty()
-    }
-
-    fn render(&self, style: EditorStyle, _: &AppContext) -> ElementBox {
-        enum CompletionTag {}
-
-        let completions = self.completions.clone();
-        let matches = self.matches.clone();
-        let selected_item = self.selected_item;
-        let container_style = style.autocomplete.container;
-        UniformList::new(self.list.clone(), matches.len(), move |range, items, cx| {
-            let start_ix = range.start;
-            for (ix, mat) in matches[range].iter().enumerate() {
-                let completion = &completions[mat.candidate_id];
-                let item_ix = start_ix + ix;
-                items.push(
-                    MouseEventHandler::new::<CompletionTag, _, _>(
-                        mat.candidate_id,
-                        cx,
-                        |state, _| {
-                            let item_style = if item_ix == selected_item {
-                                style.autocomplete.selected_item
-                            } else if state.hovered {
-                                style.autocomplete.hovered_item
-                            } else {
-                                style.autocomplete.item
-                            };
-
-                            Text::new(completion.label.text.clone(), style.text.clone())
-                                .with_soft_wrap(false)
-                                .with_highlights(combine_syntax_and_fuzzy_match_highlights(
-                                    &completion.label.text,
-                                    style.text.color.into(),
-                                    styled_runs_for_code_label(&completion.label, &style.syntax),
-                                    &mat.positions,
-                                ))
-                                .contained()
-                                .with_style(item_style)
-                                .boxed()
-                        },
-                    )
-                    .with_cursor_style(CursorStyle::PointingHand)
-                    .on_mouse_down(move |cx| {
-                        cx.dispatch_action(ConfirmCompletion(Some(item_ix)));
-                    })
-                    .boxed(),
-                );
-            }
-        })
-        .with_width_from_item(
-            self.matches
-                .iter()
-                .enumerate()
-                .max_by_key(|(_, mat)| {
-                    self.completions[mat.candidate_id]
-                        .label
-                        .text
-                        .chars()
-                        .count()
-                })
-                .map(|(ix, _)| ix),
-        )
-        .contained()
-        .with_style(container_style)
-        .boxed()
-    }
-
-    pub async fn filter(&mut self, query: Option<&str>, executor: Arc<executor::Background>) {
-        let mut matches = if let Some(query) = query {
-            fuzzy::match_strings(
-                &self.match_candidates,
-                query,
-                false,
-                100,
-                &Default::default(),
-                executor,
-            )
-            .await
-        } else {
-            self.match_candidates
-                .iter()
-                .enumerate()
-                .map(|(candidate_id, candidate)| StringMatch {
-                    candidate_id,
-                    score: Default::default(),
-                    positions: Default::default(),
-                    string: candidate.string.clone(),
-                })
-                .collect()
-        };
-        matches.sort_unstable_by_key(|mat| {
-            (
-                Reverse(OrderedFloat(mat.score)),
-                self.completions[mat.candidate_id].sort_key(),
-            )
-        });
-
-        for mat in &mut matches {
-            let filter_start = self.completions[mat.candidate_id].label.filter_range.start;
-            for position in &mut mat.positions {
-                *position += filter_start;
-            }
-        }
-
-        self.matches = matches.into();
-    }
-}
-
-#[derive(Clone)]
-struct CodeActionsMenu {
-    actions: Arc<[CodeAction]>,
-    buffer: ModelHandle<Buffer>,
-    selected_item: usize,
-    list: UniformListState,
-    deployed_from_indicator: bool,
-}
-
-impl CodeActionsMenu {
-    fn select_prev(&mut self, cx: &mut ViewContext<Editor>) {
-        if self.selected_item > 0 {
-            self.selected_item -= 1;
-            cx.notify()
-        }
-    }
-
-    fn select_next(&mut self, cx: &mut ViewContext<Editor>) {
-        if self.selected_item + 1 < self.actions.len() {
-            self.selected_item += 1;
-            cx.notify()
-        }
-    }
-
-    fn visible(&self) -> bool {
-        !self.actions.is_empty()
-    }
-
-    fn render(
-        &self,
-        mut cursor_position: DisplayPoint,
-        style: EditorStyle,
-    ) -> (DisplayPoint, ElementBox) {
-        enum ActionTag {}
-
-        let container_style = style.autocomplete.container;
-        let actions = self.actions.clone();
-        let selected_item = self.selected_item;
-        let element =
-            UniformList::new(self.list.clone(), actions.len(), move |range, items, cx| {
-                let start_ix = range.start;
-                for (ix, action) in actions[range].iter().enumerate() {
-                    let item_ix = start_ix + ix;
-                    items.push(
-                        MouseEventHandler::new::<ActionTag, _, _>(item_ix, cx, |state, _| {
-                            let item_style = if item_ix == selected_item {
-                                style.autocomplete.selected_item
-                            } else if state.hovered {
-                                style.autocomplete.hovered_item
-                            } else {
-                                style.autocomplete.item
-                            };
-
-                            Text::new(action.lsp_action.title.clone(), style.text.clone())
-                                .with_soft_wrap(false)
-                                .contained()
-                                .with_style(item_style)
-                                .boxed()
-                        })
-                        .with_cursor_style(CursorStyle::PointingHand)
-                        .on_mouse_down(move |cx| {
-                            cx.dispatch_action(ConfirmCodeAction(Some(item_ix)));
-                        })
-                        .boxed(),
-                    );
-                }
-            })
-            .with_width_from_item(
-                self.actions
-                    .iter()
-                    .enumerate()
-                    .max_by_key(|(_, action)| action.lsp_action.title.chars().count())
-                    .map(|(ix, _)| ix),
-            )
-            .contained()
-            .with_style(container_style)
-            .boxed();
-
-        if self.deployed_from_indicator {
-            *cursor_position.column_mut() = 0;
-        }
-
-        (cursor_position, element)
-    }
-}

crates/editor/src/display_map.rs 🔗

@@ -279,6 +279,23 @@ impl DisplaySnapshot {
         }
     }
 
+    pub fn expand_to_line(&self, range: Range<Point>) -> Range<Point> {
+        let mut new_start = self.prev_line_boundary(range.start).0;
+        let mut new_end = self.next_line_boundary(range.end).0;
+
+        if new_start.row == range.start.row && new_end.row == range.end.row {
+            if new_end.row < self.buffer_snapshot.max_point().row {
+                new_end.row += 1;
+                new_end.column = 0;
+            } else if new_start.row > 0 {
+                new_start.row -= 1;
+                new_start.column = self.buffer_snapshot.line_len(new_start.row);
+            }
+        }
+
+        new_start..new_end
+    }
+
     fn point_to_display_point(&self, point: Point, bias: Bias) -> DisplayPoint {
         let fold_point = self.folds_snapshot.to_fold_point(point, bias);
         let tab_point = self.tabs_snapshot.to_tab_point(fold_point);

crates/editor/src/editor.rs 🔗

@@ -3,10 +3,10 @@ mod element;
 pub mod items;
 pub mod movement;
 mod multi_buffer;
-mod selections_collection;
+pub mod selections_collection;
 
-#[cfg(test)]
-mod test;
+#[cfg(any(test, feature = "test-support"))]
+pub mod test;
 
 use aho_corasick::AhoCorasick;
 use anyhow::Result;
@@ -850,9 +850,9 @@ struct ActiveDiagnosticGroup {
 }
 
 #[derive(Serialize, Deserialize)]
-struct ClipboardSelection {
-    len: usize,
-    is_entire_line: bool,
+pub struct ClipboardSelection {
+    pub len: usize,
+    pub is_entire_line: bool,
 }
 
 #[derive(Debug)]
@@ -1038,6 +1038,10 @@ impl Editor {
         self.buffer.read(cx).replica_id()
     }
 
+    pub fn leader_replica_id(&self) -> Option<ReplicaId> {
+        self.leader_replica_id
+    }
+
     pub fn buffer(&self) -> &ModelHandle<MultiBuffer> {
         &self.buffer
     }
@@ -1332,7 +1336,11 @@ impl Editor {
     ) {
         if self.focused && self.leader_replica_id.is_none() {
             self.buffer.update(cx, |buffer, cx| {
-                buffer.set_active_selections(&self.selections.disjoint_anchors(), cx)
+                buffer.set_active_selections(
+                    &self.selections.disjoint_anchors(),
+                    self.selections.line_mode,
+                    cx,
+                )
             });
         }
 
@@ -1406,12 +1414,14 @@ impl Editor {
         let old_cursor_position = self.selections.newest_anchor().head();
         self.push_to_selection_history();
 
-        let result = self.selections.change_with(cx, change);
+        let (changed, result) = self.selections.change_with(cx, change);
 
-        if let Some(autoscroll) = autoscroll {
-            self.request_autoscroll(autoscroll, cx);
+        if changed {
+            if let Some(autoscroll) = autoscroll {
+                self.request_autoscroll(autoscroll, cx);
+            }
+            self.selections_did_change(true, &old_cursor_position, cx);
         }
-        self.selections_did_change(true, &old_cursor_position, cx);
 
         result
     }
@@ -1551,12 +1561,10 @@ impl Editor {
         }
 
         self.change_selections(Some(Autoscroll::Fit), cx, |s| {
-            if add {
-                if click_count > 1 {
-                    s.delete(newest_selection.id);
-                }
-            } else {
+            if !add {
                 s.clear_disjoint();
+            } else if click_count > 1 {
+                s.delete(newest_selection.id)
             }
 
             s.set_pending_range(start..end, mode);
@@ -1869,13 +1877,16 @@ impl Editor {
     pub fn insert(&mut self, text: &str, cx: &mut ViewContext<Self>) {
         let text: Arc<str> = text.into();
         self.transact(cx, |this, cx| {
-            let old_selections = this.selections.all::<usize>(cx);
+            let old_selections = this.selections.all_adjusted(cx);
             let selection_anchors = this.buffer.update(cx, |buffer, cx| {
                 let anchors = {
                     let snapshot = buffer.read(cx);
                     old_selections
                         .iter()
-                        .map(|s| (s.id, s.goal, snapshot.anchor_after(s.end)))
+                        .map(|s| {
+                            let anchor = snapshot.anchor_after(s.end);
+                            s.map(|_| anchor.clone())
+                        })
                         .collect::<Vec<_>>()
                 };
                 buffer.edit_with_autoindent(
@@ -1887,25 +1898,8 @@ impl Editor {
                 anchors
             });
 
-            let selections = {
-                let snapshot = this.buffer.read(cx).read(cx);
-                selection_anchors
-                    .into_iter()
-                    .map(|(id, goal, position)| {
-                        let position = position.to_offset(&snapshot);
-                        Selection {
-                            id,
-                            start: position,
-                            end: position,
-                            goal,
-                            reversed: false,
-                        }
-                    })
-                    .collect()
-            };
-
             this.change_selections(Some(Autoscroll::Fit), cx, |s| {
-                s.select(selections);
+                s.select_anchors(selection_anchors);
             })
         });
     }
@@ -2758,28 +2752,31 @@ impl Editor {
     pub fn backspace(&mut self, _: &Backspace, cx: &mut ViewContext<Self>) {
         let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
         let mut selections = self.selections.all::<Point>(cx);
-        for selection in &mut selections {
-            if selection.is_empty() {
-                let old_head = selection.head();
-                let mut new_head =
-                    movement::left(&display_map, old_head.to_display_point(&display_map))
-                        .to_point(&display_map);
-                if let Some((buffer, line_buffer_range)) = display_map
-                    .buffer_snapshot
-                    .buffer_line_for_row(old_head.row)
-                {
-                    let indent_column = buffer.indent_column_for_line(line_buffer_range.start.row);
-                    let language_name = buffer.language().map(|language| language.name());
-                    let indent = cx.global::<Settings>().tab_size(language_name.as_deref());
-                    if old_head.column <= indent_column && old_head.column > 0 {
-                        new_head = cmp::min(
-                            new_head,
-                            Point::new(old_head.row, ((old_head.column - 1) / indent) * indent),
-                        );
+        if !self.selections.line_mode {
+            for selection in &mut selections {
+                if selection.is_empty() {
+                    let old_head = selection.head();
+                    let mut new_head =
+                        movement::left(&display_map, old_head.to_display_point(&display_map))
+                            .to_point(&display_map);
+                    if let Some((buffer, line_buffer_range)) = display_map
+                        .buffer_snapshot
+                        .buffer_line_for_row(old_head.row)
+                    {
+                        let indent_column =
+                            buffer.indent_column_for_line(line_buffer_range.start.row);
+                        let language_name = buffer.language().map(|language| language.name());
+                        let indent = cx.global::<Settings>().tab_size(language_name.as_deref());
+                        if old_head.column <= indent_column && old_head.column > 0 {
+                            new_head = cmp::min(
+                                new_head,
+                                Point::new(old_head.row, ((old_head.column - 1) / indent) * indent),
+                            );
+                        }
                     }
-                }
 
-                selection.set_head(new_head, SelectionGoal::None);
+                    selection.set_head(new_head, SelectionGoal::None);
+                }
             }
         }
 
@@ -2792,8 +2789,9 @@ impl Editor {
     pub fn delete(&mut self, _: &Delete, cx: &mut ViewContext<Self>) {
         self.transact(cx, |this, cx| {
             this.change_selections(Some(Autoscroll::Fit), cx, |s| {
+                let line_mode = s.line_mode;
                 s.move_with(|map, selection| {
-                    if selection.is_empty() {
+                    if selection.is_empty() && !line_mode {
                         let cursor = movement::right(map, selection.head());
                         selection.set_head(cursor, SelectionGoal::None);
                     }
@@ -2816,7 +2814,7 @@ impl Editor {
             return;
         }
 
-        let mut selections = self.selections.all::<Point>(cx);
+        let mut selections = self.selections.all_adjusted(cx);
         if selections.iter().all(|s| s.is_empty()) {
             self.transact(cx, |this, cx| {
                 this.buffer.update(cx, |buffer, cx| {
@@ -3302,8 +3300,9 @@ impl Editor {
         self.transact(cx, |this, cx| {
             let edits = this.change_selections(Some(Autoscroll::Fit), cx, |s| {
                 let mut edits: Vec<(Range<usize>, String)> = Default::default();
+                let line_mode = s.line_mode;
                 s.move_with(|display_map, selection| {
-                    if !selection.is_empty() {
+                    if !selection.is_empty() || line_mode {
                         return;
                     }
 
@@ -3356,7 +3355,7 @@ impl Editor {
         {
             let max_point = buffer.max_point();
             for selection in &mut selections {
-                let is_entire_line = selection.is_empty();
+                let is_entire_line = selection.is_empty() || self.selections.line_mode;
                 if is_entire_line {
                     selection.start = Point::new(selection.start.row, 0);
                     selection.end = cmp::min(max_point, Point::new(selection.end.row + 1, 0));
@@ -3387,16 +3386,17 @@ impl Editor {
         let selections = self.selections.all::<Point>(cx);
         let buffer = self.buffer.read(cx).read(cx);
         let mut text = String::new();
+
         let mut clipboard_selections = Vec::with_capacity(selections.len());
         {
             let max_point = buffer.max_point();
             for selection in selections.iter() {
                 let mut start = selection.start;
                 let mut end = selection.end;
-                let is_entire_line = selection.is_empty();
+                let is_entire_line = selection.is_empty() || self.selections.line_mode;
                 if is_entire_line {
                     start = Point::new(start.row, 0);
-                    end = cmp::min(max_point, Point::new(start.row + 1, 0));
+                    end = cmp::min(max_point, Point::new(end.row + 1, 0));
                 }
                 let mut len = 0;
                 for chunk in buffer.text_for_range(start..end) {
@@ -3440,6 +3440,7 @@ impl Editor {
                         let snapshot = buffer.read(cx);
                         let mut start_offset = 0;
                         let mut edits = Vec::new();
+                        let line_mode = this.selections.line_mode;
                         for (ix, selection) in old_selections.iter().enumerate() {
                             let to_insert;
                             let entire_line;
@@ -3457,12 +3458,12 @@ impl Editor {
                             // clipboard text was written, then the entire line containing the
                             // selection was copied. If this selection is also currently empty,
                             // then paste the line before the current line of the buffer.
-                            let range = if selection.is_empty() && entire_line {
+                            let range = if selection.is_empty() && !line_mode && entire_line {
                                 let column = selection.start.to_point(&snapshot).column as usize;
                                 let line_start = selection.start - column;
                                 line_start..line_start
                             } else {
-                                selection.start..selection.end
+                                selection.range()
                             };
 
                             edits.push((range, to_insert));
@@ -3512,8 +3513,9 @@ impl Editor {
 
     pub fn move_left(&mut self, _: &MoveLeft, cx: &mut ViewContext<Self>) {
         self.change_selections(Some(Autoscroll::Fit), cx, |s| {
+            let line_mode = s.line_mode;
             s.move_with(|map, selection| {
-                let cursor = if selection.is_empty() {
+                let cursor = if selection.is_empty() && !line_mode {
                     movement::left(map, selection.start)
                 } else {
                     selection.start
@@ -3531,8 +3533,9 @@ impl Editor {
 
     pub fn move_right(&mut self, _: &MoveRight, cx: &mut ViewContext<Self>) {
         self.change_selections(Some(Autoscroll::Fit), cx, |s| {
+            let line_mode = s.line_mode;
             s.move_with(|map, selection| {
-                let cursor = if selection.is_empty() {
+                let cursor = if selection.is_empty() && !line_mode {
                     movement::right(map, selection.end)
                 } else {
                     selection.end
@@ -3565,8 +3568,9 @@ impl Editor {
         }
 
         self.change_selections(Some(Autoscroll::Fit), cx, |s| {
+            let line_mode = s.line_mode;
             s.move_with(|map, selection| {
-                if !selection.is_empty() {
+                if !selection.is_empty() && !line_mode {
                     selection.goal = SelectionGoal::None;
                 }
                 let (cursor, goal) = movement::up(&map, selection.start, selection.goal, false);
@@ -3596,8 +3600,9 @@ impl Editor {
         }
 
         self.change_selections(Some(Autoscroll::Fit), cx, |s| {
+            let line_mode = s.line_mode;
             s.move_with(|map, selection| {
-                if !selection.is_empty() {
+                if !selection.is_empty() && !line_mode {
                     selection.goal = SelectionGoal::None;
                 }
                 let (cursor, goal) = movement::down(&map, selection.end, selection.goal, false);
@@ -3679,8 +3684,9 @@ impl Editor {
     ) {
         self.transact(cx, |this, cx| {
             this.change_selections(Some(Autoscroll::Fit), cx, |s| {
+                let line_mode = s.line_mode;
                 s.move_with(|map, selection| {
-                    if selection.is_empty() {
+                    if selection.is_empty() && !line_mode {
                         let cursor = movement::previous_word_start(map, selection.head());
                         selection.set_head(cursor, SelectionGoal::None);
                     }
@@ -3697,8 +3703,9 @@ impl Editor {
     ) {
         self.transact(cx, |this, cx| {
             this.change_selections(Some(Autoscroll::Fit), cx, |s| {
+                let line_mode = s.line_mode;
                 s.move_with(|map, selection| {
-                    if selection.is_empty() {
+                    if selection.is_empty() && !line_mode {
                         let cursor = movement::previous_subword_start(map, selection.head());
                         selection.set_head(cursor, SelectionGoal::None);
                     }
@@ -3751,8 +3758,9 @@ impl Editor {
     pub fn delete_to_next_word_end(&mut self, _: &DeleteToNextWordEnd, cx: &mut ViewContext<Self>) {
         self.transact(cx, |this, cx| {
             this.change_selections(Some(Autoscroll::Fit), cx, |s| {
+                let line_mode = s.line_mode;
                 s.move_with(|map, selection| {
-                    if selection.is_empty() {
+                    if selection.is_empty() && !line_mode {
                         let cursor = movement::next_word_end(map, selection.head());
                         selection.set_head(cursor, SelectionGoal::None);
                     }
@@ -4698,6 +4706,7 @@ impl Editor {
                     // Position the selection in the rename editor so that it matches the current selection.
                     this.show_local_selections = false;
                     let rename_editor = cx.add_view(|cx| {
+                        println!("Rename editor created.");
                         let mut editor = Editor::single_line(None, cx);
                         if let Some(old_highlight_id) = old_highlight_id {
                             editor.override_text_style =
@@ -5612,7 +5621,11 @@ impl View for Editor {
             self.buffer.update(cx, |buffer, cx| {
                 buffer.finalize_last_transaction(cx);
                 if self.leader_replica_id.is_none() {
-                    buffer.set_active_selections(&self.selections.disjoint_anchors(), cx);
+                    buffer.set_active_selections(
+                        &self.selections.disjoint_anchors(),
+                        self.selections.line_mode,
+                        cx,
+                    );
                 }
             });
         }
@@ -6033,7 +6046,9 @@ pub fn styled_runs_for_code_label<'a>(
 
 #[cfg(test)]
 mod tests {
-    use crate::test::{assert_text_with_selections, select_ranges};
+    use crate::test::{
+        assert_text_with_selections, build_editor, select_ranges, EditorTestContext,
+    };
 
     use super::*;
     use gpui::{
@@ -7305,117 +7320,62 @@ mod tests {
     }
 
     #[gpui::test]
-    fn test_indent_outdent(cx: &mut gpui::MutableAppContext) {
-        cx.set_global(Settings::test(cx));
-        let buffer = MultiBuffer::build_simple(
-            indoc! {"
-                  one two
-                three
-                 four"},
-            cx,
-        );
-        let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer.clone(), cx));
+    async fn test_indent_outdent(cx: &mut gpui::TestAppContext) {
+        let mut cx = EditorTestContext::new(cx).await;
 
-        view.update(cx, |view, cx| {
-            // two selections on the same line
-            select_ranges(
-                view,
-                indoc! {"
-                      [one] [two]
-                    three
-                     four"},
-                cx,
-            );
-
-            // indent from mid-tabstop to full tabstop
-            view.tab(&Tab, cx);
-            assert_text_with_selections(
-                view,
-                indoc! {"
-                        [one] [two]
-                    three
-                     four"},
-                cx,
-            );
-
-            // outdent from 1 tabstop to 0 tabstops
-            view.tab_prev(&TabPrev, cx);
-            assert_text_with_selections(
-                view,
-                indoc! {"
-                    [one] [two]
-                    three
-                     four"},
-                cx,
-            );
-
-            // select across line ending
-            select_ranges(
-                view,
-                indoc! {"
-                    one two
-                    t[hree
-                    ] four"},
-                cx,
-            );
-
-            // indent and outdent affect only the preceding line
-            view.tab(&Tab, cx);
-            assert_text_with_selections(
-                view,
-                indoc! {"
-                    one two
-                        t[hree
-                    ] four"},
-                cx,
-            );
-            view.tab_prev(&TabPrev, cx);
-            assert_text_with_selections(
-                view,
-                indoc! {"
-                    one two
-                    t[hree
-                    ] four"},
-                cx,
-            );
-
-            // Ensure that indenting/outdenting works when the cursor is at column 0.
-            select_ranges(
-                view,
-                indoc! {"
-                    one two
-                    []three
-                     four"},
-                cx,
-            );
-            view.tab(&Tab, cx);
-            assert_text_with_selections(
-                view,
-                indoc! {"
-                    one two
-                        []three
-                     four"},
-                cx,
-            );
+        cx.set_state(indoc! {"
+              [one} [two}
+            three
+             four"});
+        cx.update_editor(|e, cx| e.tab(&Tab, cx));
+        cx.assert_editor_state(indoc! {"
+                [one} [two}
+            three
+             four"});
 
-            select_ranges(
-                view,
-                indoc! {"
-                    one two
-                    []    three
-                     four"},
-                cx,
-            );
-            view.tab_prev(&TabPrev, cx);
-            assert_text_with_selections(
-                view,
-                indoc! {"
-                    one two
-                    []three
-                     four"},
-                cx,
-            );
-        });
+        cx.update_editor(|e, cx| e.tab_prev(&TabPrev, cx));
+        cx.assert_editor_state(indoc! {"
+            [one} [two}
+            three
+             four"});
+
+        // select across line ending
+        cx.set_state(indoc! {"
+            one two
+            t[hree
+            } four"});
+        cx.update_editor(|e, cx| e.tab(&Tab, cx));
+        cx.assert_editor_state(indoc! {"
+            one two
+                t[hree
+            } four"});
+
+        cx.update_editor(|e, cx| e.tab_prev(&TabPrev, cx));
+        cx.assert_editor_state(indoc! {"
+            one two
+            t[hree
+            } four"});
+
+        // Ensure that indenting/outdenting works when the cursor is at column 0.
+        cx.set_state(indoc! {"
+            one two
+            |three
+                four"});
+        cx.update_editor(|e, cx| e.tab(&Tab, cx));
+        cx.assert_editor_state(indoc! {"
+            one two
+                |three
+                four"});
+
+        cx.set_state(indoc! {"
+            one two
+            |    three
+             four"});
+        cx.update_editor(|e, cx| e.tab_prev(&TabPrev, cx));
+        cx.assert_editor_state(indoc! {"
+            one two
+            |three
+             four"});
     }
 
     #[gpui::test]
@@ -7524,73 +7484,71 @@ mod tests {
     }
 
     #[gpui::test]
-    fn test_backspace(cx: &mut gpui::MutableAppContext) {
-        cx.set_global(Settings::test(cx));
-        let (_, view) = cx.add_window(Default::default(), |cx| {
-            build_editor(MultiBuffer::build_simple("", cx), cx)
-        });
-
-        view.update(cx, |view, cx| {
-            view.set_text("one two three\nfour five six\nseven eight nine\nten\n", cx);
-            view.change_selections(None, cx, |s| {
-                s.select_display_ranges([
-                    // an empty selection - the preceding character is deleted
-                    DisplayPoint::new(0, 2)..DisplayPoint::new(0, 2),
-                    // one character selected - it is deleted
-                    DisplayPoint::new(1, 4)..DisplayPoint::new(1, 3),
-                    // a line suffix selected - it is deleted
-                    DisplayPoint::new(2, 6)..DisplayPoint::new(3, 0),
-                ])
-            });
-            view.backspace(&Backspace, cx);
-            assert_eq!(view.text(cx), "oe two three\nfou five six\nseven ten\n");
-
-            view.set_text("    one\n        two\n        three\n   four", cx);
-            view.change_selections(None, cx, |s| {
-                s.select_display_ranges([
-                    // cursors at the the end of leading indent - last indent is deleted
-                    DisplayPoint::new(0, 4)..DisplayPoint::new(0, 4),
-                    DisplayPoint::new(1, 8)..DisplayPoint::new(1, 8),
-                    // cursors inside leading indent - overlapping indent deletions are coalesced
-                    DisplayPoint::new(2, 4)..DisplayPoint::new(2, 4),
-                    DisplayPoint::new(2, 5)..DisplayPoint::new(2, 5),
-                    DisplayPoint::new(2, 6)..DisplayPoint::new(2, 6),
-                    // cursor at the beginning of a line - preceding newline is deleted
-                    DisplayPoint::new(3, 0)..DisplayPoint::new(3, 0),
-                    // selection inside leading indent - only the selected character is deleted
-                    DisplayPoint::new(3, 2)..DisplayPoint::new(3, 3),
-                ])
-            });
-            view.backspace(&Backspace, cx);
-            assert_eq!(view.text(cx), "one\n    two\n  three  four");
-        });
+    async fn test_backspace(cx: &mut gpui::TestAppContext) {
+        let mut cx = EditorTestContext::new(cx).await;
+        // Basic backspace
+        cx.set_state(indoc! {"
+            on|e two three
+            fou[r} five six
+            seven {eight nine
+            ]ten"});
+        cx.update_editor(|e, cx| e.backspace(&Backspace, cx));
+        cx.assert_editor_state(indoc! {"
+            o|e two three
+            fou| five six
+            seven |ten"});
+
+        // Test backspace inside and around indents
+        cx.set_state(indoc! {"
+            zero
+                |one
+                    |two
+                | | |  three
+            |  |  four"});
+        cx.update_editor(|e, cx| e.backspace(&Backspace, cx));
+        cx.assert_editor_state(indoc! {"
+            zero
+            |one
+                |two
+            |  three|  four"});
+
+        // Test backspace with line_mode set to true
+        cx.update_editor(|e, _| e.selections.line_mode = true);
+        cx.set_state(indoc! {"
+            The |quick |brown
+            fox jumps over
+            the lazy dog
+            |The qu[ick b}rown"});
+        cx.update_editor(|e, cx| e.backspace(&Backspace, cx));
+        cx.assert_editor_state(indoc! {"
+            |fox jumps over
+            the lazy dog|"});
     }
 
     #[gpui::test]
-    fn test_delete(cx: &mut gpui::MutableAppContext) {
-        cx.set_global(Settings::test(cx));
-        let buffer =
-            MultiBuffer::build_simple("one two three\nfour five six\nseven eight nine\nten\n", cx);
-        let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer.clone(), cx));
-
-        view.update(cx, |view, cx| {
-            view.change_selections(None, cx, |s| {
-                s.select_display_ranges([
-                    // an empty selection - the following character is deleted
-                    DisplayPoint::new(0, 2)..DisplayPoint::new(0, 2),
-                    // one character selected - it is deleted
-                    DisplayPoint::new(1, 4)..DisplayPoint::new(1, 3),
-                    // a line suffix selected - it is deleted
-                    DisplayPoint::new(2, 6)..DisplayPoint::new(3, 0),
-                ])
-            });
-            view.delete(&Delete, cx);
-        });
-
-        assert_eq!(
-            buffer.read(cx).read(cx).text(),
-            "on two three\nfou five six\nseven ten\n"
-        );
+    async fn test_delete(cx: &mut gpui::TestAppContext) {
+        let mut cx = EditorTestContext::new(cx).await;
+
+        cx.set_state(indoc! {"
+            on|e two three
+            fou[r} five six
+            seven {eight nine
+            ]ten"});
+        cx.update_editor(|e, cx| e.delete(&Delete, cx));
+        cx.assert_editor_state(indoc! {"
+            on| two three
+            fou| five six
+            seven |ten"});
+
+        // Test backspace with line_mode set to true
+        cx.update_editor(|e, _| e.selections.line_mode = true);
+        cx.set_state(indoc! {"
+            The |quick |brown
+            fox {jum]ps over
+            the lazy dog
+            |The qu[ick b}rown"});
+        cx.update_editor(|e, cx| e.backspace(&Backspace, cx));
+        cx.assert_editor_state("|the lazy dog|");
     }
 
     #[gpui::test]
@@ -7898,131 +7856,79 @@ mod tests {
     }
 
     #[gpui::test]
-    fn test_clipboard(cx: &mut gpui::MutableAppContext) {
-        cx.set_global(Settings::test(cx));
-        let buffer = MultiBuffer::build_simple("one✅ two three four five six ", cx);
-        let view = cx
-            .add_window(Default::default(), |cx| build_editor(buffer.clone(), cx))
-            .1;
+    async fn test_clipboard(cx: &mut gpui::TestAppContext) {
+        let mut cx = EditorTestContext::new(cx).await;
 
-        // Cut with three selections. Clipboard text is divided into three slices.
-        view.update(cx, |view, cx| {
-            view.change_selections(None, cx, |s| s.select_ranges(vec![0..7, 11..17, 22..27]));
-            view.cut(&Cut, cx);
-            assert_eq!(view.display_text(cx), "two four six ");
-        });
+        cx.set_state("[one✅ }two [three }four [five }six ");
+        cx.update_editor(|e, cx| e.cut(&Cut, cx));
+        cx.assert_editor_state("|two |four |six ");
 
         // Paste with three cursors. Each cursor pastes one slice of the clipboard text.
-        view.update(cx, |view, cx| {
-            view.change_selections(None, cx, |s| s.select_ranges(vec![4..4, 9..9, 13..13]));
-            view.paste(&Paste, cx);
-            assert_eq!(view.display_text(cx), "two one✅ four three six five ");
-            assert_eq!(
-                view.selections.display_ranges(cx),
-                &[
-                    DisplayPoint::new(0, 11)..DisplayPoint::new(0, 11),
-                    DisplayPoint::new(0, 22)..DisplayPoint::new(0, 22),
-                    DisplayPoint::new(0, 31)..DisplayPoint::new(0, 31)
-                ]
-            );
-        });
+        cx.set_state("two |four |six |");
+        cx.update_editor(|e, cx| e.paste(&Paste, cx));
+        cx.assert_editor_state("two one✅ |four three |six five |");
 
         // Paste again but with only two cursors. Since the number of cursors doesn't
         // match the number of slices in the clipboard, the entire clipboard text
         // is pasted at each cursor.
-        view.update(cx, |view, cx| {
-            view.change_selections(None, cx, |s| s.select_ranges(vec![0..0, 31..31]));
-            view.handle_input(&Input("( ".into()), cx);
-            view.paste(&Paste, cx);
-            view.handle_input(&Input(") ".into()), cx);
-            assert_eq!(
-                view.display_text(cx),
-                "( one✅ \nthree \nfive ) two one✅ four three six five ( one✅ \nthree \nfive ) "
-            );
-        });
-
-        view.update(cx, |view, cx| {
-            view.change_selections(None, cx, |s| s.select_ranges(vec![0..0]));
-            view.handle_input(&Input("123\n4567\n89\n".into()), cx);
-            assert_eq!(
-                view.display_text(cx),
-                "123\n4567\n89\n( one✅ \nthree \nfive ) two one✅ four three six five ( one✅ \nthree \nfive ) "
-            );
-        });
+        cx.set_state("|two one✅ four three six five |");
+        cx.update_editor(|e, cx| {
+            e.handle_input(&Input("( ".into()), cx);
+            e.paste(&Paste, cx);
+            e.handle_input(&Input(") ".into()), cx);
+        });
+        cx.assert_editor_state(indoc! {"
+            ( one✅ 
+            three 
+            five ) |two one✅ four three six five ( one✅ 
+            three 
+            five ) |"});
 
         // Cut with three selections, one of which is full-line.
-        view.update(cx, |view, cx| {
-            view.change_selections(None, cx, |s| s.select_display_ranges(
-                [
-                    DisplayPoint::new(0, 1)..DisplayPoint::new(0, 2),
-                    DisplayPoint::new(1, 1)..DisplayPoint::new(1, 1),
-                    DisplayPoint::new(2, 0)..DisplayPoint::new(2, 1),
-                ],
-            ));
-            view.cut(&Cut, cx);
-            assert_eq!(
-                view.display_text(cx),
-                "13\n9\n( one✅ \nthree \nfive ) two one✅ four three six five ( one✅ \nthree \nfive ) "
-            );
-        });
+        cx.set_state(indoc! {"
+            1[2}3
+            4|567
+            [8}9"});
+        cx.update_editor(|e, cx| e.cut(&Cut, cx));
+        cx.assert_editor_state(indoc! {"
+            1|3
+            |9"});
 
         // Paste with three selections, noticing how the copied selection that was full-line
         // gets inserted before the second cursor.
-        view.update(cx, |view, cx| {
-            view.change_selections(None, cx, |s| s.select_display_ranges(
-                [
-                    DisplayPoint::new(0, 1)..DisplayPoint::new(0, 1),
-                    DisplayPoint::new(1, 1)..DisplayPoint::new(1, 1),
-                    DisplayPoint::new(2, 2)..DisplayPoint::new(2, 3),
-                ],
-            ));
-            view.paste(&Paste, cx);
-            assert_eq!(
-                view.display_text(cx),
-                "123\n4567\n9\n( 8ne✅ \nthree \nfive ) two one✅ four three six five ( one✅ \nthree \nfive ) "
-            );
-            assert_eq!(
-                view.selections.display_ranges(cx),
-                &[
-                    DisplayPoint::new(0, 2)..DisplayPoint::new(0, 2),
-                    DisplayPoint::new(2, 1)..DisplayPoint::new(2, 1),
-                    DisplayPoint::new(3, 3)..DisplayPoint::new(3, 3),
-                ]
-            );
-        });
+        cx.set_state(indoc! {"
+            1|3
+            9|
+            [o}ne"});
+        cx.update_editor(|e, cx| e.paste(&Paste, cx));
+        cx.assert_editor_state(indoc! {"
+            12|3
+            4567
+            9|
+            8|ne"});
 
         // Copy with a single cursor only, which writes the whole line into the clipboard.
-        view.update(cx, |view, cx| {
-            view.change_selections(None, cx, |s| {
-                s.select_display_ranges([DisplayPoint::new(0, 1)..DisplayPoint::new(0, 1)])
-            });
-            view.copy(&Copy, cx);
-        });
+        cx.set_state(indoc! {"
+            The quick brown
+            fox ju|mps over
+            the lazy dog"});
+        cx.update_editor(|e, cx| e.copy(&Copy, cx));
+        cx.assert_clipboard_content(Some("fox jumps over\n"));
 
         // Paste with three selections, noticing how the copied full-line selection is inserted
         // before the empty selections but replaces the selection that is non-empty.
-        view.update(cx, |view, cx| {
-            view.change_selections(None, cx, |s| s.select_display_ranges(
-                [
-                    DisplayPoint::new(0, 1)..DisplayPoint::new(0, 1),
-                    DisplayPoint::new(1, 0)..DisplayPoint::new(1, 2),
-                    DisplayPoint::new(2, 1)..DisplayPoint::new(2, 1),
-                ],
-            ));
-            view.paste(&Paste, cx);
-            assert_eq!(
-                view.display_text(cx),
-                "123\n123\n123\n67\n123\n9\n( 8ne✅ \nthree \nfive ) two one✅ four three six five ( one✅ \nthree \nfive ) "
-            );
-            assert_eq!(
-                view.selections.display_ranges(cx),
-                &[
-                    DisplayPoint::new(1, 1)..DisplayPoint::new(1, 1),
-                    DisplayPoint::new(3, 0)..DisplayPoint::new(3, 0),
-                    DisplayPoint::new(5, 1)..DisplayPoint::new(5, 1),
-                ]
-            );
-        });
+        cx.set_state(indoc! {"
+            T|he quick brown
+            [fo}x jumps over
+            t|he lazy dog"});
+        cx.update_editor(|e, cx| e.paste(&Paste, cx));
+        cx.assert_editor_state(indoc! {"
+            fox jumps over
+            T|he quick brown
+            fox jumps over
+            |x jumps over
+            fox jumps over
+            t|he lazy dog"});
     }
 
     #[gpui::test]
@@ -8761,8 +8667,10 @@ mod tests {
             fn assert(editor: &mut Editor, cx: &mut ViewContext<Editor>, marked_text_ranges: &str) {
                 let range_markers = ('<', '>');
                 let (expected_text, mut selection_ranges_lookup) =
-                    marked_text_ranges_by(marked_text_ranges, vec![range_markers.clone()]);
-                let selection_ranges = selection_ranges_lookup.remove(&range_markers).unwrap();
+                    marked_text_ranges_by(marked_text_ranges, vec![range_markers.clone().into()]);
+                let selection_ranges = selection_ranges_lookup
+                    .remove(&range_markers.into())
+                    .unwrap();
                 assert_eq!(editor.text(cx), expected_text);
                 assert_eq!(editor.selections.ranges::<usize>(cx), selection_ranges);
             }
@@ -9811,10 +9719,6 @@ mod tests {
         point..point
     }
 
-    fn build_editor(buffer: ModelHandle<MultiBuffer>, cx: &mut ViewContext<Editor>) -> Editor {
-        Editor::new(EditorMode::Full, buffer, None, None, None, cx)
-    }
-
     fn assert_selection_ranges(
         marked_text: &str,
         selection_marker_pairs: Vec<(char, char)>,

crates/editor/src/element.rs 🔗

@@ -3,7 +3,10 @@ use super::{
     Anchor, DisplayPoint, Editor, EditorMode, EditorSnapshot, Input, Scroll, Select, SelectPhase,
     SoftWrap, ToPoint, MAX_LINE_LEN,
 };
-use crate::{display_map::TransformBlock, EditorStyle};
+use crate::{
+    display_map::{DisplaySnapshot, TransformBlock},
+    EditorStyle,
+};
 use clock::ReplicaId;
 use collections::{BTreeMap, HashMap};
 use gpui::{
@@ -23,7 +26,7 @@ use gpui::{
     WeakViewHandle,
 };
 use json::json;
-use language::{Bias, DiagnosticSeverity};
+use language::{Bias, DiagnosticSeverity, Selection};
 use settings::Settings;
 use smallvec::SmallVec;
 use std::{
@@ -33,6 +36,35 @@ use std::{
     ops::Range,
 };
 
+struct SelectionLayout {
+    head: DisplayPoint,
+    range: Range<DisplayPoint>,
+}
+
+impl SelectionLayout {
+    fn new<T: ToPoint + ToDisplayPoint + Clone>(
+        selection: Selection<T>,
+        line_mode: bool,
+        map: &DisplaySnapshot,
+    ) -> Self {
+        if line_mode {
+            let selection = selection.map(|p| p.to_point(&map.buffer_snapshot));
+            let point_range = map.expand_to_line(selection.range());
+            Self {
+                head: selection.head().to_display_point(map),
+                range: point_range.start.to_display_point(map)
+                    ..point_range.end.to_display_point(map),
+            }
+        } else {
+            let selection = selection.map(|p| p.to_display_point(map));
+            Self {
+                head: selection.head(),
+                range: selection.range(),
+            }
+        }
+    }
+}
+
 pub struct EditorElement {
     view: WeakViewHandle<Editor>,
     style: EditorStyle,
@@ -360,7 +392,7 @@ impl EditorElement {
 
             for selection in selections {
                 self.paint_highlighted_range(
-                    selection.start..selection.end,
+                    selection.range.clone(),
                     start_row,
                     end_row,
                     selection_style.selection,
@@ -375,7 +407,7 @@ impl EditorElement {
                 );
 
                 if view.show_local_cursors() || *replica_id != local_replica_id {
-                    let cursor_position = selection.head();
+                    let cursor_position = selection.head;
                     if (start_row..end_row).contains(&cursor_position.row()) {
                         let cursor_row_layout =
                             &layout.line_layouts[(cursor_position.row() - start_row) as usize];
@@ -922,7 +954,7 @@ impl Element for EditorElement {
                 .anchor_before(DisplayPoint::new(end_row, 0).to_offset(&snapshot, Bias::Right))
         };
 
-        let mut selections = Vec::new();
+        let mut selections: Vec<(ReplicaId, Vec<SelectionLayout>)> = Vec::new();
         let mut active_rows = BTreeMap::new();
         let mut highlighted_rows = None;
         let mut highlighted_ranges = Vec::new();
@@ -938,7 +970,7 @@ impl Element for EditorElement {
             );
 
             let mut remote_selections = HashMap::default();
-            for (replica_id, selection) in display_map
+            for (replica_id, line_mode, selection) in display_map
                 .buffer_snapshot
                 .remote_selections_in_range(&(start_anchor.clone()..end_anchor.clone()))
             {
@@ -946,17 +978,10 @@ impl Element for EditorElement {
                 if Some(replica_id) == view.leader_replica_id {
                     continue;
                 }
-
                 remote_selections
                     .entry(replica_id)
                     .or_insert(Vec::new())
-                    .push(crate::Selection {
-                        id: selection.id,
-                        goal: selection.goal,
-                        reversed: selection.reversed,
-                        start: selection.start.to_display_point(&display_map),
-                        end: selection.end.to_display_point(&display_map),
-                    });
+                    .push(SelectionLayout::new(selection, line_mode, &display_map));
             }
             selections.extend(remote_selections);
 
@@ -985,12 +1010,8 @@ impl Element for EditorElement {
                     local_replica_id,
                     local_selections
                         .into_iter()
-                        .map(|selection| crate::Selection {
-                            id: selection.id,
-                            goal: selection.goal,
-                            reversed: selection.reversed,
-                            start: selection.start.to_display_point(&display_map),
-                            end: selection.end.to_display_point(&display_map),
+                        .map(|selection| {
+                            SelectionLayout::new(selection, view.selections.line_mode, &display_map)
                         })
                         .collect(),
                 ));
@@ -1243,7 +1264,7 @@ pub struct LayoutState {
     em_width: f32,
     em_advance: f32,
     highlighted_ranges: Vec<(Range<DisplayPoint>, Color)>,
-    selections: Vec<(ReplicaId, Vec<text::Selection<DisplayPoint>>)>,
+    selections: Vec<(ReplicaId, Vec<SelectionLayout>)>,
     context_menu: Option<(DisplayPoint, ElementBox)>,
     code_actions_indicator: Option<(u32, ElementBox)>,
 }

crates/editor/src/items.rs 🔗

@@ -103,7 +103,11 @@ impl FollowableItem for Editor {
         } else {
             self.buffer.update(cx, |buffer, cx| {
                 if self.focused {
-                    buffer.set_active_selections(&self.selections.disjoint_anchors(), cx);
+                    buffer.set_active_selections(
+                        &self.selections.disjoint_anchors(),
+                        self.selections.line_mode,
+                        cx,
+                    );
                 }
             });
         }

crates/editor/src/multi_buffer.rs 🔗

@@ -509,6 +509,7 @@ impl MultiBuffer {
     pub fn set_active_selections(
         &mut self,
         selections: &[Selection<Anchor>],
+        line_mode: bool,
         cx: &mut ModelContext<Self>,
     ) {
         let mut selections_by_buffer: HashMap<usize, Vec<Selection<text::Anchor>>> =
@@ -573,7 +574,7 @@ impl MultiBuffer {
                         }
                         Some(selection)
                     }));
-                    buffer.set_active_selections(merged_selections, cx);
+                    buffer.set_active_selections(merged_selections, line_mode, cx);
                 });
         }
     }
@@ -2397,7 +2398,7 @@ impl MultiBufferSnapshot {
     pub fn remote_selections_in_range<'a>(
         &'a self,
         range: &'a Range<Anchor>,
-    ) -> impl 'a + Iterator<Item = (ReplicaId, Selection<Anchor>)> {
+    ) -> impl 'a + Iterator<Item = (ReplicaId, bool, Selection<Anchor>)> {
         let mut cursor = self.excerpts.cursor::<Option<&ExcerptId>>();
         cursor.seek(&Some(&range.start.excerpt_id), Bias::Left, &());
         cursor
@@ -2414,7 +2415,7 @@ impl MultiBufferSnapshot {
                 excerpt
                     .buffer
                     .remote_selections_in_range(query_range)
-                    .flat_map(move |(replica_id, selections)| {
+                    .flat_map(move |(replica_id, line_mode, selections)| {
                         selections.map(move |selection| {
                             let mut start = Anchor {
                                 buffer_id: Some(excerpt.buffer_id),
@@ -2435,6 +2436,7 @@ impl MultiBufferSnapshot {
 
                             (
                                 replica_id,
+                                line_mode,
                                 Selection {
                                     id: selection.id,
                                     start,

crates/editor/src/selections_collection.rs 🔗

@@ -27,6 +27,7 @@ pub struct SelectionsCollection {
     display_map: ModelHandle<DisplayMap>,
     buffer: ModelHandle<MultiBuffer>,
     pub next_selection_id: usize,
+    pub line_mode: bool,
     disjoint: Arc<[Selection<Anchor>]>,
     pending: Option<PendingSelection>,
 }
@@ -37,6 +38,7 @@ impl SelectionsCollection {
             display_map,
             buffer,
             next_selection_id: 1,
+            line_mode: false,
             disjoint: Arc::from([]),
             pending: Some(PendingSelection {
                 selection: Selection {
@@ -126,6 +128,20 @@ impl SelectionsCollection {
         .collect()
     }
 
+    // Returns all of the selections, adjusted to take into account the selection line_mode
+    pub fn all_adjusted(&self, cx: &mut MutableAppContext) -> Vec<Selection<Point>> {
+        let mut selections = self.all::<Point>(cx);
+        if self.line_mode {
+            let map = self.display_map(cx);
+            for selection in &mut selections {
+                let new_range = map.expand_to_line(selection.range());
+                selection.start = new_range.start;
+                selection.end = new_range.end;
+            }
+        }
+        selections
+    }
+
     pub fn disjoint_in_range<'a, D>(
         &self,
         range: Range<Anchor>,
@@ -273,9 +289,10 @@ impl SelectionsCollection {
         &mut self,
         cx: &mut MutableAppContext,
         change: impl FnOnce(&mut MutableSelectionsCollection) -> R,
-    ) -> R {
+    ) -> (bool, R) {
         let mut mutable_collection = MutableSelectionsCollection {
             collection: self,
+            selections_changed: false,
             cx,
         };
 
@@ -284,12 +301,13 @@ impl SelectionsCollection {
             !mutable_collection.disjoint.is_empty() || mutable_collection.pending.is_some(),
             "There must be at least one selection"
         );
-        result
+        (mutable_collection.selections_changed, result)
     }
 }
 
 pub struct MutableSelectionsCollection<'a> {
     collection: &'a mut SelectionsCollection,
+    selections_changed: bool,
     cx: &'a mut MutableAppContext,
 }
 
@@ -307,16 +325,26 @@ impl<'a> MutableSelectionsCollection<'a> {
     }
 
     pub fn delete(&mut self, selection_id: usize) {
+        let mut changed = false;
         self.collection.disjoint = self
             .disjoint
             .into_iter()
-            .filter(|selection| selection.id != selection_id)
+            .filter(|selection| {
+                let found = selection.id == selection_id;
+                changed |= found;
+                !found
+            })
             .cloned()
             .collect();
+
+        self.selections_changed |= changed;
     }
 
     pub fn clear_pending(&mut self) {
-        self.collection.pending = None;
+        if self.collection.pending.is_some() {
+            self.collection.pending = None;
+            self.selections_changed = true;
+        }
     }
 
     pub fn set_pending_range(&mut self, range: Range<Anchor>, mode: SelectMode) {
@@ -329,11 +357,13 @@ impl<'a> MutableSelectionsCollection<'a> {
                 goal: SelectionGoal::None,
             },
             mode,
-        })
+        });
+        self.selections_changed = true;
     }
 
     pub fn set_pending(&mut self, selection: Selection<Anchor>, mode: SelectMode) {
         self.collection.pending = Some(PendingSelection { selection, mode });
+        self.selections_changed = true;
     }
 
     pub fn try_cancel(&mut self) -> bool {
@@ -341,12 +371,14 @@ impl<'a> MutableSelectionsCollection<'a> {
             if self.disjoint.is_empty() {
                 self.collection.disjoint = Arc::from([pending.selection]);
             }
+            self.selections_changed = true;
             return true;
         }
 
         let mut oldest = self.oldest_anchor().clone();
         if self.count() > 1 {
             self.collection.disjoint = Arc::from([oldest]);
+            self.selections_changed = true;
             return true;
         }
 
@@ -355,27 +387,13 @@ impl<'a> MutableSelectionsCollection<'a> {
             oldest.start = head.clone();
             oldest.end = head;
             self.collection.disjoint = Arc::from([oldest]);
+            self.selections_changed = true;
             return true;
         }
 
         return false;
     }
 
-    pub fn reset_biases(&mut self) {
-        let buffer = self.buffer.read(self.cx).snapshot(self.cx);
-        self.collection.disjoint = self
-            .collection
-            .disjoint
-            .into_iter()
-            .cloned()
-            .map(|selection| reset_biases(selection, &buffer))
-            .collect();
-
-        if let Some(pending) = self.collection.pending.as_mut() {
-            pending.selection = reset_biases(pending.selection.clone(), &buffer);
-        }
-    }
-
     pub fn insert_range<T>(&mut self, range: Range<T>)
     where
         T: 'a + ToOffset + ToPoint + TextDimension + Ord + Sub<T, Output = T> + std::marker::Copy,
@@ -437,6 +455,7 @@ impl<'a> MutableSelectionsCollection<'a> {
         }));
 
         self.collection.pending = None;
+        self.selections_changed = true;
     }
 
     pub fn select_anchors(&mut self, selections: Vec<Selection<Anchor>>) {
@@ -535,18 +554,27 @@ impl<'a> MutableSelectionsCollection<'a> {
         &mut self,
         mut move_selection: impl FnMut(&DisplaySnapshot, &mut Selection<DisplayPoint>),
     ) {
+        let mut changed = false;
         let display_map = self.display_map();
         let selections = self
             .all::<Point>(self.cx)
             .into_iter()
             .map(|selection| {
-                let mut selection = selection.map(|point| point.to_display_point(&display_map));
-                move_selection(&display_map, &mut selection);
-                selection.map(|display_point| display_point.to_point(&display_map))
+                let mut moved_selection =
+                    selection.map(|point| point.to_display_point(&display_map));
+                move_selection(&display_map, &mut moved_selection);
+                let moved_selection =
+                    moved_selection.map(|display_point| display_point.to_point(&display_map));
+                if selection != moved_selection {
+                    changed = true;
+                }
+                moved_selection
             })
             .collect();
 
-        self.select(selections)
+        if changed {
+            self.select(selections)
+        }
     }
 
     pub fn move_heads_with(
@@ -670,6 +698,7 @@ impl<'a> MutableSelectionsCollection<'a> {
             pending.selection.end = end;
         }
         self.collection.pending = pending;
+        self.selections_changed = true;
 
         selections_with_lost_position
     }
@@ -714,17 +743,3 @@ fn resolve<D: TextDimension + Ord + Sub<D, Output = D>>(
 ) -> Selection<D> {
     selection.map(|p| p.summary::<D>(&buffer))
 }
-
-fn reset_biases(
-    mut selection: Selection<Anchor>,
-    buffer: &MultiBufferSnapshot,
-) -> Selection<Anchor> {
-    let end_bias = if selection.end.to_offset(buffer) > selection.start.to_offset(buffer) {
-        Bias::Left
-    } else {
-        Bias::Right
-    };
-    selection.start = buffer.anchor_after(selection.start);
-    selection.end = buffer.anchor_at(selection.end, end_bias);
-    selection
-}

crates/editor/src/test.rs 🔗

@@ -1,9 +1,19 @@
-use gpui::ViewContext;
-use util::test::{marked_text, marked_text_ranges};
+use std::ops::{Deref, DerefMut, Range};
+
+use indoc::indoc;
+
+use collections::BTreeMap;
+use gpui::{keymap::Keystroke, ModelHandle, ViewContext, ViewHandle};
+use language::Selection;
+use settings::Settings;
+use util::{
+    set_eq,
+    test::{marked_text, marked_text_ranges, marked_text_ranges_by, SetEqError},
+};
 
 use crate::{
     display_map::{DisplayMap, DisplaySnapshot, ToDisplayPoint},
-    DisplayPoint, Editor, MultiBuffer,
+    Autoscroll, DisplayPoint, Editor, EditorMode, MultiBuffer,
 };
 
 #[cfg(test)]
@@ -56,3 +66,301 @@ pub fn assert_text_with_selections(
     assert_eq!(editor.text(cx), unmarked_text);
     assert_eq!(editor.selections.ranges(cx), text_ranges);
 }
+
+pub(crate) fn build_editor(
+    buffer: ModelHandle<MultiBuffer>,
+    cx: &mut ViewContext<Editor>,
+) -> Editor {
+    Editor::new(EditorMode::Full, buffer, None, None, None, cx)
+}
+
+pub struct EditorTestContext<'a> {
+    pub cx: &'a mut gpui::TestAppContext,
+    pub window_id: usize,
+    pub editor: ViewHandle<Editor>,
+}
+
+impl<'a> EditorTestContext<'a> {
+    pub async fn new(cx: &'a mut gpui::TestAppContext) -> EditorTestContext<'a> {
+        let (window_id, editor) = cx.update(|cx| {
+            cx.set_global(Settings::test(cx));
+            crate::init(cx);
+
+            let (window_id, editor) = cx.add_window(Default::default(), |cx| {
+                build_editor(MultiBuffer::build_simple("", cx), cx)
+            });
+
+            editor.update(cx, |_, cx| cx.focus_self());
+
+            (window_id, editor)
+        });
+
+        Self {
+            cx,
+            window_id,
+            editor,
+        }
+    }
+
+    pub fn update_editor<F, T>(&mut self, update: F) -> T
+    where
+        F: FnOnce(&mut Editor, &mut ViewContext<Editor>) -> T,
+    {
+        self.editor.update(self.cx, update)
+    }
+
+    pub fn editor_text(&mut self) -> String {
+        self.editor
+            .update(self.cx, |editor, cx| editor.snapshot(cx).text())
+    }
+
+    pub fn simulate_keystroke(&mut self, keystroke_text: &str) {
+        let keystroke = Keystroke::parse(keystroke_text).unwrap();
+        let input = if keystroke.modified() {
+            None
+        } else {
+            Some(keystroke.key.clone())
+        };
+        self.cx
+            .dispatch_keystroke(self.window_id, keystroke, input, false);
+    }
+
+    pub fn simulate_keystrokes<const COUNT: usize>(&mut self, keystroke_texts: [&str; COUNT]) {
+        for keystroke_text in keystroke_texts.into_iter() {
+            self.simulate_keystroke(keystroke_text);
+        }
+    }
+
+    // Sets the editor state via a marked string.
+    // `|` characters represent empty selections
+    // `[` to `}` represents a non empty selection with the head at `}`
+    // `{` to `]` represents a non empty selection with the head at `{`
+    pub fn set_state(&mut self, text: &str) {
+        self.editor.update(self.cx, |editor, cx| {
+            let (unmarked_text, mut selection_ranges) = marked_text_ranges_by(
+                &text,
+                vec!['|'.into(), ('[', '}').into(), ('{', ']').into()],
+            );
+            editor.set_text(unmarked_text, cx);
+
+            let mut selections: Vec<Range<usize>> =
+                selection_ranges.remove(&'|'.into()).unwrap_or_default();
+            selections.extend(
+                selection_ranges
+                    .remove(&('{', ']').into())
+                    .unwrap_or_default()
+                    .into_iter()
+                    .map(|range| range.end..range.start),
+            );
+            selections.extend(
+                selection_ranges
+                    .remove(&('[', '}').into())
+                    .unwrap_or_default(),
+            );
+
+            editor.change_selections(Some(Autoscroll::Fit), cx, |s| s.select_ranges(selections));
+        })
+    }
+
+    // Asserts the editor state via a marked string.
+    // `|` characters represent empty selections
+    // `[` to `}` represents a non empty selection with the head at `}`
+    // `{` to `]` represents a non empty selection with the head at `{`
+    pub fn assert_editor_state(&mut self, text: &str) {
+        let (unmarked_text, mut selection_ranges) = marked_text_ranges_by(
+            &text,
+            vec!['|'.into(), ('[', '}').into(), ('{', ']').into()],
+        );
+        let editor_text = self.editor_text();
+        assert_eq!(
+            editor_text, unmarked_text,
+            "Unmarked text doesn't match editor text"
+        );
+
+        let expected_empty_selections = selection_ranges.remove(&'|'.into()).unwrap_or_default();
+        let expected_reverse_selections = selection_ranges
+            .remove(&('{', ']').into())
+            .unwrap_or_default();
+        let expected_forward_selections = selection_ranges
+            .remove(&('[', '}').into())
+            .unwrap_or_default();
+
+        self.assert_selections(
+            expected_empty_selections,
+            expected_reverse_selections,
+            expected_forward_selections,
+            Some(text.to_string()),
+        )
+    }
+
+    pub fn assert_editor_selections(&mut self, expected_selections: Vec<Selection<usize>>) {
+        let mut empty_selections = Vec::new();
+        let mut reverse_selections = Vec::new();
+        let mut forward_selections = Vec::new();
+
+        for selection in expected_selections {
+            let range = selection.range();
+            if selection.is_empty() {
+                empty_selections.push(range);
+            } else if selection.reversed {
+                reverse_selections.push(range);
+            } else {
+                forward_selections.push(range)
+            }
+        }
+
+        self.assert_selections(
+            empty_selections,
+            reverse_selections,
+            forward_selections,
+            None,
+        )
+    }
+
+    fn assert_selections(
+        &mut self,
+        expected_empty_selections: Vec<Range<usize>>,
+        expected_reverse_selections: Vec<Range<usize>>,
+        expected_forward_selections: Vec<Range<usize>>,
+        asserted_text: Option<String>,
+    ) {
+        let (empty_selections, reverse_selections, forward_selections) =
+            self.editor.read_with(self.cx, |editor, cx| {
+                let mut empty_selections = Vec::new();
+                let mut reverse_selections = Vec::new();
+                let mut forward_selections = Vec::new();
+
+                for selection in editor.selections.all::<usize>(cx) {
+                    let range = selection.range();
+                    if selection.is_empty() {
+                        empty_selections.push(range);
+                    } else if selection.reversed {
+                        reverse_selections.push(range);
+                    } else {
+                        forward_selections.push(range)
+                    }
+                }
+
+                (empty_selections, reverse_selections, forward_selections)
+            });
+
+        let asserted_selections = asserted_text.unwrap_or_else(|| {
+            self.insert_markers(
+                &expected_empty_selections,
+                &expected_reverse_selections,
+                &expected_forward_selections,
+            )
+        });
+        let actual_selections =
+            self.insert_markers(&empty_selections, &reverse_selections, &forward_selections);
+
+        let unmarked_text = self.editor_text();
+        let all_eq: Result<(), SetEqError<String>> =
+            set_eq!(expected_empty_selections, empty_selections)
+                .map_err(|err| {
+                    err.map(|missing| {
+                        let mut error_text = unmarked_text.clone();
+                        error_text.insert(missing.start, '|');
+                        error_text
+                    })
+                })
+                .and_then(|_| {
+                    set_eq!(expected_reverse_selections, reverse_selections).map_err(|err| {
+                        err.map(|missing| {
+                            let mut error_text = unmarked_text.clone();
+                            error_text.insert(missing.start, '{');
+                            error_text.insert(missing.end, ']');
+                            error_text
+                        })
+                    })
+                })
+                .and_then(|_| {
+                    set_eq!(expected_forward_selections, forward_selections).map_err(|err| {
+                        err.map(|missing| {
+                            let mut error_text = unmarked_text.clone();
+                            error_text.insert(missing.start, '[');
+                            error_text.insert(missing.end, '}');
+                            error_text
+                        })
+                    })
+                });
+
+        match all_eq {
+            Err(SetEqError::LeftMissing(location_text)) => {
+                panic!(
+                    indoc! {"
+                        Editor has extra selection
+                        Extra Selection Location:
+                        {}
+                        Asserted selections:
+                        {}
+                        Actual selections:
+                        {}"},
+                    location_text, asserted_selections, actual_selections,
+                );
+            }
+            Err(SetEqError::RightMissing(location_text)) => {
+                panic!(
+                    indoc! {"
+                        Editor is missing empty selection
+                        Missing Selection Location:
+                        {}
+                        Asserted selections:
+                        {}
+                        Actual selections:
+                        {}"},
+                    location_text, asserted_selections, actual_selections,
+                );
+            }
+            _ => {}
+        }
+    }
+
+    fn insert_markers(
+        &mut self,
+        empty_selections: &Vec<Range<usize>>,
+        reverse_selections: &Vec<Range<usize>>,
+        forward_selections: &Vec<Range<usize>>,
+    ) -> String {
+        let mut editor_text_with_selections = self.editor_text();
+        let mut selection_marks = BTreeMap::new();
+        for range in empty_selections {
+            selection_marks.insert(&range.start, '|');
+        }
+        for range in reverse_selections {
+            selection_marks.insert(&range.start, '{');
+            selection_marks.insert(&range.end, ']');
+        }
+        for range in forward_selections {
+            selection_marks.insert(&range.start, '[');
+            selection_marks.insert(&range.end, '}');
+        }
+        for (offset, mark) in selection_marks.into_iter().rev() {
+            editor_text_with_selections.insert(*offset, mark);
+        }
+
+        editor_text_with_selections
+    }
+
+    pub fn assert_clipboard_content(&mut self, expected_content: Option<&str>) {
+        self.cx.update(|cx| {
+            let actual_content = cx.read_from_clipboard().map(|item| item.text().to_owned());
+            let expected_content = expected_content.map(|content| content.to_owned());
+            assert_eq!(actual_content, expected_content);
+        })
+    }
+}
+
+impl<'a> Deref for EditorTestContext<'a> {
+    type Target = gpui::TestAppContext;
+
+    fn deref(&self) -> &Self::Target {
+        self.cx
+    }
+}
+
+impl<'a> DerefMut for EditorTestContext<'a> {
+    fn deref_mut(&mut self) -> &mut Self::Target {
+        &mut self.cx
+    }
+}

crates/gpui/src/app.rs 🔗

@@ -544,12 +544,23 @@ impl TestAppContext {
         !prompts.is_empty()
     }
 
-    #[cfg(any(test, feature = "test-support"))]
+    pub fn current_window_title(&self, window_id: usize) -> Option<String> {
+        let mut state = self.cx.borrow_mut();
+        let (_, window) = state
+            .presenters_and_platform_windows
+            .get_mut(&window_id)
+            .unwrap();
+        let test_window = window
+            .as_any_mut()
+            .downcast_mut::<platform::test::Window>()
+            .unwrap();
+        test_window.title.clone()
+    }
+
     pub fn leak_detector(&self) -> Arc<Mutex<LeakDetector>> {
         self.cx.borrow().leak_detector()
     }
 
-    #[cfg(any(test, feature = "test-support"))]
     pub fn assert_dropped(&self, handle: impl WeakHandle) {
         self.cx
             .borrow()
@@ -757,7 +768,7 @@ type SubscriptionCallback = Box<dyn FnMut(&dyn Any, &mut MutableAppContext) -> b
 type GlobalSubscriptionCallback = Box<dyn FnMut(&dyn Any, &mut MutableAppContext)>;
 type ObservationCallback = Box<dyn FnMut(&mut MutableAppContext) -> bool>;
 type FocusObservationCallback = Box<dyn FnMut(&mut MutableAppContext) -> bool>;
-type GlobalObservationCallback = Box<dyn FnMut(&dyn Any, &mut MutableAppContext)>;
+type GlobalObservationCallback = Box<dyn FnMut(&mut MutableAppContext)>;
 type ReleaseObservationCallback = Box<dyn FnOnce(&dyn Any, &mut MutableAppContext)>;
 type ActionObservationCallback = Box<dyn FnMut(TypeId, &mut MutableAppContext)>;
 type DeserializeActionCallback = fn(json: &str) -> anyhow::Result<Box<dyn Action>>;
@@ -1272,7 +1283,7 @@ impl MutableAppContext {
     pub fn observe_global<G, F>(&mut self, mut observe: F) -> Subscription
     where
         G: Any,
-        F: 'static + FnMut(&G, &mut MutableAppContext),
+        F: 'static + FnMut(&mut MutableAppContext),
     {
         let type_id = TypeId::of::<G>();
         let id = post_inc(&mut self.next_subscription_id);
@@ -1283,11 +1294,8 @@ impl MutableAppContext {
             .or_default()
             .insert(
                 id,
-                Some(
-                    Box::new(move |global: &dyn Any, cx: &mut MutableAppContext| {
-                        observe(global.downcast_ref().unwrap(), cx)
-                    }) as GlobalObservationCallback,
-                ),
+                Some(Box::new(move |cx: &mut MutableAppContext| observe(cx))
+                    as GlobalObservationCallback),
             );
 
         Subscription::GlobalObservation {
@@ -2304,27 +2312,24 @@ impl MutableAppContext {
     fn handle_global_notification_effect(&mut self, observed_type_id: TypeId) {
         let callbacks = self.global_observations.lock().remove(&observed_type_id);
         if let Some(callbacks) = callbacks {
-            if let Some(global) = self.cx.globals.remove(&observed_type_id) {
-                for (id, callback) in callbacks {
-                    if let Some(mut callback) = callback {
-                        callback(global.as_ref(), self);
-                        match self
-                            .global_observations
-                            .lock()
-                            .entry(observed_type_id)
-                            .or_default()
-                            .entry(id)
-                        {
-                            collections::btree_map::Entry::Vacant(entry) => {
-                                entry.insert(Some(callback));
-                            }
-                            collections::btree_map::Entry::Occupied(entry) => {
-                                entry.remove();
-                            }
+            for (id, callback) in callbacks {
+                if let Some(mut callback) = callback {
+                    callback(self);
+                    match self
+                        .global_observations
+                        .lock()
+                        .entry(observed_type_id)
+                        .or_default()
+                        .entry(id)
+                    {
+                        collections::btree_map::Entry::Vacant(entry) => {
+                            entry.insert(Some(callback));
+                        }
+                        collections::btree_map::Entry::Occupied(entry) => {
+                            entry.remove();
                         }
                     }
                 }
-                self.cx.globals.insert(observed_type_id, global);
             }
         }
     }
@@ -3303,6 +3308,13 @@ impl<'a, T: View> ViewContext<'a, T> {
         self.app.focus(self.window_id, None);
     }
 
+    pub fn set_window_title(&mut self, title: &str) {
+        let window_id = self.window_id();
+        if let Some((_, window)) = self.presenters_and_platform_windows.get_mut(&window_id) {
+            window.set_title(title);
+        }
+    }
+
     pub fn add_model<S, F>(&mut self, build_model: F) -> ModelHandle<S>
     where
         S: Entity,
@@ -5723,7 +5735,7 @@ mod tests {
         let observation_count = Rc::new(RefCell::new(0));
         let subscription = cx.observe_global::<Global, _>({
             let observation_count = observation_count.clone();
-            move |_, _| {
+            move |_| {
                 *observation_count.borrow_mut() += 1;
             }
         });
@@ -5753,7 +5765,7 @@ mod tests {
         let observation_count = Rc::new(RefCell::new(0));
         cx.observe_global::<OtherGlobal, _>({
             let observation_count = observation_count.clone();
-            move |_, _| {
+            move |_| {
                 *observation_count.borrow_mut() += 1;
             }
         })
@@ -6127,7 +6139,7 @@ mod tests {
         *subscription.borrow_mut() = Some(cx.observe_global::<(), _>({
             let observation_count = observation_count.clone();
             let subscription = subscription.clone();
-            move |_, _| {
+            move |_| {
                 subscription.borrow_mut().take();
                 *observation_count.borrow_mut() += 1;
             }

crates/gpui/src/platform.rs 🔗

@@ -96,6 +96,7 @@ pub trait Window: WindowContext {
     fn on_close(&mut self, callback: Box<dyn FnOnce()>);
     fn prompt(&self, level: PromptLevel, msg: &str, answers: &[&str]) -> oneshot::Receiver<usize>;
     fn activate(&self);
+    fn set_title(&mut self, title: &str);
 }
 
 pub trait WindowContext {

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

@@ -202,6 +202,11 @@ impl MacForegroundPlatform {
 
             menu_bar_item.setSubmenu_(menu);
             menu_bar.addItem_(menu_bar_item);
+
+            if menu_name == "Window" {
+                let app: id = msg_send![APP_CLASS, sharedApplication];
+                app.setWindowsMenu_(menu);
+            }
         }
 
         menu_bar

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

@@ -386,8 +386,15 @@ impl platform::Window for Window {
     }
 
     fn activate(&self) {
+        unsafe { msg_send![self.0.borrow().native_window, makeKeyAndOrderFront: nil] }
+    }
+
+    fn set_title(&mut self, title: &str) {
         unsafe {
-            let _: () = msg_send![self.0.borrow().native_window, makeKeyAndOrderFront: nil];
+            let app = NSApplication::sharedApplication(nil);
+            let window = self.0.borrow().native_window;
+            let title = ns_string(title);
+            msg_send![app, changeWindowsItem:window title:title filename:false]
         }
     }
 }

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

@@ -37,6 +37,7 @@ pub struct Window {
     event_handlers: Vec<Box<dyn FnMut(super::Event)>>,
     resize_handlers: Vec<Box<dyn FnMut()>>,
     close_handlers: Vec<Box<dyn FnOnce()>>,
+    pub(crate) title: Option<String>,
     pub(crate) pending_prompts: RefCell<VecDeque<oneshot::Sender<usize>>>,
 }
 
@@ -189,9 +190,14 @@ impl Window {
             close_handlers: Vec::new(),
             scale_factor: 1.0,
             current_scene: None,
+            title: None,
             pending_prompts: Default::default(),
         }
     }
+
+    pub fn title(&self) -> Option<String> {
+        self.title.clone()
+    }
 }
 
 impl super::Dispatcher for Dispatcher {
@@ -248,6 +254,10 @@ impl super::Window for Window {
     }
 
     fn activate(&self) {}
+
+    fn set_title(&mut self, title: &str) {
+        self.title = Some(title.to_string())
+    }
 }
 
 pub fn platform() -> Platform {

crates/language/src/buffer.rs 🔗

@@ -83,6 +83,7 @@ pub struct BufferSnapshot {
 
 #[derive(Clone, Debug)]
 struct SelectionSet {
+    line_mode: bool,
     selections: Arc<[Selection<Anchor>]>,
     lamport_timestamp: clock::Lamport,
 }
@@ -129,6 +130,7 @@ pub enum Operation {
     UpdateSelections {
         selections: Arc<[Selection<Anchor>]>,
         lamport_timestamp: clock::Lamport,
+        line_mode: bool,
     },
     UpdateCompletionTriggers {
         triggers: Vec<String>,
@@ -343,6 +345,7 @@ impl Buffer {
             this.remote_selections.insert(
                 selection_set.replica_id as ReplicaId,
                 SelectionSet {
+                    line_mode: selection_set.line_mode,
                     selections: proto::deserialize_selections(selection_set.selections),
                     lamport_timestamp,
                 },
@@ -385,6 +388,7 @@ impl Buffer {
                     replica_id: *replica_id as u32,
                     selections: proto::serialize_selections(&set.selections),
                     lamport_timestamp: set.lamport_timestamp.value,
+                    line_mode: set.line_mode,
                 })
                 .collect(),
             diagnostics: proto::serialize_diagnostics(self.diagnostics.iter()),
@@ -1030,6 +1034,7 @@ impl Buffer {
     pub fn set_active_selections(
         &mut self,
         selections: Arc<[Selection<Anchor>]>,
+        line_mode: bool,
         cx: &mut ModelContext<Self>,
     ) {
         let lamport_timestamp = self.text.lamport_clock.tick();
@@ -1038,11 +1043,13 @@ impl Buffer {
             SelectionSet {
                 selections: selections.clone(),
                 lamport_timestamp,
+                line_mode,
             },
         );
         self.send_operation(
             Operation::UpdateSelections {
                 selections,
+                line_mode,
                 lamport_timestamp,
             },
             cx,
@@ -1050,7 +1057,7 @@ impl Buffer {
     }
 
     pub fn remove_active_selections(&mut self, cx: &mut ModelContext<Self>) {
-        self.set_active_selections(Arc::from([]), cx);
+        self.set_active_selections(Arc::from([]), false, cx);
     }
 
     pub fn set_text<T>(&mut self, text: T, cx: &mut ModelContext<Self>) -> Option<clock::Local>
@@ -1287,6 +1294,7 @@ impl Buffer {
             Operation::UpdateSelections {
                 selections,
                 lamport_timestamp,
+                line_mode,
             } => {
                 if let Some(set) = self.remote_selections.get(&lamport_timestamp.replica_id) {
                     if set.lamport_timestamp > lamport_timestamp {
@@ -1299,6 +1307,7 @@ impl Buffer {
                     SelectionSet {
                         selections,
                         lamport_timestamp,
+                        line_mode,
                     },
                 );
                 self.text.lamport_clock.observe(lamport_timestamp);
@@ -1890,8 +1899,14 @@ impl BufferSnapshot {
     pub fn remote_selections_in_range<'a>(
         &'a self,
         range: Range<Anchor>,
-    ) -> impl 'a + Iterator<Item = (ReplicaId, impl 'a + Iterator<Item = &'a Selection<Anchor>>)>
-    {
+    ) -> impl 'a
+           + Iterator<
+        Item = (
+            ReplicaId,
+            bool,
+            impl 'a + Iterator<Item = &'a Selection<Anchor>>,
+        ),
+    > {
         self.remote_selections
             .iter()
             .filter(|(replica_id, set)| {
@@ -1909,7 +1924,11 @@ impl BufferSnapshot {
                     Ok(ix) | Err(ix) => ix,
                 };
 
-                (*replica_id, set.selections[start_ix..end_ix].iter())
+                (
+                    *replica_id,
+                    set.line_mode,
+                    set.selections[start_ix..end_ix].iter(),
+                )
             })
     }
 

crates/language/src/proto.rs 🔗

@@ -43,11 +43,13 @@ pub fn serialize_operation(operation: &Operation) -> proto::Operation {
             }),
             Operation::UpdateSelections {
                 selections,
+                line_mode,
                 lamport_timestamp,
             } => proto::operation::Variant::UpdateSelections(proto::operation::UpdateSelections {
                 replica_id: lamport_timestamp.replica_id as u32,
                 lamport_timestamp: lamport_timestamp.value,
                 selections: serialize_selections(selections),
+                line_mode: *line_mode,
             }),
             Operation::UpdateDiagnostics {
                 diagnostics,
@@ -217,6 +219,7 @@ pub fn deserialize_operation(message: proto::Operation) -> Result<Operation> {
                         value: message.lamport_timestamp,
                     },
                     selections: Arc::from(selections),
+                    line_mode: message.line_mode,
                 }
             }
             proto::operation::Variant::UpdateDiagnostics(message) => Operation::UpdateDiagnostics {

crates/language/src/tests.rs 🔗

@@ -828,7 +828,7 @@ fn test_random_collaboration(cx: &mut MutableAppContext, mut rng: StdRng) {
                         selections
                     );
                     active_selections.insert(replica_id, selections.clone());
-                    buffer.set_active_selections(selections, cx);
+                    buffer.set_active_selections(selections, false, cx);
                 });
                 mutation_count -= 1;
             }
@@ -984,7 +984,7 @@ fn test_random_collaboration(cx: &mut MutableAppContext, mut rng: StdRng) {
         let buffer = buffer.read(cx).snapshot();
         let actual_remote_selections = buffer
             .remote_selections_in_range(Anchor::MIN..Anchor::MAX)
-            .map(|(replica_id, selections)| (replica_id, selections.collect::<Vec<_>>()))
+            .map(|(replica_id, _, selections)| (replica_id, selections.collect::<Vec<_>>()))
             .collect::<Vec<_>>();
         let expected_remote_selections = active_selections
             .iter()

crates/project/src/project.rs 🔗

@@ -139,6 +139,7 @@ pub struct Collaborator {
 #[derive(Clone, Debug, PartialEq, Eq)]
 pub enum Event {
     ActiveEntryChanged(Option<ProjectEntryId>),
+    WorktreeAdded,
     WorktreeRemoved(WorktreeId),
     DiskBasedDiagnosticsStarted,
     DiskBasedDiagnosticsUpdated,
@@ -3655,11 +3656,19 @@ impl Project {
         })
     }
 
-    pub fn remove_worktree(&mut self, id: WorktreeId, cx: &mut ModelContext<Self>) {
+    pub fn remove_worktree(&mut self, id_to_remove: WorktreeId, cx: &mut ModelContext<Self>) {
         self.worktrees.retain(|worktree| {
-            worktree
-                .upgrade(cx)
-                .map_or(false, |w| w.read(cx).id() != id)
+            if let Some(worktree) = worktree.upgrade(cx) {
+                let id = worktree.read(cx).id();
+                if id == id_to_remove {
+                    cx.emit(Event::WorktreeRemoved(id));
+                    false
+                } else {
+                    true
+                }
+            } else {
+                false
+            }
         });
         cx.notify();
     }
@@ -3690,6 +3699,7 @@ impl Project {
             self.worktrees
                 .push(WorktreeHandle::Weak(worktree.downgrade()));
         }
+        cx.emit(Event::WorktreeAdded);
         cx.notify();
     }
 

crates/rpc/proto/zed.proto 🔗

@@ -787,6 +787,7 @@ message SelectionSet {
     uint32 replica_id = 1;
     repeated Selection selections = 2;
     uint32 lamport_timestamp = 3;
+    bool line_mode = 4;
 }
 
 message Selection {
@@ -862,6 +863,7 @@ message Operation {
         uint32 replica_id = 1;
         uint32 lamport_timestamp = 2;
         repeated Selection selections = 3;
+        bool line_mode = 4;
     }
 
     message UpdateCompletionTriggers {

crates/rpc/src/rpc.rs 🔗

@@ -6,4 +6,4 @@ pub use conn::Connection;
 pub use peer::*;
 mod macros;
 
-pub const PROTOCOL_VERSION: u32 = 19;
+pub const PROTOCOL_VERSION: u32 = 20;

crates/settings/src/settings.rs 🔗

@@ -21,6 +21,7 @@ pub use keymap_file::{keymap_file_json_schema, KeymapFileContent};
 pub struct Settings {
     pub buffer_font_family: FamilyId,
     pub buffer_font_size: f32,
+    pub default_buffer_font_size: f32,
     pub vim_mode: bool,
     pub tab_size: u32,
     pub soft_wrap: SoftWrap,
@@ -73,6 +74,7 @@ impl Settings {
         Ok(Self {
             buffer_font_family: font_cache.load_family(&[buffer_font_family])?,
             buffer_font_size: 15.,
+            default_buffer_font_size: 15.,
             vim_mode: false,
             tab_size: 4,
             soft_wrap: SoftWrap::None,
@@ -126,6 +128,7 @@ impl Settings {
         Settings {
             buffer_font_family: cx.font_cache().load_family(&["Monaco"]).unwrap(),
             buffer_font_size: 14.,
+            default_buffer_font_size: 14.,
             vim_mode: false,
             tab_size: 4,
             soft_wrap: SoftWrap::None,
@@ -162,6 +165,7 @@ impl Settings {
         }
 
         merge(&mut self.buffer_font_size, data.buffer_font_size);
+        merge(&mut self.default_buffer_font_size, data.buffer_font_size);
         merge(&mut self.vim_mode, data.vim_mode);
         merge(&mut self.format_on_save, data.format_on_save);
         merge(&mut self.soft_wrap, data.editor.soft_wrap);

crates/text/src/selection.rs 🔗

@@ -1,6 +1,7 @@
 use crate::Anchor;
 use crate::{rope::TextDimension, BufferSnapshot};
 use std::cmp::Ordering;
+use std::ops::Range;
 
 #[derive(Copy, Clone, Debug, Eq, PartialEq)]
 pub enum SelectionGoal {
@@ -83,6 +84,10 @@ impl<T: Copy + Ord> Selection<T> {
         self.goal = new_goal;
         self.reversed = false;
     }
+
+    pub fn range(&self) -> Range<T> {
+        self.start..self.end
+    }
 }
 
 impl Selection<usize> {

crates/util/src/test/marked_text.rs 🔗

@@ -24,31 +24,67 @@ pub fn marked_text(marked_text: &str) -> (String, Vec<usize>) {
     (unmarked_text, markers.remove(&'|').unwrap_or_default())
 }
 
+#[derive(Eq, PartialEq, Hash)]
+pub enum TextRangeMarker {
+    Empty(char),
+    Range(char, char),
+}
+
+impl TextRangeMarker {
+    fn markers(&self) -> Vec<char> {
+        match self {
+            Self::Empty(m) => vec![*m],
+            Self::Range(l, r) => vec![*l, *r],
+        }
+    }
+}
+
+impl From<char> for TextRangeMarker {
+    fn from(marker: char) -> Self {
+        Self::Empty(marker)
+    }
+}
+
+impl From<(char, char)> for TextRangeMarker {
+    fn from((left_marker, right_marker): (char, char)) -> Self {
+        Self::Range(left_marker, right_marker)
+    }
+}
+
 pub fn marked_text_ranges_by(
     marked_text: &str,
-    delimiters: Vec<(char, char)>,
-) -> (String, HashMap<(char, char), Vec<Range<usize>>>) {
-    let all_markers = delimiters
-        .iter()
-        .flat_map(|(start, end)| [*start, *end])
-        .collect();
-    let (unmarked_text, mut markers) = marked_text_by(marked_text, all_markers);
-    let range_lookup = delimiters
+    markers: Vec<TextRangeMarker>,
+) -> (String, HashMap<TextRangeMarker, Vec<Range<usize>>>) {
+    let all_markers = markers.iter().flat_map(|m| m.markers()).collect();
+
+    let (unmarked_text, mut marker_offsets) = marked_text_by(marked_text, all_markers);
+    let range_lookup = markers
         .into_iter()
-        .map(|(start_marker, end_marker)| {
-            let starts = markers.remove(&start_marker).unwrap_or_default();
-            let ends = markers.remove(&end_marker).unwrap_or_default();
-            assert_eq!(starts.len(), ends.len(), "marked ranges are unbalanced");
+        .map(|marker| match marker {
+            TextRangeMarker::Empty(empty_marker_char) => {
+                let ranges = marker_offsets
+                    .remove(&empty_marker_char)
+                    .unwrap_or_default()
+                    .into_iter()
+                    .map(|empty_index| empty_index..empty_index)
+                    .collect::<Vec<Range<usize>>>();
+                (marker, ranges)
+            }
+            TextRangeMarker::Range(start_marker, end_marker) => {
+                let starts = marker_offsets.remove(&start_marker).unwrap_or_default();
+                let ends = marker_offsets.remove(&end_marker).unwrap_or_default();
+                assert_eq!(starts.len(), ends.len(), "marked ranges are unbalanced");
 
-            let ranges = starts
-                .into_iter()
-                .zip(ends)
-                .map(|(start, end)| {
-                    assert!(end >= start, "marked ranges must be disjoint");
-                    start..end
-                })
-                .collect::<Vec<Range<usize>>>();
-            ((start_marker, end_marker), ranges)
+                let ranges = starts
+                    .into_iter()
+                    .zip(ends)
+                    .map(|(start, end)| {
+                        assert!(end >= start, "marked ranges must be disjoint");
+                        start..end
+                    })
+                    .collect::<Vec<Range<usize>>>();
+                (marker, ranges)
+            }
         })
         .collect();
 
@@ -58,14 +94,16 @@ pub fn marked_text_ranges_by(
 // Returns ranges delimited by (), [], and <> ranges. Ranges using the same markers
 // must not be overlapping. May also include | for empty ranges
 pub fn marked_text_ranges(full_marked_text: &str) -> (String, Vec<Range<usize>>) {
-    let (range_marked_text, empty_offsets) = marked_text(full_marked_text);
-    let (unmarked, range_lookup) =
-        marked_text_ranges_by(&range_marked_text, vec![('[', ']'), ('(', ')'), ('<', '>')]);
-    let mut combined_ranges: Vec<_> = range_lookup
-        .into_values()
-        .flatten()
-        .chain(empty_offsets.into_iter().map(|offset| offset..offset))
-        .collect();
+    let (unmarked, range_lookup) = marked_text_ranges_by(
+        &full_marked_text,
+        vec![
+            '|'.into(),
+            ('[', ']').into(),
+            ('(', ')').into(),
+            ('<', '>').into(),
+        ],
+    );
+    let mut combined_ranges: Vec<_> = range_lookup.into_values().flatten().collect();
 
     combined_ranges.sort_by_key(|range| range.start);
     (unmarked, combined_ranges)

crates/vim/src/editor_events.rs 🔗

@@ -18,22 +18,31 @@ fn editor_created(EditorCreated(editor): &EditorCreated, cx: &mut MutableAppCont
 }
 
 fn editor_focused(EditorFocused(editor): &EditorFocused, cx: &mut MutableAppContext) {
-    Vim::update(cx, |state, cx| {
-        state.active_editor = Some(editor.downgrade());
+    Vim::update(cx, |vim, cx| {
+        vim.active_editor = Some(editor.downgrade());
+        vim.selection_subscription = Some(cx.subscribe(editor, |editor, event, cx| {
+            if editor.read(cx).leader_replica_id().is_none() {
+                if let editor::Event::SelectionsChanged { local: true } = event {
+                    let newest_empty = editor.read(cx).selections.newest::<usize>(cx).is_empty();
+                    editor_local_selections_changed(newest_empty, cx);
+                }
+            }
+        }));
+
         if editor.read(cx).mode() != EditorMode::Full {
-            state.switch_mode(Mode::Insert, cx);
+            vim.switch_mode(Mode::Insert, cx);
         }
     });
 }
 
 fn editor_blurred(EditorBlurred(editor): &EditorBlurred, cx: &mut MutableAppContext) {
-    Vim::update(cx, |state, cx| {
-        if let Some(previous_editor) = state.active_editor.clone() {
+    Vim::update(cx, |vim, cx| {
+        if let Some(previous_editor) = vim.active_editor.clone() {
             if previous_editor == editor.clone() {
-                state.active_editor = None;
+                vim.active_editor = None;
             }
         }
-        state.sync_editor_options(cx);
+        vim.sync_editor_options(cx);
     })
 }
 
@@ -47,3 +56,11 @@ fn editor_released(EditorReleased(editor): &EditorReleased, cx: &mut MutableAppC
         }
     });
 }
+
+fn editor_local_selections_changed(newest_empty: bool, cx: &mut MutableAppContext) {
+    Vim::update(cx, |vim, cx| {
+        if vim.enabled && vim.state.mode == Mode::Normal && !newest_empty {
+            vim.switch_mode(Mode::Visual { line: false }, cx)
+        }
+    })
+}

crates/vim/src/motion.rs 🔗

@@ -111,7 +111,7 @@ fn motion(motion: Motion, cx: &mut MutableAppContext) {
     });
     match Vim::read(cx).state.mode {
         Mode::Normal => normal_motion(motion, cx),
-        Mode::Visual => visual_motion(motion, cx),
+        Mode::Visual { .. } => visual_motion(motion, cx),
         Mode::Insert => {
             // Shouldn't execute a motion in insert mode. Ignoring
         }
@@ -192,11 +192,13 @@ impl Motion {
                 if selection.end.row() < map.max_point().row() {
                     *selection.end.row_mut() += 1;
                     *selection.end.column_mut() = 0;
+                    selection.end = map.clip_point(selection.end, Bias::Right);
                     // Don't reset the end here
                     return;
                 } else if selection.start.row() > 0 {
                     *selection.start.row_mut() -= 1;
                     *selection.start.column_mut() = map.line_len(selection.start.row());
+                    selection.start = map.clip_point(selection.start, Bias::Left);
                 }
             }
 

crates/vim/src/normal.rs 🔗

@@ -1,5 +1,8 @@
 mod change;
 mod delete;
+mod yank;
+
+use std::borrow::Cow;
 
 use crate::{
     motion::Motion,
@@ -8,12 +11,12 @@ use crate::{
 };
 use change::init as change_init;
 use collections::HashSet;
-use editor::{Autoscroll, Bias, DisplayPoint};
+use editor::{Autoscroll, Bias, ClipboardSelection, DisplayPoint};
 use gpui::{actions, MutableAppContext, ViewContext};
-use language::SelectionGoal;
+use language::{Point, SelectionGoal};
 use workspace::Workspace;
 
-use self::{change::change_over, delete::delete_over};
+use self::{change::change_over, delete::delete_over, yank::yank_over};
 
 actions!(
     vim,
@@ -27,6 +30,8 @@ actions!(
         DeleteRight,
         ChangeToEndOfLine,
         DeleteToEndOfLine,
+        Paste,
+        Yank,
     ]
 );
 
@@ -56,6 +61,7 @@ pub fn init(cx: &mut MutableAppContext) {
             delete_over(vim, Motion::EndOfLine, cx);
         })
     });
+    cx.add_action(paste);
 
     change_init(cx);
 }
@@ -64,11 +70,12 @@ pub fn normal_motion(motion: Motion, cx: &mut MutableAppContext) {
     Vim::update(cx, |vim, cx| {
         match vim.state.operator_stack.pop() {
             None => move_cursor(vim, motion, cx),
-            Some(Operator::Change) => change_over(vim, motion, cx),
-            Some(Operator::Delete) => delete_over(vim, motion, cx),
             Some(Operator::Namespace(_)) => {
                 // Can't do anything for a namespace operator. Ignoring
             }
+            Some(Operator::Change) => change_over(vim, motion, cx),
+            Some(Operator::Delete) => delete_over(vim, motion, cx),
+            Some(Operator::Yank) => yank_over(vim, motion, cx),
         }
         vim.clear_operator(cx);
     });
@@ -187,6 +194,116 @@ fn insert_line_below(_: &mut Workspace, _: &InsertLineBelow, cx: &mut ViewContex
     });
 }
 
+// Supports non empty selections so it can be bound and called from visual mode
+fn paste(_: &mut Workspace, _: &Paste, cx: &mut ViewContext<Workspace>) {
+    Vim::update(cx, |vim, cx| {
+        vim.update_active_editor(cx, |editor, cx| {
+            editor.transact(cx, |editor, cx| {
+                if let Some(item) = cx.as_mut().read_from_clipboard() {
+                    let mut clipboard_text = Cow::Borrowed(item.text());
+                    if let Some(mut clipboard_selections) =
+                        item.metadata::<Vec<ClipboardSelection>>()
+                    {
+                        let (display_map, selections) = editor.selections.all_display(cx);
+                        let all_selections_were_entire_line =
+                            clipboard_selections.iter().all(|s| s.is_entire_line);
+                        if clipboard_selections.len() != selections.len() {
+                            let mut newline_separated_text = String::new();
+                            let mut clipboard_selections =
+                                clipboard_selections.drain(..).peekable();
+                            let mut ix = 0;
+                            while let Some(clipboard_selection) = clipboard_selections.next() {
+                                newline_separated_text
+                                    .push_str(&clipboard_text[ix..ix + clipboard_selection.len]);
+                                ix += clipboard_selection.len;
+                                if clipboard_selections.peek().is_some() {
+                                    newline_separated_text.push('\n');
+                                }
+                            }
+                            clipboard_text = Cow::Owned(newline_separated_text);
+                        }
+
+                        let mut new_selections = Vec::new();
+                        editor.buffer().update(cx, |buffer, cx| {
+                            let snapshot = buffer.snapshot(cx);
+                            let mut start_offset = 0;
+                            let mut edits = Vec::new();
+                            for (ix, selection) in selections.iter().enumerate() {
+                                let to_insert;
+                                let linewise;
+                                if let Some(clipboard_selection) = clipboard_selections.get(ix) {
+                                    let end_offset = start_offset + clipboard_selection.len;
+                                    to_insert = &clipboard_text[start_offset..end_offset];
+                                    linewise = clipboard_selection.is_entire_line;
+                                    start_offset = end_offset;
+                                } else {
+                                    to_insert = clipboard_text.as_str();
+                                    linewise = all_selections_were_entire_line;
+                                }
+
+                                // If the clipboard text was copied linewise, and the current selection
+                                // is empty, then paste the text after this line and move the selection
+                                // to the start of the pasted text
+                                let range = if selection.is_empty() && linewise {
+                                    let (point, _) = display_map
+                                        .next_line_boundary(selection.start.to_point(&display_map));
+
+                                    if !to_insert.starts_with('\n') {
+                                        // Add newline before pasted text so that it shows up
+                                        edits.push((point..point, "\n"));
+                                    }
+                                    // Drop selection at the start of the next line
+                                    let selection_point = Point::new(point.row + 1, 0);
+                                    new_selections.push(selection.map(|_| selection_point.clone()));
+                                    point..point
+                                } else {
+                                    let mut selection = selection.clone();
+                                    if !selection.reversed {
+                                        let mut adjusted = selection.end;
+                                        // Head is at the end of the selection. Adjust the end position to
+                                        // to include the character under the cursor.
+                                        *adjusted.column_mut() = adjusted.column() + 1;
+                                        adjusted = display_map.clip_point(adjusted, Bias::Right);
+                                        // If the selection is empty, move both the start and end forward one
+                                        // character
+                                        if selection.is_empty() {
+                                            selection.start = adjusted;
+                                            selection.end = adjusted;
+                                        } else {
+                                            selection.end = adjusted;
+                                        }
+                                    }
+
+                                    let range = selection.map(|p| p.to_point(&display_map)).range();
+                                    new_selections.push(selection.map(|_| range.start.clone()));
+                                    range
+                                };
+
+                                if linewise && to_insert.ends_with('\n') {
+                                    edits.push((
+                                        range,
+                                        &to_insert[0..to_insert.len().saturating_sub(1)],
+                                    ))
+                                } else {
+                                    edits.push((range, to_insert));
+                                }
+                            }
+                            drop(snapshot);
+                            buffer.edit_with_autoindent(edits, cx);
+                        });
+
+                        editor.change_selections(Some(Autoscroll::Fit), cx, |s| {
+                            s.select(new_selections)
+                        });
+                    } else {
+                        editor.insert(&clipboard_text, cx);
+                    }
+                }
+            });
+        });
+    });
+}
+
 #[cfg(test)]
 mod test {
     use indoc::indoc;
@@ -678,14 +795,8 @@ mod test {
                 |
                 The quick"},
         );
-        cx.assert(
-            indoc! {"
-                    |
-                The quick"},
-            indoc! {"
-                    |
-                The quick"},
-        );
+        // Indoc disallows trailing whitspace.
+        cx.assert("   | \nThe quick", "   | \nThe quick");
     }
 
     #[gpui::test]
@@ -1026,4 +1137,48 @@ mod test {
                 brown fox"},
         );
     }
+
+    #[gpui::test]
+    async fn test_p(cx: &mut gpui::TestAppContext) {
+        let mut cx = VimTestContext::new(cx, true).await;
+        cx.set_state(
+            indoc! {"
+                The quick brown
+                fox ju|mps over
+                the lazy dog"},
+            Mode::Normal,
+        );
+
+        cx.simulate_keystrokes(["d", "d"]);
+        cx.assert_editor_state(indoc! {"
+            The quick brown
+            the la|zy dog"});
+
+        cx.simulate_keystroke("p");
+        cx.assert_editor_state(indoc! {"
+            The quick brown
+            the lazy dog
+            |fox jumps over"});
+
+        cx.set_state(
+            indoc! {"
+                The quick brown
+                fox [jump}s over
+                the lazy dog"},
+            Mode::Normal,
+        );
+        cx.simulate_keystroke("y");
+        cx.set_state(
+            indoc! {"
+                The quick brown
+                fox jump|s over
+                the lazy dog"},
+            Mode::Normal,
+        );
+        cx.simulate_keystroke("p");
+        cx.assert_editor_state(indoc! {"
+            The quick brown
+            fox jumps|jumps over
+            the lazy dog"});
+    }
 }

crates/vim/src/normal/change.rs 🔗

@@ -1,4 +1,4 @@
-use crate::{motion::Motion, state::Mode, Vim};
+use crate::{motion::Motion, state::Mode, utils::copy_selections_content, Vim};
 use editor::{char_kind, movement, Autoscroll};
 use gpui::{impl_actions, MutableAppContext, ViewContext};
 use serde::Deserialize;
@@ -27,6 +27,7 @@ pub fn change_over(vim: &mut Vim, motion: Motion, cx: &mut MutableAppContext) {
                     motion.expand_selection(map, selection, false);
                 });
             });
+            copy_selections_content(editor, motion.linewise(), cx);
             editor.insert(&"", cx);
         });
     });
@@ -65,6 +66,7 @@ fn change_word(
                             });
                     });
                 });
+                copy_selections_content(editor, false, cx);
                 editor.insert(&"", cx);
             });
         });

crates/vim/src/normal/delete.rs 🔗

@@ -1,4 +1,4 @@
-use crate::{motion::Motion, Vim};
+use crate::{motion::Motion, utils::copy_selections_content, Vim};
 use collections::HashMap;
 use editor::{Autoscroll, Bias};
 use gpui::MutableAppContext;
@@ -15,6 +15,7 @@ pub fn delete_over(vim: &mut Vim, motion: Motion, cx: &mut MutableAppContext) {
                     original_columns.insert(selection.id, original_head.column());
                 });
             });
+            copy_selections_content(editor, motion.linewise(), cx);
             editor.insert(&"", cx);
 
             // Fixup cursor position after the deletion

crates/vim/src/normal/yank.rs 🔗

@@ -0,0 +1,26 @@
+use crate::{motion::Motion, utils::copy_selections_content, Vim};
+use collections::HashMap;
+use gpui::MutableAppContext;
+
+pub fn yank_over(vim: &mut Vim, motion: Motion, cx: &mut MutableAppContext) {
+    vim.update_active_editor(cx, |editor, cx| {
+        editor.transact(cx, |editor, cx| {
+            editor.set_clip_at_line_ends(false, cx);
+            let mut original_positions: HashMap<_, _> = Default::default();
+            editor.change_selections(None, cx, |s| {
+                s.move_with(|map, selection| {
+                    let original_position = (selection.head(), selection.goal);
+                    motion.expand_selection(map, selection, true);
+                    original_positions.insert(selection.id, original_position);
+                });
+            });
+            copy_selections_content(editor, motion.linewise(), cx);
+            editor.change_selections(None, cx, |s| {
+                s.move_with(|_, selection| {
+                    let (head, goal) = original_positions.remove(&selection.id).unwrap();
+                    selection.collapse_to(head, goal);
+                });
+            });
+        });
+    });
+}

crates/vim/src/state.rs 🔗

@@ -6,7 +6,7 @@ use serde::Deserialize;
 pub enum Mode {
     Normal,
     Insert,
-    Visual,
+    Visual { line: bool },
 }
 
 impl Default for Mode {
@@ -25,6 +25,7 @@ pub enum Operator {
     Namespace(Namespace),
     Change,
     Delete,
+    Yank,
 }
 
 #[derive(Default)]
@@ -36,8 +37,7 @@ pub struct VimState {
 impl VimState {
     pub fn cursor_shape(&self) -> CursorShape {
         match self.mode {
-            Mode::Normal => CursorShape::Block,
-            Mode::Visual => CursorShape::Block,
+            Mode::Normal | Mode::Visual { .. } => CursorShape::Block,
             Mode::Insert => CursorShape::Bar,
         }
     }
@@ -46,13 +46,24 @@ impl VimState {
         !matches!(self.mode, Mode::Insert)
     }
 
+    pub fn clip_at_line_end(&self) -> bool {
+        match self.mode {
+            Mode::Insert | Mode::Visual { .. } => false,
+            _ => true,
+        }
+    }
+
+    pub fn empty_selections_only(&self) -> bool {
+        !matches!(self.mode, Mode::Visual { .. })
+    }
+
     pub fn keymap_context_layer(&self) -> Context {
         let mut context = Context::default();
         context.map.insert(
             "vim_mode".to_string(),
             match self.mode {
                 Mode::Normal => "normal",
-                Mode::Visual => "visual",
+                Mode::Visual { .. } => "visual",
                 Mode::Insert => "insert",
             }
             .to_string(),
@@ -75,6 +86,7 @@ impl Operator {
             Operator::Namespace(Namespace::G) => "g",
             Operator::Change => "c",
             Operator::Delete => "d",
+            Operator::Yank => "y",
         }
         .to_owned();
 

crates/vim/src/utils.rs 🔗

@@ -0,0 +1,25 @@
+use editor::{ClipboardSelection, Editor};
+use gpui::{ClipboardItem, MutableAppContext};
+
+pub fn copy_selections_content(editor: &mut Editor, linewise: bool, cx: &mut MutableAppContext) {
+    let selections = editor.selections.all_adjusted(cx);
+    let buffer = editor.buffer().read(cx).snapshot(cx);
+    let mut text = String::new();
+    let mut clipboard_selections = Vec::with_capacity(selections.len());
+    {
+        for selection in selections.iter() {
+            let initial_len = text.len();
+            let start = selection.start;
+            let end = selection.end;
+            for chunk in buffer.text_for_range(start..end) {
+                text.push_str(chunk);
+            }
+            clipboard_selections.push(ClipboardSelection {
+                len: text.len() - initial_len,
+                is_entire_line: linewise,
+            });
+        }
+    }
+
+    cx.write_to_clipboard(ClipboardItem::new(text).with_metadata(clipboard_selections));
+}

crates/vim/src/vim.rs 🔗

@@ -6,11 +6,12 @@ mod insert;
 mod motion;
 mod normal;
 mod state;
+mod utils;
 mod visual;
 
 use collections::HashMap;
-use editor::{CursorShape, Editor};
-use gpui::{impl_actions, MutableAppContext, ViewContext, WeakViewHandle};
+use editor::{Bias, CursorShape, Editor, Input};
+use gpui::{impl_actions, MutableAppContext, Subscription, ViewContext, WeakViewHandle};
 use serde::Deserialize;
 
 use settings::Settings;
@@ -40,9 +41,19 @@ pub fn init(cx: &mut MutableAppContext) {
             Vim::update(cx, |vim, cx| vim.push_operator(operator, cx))
         },
     );
+    cx.add_action(|_: &mut Editor, _: &Input, cx| {
+        if Vim::read(cx).active_operator().is_some() {
+            // Defer without updating editor
+            MutableAppContext::defer(cx, |cx| Vim::update(cx, |vim, cx| vim.clear_operator(cx)))
+        } else {
+            cx.propagate_action()
+        }
+    });
 
-    cx.observe_global::<Settings, _>(|settings, cx| {
-        Vim::update(cx, |state, cx| state.set_enabled(settings.vim_mode, cx))
+    cx.observe_global::<Settings, _>(|cx| {
+        Vim::update(cx, |state, cx| {
+            state.set_enabled(cx.global::<Settings>().vim_mode, cx)
+        })
     })
     .detach();
 }
@@ -51,6 +62,7 @@ pub fn init(cx: &mut MutableAppContext) {
 pub struct Vim {
     editors: HashMap<usize, WeakViewHandle<Editor>>,
     active_editor: Option<WeakViewHandle<Editor>>,
+    selection_subscription: Option<Subscription>,
 
     enabled: bool,
     state: VimState,
@@ -101,7 +113,7 @@ impl Vim {
         self.sync_editor_options(cx);
     }
 
-    fn active_operator(&mut self) -> Option<Operator> {
+    fn active_operator(&self) -> Option<Operator> {
         self.state.operator_stack.last().copied()
     }
 
@@ -118,23 +130,38 @@ impl Vim {
 
     fn sync_editor_options(&self, cx: &mut MutableAppContext) {
         let state = &self.state;
-
         let cursor_shape = state.cursor_shape();
+
         for editor in self.editors.values() {
             if let Some(editor) = editor.upgrade(cx) {
                 editor.update(cx, |editor, cx| {
                     if self.enabled {
                         editor.set_cursor_shape(cursor_shape, cx);
-                        editor.set_clip_at_line_ends(cursor_shape == CursorShape::Block, cx);
+                        editor.set_clip_at_line_ends(state.clip_at_line_end(), cx);
                         editor.set_input_enabled(!state.vim_controlled());
+                        editor.selections.line_mode =
+                            matches!(state.mode, Mode::Visual { line: true });
                         let context_layer = state.keymap_context_layer();
                         editor.set_keymap_context_layer::<Self>(context_layer);
                     } else {
                         editor.set_cursor_shape(CursorShape::Bar, cx);
                         editor.set_clip_at_line_ends(false, cx);
                         editor.set_input_enabled(true);
+                        editor.selections.line_mode = false;
                         editor.remove_keymap_context_layer::<Self>();
                     }
+
+                    editor.change_selections(None, cx, |s| {
+                        s.move_with(|map, selection| {
+                            selection.set_head(
+                                map.clip_point(selection.head(), Bias::Left),
+                                selection.goal,
+                            );
+                            if state.empty_selections_only() {
+                                selection.collapse_to(selection.head(), selection.goal)
+                            }
+                        });
+                    })
                 });
             }
         }
@@ -169,9 +196,9 @@ mod test {
         assert_eq!(cx.mode(), Mode::Normal);
         cx.simulate_keystrokes(["h", "h", "h", "l"]);
         assert_eq!(cx.editor_text(), "hjkl".to_owned());
-        cx.assert_editor_state("hj|kl");
+        cx.assert_editor_state("h|jkl");
         cx.simulate_keystrokes(["i", "T", "e", "s", "t"]);
-        cx.assert_editor_state("hjTest|kl");
+        cx.assert_editor_state("hTest|jkl");
 
         // Disabling and enabling resets to normal mode
         assert_eq!(cx.mode(), Mode::Insert);

crates/vim/src/vim_test_context.rs 🔗

@@ -1,31 +1,21 @@
-use std::ops::{Deref, Range};
+use std::ops::{Deref, DerefMut};
 
-use collections::BTreeMap;
-use itertools::{Either, Itertools};
-
-use editor::{display_map::ToDisplayPoint, Autoscroll};
-use gpui::{json::json, keymap::Keystroke, ViewHandle};
-use indoc::indoc;
-use language::Selection;
+use editor::test::EditorTestContext;
+use gpui::json::json;
 use project::Project;
-use util::{
-    set_eq,
-    test::{marked_text, marked_text_ranges_by, SetEqError},
-};
-use workspace::{AppState, WorkspaceHandle};
+use workspace::{pane, AppState, WorkspaceHandle};
 
 use crate::{state::Operator, *};
 
 pub struct VimTestContext<'a> {
-    cx: &'a mut gpui::TestAppContext,
-    window_id: usize,
-    editor: ViewHandle<Editor>,
+    cx: EditorTestContext<'a>,
 }
 
 impl<'a> VimTestContext<'a> {
     pub async fn new(cx: &'a mut gpui::TestAppContext, enabled: bool) -> VimTestContext<'a> {
         cx.update(|cx| {
             editor::init(cx);
+            pane::init(cx);
             crate::init(cx);
 
             settings::KeymapFileContent::load("keymaps/vim.json", cx).unwrap();
@@ -69,9 +59,11 @@ impl<'a> VimTestContext<'a> {
         editor.update(cx, |_, cx| cx.focus_self());
 
         Self {
-            cx,
-            window_id,
-            editor,
+            cx: EditorTestContext {
+                cx,
+                window_id,
+                editor,
+            },
         }
     }
 
@@ -100,219 +92,13 @@ impl<'a> VimTestContext<'a> {
             .read(|cx| cx.global::<Vim>().state.operator_stack.last().copied())
     }
 
-    pub fn editor_text(&mut self) -> String {
-        self.editor
-            .update(self.cx, |editor, cx| editor.snapshot(cx).text())
-    }
-
-    pub fn simulate_keystroke(&mut self, keystroke_text: &str) {
-        let keystroke = Keystroke::parse(keystroke_text).unwrap();
-        let input = if keystroke.modified() {
-            None
-        } else {
-            Some(keystroke.key.clone())
-        };
-        self.cx
-            .dispatch_keystroke(self.window_id, keystroke, input, false);
-    }
-
-    pub fn simulate_keystrokes<const COUNT: usize>(&mut self, keystroke_texts: [&str; COUNT]) {
-        for keystroke_text in keystroke_texts.into_iter() {
-            self.simulate_keystroke(keystroke_text);
-        }
-    }
-
     pub fn set_state(&mut self, text: &str, mode: Mode) {
-        self.cx
-            .update(|cx| Vim::update(cx, |vim, cx| vim.switch_mode(mode, cx)));
-        self.editor.update(self.cx, |editor, cx| {
-            let (unmarked_text, markers) = marked_text(&text);
-            editor.set_text(unmarked_text, cx);
-            let cursor_offset = markers[0];
-            editor.change_selections(Some(Autoscroll::Fit), cx, |s| {
-                s.replace_cursors_with(|map| vec![cursor_offset.to_display_point(map)])
-            });
-        })
-    }
-
-    // Asserts the editor state via a marked string.
-    // `|` characters represent empty selections
-    // `[` to `}` represents a non empty selection with the head at `}`
-    // `{` to `]` represents a non empty selection with the head at `{`
-    pub fn assert_editor_state(&mut self, text: &str) {
-        let (text_with_ranges, expected_empty_selections) = marked_text(&text);
-        let (unmarked_text, mut selection_ranges) =
-            marked_text_ranges_by(&text_with_ranges, vec![('[', '}'), ('{', ']')]);
-        let editor_text = self.editor_text();
-        assert_eq!(
-            editor_text, unmarked_text,
-            "Unmarked text doesn't match editor text"
-        );
-
-        let expected_reverse_selections = selection_ranges.remove(&('{', ']')).unwrap_or_default();
-        let expected_forward_selections = selection_ranges.remove(&('[', '}')).unwrap_or_default();
-
-        self.assert_selections(
-            expected_empty_selections,
-            expected_reverse_selections,
-            expected_forward_selections,
-            Some(text.to_string()),
-        )
-    }
-
-    pub fn assert_editor_selections(&mut self, expected_selections: Vec<Selection<usize>>) {
-        let (expected_empty_selections, expected_non_empty_selections): (Vec<_>, Vec<_>) =
-            expected_selections.into_iter().partition_map(|selection| {
-                if selection.is_empty() {
-                    Either::Left(selection.head())
-                } else {
-                    Either::Right(selection)
-                }
-            });
-
-        let (expected_reverse_selections, expected_forward_selections): (Vec<_>, Vec<_>) =
-            expected_non_empty_selections
-                .into_iter()
-                .partition_map(|selection| {
-                    let range = selection.start..selection.end;
-                    if selection.reversed {
-                        Either::Left(range)
-                    } else {
-                        Either::Right(range)
-                    }
-                });
-
-        self.assert_selections(
-            expected_empty_selections,
-            expected_reverse_selections,
-            expected_forward_selections,
-            None,
-        )
-    }
-
-    fn assert_selections(
-        &mut self,
-        expected_empty_selections: Vec<usize>,
-        expected_reverse_selections: Vec<Range<usize>>,
-        expected_forward_selections: Vec<Range<usize>>,
-        asserted_text: Option<String>,
-    ) {
-        let (empty_selections, reverse_selections, forward_selections) =
-            self.editor.read_with(self.cx, |editor, cx| {
-                let (empty_selections, non_empty_selections): (Vec<_>, Vec<_>) = editor
-                    .selections
-                    .all::<usize>(cx)
-                    .into_iter()
-                    .partition_map(|selection| {
-                        if selection.is_empty() {
-                            Either::Left(selection.head())
-                        } else {
-                            Either::Right(selection)
-                        }
-                    });
-
-                let (reverse_selections, forward_selections): (Vec<_>, Vec<_>) =
-                    non_empty_selections.into_iter().partition_map(|selection| {
-                        let range = selection.start..selection.end;
-                        if selection.reversed {
-                            Either::Left(range)
-                        } else {
-                            Either::Right(range)
-                        }
-                    });
-                (empty_selections, reverse_selections, forward_selections)
-            });
-
-        let asserted_selections = asserted_text.unwrap_or_else(|| {
-            self.insert_markers(
-                &expected_empty_selections,
-                &expected_reverse_selections,
-                &expected_forward_selections,
-            )
+        self.cx.update(|cx| {
+            Vim::update(cx, |vim, cx| {
+                vim.switch_mode(mode, cx);
+            })
         });
-        let actual_selections =
-            self.insert_markers(&empty_selections, &reverse_selections, &forward_selections);
-
-        let unmarked_text = self.editor_text();
-        let all_eq: Result<(), SetEqError<String>> =
-            set_eq!(expected_empty_selections, empty_selections)
-                .map_err(|err| {
-                    err.map(|missing| {
-                        let mut error_text = unmarked_text.clone();
-                        error_text.insert(missing, '|');
-                        error_text
-                    })
-                })
-                .and_then(|_| {
-                    set_eq!(expected_reverse_selections, reverse_selections).map_err(|err| {
-                        err.map(|missing| {
-                            let mut error_text = unmarked_text.clone();
-                            error_text.insert(missing.start, '{');
-                            error_text.insert(missing.end, ']');
-                            error_text
-                        })
-                    })
-                })
-                .and_then(|_| {
-                    set_eq!(expected_forward_selections, forward_selections).map_err(|err| {
-                        err.map(|missing| {
-                            let mut error_text = unmarked_text.clone();
-                            error_text.insert(missing.start, '[');
-                            error_text.insert(missing.end, '}');
-                            error_text
-                        })
-                    })
-                });
-
-        match all_eq {
-            Err(SetEqError::LeftMissing(location_text)) => {
-                panic!(
-                    indoc! {"
-                        Editor has extra selection
-                        Extra Selection Location: {}
-                        Asserted selections: {}
-                        Actual selections: {}"},
-                    location_text, asserted_selections, actual_selections,
-                );
-            }
-            Err(SetEqError::RightMissing(location_text)) => {
-                panic!(
-                    indoc! {"
-                        Editor is missing empty selection
-                        Missing Selection Location: {}
-                        Asserted selections: {}
-                        Actual selections: {}"},
-                    location_text, asserted_selections, actual_selections,
-                );
-            }
-            _ => {}
-        }
-    }
-
-    fn insert_markers(
-        &mut self,
-        empty_selections: &Vec<usize>,
-        reverse_selections: &Vec<Range<usize>>,
-        forward_selections: &Vec<Range<usize>>,
-    ) -> String {
-        let mut editor_text_with_selections = self.editor_text();
-        let mut selection_marks = BTreeMap::new();
-        for offset in empty_selections {
-            selection_marks.insert(offset, '|');
-        }
-        for range in reverse_selections {
-            selection_marks.insert(&range.start, '{');
-            selection_marks.insert(&range.end, ']');
-        }
-        for range in forward_selections {
-            selection_marks.insert(&range.start, '[');
-            selection_marks.insert(&range.end, '}');
-        }
-        for (offset, mark) in selection_marks.into_iter().rev() {
-            editor_text_with_selections.insert(*offset, mark);
-        }
-
-        editor_text_with_selections
+        self.cx.set_state(text);
     }
 
     pub fn assert_binding<const COUNT: usize>(
@@ -324,8 +110,8 @@ impl<'a> VimTestContext<'a> {
         mode_after: Mode,
     ) {
         self.set_state(initial_state, initial_mode);
-        self.simulate_keystrokes(keystrokes);
-        self.assert_editor_state(state_after);
+        self.cx.simulate_keystrokes(keystrokes);
+        self.cx.assert_editor_state(state_after);
         assert_eq!(self.mode(), mode_after);
         assert_eq!(self.active_operator(), None);
     }
@@ -337,13 +123,27 @@ impl<'a> VimTestContext<'a> {
         let mode = self.mode();
         VimBindingTestContext::new(keystrokes, mode, mode, self)
     }
+
+    pub fn assert_clipboard_content(&mut self, expected_content: Option<&str>) {
+        self.cx.update(|cx| {
+            let actual_content = cx.read_from_clipboard().map(|item| item.text().to_owned());
+            let expected_content = expected_content.map(|content| content.to_owned());
+            assert_eq!(actual_content, expected_content);
+        })
+    }
 }
 
 impl<'a> Deref for VimTestContext<'a> {
-    type Target = gpui::TestAppContext;
+    type Target = EditorTestContext<'a>;
 
     fn deref(&self) -> &Self::Target {
-        self.cx
+        &self.cx
+    }
+}
+
+impl<'a> DerefMut for VimTestContext<'a> {
+    fn deref_mut(&mut self) -> &mut Self::Target {
+        &mut self.cx
     }
 }
 
@@ -404,3 +204,9 @@ impl<'a, const COUNT: usize> Deref for VimBindingTestContext<'a, COUNT> {
         &self.cx
     }
 }
+
+impl<'a, const COUNT: usize> DerefMut for VimBindingTestContext<'a, COUNT> {
+    fn deref_mut(&mut self) -> &mut Self::Target {
+        &mut self.cx
+    }
+}

crates/vim/src/visual.rs 🔗

@@ -1,14 +1,17 @@
-use editor::{Autoscroll, Bias};
+use collections::HashMap;
+use editor::{display_map::ToDisplayPoint, Autoscroll, Bias};
 use gpui::{actions, MutableAppContext, ViewContext};
+use language::SelectionGoal;
 use workspace::Workspace;
 
-use crate::{motion::Motion, state::Mode, Vim};
+use crate::{motion::Motion, state::Mode, utils::copy_selections_content, Vim};
 
-actions!(vim, [VisualDelete, VisualChange]);
+actions!(vim, [VisualDelete, VisualChange, VisualYank]);
 
 pub fn init(cx: &mut MutableAppContext) {
     cx.add_action(change);
     cx.add_action(delete);
+    cx.add_action(yank);
 }
 
 pub fn visual_motion(motion: Motion, cx: &mut MutableAppContext) {
@@ -17,7 +20,6 @@ pub fn visual_motion(motion: Motion, cx: &mut MutableAppContext) {
             editor.change_selections(Some(Autoscroll::Fit), cx, |s| {
                 s.move_with(|map, selection| {
                     let (new_head, goal) = motion.move_point(map, selection.head(), selection.goal);
-                    let new_head = map.clip_at_line_end(new_head);
                     let was_reversed = selection.reversed;
                     selection.set_head(new_head, goal);
 
@@ -30,7 +32,7 @@ pub fn visual_motion(motion: Motion, cx: &mut MutableAppContext) {
                         // Head was at the end of the selection, and now is at the start. We need to move the end
                         // forward by one if possible in order to compensate for this change.
                         *selection.end.column_mut() = selection.end.column() + 1;
-                        selection.end = map.clip_point(selection.end, Bias::Left);
+                        selection.end = map.clip_point(selection.end, Bias::Right);
                     }
                 });
             });
@@ -42,17 +44,47 @@ pub fn change(_: &mut Workspace, _: &VisualChange, cx: &mut ViewContext<Workspac
     Vim::update(cx, |vim, cx| {
         vim.update_active_editor(cx, |editor, cx| {
             editor.set_clip_at_line_ends(false, cx);
-            editor.change_selections(Some(Autoscroll::Fit), cx, |s| {
+            // Compute edits and resulting anchor selections. If in line mode, adjust
+            // the anchor location and additional newline
+            let mut edits = Vec::new();
+            let mut new_selections = Vec::new();
+            let line_mode = editor.selections.line_mode;
+            editor.change_selections(None, cx, |s| {
                 s.move_with(|map, selection| {
                     if !selection.reversed {
-                        // Head was at the end of the selection, and now is at the start. We need to move the end
-                        // forward by one if possible in order to compensate for this change.
+                        // Head is at the end of the selection. Adjust the end position to
+                        // to include the character under the cursor.
                         *selection.end.column_mut() = selection.end.column() + 1;
-                        selection.end = map.clip_point(selection.end, Bias::Left);
+                        selection.end = map.clip_point(selection.end, Bias::Right);
+                    }
+
+                    if line_mode {
+                        let range = selection.map(|p| p.to_point(map)).range();
+                        let expanded_range = map.expand_to_line(range);
+                        // If we are at the last line, the anchor needs to be after the newline so that
+                        // it is on a line of its own. Otherwise, the anchor may be after the newline
+                        let anchor = if expanded_range.end == map.buffer_snapshot.max_point() {
+                            map.buffer_snapshot.anchor_after(expanded_range.end)
+                        } else {
+                            map.buffer_snapshot.anchor_before(expanded_range.start)
+                        };
+
+                        edits.push((expanded_range, "\n"));
+                        new_selections.push(selection.map(|_| anchor.clone()));
+                    } else {
+                        let range = selection.map(|p| p.to_point(map)).range();
+                        let anchor = map.buffer_snapshot.anchor_after(range.end);
+                        edits.push((range, ""));
+                        new_selections.push(selection.map(|_| anchor.clone()));
                     }
+                    selection.goal = SelectionGoal::None;
                 });
             });
-            editor.insert("", cx);
+            copy_selections_content(editor, editor.selections.line_mode, cx);
+            editor.edit_with_autoindent(edits, cx);
+            editor.change_selections(Some(Autoscroll::Fit), cx, |s| {
+                s.select_anchors(new_selections);
+            });
         });
         vim.switch_mode(Mode::Insert, cx);
     });
@@ -60,31 +92,70 @@ pub fn change(_: &mut Workspace, _: &VisualChange, cx: &mut ViewContext<Workspac
 
 pub fn delete(_: &mut Workspace, _: &VisualDelete, cx: &mut ViewContext<Workspace>) {
     Vim::update(cx, |vim, cx| {
-        vim.switch_mode(Mode::Normal, cx);
         vim.update_active_editor(cx, |editor, cx| {
             editor.set_clip_at_line_ends(false, cx);
+            let mut original_columns: HashMap<_, _> = Default::default();
+            let line_mode = editor.selections.line_mode;
             editor.change_selections(Some(Autoscroll::Fit), cx, |s| {
                 s.move_with(|map, selection| {
-                    if !selection.reversed {
-                        // Head was at the end of the selection, and now is at the start. We need to move the end
-                        // forward by one if possible in order to compensate for this change.
+                    if line_mode {
+                        original_columns
+                            .insert(selection.id, selection.head().to_point(&map).column);
+                    } else if !selection.reversed {
+                        // Head is at the end of the selection. Adjust the end position to
+                        // to include the character under the cursor.
                         *selection.end.column_mut() = selection.end.column() + 1;
-                        selection.end = map.clip_point(selection.end, Bias::Left);
+                        selection.end = map.clip_point(selection.end, Bias::Right);
                     }
+                    selection.goal = SelectionGoal::None;
                 });
             });
+            copy_selections_content(editor, line_mode, cx);
             editor.insert("", cx);
 
             // Fixup cursor position after the deletion
             editor.set_clip_at_line_ends(true, cx);
             editor.change_selections(Some(Autoscroll::Fit), cx, |s| {
                 s.move_with(|map, selection| {
-                    let mut cursor = selection.head();
-                    cursor = map.clip_point(cursor, Bias::Left);
+                    let mut cursor = selection.head().to_point(map);
+
+                    if let Some(column) = original_columns.get(&selection.id) {
+                        cursor.column = *column
+                    }
+                    let cursor = map.clip_point(cursor.to_display_point(map), Bias::Left);
                     selection.collapse_to(cursor, selection.goal)
                 });
             });
         });
+        vim.switch_mode(Mode::Normal, cx);
+    });
+}
+
+pub fn yank(_: &mut Workspace, _: &VisualYank, cx: &mut ViewContext<Workspace>) {
+    Vim::update(cx, |vim, cx| {
+        vim.update_active_editor(cx, |editor, cx| {
+            editor.set_clip_at_line_ends(false, cx);
+            let line_mode = editor.selections.line_mode;
+            if !editor.selections.line_mode {
+                editor.change_selections(None, cx, |s| {
+                    s.move_with(|map, selection| {
+                        if !selection.reversed {
+                            // Head is at the end of the selection. Adjust the end position to
+                            // to include the character under the cursor.
+                            *selection.end.column_mut() = selection.end.column() + 1;
+                            selection.end = map.clip_point(selection.end, Bias::Right);
+                        }
+                    });
+                });
+            }
+            copy_selections_content(editor, line_mode, cx);
+            editor.change_selections(None, cx, |s| {
+                s.move_with(|_, selection| {
+                    selection.collapse_to(selection.start, SelectionGoal::None)
+                });
+            });
+        });
+        vim.switch_mode(Mode::Normal, cx);
     });
 }
 
@@ -97,7 +168,9 @@ mod test {
     #[gpui::test]
     async fn test_enter_visual_mode(cx: &mut gpui::TestAppContext) {
         let cx = VimTestContext::new(cx, true).await;
-        let mut cx = cx.binding(["v", "w", "j"]).mode_after(Mode::Visual);
+        let mut cx = cx
+            .binding(["v", "w", "j"])
+            .mode_after(Mode::Visual { line: false });
         cx.assert(
             indoc! {"
                 The |quick brown
@@ -128,7 +201,9 @@ mod test {
                 fox jumps [over
                 }the lazy dog"},
         );
-        let mut cx = cx.binding(["v", "b", "k"]).mode_after(Mode::Visual);
+        let mut cx = cx
+            .binding(["v", "b", "k"])
+            .mode_after(Mode::Visual { line: false });
         cx.assert(
             indoc! {"
                 The |quick brown
@@ -176,6 +251,13 @@ mod test {
                 The |ver
                 the lazy dog"},
         );
+        // Test pasting code copied on delete
+        cx.simulate_keystrokes(["j", "p"]);
+        cx.assert_editor_state(indoc! {"
+            The ver
+            the l|quick brown
+            fox jumps oazy dog"});
+
         cx.assert(
             indoc! {"
                 The quick brown
@@ -226,6 +308,77 @@ mod test {
         );
     }
 
+    #[gpui::test]
+    async fn test_visual_line_delete(cx: &mut gpui::TestAppContext) {
+        let cx = VimTestContext::new(cx, true).await;
+        let mut cx = cx.binding(["shift-V", "x"]);
+        cx.assert(
+            indoc! {"
+                The qu|ick brown
+                fox jumps over
+                the lazy dog"},
+            indoc! {"
+                fox ju|mps over
+                the lazy dog"},
+        );
+        // Test pasting code copied on delete
+        cx.simulate_keystroke("p");
+        cx.assert_editor_state(indoc! {"
+            fox jumps over
+            |The quick brown
+            the lazy dog"});
+
+        cx.assert(
+            indoc! {"
+                The quick brown
+                fox ju|mps over
+                the lazy dog"},
+            indoc! {"
+                The quick brown
+                the la|zy dog"},
+        );
+        cx.assert(
+            indoc! {"
+                The quick brown
+                fox jumps over
+                the la|zy dog"},
+            indoc! {"
+                The quick brown
+                fox ju|mps over"},
+        );
+        let mut cx = cx.binding(["shift-V", "j", "x"]);
+        cx.assert(
+            indoc! {"
+                The qu|ick brown
+                fox jumps over
+                the lazy dog"},
+            "the la|zy dog",
+        );
+        // Test pasting code copied on delete
+        cx.simulate_keystroke("p");
+        cx.assert_editor_state(indoc! {"
+            the lazy dog
+            |The quick brown
+            fox jumps over"});
+
+        cx.assert(
+            indoc! {"
+                The quick brown
+                fox ju|mps over
+                the lazy dog"},
+            "The qu|ick brown",
+        );
+        cx.assert(
+            indoc! {"
+                The quick brown
+                fox jumps over
+                the la|zy dog"},
+            indoc! {"
+                The quick brown
+                fox ju|mps over"},
+        );
+    }
+
     #[gpui::test]
     async fn test_visual_change(cx: &mut gpui::TestAppContext) {
         let cx = VimTestContext::new(cx, true).await;
@@ -290,4 +443,168 @@ mod test {
                 the lazy dog"},
         );
     }
+
+    #[gpui::test]
+    async fn test_visual_line_change(cx: &mut gpui::TestAppContext) {
+        let cx = VimTestContext::new(cx, true).await;
+        let mut cx = cx.binding(["shift-V", "c"]).mode_after(Mode::Insert);
+        cx.assert(
+            indoc! {"
+                The qu|ick brown
+                fox jumps over
+                the lazy dog"},
+            indoc! {"
+                |
+                fox jumps over
+                the lazy dog"},
+        );
+        // Test pasting code copied on change
+        cx.simulate_keystrokes(["escape", "j", "p"]);
+        cx.assert_editor_state(indoc! {"
+            
+            fox jumps over
+            |The quick brown
+            the lazy dog"});
+
+        cx.assert(
+            indoc! {"
+                The quick brown
+                fox ju|mps over
+                the lazy dog"},
+            indoc! {"
+                The quick brown
+                |
+                the lazy dog"},
+        );
+        cx.assert(
+            indoc! {"
+                The quick brown
+                fox jumps over
+                the la|zy dog"},
+            indoc! {"
+                The quick brown
+                fox jumps over
+                |"},
+        );
+        let mut cx = cx.binding(["shift-V", "j", "c"]).mode_after(Mode::Insert);
+        cx.assert(
+            indoc! {"
+                The qu|ick brown
+                fox jumps over
+                the lazy dog"},
+            indoc! {"
+                |
+                the lazy dog"},
+        );
+        // Test pasting code copied on delete
+        cx.simulate_keystrokes(["escape", "j", "p"]);
+        cx.assert_editor_state(indoc! {"
+            
+            the lazy dog
+            |The quick brown
+            fox jumps over"});
+        cx.assert(
+            indoc! {"
+                The quick brown
+                fox ju|mps over
+                the lazy dog"},
+            indoc! {"
+                The quick brown
+                |"},
+        );
+        cx.assert(
+            indoc! {"
+                The quick brown
+                fox jumps over
+                the la|zy dog"},
+            indoc! {"
+                The quick brown
+                fox jumps over
+                |"},
+        );
+    }
+
+    #[gpui::test]
+    async fn test_visual_yank(cx: &mut gpui::TestAppContext) {
+        let cx = VimTestContext::new(cx, true).await;
+        let mut cx = cx.binding(["v", "w", "y"]);
+        cx.assert("The quick |brown", "The quick |brown");
+        cx.assert_clipboard_content(Some("brown"));
+        let mut cx = cx.binding(["v", "w", "j", "y"]);
+        cx.assert(
+            indoc! {"
+                The |quick brown
+                fox jumps over
+                the lazy dog"},
+            indoc! {"
+                The |quick brown
+                fox jumps over
+                the lazy dog"},
+        );
+        cx.assert_clipboard_content(Some(indoc! {"
+            quick brown
+            fox jumps o"}));
+        cx.assert(
+            indoc! {"
+                The quick brown
+                fox jumps over
+                the |lazy dog"},
+            indoc! {"
+                The quick brown
+                fox jumps over
+                the |lazy dog"},
+        );
+        cx.assert_clipboard_content(Some("lazy d"));
+        cx.assert(
+            indoc! {"
+                The quick brown
+                fox jumps |over
+                the lazy dog"},
+            indoc! {"
+                The quick brown
+                fox jumps |over
+                the lazy dog"},
+        );
+        cx.assert_clipboard_content(Some(indoc! {"
+                over
+                t"}));
+        let mut cx = cx.binding(["v", "b", "k", "y"]);
+        cx.assert(
+            indoc! {"
+                The |quick brown
+                fox jumps over
+                the lazy dog"},
+            indoc! {"
+                |The quick brown
+                fox jumps over
+                the lazy dog"},
+        );
+        cx.assert_clipboard_content(Some("The q"));
+        cx.assert(
+            indoc! {"
+                The quick brown
+                fox jumps over
+                the |lazy dog"},
+            indoc! {"
+                The quick brown
+                |fox jumps over
+                the lazy dog"},
+        );
+        cx.assert_clipboard_content(Some(indoc! {"
+            fox jumps over
+            the l"}));
+        cx.assert(
+            indoc! {"
+                The quick brown
+                fox jumps |over
+                the lazy dog"},
+            indoc! {"
+                The |quick brown
+                fox jumps over
+                the lazy dog"},
+        );
+        cx.assert_clipboard_content(Some(indoc! {"
+            quick brown
+            fox jumps o"}));
+    }
 }

crates/workspace/src/pane.rs 🔗

@@ -15,7 +15,7 @@ use gpui::{
 use project::{Project, ProjectEntryId, ProjectPath};
 use serde::Deserialize;
 use settings::Settings;
-use std::{any::Any, cell::RefCell, cmp, mem, path::Path, rc::Rc};
+use std::{any::Any, cell::RefCell, mem, path::Path, rc::Rc};
 use util::ResultExt;
 
 actions!(
@@ -109,6 +109,7 @@ pub enum Event {
     ActivateItem { local: bool },
     Remove,
     Split(SplitDirection),
+    ChangeItemTitle,
 }
 
 pub struct Pane {
@@ -334,9 +335,20 @@ impl Pane {
         item.set_nav_history(pane.read(cx).nav_history.clone(), cx);
         item.added_to_pane(workspace, pane.clone(), cx);
         pane.update(cx, |pane, cx| {
-            let item_idx = cmp::min(pane.active_item_index + 1, pane.items.len());
-            pane.items.insert(item_idx, item);
-            pane.activate_item(item_idx, activate_pane, focus_item, cx);
+            // If there is already an active item, then insert the new item
+            // right after it. Otherwise, adjust the `active_item_index` field
+            // before activating the new item, so that in the `activate_item`
+            // method, we can detect that the active item is changing.
+            let item_ix;
+            if pane.active_item_index < pane.items.len() {
+                item_ix = pane.active_item_index + 1
+            } else {
+                item_ix = pane.items.len();
+                pane.active_item_index = usize::MAX;
+            };
+
+            pane.items.insert(item_ix, item);
+            pane.activate_item(item_ix, activate_pane, focus_item, cx);
             cx.notify();
         });
     }
@@ -383,11 +395,12 @@ impl Pane {
         use NavigationMode::{GoingBack, GoingForward};
         if index < self.items.len() {
             let prev_active_item_ix = mem::replace(&mut self.active_item_index, index);
-            if matches!(self.nav_history.borrow().mode, GoingBack | GoingForward)
-                || (prev_active_item_ix != self.active_item_index
-                    && prev_active_item_ix < self.items.len())
+            if prev_active_item_ix != self.active_item_index
+                || matches!(self.nav_history.borrow().mode, GoingBack | GoingForward)
             {
-                self.items[prev_active_item_ix].deactivated(cx);
+                if let Some(prev_item) = self.items.get(prev_active_item_ix) {
+                    prev_item.deactivated(cx);
+                }
                 cx.emit(Event::ActivateItem {
                     local: activate_pane,
                 });
@@ -424,7 +437,7 @@ impl Pane {
         self.activate_item(index, true, true, cx);
     }
 
-    fn close_active_item(
+    pub fn close_active_item(
         workspace: &mut Workspace,
         _: &CloseActiveItem,
         cx: &mut ViewContext<Workspace>,

crates/workspace/src/workspace.rs 🔗

@@ -37,6 +37,7 @@ use status_bar::StatusBar;
 pub use status_bar::StatusItemView;
 use std::{
     any::{Any, TypeId},
+    borrow::Cow,
     cell::RefCell,
     fmt,
     future::Future,
@@ -543,7 +544,10 @@ impl<T: Item> ItemHandle for ViewHandle<T> {
             }
 
             if T::should_update_tab_on_event(event) {
-                pane.update(cx, |_, cx| cx.notify());
+                pane.update(cx, |_, cx| {
+                    cx.emit(pane::Event::ChangeItemTitle);
+                    cx.notify();
+                });
             }
         })
         .detach();
@@ -755,6 +759,9 @@ impl Workspace {
                 project::Event::CollaboratorLeft(peer_id) => {
                     this.collaborator_left(*peer_id, cx);
                 }
+                project::Event::WorktreeRemoved(_) | project::Event::WorktreeAdded => {
+                    this.update_window_title(cx);
+                }
                 _ => {}
             }
             if project.read(cx).is_read_only() {
@@ -766,14 +773,8 @@ impl Workspace {
 
         let pane = cx.add_view(|cx| Pane::new(cx));
         let pane_id = pane.id();
-        cx.observe(&pane, move |me, _, cx| {
-            let active_entry = me.active_project_path(cx);
-            me.project
-                .update(cx, |project, cx| project.set_active_path(active_entry, cx));
-        })
-        .detach();
-        cx.subscribe(&pane, move |me, _, event, cx| {
-            me.handle_pane_event(pane_id, event, cx)
+        cx.subscribe(&pane, move |this, _, event, cx| {
+            this.handle_pane_event(pane_id, event, cx)
         })
         .detach();
         cx.focus(&pane);
@@ -836,6 +837,11 @@ impl Workspace {
             _observe_current_user,
         };
         this.project_remote_id_changed(this.project.read(cx).remote_id(), cx);
+
+        cx.defer(|this, cx| {
+            this.update_window_title(cx);
+        });
+
         this
     }
 
@@ -1258,14 +1264,8 @@ impl Workspace {
     fn add_pane(&mut self, cx: &mut ViewContext<Self>) -> ViewHandle<Pane> {
         let pane = cx.add_view(|cx| Pane::new(cx));
         let pane_id = pane.id();
-        cx.observe(&pane, move |me, _, cx| {
-            let active_entry = me.active_project_path(cx);
-            me.project
-                .update(cx, |project, cx| project.set_active_path(active_entry, cx));
-        })
-        .detach();
-        cx.subscribe(&pane, move |me, _, event, cx| {
-            me.handle_pane_event(pane_id, event, cx)
+        cx.subscribe(&pane, move |this, _, event, cx| {
+            this.handle_pane_event(pane_id, event, cx)
         })
         .detach();
         self.panes.push(pane.clone());
@@ -1405,6 +1405,7 @@ impl Workspace {
             self.status_bar.update(cx, |status_bar, cx| {
                 status_bar.set_active_pane(&self.active_pane, cx);
             });
+            self.active_item_path_changed(cx);
             cx.focus(&self.active_pane);
             cx.notify();
         }
@@ -1439,6 +1440,14 @@ impl Workspace {
                     if *local {
                         self.unfollow(&pane, cx);
                     }
+                    if pane == self.active_pane {
+                        self.active_item_path_changed(cx);
+                    }
+                }
+                pane::Event::ChangeItemTitle => {
+                    if pane == self.active_pane {
+                        self.active_item_path_changed(cx);
+                    }
                 }
             }
         } else {
@@ -1471,6 +1480,8 @@ impl Workspace {
             self.unfollow(&pane, cx);
             self.last_leaders_by_pane.remove(&pane.downgrade());
             cx.notify();
+        } else {
+            self.active_item_path_changed(cx);
         }
     }
 
@@ -1658,15 +1669,7 @@ impl Workspace {
 
     fn render_titlebar(&self, theme: &Theme, cx: &mut RenderContext<Self>) -> ElementBox {
         let mut worktree_root_names = String::new();
-        {
-            let mut worktrees = self.project.read(cx).visible_worktrees(cx).peekable();
-            while let Some(worktree) = worktrees.next() {
-                worktree_root_names.push_str(worktree.read(cx).root_name());
-                if worktrees.peek().is_some() {
-                    worktree_root_names.push_str(", ");
-                }
-            }
-        }
+        self.worktree_root_names(&mut worktree_root_names, cx);
 
         ConstrainedBox::new(
             Container::new(
@@ -1702,6 +1705,50 @@ impl Workspace {
         .named("titlebar")
     }
 
+    fn active_item_path_changed(&mut self, cx: &mut ViewContext<Self>) {
+        let active_entry = self.active_project_path(cx);
+        self.project
+            .update(cx, |project, cx| project.set_active_path(active_entry, cx));
+        self.update_window_title(cx);
+    }
+
+    fn update_window_title(&mut self, cx: &mut ViewContext<Self>) {
+        let mut title = String::new();
+        if let Some(path) = self.active_item(cx).and_then(|item| item.project_path(cx)) {
+            let filename = path
+                .path
+                .file_name()
+                .map(|s| s.to_string_lossy())
+                .or_else(|| {
+                    Some(Cow::Borrowed(
+                        self.project()
+                            .read(cx)
+                            .worktree_for_id(path.worktree_id, cx)?
+                            .read(cx)
+                            .root_name(),
+                    ))
+                });
+            if let Some(filename) = filename {
+                title.push_str(filename.as_ref());
+                title.push_str(" — ");
+            }
+        }
+        self.worktree_root_names(&mut title, cx);
+        if title.is_empty() {
+            title = "empty project".to_string();
+        }
+        cx.set_window_title(&title);
+    }
+
+    fn worktree_root_names(&self, string: &mut String, cx: &mut MutableAppContext) {
+        for (i, worktree) in self.project.read(cx).visible_worktrees(cx).enumerate() {
+            if i != 0 {
+                string.push_str(", ");
+            }
+            string.push_str(worktree.read(cx).root_name());
+        }
+    }
+
     fn render_collaborators(&self, theme: &Theme, cx: &mut RenderContext<Self>) -> Vec<ElementBox> {
         let mut collaborators = self
             .project
@@ -2437,6 +2484,110 @@ mod tests {
     use project::{FakeFs, Project, ProjectEntryId};
     use serde_json::json;
 
+    #[gpui::test]
+    async fn test_tracking_active_path(cx: &mut TestAppContext) {
+        cx.foreground().forbid_parking();
+        Settings::test_async(cx);
+        let fs = FakeFs::new(cx.background());
+        fs.insert_tree(
+            "/root1",
+            json!({
+                "one.txt": "",
+                "two.txt": "",
+            }),
+        )
+        .await;
+        fs.insert_tree(
+            "/root2",
+            json!({
+                "three.txt": "",
+            }),
+        )
+        .await;
+
+        let project = Project::test(fs, ["root1".as_ref()], cx).await;
+        let (window_id, workspace) = cx.add_window(|cx| Workspace::new(project.clone(), cx));
+        let worktree_id = project.read_with(cx, |project, cx| {
+            project.worktrees(cx).next().unwrap().read(cx).id()
+        });
+
+        let item1 = cx.add_view(window_id, |_| {
+            let mut item = TestItem::new();
+            item.project_path = Some((worktree_id, "one.txt").into());
+            item
+        });
+        let item2 = cx.add_view(window_id, |_| {
+            let mut item = TestItem::new();
+            item.project_path = Some((worktree_id, "two.txt").into());
+            item
+        });
+
+        // Add an item to an empty pane
+        workspace.update(cx, |workspace, cx| workspace.add_item(Box::new(item1), cx));
+        project.read_with(cx, |project, cx| {
+            assert_eq!(
+                project.active_entry(),
+                project.entry_for_path(&(worktree_id, "one.txt").into(), cx)
+            );
+        });
+        assert_eq!(
+            cx.current_window_title(window_id).as_deref(),
+            Some("one.txt — root1")
+        );
+
+        // Add a second item to a non-empty pane
+        workspace.update(cx, |workspace, cx| workspace.add_item(Box::new(item2), cx));
+        assert_eq!(
+            cx.current_window_title(window_id).as_deref(),
+            Some("two.txt — root1")
+        );
+        project.read_with(cx, |project, cx| {
+            assert_eq!(
+                project.active_entry(),
+                project.entry_for_path(&(worktree_id, "two.txt").into(), cx)
+            );
+        });
+
+        // Close the active item
+        workspace
+            .update(cx, |workspace, cx| {
+                Pane::close_active_item(workspace, &Default::default(), cx).unwrap()
+            })
+            .await
+            .unwrap();
+        assert_eq!(
+            cx.current_window_title(window_id).as_deref(),
+            Some("one.txt — root1")
+        );
+        project.read_with(cx, |project, cx| {
+            assert_eq!(
+                project.active_entry(),
+                project.entry_for_path(&(worktree_id, "one.txt").into(), cx)
+            );
+        });
+
+        // Add a project folder
+        project
+            .update(cx, |project, cx| {
+                project.find_or_create_local_worktree("/root2", true, cx)
+            })
+            .await
+            .unwrap();
+        assert_eq!(
+            cx.current_window_title(window_id).as_deref(),
+            Some("one.txt — root1, root2")
+        );
+
+        // Remove a project folder
+        project.update(cx, |project, cx| {
+            project.remove_worktree(worktree_id, cx);
+        });
+        assert_eq!(
+            cx.current_window_title(window_id).as_deref(),
+            Some("one.txt — root2")
+        );
+    }
+
     #[gpui::test]
     async fn test_close_window(cx: &mut TestAppContext) {
         cx.foreground().forbid_parking();
@@ -2476,18 +2627,6 @@ mod tests {
         cx.foreground().run_until_parked();
         assert!(!cx.has_pending_prompt(window_id));
         assert_eq!(task.await.unwrap(), false);
-
-        // If there are multiple dirty items representing the same project entry.
-        workspace.update(cx, |w, cx| {
-            w.add_item(Box::new(item2.clone()), cx);
-            w.add_item(Box::new(item3.clone()), cx);
-        });
-        let task = workspace.update(cx, |w, cx| w.prepare_to_close(cx));
-        cx.foreground().run_until_parked();
-        cx.simulate_prompt_answer(window_id, 2 /* cancel */);
-        cx.foreground().run_until_parked();
-        assert!(!cx.has_pending_prompt(window_id));
-        assert_eq!(task.await.unwrap(), false);
     }
 
     #[gpui::test]
@@ -2687,6 +2826,7 @@ mod tests {
         is_dirty: bool,
         has_conflict: bool,
         project_entry_ids: Vec<ProjectEntryId>,
+        project_path: Option<ProjectPath>,
         is_singleton: bool,
     }
 
@@ -2699,6 +2839,7 @@ mod tests {
                 is_dirty: false,
                 has_conflict: false,
                 project_entry_ids: Vec::new(),
+                project_path: None,
                 is_singleton: true,
             }
         }
@@ -2724,7 +2865,7 @@ mod tests {
         }
 
         fn project_path(&self, _: &AppContext) -> Option<ProjectPath> {
-            None
+            self.project_path.clone()
         }
 
         fn project_entry_ids(&self, _: &AppContext) -> SmallVec<[ProjectEntryId; 3]> {
@@ -2783,5 +2924,9 @@ mod tests {
             self.reload_count += 1;
             Task::ready(Ok(()))
         }
+
+        fn should_update_tab_on_event(_: &Self::Event) -> bool {
+            true
+        }
     }
 }

crates/zed/src/main.rs 🔗

@@ -180,8 +180,8 @@ fn main() {
 
         cx.observe_global::<Settings, _>({
             let languages = languages.clone();
-            move |settings, _| {
-                languages.set_theme(&settings.theme.editor.syntax);
+            move |cx| {
+                languages.set_theme(&cx.global::<Settings>().theme.editor.syntax);
             }
         })
         .detach();

crates/zed/src/menus.rs 🔗

@@ -15,6 +15,14 @@ pub fn menus() -> Vec<Menu<'static>> {
                     action: Box::new(auto_update::Check),
                 },
                 MenuItem::Separator,
+                MenuItem::Action {
+                    name: "Open Settings",
+                    action: Box::new(super::OpenSettings),
+                },
+                MenuItem::Action {
+                    name: "Open Key Bindings",
+                    action: Box::new(super::OpenKeymap),
+                },
                 MenuItem::Action {
                     name: "Install CLI",
                     action: Box::new(super::InstallCommandLineInterface),
@@ -164,6 +172,10 @@ pub fn menus() -> Vec<Menu<'static>> {
                     name: "Zoom Out",
                     action: Box::new(super::DecreaseBufferFontSize),
                 },
+                MenuItem::Action {
+                    name: "Reset Zoom",
+                    action: Box::new(super::ResetBufferFontSize),
+                },
                 MenuItem::Separator,
                 MenuItem::Action {
                     name: "Project Browser",
@@ -229,12 +241,31 @@ pub fn menus() -> Vec<Menu<'static>> {
                 },
             ],
         },
+        Menu {
+            name: "Window",
+            items: vec![MenuItem::Separator],
+        },
         Menu {
             name: "Help",
-            items: vec![MenuItem::Action {
-                name: "Command Palette",
-                action: Box::new(command_palette::Toggle),
-            }],
+            items: vec![
+                MenuItem::Action {
+                    name: "Command Palette",
+                    action: Box::new(command_palette::Toggle),
+                },
+                MenuItem::Separator,
+                MenuItem::Action {
+                    name: "Zed.dev",
+                    action: Box::new(crate::OpenBrowser {
+                        url: "https://zed.dev".into(),
+                    }),
+                },
+                MenuItem::Action {
+                    name: "Zed Twitter",
+                    action: Box::new(crate::OpenBrowser {
+                        url: "https://twitter.com/zeddotdev".into(),
+                    }),
+                },
+            ],
         },
     ]
 }

crates/zed/src/zed.rs 🔗

@@ -14,6 +14,7 @@ use editor::Editor;
 use gpui::{
     actions,
     geometry::vector::vec2f,
+    impl_actions,
     platform::{WindowBounds, WindowOptions},
     AsyncAppContext, ViewContext,
 };
@@ -23,6 +24,7 @@ use project::Project;
 pub use project::{self, fs};
 use project_panel::ProjectPanel;
 use search::{BufferSearchBar, ProjectSearchBar};
+use serde::Deserialize;
 use serde_json::to_string_pretty;
 use settings::{keymap_file_json_schema, settings_file_json_schema, Settings};
 use std::{
@@ -33,6 +35,13 @@ use util::ResultExt;
 pub use workspace;
 use workspace::{AppState, Workspace};
 
+#[derive(Deserialize, Clone)]
+struct OpenBrowser {
+    url: Arc<str>,
+}
+
+impl_actions!(zed, [OpenBrowser]);
+
 actions!(
     zed,
     [
@@ -43,6 +52,7 @@ actions!(
         OpenKeymap,
         IncreaseBufferFontSize,
         DecreaseBufferFontSize,
+        ResetBufferFontSize,
         InstallCommandLineInterface,
     ]
 );
@@ -60,6 +70,7 @@ lazy_static! {
 pub fn init(app_state: &Arc<AppState>, cx: &mut gpui::MutableAppContext) {
     cx.add_action(about);
     cx.add_global_action(quit);
+    cx.add_global_action(move |action: &OpenBrowser, cx| cx.platform().open_url(&action.url));
     cx.add_global_action(move |_: &IncreaseBufferFontSize, cx| {
         cx.update_global::<Settings, _, _>(|settings, cx| {
             settings.buffer_font_size = (settings.buffer_font_size + 1.0).max(MIN_FONT_SIZE);
@@ -72,6 +83,12 @@ pub fn init(app_state: &Arc<AppState>, cx: &mut gpui::MutableAppContext) {
             cx.refresh_windows();
         });
     });
+    cx.add_global_action(move |_: &ResetBufferFontSize, cx| {
+        cx.update_global::<Settings, _, _>(|settings, cx| {
+            settings.buffer_font_size = settings.default_buffer_font_size;
+            cx.refresh_windows();
+        });
+    });
     cx.add_global_action(move |_: &InstallCommandLineInterface, cx| {
         cx.spawn(|cx| async move { install_cli(&cx).await.context("error creating CLI symlink") })
             .detach_and_log_err(cx);