Merge branch 'main' into window_context_2

Antonio Scandurra created

Change summary

Cargo.lock                                              |   9 
Cargo.toml                                              |   2 
crates/auto_update/src/auto_update.rs                   |   4 
crates/call/src/room.rs                                 |   6 
crates/client/Cargo.toml                                |   1 
crates/client/src/client.rs                             |  50 
crates/collab/Cargo.toml                                |   2 
crates/collab/src/db.rs                                 |  38 
crates/collab/src/rpc.rs                                |  80 
crates/collab/src/tests.rs                              |  61 
crates/collab/src/tests/integration_tests.rs            |  24 
crates/collab/src/tests/randomized_integration_tests.rs | 919 ++++++++--
crates/collab_ui/src/collab_titlebar_item.rs            |   4 
crates/editor/src/blink_manager.rs                      |   9 
crates/editor/src/editor.rs                             |   9 
crates/editor/src/editor_tests.rs                       | 104 +
crates/fs/src/fs.rs                                     | 305 +-
crates/gpui/src/app.rs                                  |   6 
crates/gpui/src/app/test_app_context.rs                 |   1 
crates/gpui/src/executor.rs                             |  10 
crates/gpui/src/test.rs                                 |   2 
crates/gpui_macros/src/gpui_macros.rs                   |  18 
crates/language/src/buffer.rs                           |  12 
crates/language/src/proto.rs                            |   8 
crates/project/src/lsp_command.rs                       | 467 +++++
crates/project/src/project.rs                           | 728 +++-----
crates/project/src/project_tests.rs                     |   2 
crates/project/src/worktree.rs                          | 710 +++----
crates/rpc/src/peer.rs                                  |  25 
crates/rpc/src/proto.rs                                 |   2 
crates/rpc/src/rpc.rs                                   |   2 
crates/sum_tree/src/tree_map.rs                         |   6 
crates/text/src/text.rs                                 |  74 
crates/util/src/github.rs                               |  16 
crates/workspace/src/item.rs                            |   2 
crates/workspace/src/pane.rs                            | 348 +++
crates/workspace/src/workspace.rs                       |   8 
crates/zed/Cargo.toml                                   |   2 
crates/zed/src/main.rs                                  |   2 
crates/zed/src/zed.rs                                   |  12 
styles/.prettierignore                                  |   3 
styles/package-lock.json                                |  31 
styles/package.json                                     |   1 
styles/src/styleTree/components.ts                      |  10 
styles/src/styleTree/copilot.ts                         | 127 +
styles/src/styleTree/simpleMessageNotification.ts       |   1 
styles/src/styleTree/workspace.ts                       |  39 
styles/src/themes/ayu-dark.ts                           |  17 
styles/src/themes/ayu-light.ts                          |  17 
styles/src/themes/ayu-mirage.ts                         |  17 
styles/src/themes/common/ayu-common.ts                  |  90 +
styles/src/themes/staff/ayu-mirage.ts                   |  31 
styles/src/themes/staff/ayu.ts                          |  52 
53 files changed, 2,944 insertions(+), 1,582 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -1192,7 +1192,7 @@ dependencies = [
 
 [[package]]
 name = "collab"
-version = "0.8.2"
+version = "0.8.3"
 dependencies = [
  "anyhow",
  "async-tungstenite",
@@ -4589,14 +4589,15 @@ checksum = "5da3b0203fd7ee5720aa0b5e790b591aa5d3f41c3ed2c34a3a393382198af2f7"
 
 [[package]]
 name = "postage"
-version = "0.4.1"
+version = "0.5.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a63d25391d04a097954b76aba742b6b5b74f213dfe3dbaeeb36e8ddc1c657f0b"
+checksum = "af3fb618632874fb76937c2361a7f22afd393c982a2165595407edc75b06d3c1"
 dependencies = [
  "atomic",
  "crossbeam-queue",
  "futures 0.3.25",
  "log",
+ "parking_lot 0.12.1",
  "pin-project",
  "pollster",
  "static_assertions",
@@ -8515,7 +8516,7 @@ checksum = "09041cd90cf85f7f8b2df60c646f853b7f535ce68f85244eb6731cf89fa498ec"
 
 [[package]]
 name = "zed"
-version = "0.82.0"
+version = "0.83.0"
 dependencies = [
  "activity_indicator",
  "anyhow",

Cargo.toml 🔗

@@ -75,7 +75,7 @@ serde = { version = "1.0", features = ["derive", "rc"] }
 serde_derive = { version = "1.0", features = ["deserialize_in_place"] }
 serde_json = { version = "1.0", features = ["preserve_order", "raw_value"] }
 rand = { version = "0.8" }
-postage = { version = "0.4.1", features = ["futures-traits"] }
+postage = { version = "0.5", features = ["futures-traits"] }
 
 [patch.crates-io]
 tree-sitter = { git = "https://github.com/tree-sitter/tree-sitter", rev = "c51896d32dcc11a38e41f36e3deb1a6a9c4f4b14" }

crates/auto_update/src/auto_update.rs 🔗

@@ -63,10 +63,10 @@ pub fn init(http_client: Arc<dyn HttpClient>, server_url: String, cx: &mut AppCo
             cx.observe_global::<Settings, _>(move |updater, cx| {
                 if cx.global::<Settings>().auto_update {
                     if update_subscription.is_none() {
-                        *(&mut update_subscription) = Some(updater.start_polling(cx))
+                        update_subscription = Some(updater.start_polling(cx))
                     }
                 } else {
-                    (&mut update_subscription).take();
+                    update_subscription.take();
                 }
             })
             .detach();

crates/call/src/room.rs 🔗

@@ -419,7 +419,7 @@ impl Room {
             false
         });
 
-        let response = self.client.request(proto::RejoinRoom {
+        let response = self.client.request_envelope(proto::RejoinRoom {
             id: self.id,
             reshared_projects,
             rejoined_projects,
@@ -427,6 +427,8 @@ impl Room {
 
         cx.spawn(|this, mut cx| async move {
             let response = response.await?;
+            let message_id = response.message_id;
+            let response = response.payload;
             let room_proto = response.room.ok_or_else(|| anyhow!("invalid room"))?;
             this.update(&mut cx, |this, cx| {
                 this.status = RoomStatus::Online;
@@ -443,7 +445,7 @@ impl Room {
                 for rejoined_project in response.rejoined_projects {
                     if let Some(project) = projects.get(&rejoined_project.id) {
                         project.update(cx, |project, cx| {
-                            project.rejoined(rejoined_project, cx).log_err();
+                            project.rejoined(rejoined_project, message_id, cx).log_err();
                         });
                     }
                 }

crates/client/Cargo.toml 🔗

@@ -45,3 +45,4 @@ collections = { path = "../collections", features = ["test-support"] }
 gpui = { path = "../gpui", features = ["test-support"] }
 rpc = { path = "../rpc", features = ["test-support"] }
 settings = { path = "../settings", features = ["test-support"] }
+util = { path = "../util", features = ["test-support"] }

crates/client/src/client.rs 🔗

@@ -10,7 +10,10 @@ use async_tungstenite::tungstenite::{
     error::Error as WebsocketError,
     http::{Request, StatusCode},
 };
-use futures::{future::LocalBoxFuture, AsyncReadExt, FutureExt, SinkExt, StreamExt, TryStreamExt};
+use futures::{
+    future::LocalBoxFuture, AsyncReadExt, FutureExt, SinkExt, StreamExt, TryFutureExt as _,
+    TryStreamExt,
+};
 use gpui::{
     actions,
     platform::AppVersion,
@@ -471,18 +474,22 @@ impl Client {
     pub fn subscribe_to_entity<T: Entity>(
         self: &Arc<Self>,
         remote_id: u64,
-    ) -> PendingEntitySubscription<T> {
+    ) -> Result<PendingEntitySubscription<T>> {
         let id = (TypeId::of::<T>(), remote_id);
-        self.state
-            .write()
-            .entities_by_type_and_remote_id
-            .insert(id, WeakSubscriber::Pending(Default::default()));
 
-        PendingEntitySubscription {
-            client: self.clone(),
-            remote_id,
-            consumed: false,
-            _entity_type: PhantomData,
+        let mut state = self.state.write();
+        if state.entities_by_type_and_remote_id.contains_key(&id) {
+            return Err(anyhow!("already subscribed to entity"));
+        } else {
+            state
+                .entities_by_type_and_remote_id
+                .insert(id, WeakSubscriber::Pending(Default::default()));
+            Ok(PendingEntitySubscription {
+                client: self.clone(),
+                remote_id,
+                consumed: false,
+                _entity_type: PhantomData,
+            })
         }
     }
 
@@ -1188,6 +1195,14 @@ impl Client {
         &self,
         request: T,
     ) -> impl Future<Output = Result<T::Response>> {
+        self.request_envelope(request)
+            .map_ok(|envelope| envelope.payload)
+    }
+
+    pub fn request_envelope<T: RequestMessage>(
+        &self,
+        request: T,
+    ) -> impl Future<Output = Result<TypedEnvelope<T::Response>>> {
         let client_id = self.id;
         log::debug!(
             "rpc request start. client_id:{}. name:{}",
@@ -1196,7 +1211,7 @@ impl Client {
         );
         let response = self
             .connection_id()
-            .map(|conn_id| self.peer.request(conn_id, request));
+            .map(|conn_id| self.peer.request_envelope(conn_id, request));
         async move {
             let response = response?.await;
             log::debug!(
@@ -1595,14 +1610,17 @@ mod tests {
 
         let _subscription1 = client
             .subscribe_to_entity(1)
+            .unwrap()
             .set_model(&model1, &mut cx.to_async());
         let _subscription2 = client
             .subscribe_to_entity(2)
+            .unwrap()
             .set_model(&model2, &mut cx.to_async());
         // Ensure dropping a subscription for the same entity type still allows receiving of
         // messages for other entity IDs of the same type.
         let subscription3 = client
             .subscribe_to_entity(3)
+            .unwrap()
             .set_model(&model3, &mut cx.to_async());
         drop(subscription3);
 
@@ -1631,11 +1649,13 @@ mod tests {
             },
         );
         drop(subscription1);
-        let _subscription2 =
-            client.add_message_handler(model, move |_, _: TypedEnvelope<proto::Ping>, _, _| {
+        let _subscription2 = client.add_message_handler(
+            model.clone(),
+            move |_, _: TypedEnvelope<proto::Ping>, _, _| {
                 done_tx2.try_send(()).unwrap();
                 async { Ok(()) }
-            });
+            },
+        );
         server.send(proto::Ping {});
         done_rx2.next().await.unwrap();
     }

crates/collab/Cargo.toml 🔗

@@ -3,7 +3,7 @@ authors = ["Nathan Sobo <nathan@zed.dev>"]
 default-run = "collab"
 edition = "2021"
 name = "collab"
-version = "0.8.2"
+version = "0.8.3"
 publish = false
 
 [[bin]]

crates/collab/src/db.rs 🔗

@@ -175,25 +175,39 @@ impl Database {
                 .map(|participant| participant.user_id)
                 .collect::<Vec<_>>();
 
-            // Delete participants who failed to reconnect.
+            // Delete participants who failed to reconnect and cancel their calls.
+            let mut canceled_calls_to_user_ids = Vec::new();
             room_participant::Entity::delete_many()
                 .filter(stale_participant_filter)
                 .exec(&*tx)
                 .await?;
+            let called_participants = room_participant::Entity::find()
+                .filter(
+                    Condition::all()
+                        .add(
+                            room_participant::Column::CallingUserId
+                                .is_in(stale_participant_user_ids.iter().copied()),
+                        )
+                        .add(room_participant::Column::AnsweringConnectionId.is_null()),
+                )
+                .all(&*tx)
+                .await?;
+            room_participant::Entity::delete_many()
+                .filter(
+                    room_participant::Column::Id
+                        .is_in(called_participants.iter().map(|participant| participant.id)),
+                )
+                .exec(&*tx)
+                .await?;
+            canceled_calls_to_user_ids.extend(
+                called_participants
+                    .into_iter()
+                    .map(|participant| participant.user_id),
+            );
 
             let room = self.get_room(room_id, &tx).await?;
-            let mut canceled_calls_to_user_ids = Vec::new();
-            // Delete the room if it becomes empty and cancel pending calls.
+            // Delete the room if it becomes empty.
             if room.participants.is_empty() {
-                canceled_calls_to_user_ids.extend(
-                    room.pending_participants
-                        .iter()
-                        .map(|pending_participant| UserId::from_proto(pending_participant.user_id)),
-                );
-                room_participant::Entity::delete_many()
-                    .filter(room_participant::Column::RoomId.eq(room_id))
-                    .exec(&*tx)
-                    .await?;
                 project::Entity::delete_many()
                     .filter(project::Column::RoomId.eq(room_id))
                     .exec(&*tx)

crates/collab/src/rpc.rs 🔗

@@ -228,7 +228,7 @@ impl Server {
             .add_message_handler(update_buffer_file)
             .add_message_handler(buffer_reloaded)
             .add_message_handler(buffer_saved)
-            .add_request_handler(save_buffer)
+            .add_request_handler(forward_project_request::<proto::SaveBuffer>)
             .add_request_handler(get_users)
             .add_request_handler(fuzzy_search_users)
             .add_request_handler(request_contact)
@@ -1591,51 +1591,6 @@ where
     Ok(())
 }
 
-async fn save_buffer(
-    request: proto::SaveBuffer,
-    response: Response<proto::SaveBuffer>,
-    session: Session,
-) -> Result<()> {
-    let project_id = ProjectId::from_proto(request.project_id);
-    let host_connection_id = {
-        let collaborators = session
-            .db()
-            .await
-            .project_collaborators(project_id, session.connection_id)
-            .await?;
-        collaborators
-            .iter()
-            .find(|collaborator| collaborator.is_host)
-            .ok_or_else(|| anyhow!("host not found"))?
-            .connection_id
-    };
-    let response_payload = session
-        .peer
-        .forward_request(session.connection_id, host_connection_id, request.clone())
-        .await?;
-
-    let mut collaborators = session
-        .db()
-        .await
-        .project_collaborators(project_id, session.connection_id)
-        .await?;
-    collaborators.retain(|collaborator| collaborator.connection_id != session.connection_id);
-    let project_connection_ids = collaborators
-        .iter()
-        .map(|collaborator| collaborator.connection_id);
-    broadcast(
-        Some(host_connection_id),
-        project_connection_ids,
-        |conn_id| {
-            session
-                .peer
-                .forward_send(host_connection_id, conn_id, response_payload.clone())
-        },
-    );
-    response.send(response_payload)?;
-    Ok(())
-}
-
 async fn create_buffer_for_peer(
     request: proto::CreateBufferForPeer,
     session: Session,
@@ -1655,23 +1610,42 @@ async fn update_buffer(
 ) -> Result<()> {
     session.executor.record_backtrace();
     let project_id = ProjectId::from_proto(request.project_id);
-    let project_connection_ids = session
-        .db()
-        .await
-        .project_connection_ids(project_id, session.connection_id)
-        .await?;
+    let mut guest_connection_ids;
+    let mut host_connection_id = None;
+    {
+        let collaborators = session
+            .db()
+            .await
+            .project_collaborators(project_id, session.connection_id)
+            .await?;
+        guest_connection_ids = Vec::with_capacity(collaborators.len() - 1);
+        for collaborator in collaborators.iter() {
+            if collaborator.is_host {
+                host_connection_id = Some(collaborator.connection_id);
+            } else {
+                guest_connection_ids.push(collaborator.connection_id);
+            }
+        }
+    }
+    let host_connection_id = host_connection_id.ok_or_else(|| anyhow!("host not found"))?;
 
     session.executor.record_backtrace();
-
     broadcast(
         Some(session.connection_id),
-        project_connection_ids.iter().copied(),
+        guest_connection_ids,
         |connection_id| {
             session
                 .peer
                 .forward_send(session.connection_id, connection_id, request.clone())
         },
     );
+    if host_connection_id != session.connection_id {
+        session
+            .peer
+            .forward_request(session.connection_id, host_connection_id, request.clone())
+            .await?;
+    }
+
     response.send(proto::Ack {})?;
     Ok(())
 }

crates/collab/src/tests.rs 🔗

@@ -18,9 +18,10 @@ use parking_lot::Mutex;
 use project::{Project, WorktreeId};
 use settings::Settings;
 use std::{
+    cell::{Ref, RefCell, RefMut},
     env,
-    ops::Deref,
-    path::{Path, PathBuf},
+    ops::{Deref, DerefMut},
+    path::Path,
     sync::{
         atomic::{AtomicBool, AtomicUsize, Ordering::SeqCst},
         Arc,
@@ -209,13 +210,10 @@ impl TestServer {
         let client = TestClient {
             client,
             username: name.to_string(),
-            local_projects: Default::default(),
-            remote_projects: Default::default(),
-            next_root_dir_id: 0,
+            state: Default::default(),
             user_store,
             fs,
             language_registry: Arc::new(LanguageRegistry::test()),
-            buffers: Default::default(),
         };
         client.wait_for_current_user(cx).await;
         client
@@ -314,12 +312,16 @@ impl Drop for TestServer {
 struct TestClient {
     client: Arc<Client>,
     username: String,
-    local_projects: Vec<ModelHandle<Project>>,
-    remote_projects: Vec<ModelHandle<Project>>,
-    next_root_dir_id: usize,
+    state: RefCell<TestClientState>,
     pub user_store: ModelHandle<UserStore>,
     language_registry: Arc<LanguageRegistry>,
     fs: Arc<FakeFs>,
+}
+
+#[derive(Default)]
+struct TestClientState {
+    local_projects: Vec<ModelHandle<Project>>,
+    remote_projects: Vec<ModelHandle<Project>>,
     buffers: HashMap<ModelHandle<Project>, HashSet<ModelHandle<language::Buffer>>>,
 }
 
@@ -358,6 +360,38 @@ impl TestClient {
             .await;
     }
 
+    fn local_projects<'a>(&'a self) -> impl Deref<Target = Vec<ModelHandle<Project>>> + 'a {
+        Ref::map(self.state.borrow(), |state| &state.local_projects)
+    }
+
+    fn remote_projects<'a>(&'a self) -> impl Deref<Target = Vec<ModelHandle<Project>>> + 'a {
+        Ref::map(self.state.borrow(), |state| &state.remote_projects)
+    }
+
+    fn local_projects_mut<'a>(&'a self) -> impl DerefMut<Target = Vec<ModelHandle<Project>>> + 'a {
+        RefMut::map(self.state.borrow_mut(), |state| &mut state.local_projects)
+    }
+
+    fn remote_projects_mut<'a>(&'a self) -> impl DerefMut<Target = Vec<ModelHandle<Project>>> + 'a {
+        RefMut::map(self.state.borrow_mut(), |state| &mut state.remote_projects)
+    }
+
+    fn buffers_for_project<'a>(
+        &'a self,
+        project: &ModelHandle<Project>,
+    ) -> impl DerefMut<Target = HashSet<ModelHandle<language::Buffer>>> + 'a {
+        RefMut::map(self.state.borrow_mut(), |state| {
+            state.buffers.entry(project.clone()).or_default()
+        })
+    }
+
+    fn buffers<'a>(
+        &'a self,
+    ) -> impl DerefMut<Target = HashMap<ModelHandle<Project>, HashSet<ModelHandle<language::Buffer>>>> + 'a
+    {
+        RefMut::map(self.state.borrow_mut(), |state| &mut state.buffers)
+    }
+
     fn summarize_contacts(&self, cx: &TestAppContext) -> ContactsSummary {
         self.user_store.read_with(cx, |store, _| ContactsSummary {
             current: store
@@ -431,15 +465,6 @@ impl TestClient {
         let (_, root_view) = cx.add_window(|_| EmptyView);
         cx.add_view(&root_view, |cx| Workspace::test_new(project.clone(), cx))
     }
-
-    fn create_new_root_dir(&mut self) -> PathBuf {
-        format!(
-            "/{}-root-{}",
-            self.username,
-            util::post_inc(&mut self.next_root_dir_id)
-        )
-        .into()
-    }
 }
 
 impl Drop for TestClient {

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

@@ -1629,9 +1629,7 @@ async fn test_project_reconnect(
         })
         .await
         .unwrap();
-    worktree_a2
-        .read_with(cx_a, |tree, _| tree.as_local().unwrap().scan_complete())
-        .await;
+    deterministic.run_until_parked();
     let worktree2_id = worktree_a2.read_with(cx_a, |tree, _| {
         assert!(tree.as_local().unwrap().is_shared());
         tree.id()
@@ -1692,11 +1690,9 @@ async fn test_project_reconnect(
         .unwrap();
 
     // While client A is disconnected, add and remove worktrees from client A's project.
-    project_a1
-        .update(cx_a, |project, cx| {
-            project.remove_worktree(worktree2_id, cx)
-        })
-        .await;
+    project_a1.update(cx_a, |project, cx| {
+        project.remove_worktree(worktree2_id, cx)
+    });
     let (worktree_a3, _) = project_a1
         .update(cx_a, |p, cx| {
             p.find_or_create_local_worktree("/root-1/dir3", true, cx)
@@ -1820,18 +1816,14 @@ async fn test_project_reconnect(
         })
         .await
         .unwrap();
-    worktree_a4
-        .read_with(cx_a, |tree, _| tree.as_local().unwrap().scan_complete())
-        .await;
+    deterministic.run_until_parked();
     let worktree4_id = worktree_a4.read_with(cx_a, |tree, _| {
         assert!(tree.as_local().unwrap().is_shared());
         tree.id()
     });
-    project_a1
-        .update(cx_a, |project, cx| {
-            project.remove_worktree(worktree3_id, cx)
-        })
-        .await;
+    project_a1.update(cx_a, |project, cx| {
+        project.remove_worktree(worktree3_id, cx)
+    });
     deterministic.run_until_parked();
 
     // While client B is disconnected, mutate a buffer on both the host and the guest.

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

@@ -1,5 +1,5 @@
 use crate::{
-    db::{self, NewUserParams},
+    db::{self, NewUserParams, UserId},
     rpc::{CLEANUP_TIMEOUT, RECONNECT_TIMEOUT},
     tests::{TestClient, TestServer},
 };
@@ -7,38 +7,50 @@ use anyhow::{anyhow, Result};
 use call::ActiveCall;
 use client::RECEIVE_TIMEOUT;
 use collections::BTreeMap;
+use editor::Bias;
 use fs::{FakeFs, Fs as _};
 use futures::StreamExt as _;
-use gpui::{executor::Deterministic, ModelHandle, TestAppContext};
-use language::{range_to_lsp, FakeLspAdapter, Language, LanguageConfig, PointUtf16, Rope};
+use gpui::{executor::Deterministic, ModelHandle, Task, TestAppContext};
+use language::{range_to_lsp, FakeLspAdapter, Language, LanguageConfig, PointUtf16};
 use lsp::FakeLanguageServer;
 use parking_lot::Mutex;
-use project::{search::SearchQuery, Project};
+use project::{search::SearchQuery, Project, ProjectPath};
 use rand::{
     distributions::{Alphanumeric, DistString},
     prelude::*,
 };
+use serde::{Deserialize, Serialize};
 use settings::Settings;
 use std::{
     env,
-    ffi::OsStr,
+    ops::Range,
     path::{Path, PathBuf},
-    sync::Arc,
+    rc::Rc,
+    sync::{
+        atomic::{AtomicBool, Ordering::SeqCst},
+        Arc,
+    },
 };
+use util::ResultExt;
 
-#[gpui::test(iterations = 100)]
+lazy_static::lazy_static! {
+    static ref PLAN_LOAD_PATH: Option<PathBuf> = path_env_var("LOAD_PLAN");
+    static ref PLAN_SAVE_PATH: Option<PathBuf> = path_env_var("SAVE_PLAN");
+    static ref LOADED_PLAN_JSON: Mutex<Option<Vec<u8>>> = Default::default();
+    static ref PLAN: Mutex<Option<Arc<Mutex<TestPlan>>>> = Default::default();
+}
+
+#[gpui::test(iterations = 100, on_failure = "on_failure")]
 async fn test_random_collaboration(
     cx: &mut TestAppContext,
     deterministic: Arc<Deterministic>,
     rng: StdRng,
 ) {
     deterministic.forbid_parking();
-    let rng = Arc::new(Mutex::new(rng));
 
     let max_peers = env::var("MAX_PEERS")
         .map(|i| i.parse().expect("invalid `MAX_PEERS` variable"))
-        .unwrap_or(5);
-
+        .unwrap_or(3);
     let max_operations = env::var("OPERATIONS")
         .map(|i| i.parse().expect("invalid `OPERATIONS` variable"))
         .unwrap_or(10);
@@ -46,7 +58,7 @@ async fn test_random_collaboration(
     let mut server = TestServer::start(&deterministic).await;
     let db = server.app_state.db.clone();
 
-    let mut available_users = Vec::new();
+    let mut users = Vec::new();
     for ix in 0..max_peers {
         let username = format!("user-{}", ix + 1);
         let user_id = db
@@ -62,195 +74,735 @@ async fn test_random_collaboration(
             .await
             .unwrap()
             .user_id;
-        available_users.push((user_id, username));
+        users.push(UserTestPlan {
+            user_id,
+            username,
+            online: false,
+            next_root_id: 0,
+            operation_ix: 0,
+        });
     }
 
-    for (ix, (user_id_a, _)) in available_users.iter().enumerate() {
-        for (user_id_b, _) in &available_users[ix + 1..] {
+    for (ix, user_a) in users.iter().enumerate() {
+        for user_b in &users[ix + 1..] {
             server
                 .app_state
                 .db
-                .send_contact_request(*user_id_a, *user_id_b)
+                .send_contact_request(user_a.user_id, user_b.user_id)
                 .await
                 .unwrap();
             server
                 .app_state
                 .db
-                .respond_to_contact_request(*user_id_b, *user_id_a, true)
+                .respond_to_contact_request(user_b.user_id, user_a.user_id, true)
                 .await
                 .unwrap();
         }
     }
 
+    let plan = Arc::new(Mutex::new(TestPlan::new(rng, users, max_operations)));
+
+    if let Some(path) = &*PLAN_LOAD_PATH {
+        let json = LOADED_PLAN_JSON
+            .lock()
+            .get_or_insert_with(|| {
+                eprintln!("loaded test plan from path {:?}", path);
+                std::fs::read(path).unwrap()
+            })
+            .clone();
+        plan.lock().deserialize(json);
+    }
+
+    PLAN.lock().replace(plan.clone());
+
     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 allow_server_restarts = rng.lock().gen_bool(0.7);
-    let allow_client_reconnection = rng.lock().gen_bool(0.7);
-    let allow_client_disconnection = rng.lock().gen_bool(0.1);
-
-    let mut operations = 0;
-    while operations < max_operations {
-        let distribution = rng.lock().gen_range(0..100);
-        match distribution {
-            0..=19 if !available_users.is_empty() => {
-                let client_ix = rng.lock().gen_range(0..available_users.len());
-                let (_, username) = available_users.remove(client_ix);
-                log::info!("Adding new connection for {}", username);
-                next_entity_id += 100000;
-                let mut client_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,
-                    cx.function_name.clone(),
-                );
+    let mut client_tasks = Vec::new();
+    let mut operation_channels = Vec::new();
+
+    loop {
+        let Some((next_operation, applied)) = plan.lock().next_server_operation(&clients) else { break };
+        applied.store(true, SeqCst);
+        let did_apply = apply_server_operation(
+            deterministic.clone(),
+            &mut server,
+            &mut clients,
+            &mut client_tasks,
+            &mut operation_channels,
+            plan.clone(),
+            next_operation,
+            cx,
+        )
+        .await;
+        if !did_apply {
+            applied.store(false, SeqCst);
+        }
+    }
 
-                client_cx.update(|cx| cx.set_global(Settings::test(cx)));
-
-                let op_start_signal = futures::channel::mpsc::unbounded();
-                let client = server.create_client(&mut client_cx, &username).await;
-                user_ids.push(client.current_user_id(&client_cx));
-                op_start_signals.push(op_start_signal.0);
-                clients.push(client_cx.foreground().spawn(simulate_client(
-                    client,
-                    op_start_signal.1,
-                    allow_client_disconnection,
-                    rng.clone(),
-                    client_cx,
-                )));
-
-                log::info!("Added connection for {}", username);
-                operations += 1;
-            }
+    drop(operation_channels);
+    deterministic.start_waiting();
+    futures::future::join_all(client_tasks).await;
+    deterministic.finish_waiting();
+    deterministic.run_until_parked();
 
-            20..=24 if clients.len() > 1 && allow_client_disconnection => {
-                let client_ix = rng.lock().gen_range(1..clients.len());
-                log::info!(
-                    "Simulating full disconnection of user {}",
-                    user_ids[client_ix]
-                );
-                let removed_user_id = user_ids.remove(client_ix);
-                let user_connection_ids = server
-                    .connection_pool
-                    .lock()
-                    .user_connection_ids(removed_user_id)
-                    .collect::<Vec<_>>();
-                assert_eq!(user_connection_ids.len(), 1);
-                let removed_peer_id = user_connection_ids[0].into();
-                let client = clients.remove(client_ix);
-                op_start_signals.remove(client_ix);
-                server.forbid_connections();
-                server.disconnect_client(removed_peer_id);
-                deterministic.advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT);
-                deterministic.start_waiting();
-                log::info!("Waiting for user {} to exit...", removed_user_id);
-                let (client, mut client_cx) = client.await;
-                deterministic.finish_waiting();
-                server.allow_connections();
-
-                for project in &client.remote_projects {
-                    project.read_with(&client_cx, |project, _| {
-                        assert!(
-                            project.is_read_only(),
-                            "project {:?} should be read only",
-                            project.remote_id()
-                        )
-                    });
+    check_consistency_between_clients(&clients);
+
+    for (client, mut cx) in clients {
+        cx.update(|cx| {
+            cx.clear_globals();
+            cx.set_global(Settings::test(cx));
+            drop(client);
+        });
+    }
+
+    deterministic.run_until_parked();
+}
+
+fn on_failure() {
+    if let Some(plan) = PLAN.lock().clone() {
+        if let Some(path) = &*PLAN_SAVE_PATH {
+            eprintln!("saved test plan to path {:?}", path);
+            std::fs::write(path, plan.lock().serialize()).unwrap();
+        }
+    }
+}
+
+async fn apply_server_operation(
+    deterministic: Arc<Deterministic>,
+    server: &mut TestServer,
+    clients: &mut Vec<(Rc<TestClient>, TestAppContext)>,
+    client_tasks: &mut Vec<Task<()>>,
+    operation_channels: &mut Vec<futures::channel::mpsc::UnboundedSender<usize>>,
+    plan: Arc<Mutex<TestPlan>>,
+    operation: Operation,
+    cx: &mut TestAppContext,
+) -> bool {
+    match operation {
+        Operation::AddConnection { user_id } => {
+            let username;
+            {
+                let mut plan = plan.lock();
+                let mut user = plan.user(user_id);
+                if user.online {
+                    return false;
                 }
-                for user_id in &user_ids {
-                    let contacts = server.app_state.db.get_contacts(*user_id).await.unwrap();
-                    let pool = server.connection_pool.lock();
-                    for contact in contacts {
-                        if let db::Contact::Accepted { user_id, busy, .. } = contact {
-                            if user_id == removed_user_id {
-                                assert!(!pool.is_user_online(user_id));
-                                assert!(!busy);
-                            }
+                user.online = true;
+                username = user.username.clone();
+            };
+            log::info!("Adding new connection for {}", username);
+            let next_entity_id = (user_id.0 * 10_000) as usize;
+            let mut client_cx = TestAppContext::new(
+                cx.foreground_platform(),
+                cx.platform(),
+                deterministic.build_foreground(user_id.0 as usize),
+                deterministic.build_background(),
+                cx.font_cache(),
+                cx.leak_detector(),
+                next_entity_id,
+                cx.function_name.clone(),
+            );
+
+            let (operation_tx, operation_rx) = futures::channel::mpsc::unbounded();
+            let client = Rc::new(server.create_client(&mut client_cx, &username).await);
+            operation_channels.push(operation_tx);
+            clients.push((client.clone(), client_cx.clone()));
+            client_tasks.push(client_cx.foreground().spawn(simulate_client(
+                client,
+                operation_rx,
+                plan.clone(),
+                client_cx,
+            )));
+
+            log::info!("Added connection for {}", username);
+        }
+
+        Operation::RemoveConnection {
+            user_id: removed_user_id,
+        } => {
+            log::info!("Simulating full disconnection of user {}", removed_user_id);
+            let client_ix = clients
+                .iter()
+                .position(|(client, cx)| client.current_user_id(cx) == removed_user_id);
+            let Some(client_ix) = client_ix else { return false };
+            let user_connection_ids = server
+                .connection_pool
+                .lock()
+                .user_connection_ids(removed_user_id)
+                .collect::<Vec<_>>();
+            assert_eq!(user_connection_ids.len(), 1);
+            let removed_peer_id = user_connection_ids[0].into();
+            let (client, mut client_cx) = clients.remove(client_ix);
+            let client_task = client_tasks.remove(client_ix);
+            operation_channels.remove(client_ix);
+            server.forbid_connections();
+            server.disconnect_client(removed_peer_id);
+            deterministic.advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT);
+            deterministic.start_waiting();
+            log::info!("Waiting for user {} to exit...", removed_user_id);
+            client_task.await;
+            deterministic.finish_waiting();
+            server.allow_connections();
+
+            for project in client.remote_projects().iter() {
+                project.read_with(&client_cx, |project, _| {
+                    assert!(
+                        project.is_read_only(),
+                        "project {:?} should be read only",
+                        project.remote_id()
+                    )
+                });
+            }
+
+            for (client, cx) in clients {
+                let contacts = server
+                    .app_state
+                    .db
+                    .get_contacts(client.current_user_id(cx))
+                    .await
+                    .unwrap();
+                let pool = server.connection_pool.lock();
+                for contact in contacts {
+                    if let db::Contact::Accepted { user_id, busy, .. } = contact {
+                        if user_id == removed_user_id {
+                            assert!(!pool.is_user_online(user_id));
+                            assert!(!busy);
                         }
                     }
                 }
+            }
 
-                log::info!("{} removed", client.username);
-                available_users.push((removed_user_id, client.username.clone()));
-                client_cx.update(|cx| {
-                    cx.clear_globals();
-                    cx.set_global(Settings::test(cx));
-                    drop(client);
-                });
+            log::info!("{} removed", client.username);
+            plan.lock().user(removed_user_id).online = false;
+            client_cx.update(|cx| {
+                cx.clear_globals();
+                drop(client);
+            });
+        }
 
-                operations += 1;
+        Operation::BounceConnection { user_id } => {
+            log::info!("Simulating temporary disconnection of user {}", user_id);
+            let user_connection_ids = server
+                .connection_pool
+                .lock()
+                .user_connection_ids(user_id)
+                .collect::<Vec<_>>();
+            if user_connection_ids.is_empty() {
+                return false;
             }
+            assert_eq!(user_connection_ids.len(), 1);
+            let peer_id = user_connection_ids[0].into();
+            server.disconnect_client(peer_id);
+            deterministic.advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT);
+        }
 
-            25..=29 if clients.len() > 1 && allow_client_reconnection => {
-                let client_ix = rng.lock().gen_range(1..clients.len());
-                let user_id = user_ids[client_ix];
-                log::info!("Simulating temporary disconnection of user {}", user_id);
-                let user_connection_ids = server
-                    .connection_pool
-                    .lock()
-                    .user_connection_ids(user_id)
-                    .collect::<Vec<_>>();
-                assert_eq!(user_connection_ids.len(), 1);
-                let peer_id = user_connection_ids[0].into();
-                server.disconnect_client(peer_id);
-                deterministic.advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT);
-                operations += 1;
+        Operation::RestartServer => {
+            log::info!("Simulating server restart");
+            server.reset().await;
+            deterministic.advance_clock(RECEIVE_TIMEOUT);
+            server.start().await.unwrap();
+            deterministic.advance_clock(CLEANUP_TIMEOUT);
+            let environment = &server.app_state.config.zed_environment;
+            let stale_room_ids = server
+                .app_state
+                .db
+                .stale_room_ids(environment, server.id())
+                .await
+                .unwrap();
+            assert_eq!(stale_room_ids, vec![]);
+        }
+
+        Operation::MutateClients {
+            user_ids,
+            batch_id,
+            quiesce,
+        } => {
+            let mut applied = false;
+            for user_id in user_ids {
+                let client_ix = clients
+                    .iter()
+                    .position(|(client, cx)| client.current_user_id(cx) == user_id);
+                let Some(client_ix) = client_ix else { continue };
+                applied = true;
+                if let Err(err) = operation_channels[client_ix].unbounded_send(batch_id) {
+                    log::error!("error signaling user {user_id}: {err}");
+                }
             }
 
-            30..=34 if allow_server_restarts => {
-                log::info!("Simulating server restart");
-                server.reset().await;
-                deterministic.advance_clock(RECEIVE_TIMEOUT);
-                server.start().await.unwrap();
-                deterministic.advance_clock(CLEANUP_TIMEOUT);
-                let environment = &server.app_state.config.zed_environment;
-                let stale_room_ids = server
-                    .app_state
-                    .db
-                    .stale_room_ids(environment, server.id())
+            if quiesce && applied {
+                deterministic.run_until_parked();
+                check_consistency_between_clients(&clients);
+            }
+
+            return applied;
+        }
+    }
+    true
+}
+
+async fn apply_client_operation(
+    client: &TestClient,
+    operation: ClientOperation,
+    cx: &mut TestAppContext,
+) -> Result<(), TestError> {
+    match operation {
+        ClientOperation::AcceptIncomingCall => {
+            let active_call = cx.read(ActiveCall::global);
+            if active_call.read_with(cx, |call, _| call.incoming().borrow().is_none()) {
+                Err(TestError::Inapplicable)?;
+            }
+
+            log::info!("{}: accepting incoming call", client.username);
+            active_call
+                .update(cx, |call, cx| call.accept_incoming(cx))
+                .await?;
+        }
+
+        ClientOperation::RejectIncomingCall => {
+            let active_call = cx.read(ActiveCall::global);
+            if active_call.read_with(cx, |call, _| call.incoming().borrow().is_none()) {
+                Err(TestError::Inapplicable)?;
+            }
+
+            log::info!("{}: declining incoming call", client.username);
+            active_call.update(cx, |call, _| call.decline_incoming())?;
+        }
+
+        ClientOperation::LeaveCall => {
+            let active_call = cx.read(ActiveCall::global);
+            if active_call.read_with(cx, |call, _| call.room().is_none()) {
+                Err(TestError::Inapplicable)?;
+            }
+
+            log::info!("{}: hanging up", client.username);
+            active_call.update(cx, |call, cx| call.hang_up(cx)).await?;
+        }
+
+        ClientOperation::InviteContactToCall { user_id } => {
+            let active_call = cx.read(ActiveCall::global);
+
+            log::info!("{}: inviting {}", client.username, user_id,);
+            active_call
+                .update(cx, |call, cx| call.invite(user_id.to_proto(), None, cx))
+                .await
+                .log_err();
+        }
+
+        ClientOperation::OpenLocalProject { first_root_name } => {
+            log::info!(
+                "{}: opening local project at {:?}",
+                client.username,
+                first_root_name
+            );
+
+            let root_path = Path::new("/").join(&first_root_name);
+            client.fs.create_dir(&root_path).await.unwrap();
+            client
+                .fs
+                .create_file(&root_path.join("main.rs"), Default::default())
+                .await
+                .unwrap();
+            let project = client.build_local_project(root_path, cx).await.0;
+            ensure_project_shared(&project, client, cx).await;
+            client.local_projects_mut().push(project.clone());
+        }
+
+        ClientOperation::AddWorktreeToProject {
+            project_root_name,
+            new_root_path,
+        } => {
+            let project = project_for_root_name(client, &project_root_name, cx)
+                .ok_or(TestError::Inapplicable)?;
+
+            log::info!(
+                "{}: finding/creating local worktree at {:?} to project with root path {}",
+                client.username,
+                new_root_path,
+                project_root_name
+            );
+
+            ensure_project_shared(&project, client, cx).await;
+            if !client.fs.paths().contains(&new_root_path) {
+                client.fs.create_dir(&new_root_path).await.unwrap();
+            }
+            project
+                .update(cx, |project, cx| {
+                    project.find_or_create_local_worktree(&new_root_path, true, cx)
+                })
+                .await
+                .unwrap();
+        }
+
+        ClientOperation::CloseRemoteProject { project_root_name } => {
+            let project = project_for_root_name(client, &project_root_name, cx)
+                .ok_or(TestError::Inapplicable)?;
+
+            log::info!(
+                "{}: closing remote project with root path {}",
+                client.username,
+                project_root_name,
+            );
+
+            let ix = client
+                .remote_projects()
+                .iter()
+                .position(|p| p == &project)
+                .unwrap();
+            cx.update(|_| {
+                client.remote_projects_mut().remove(ix);
+                client.buffers().retain(|p, _| *p != project);
+                drop(project);
+            });
+        }
+
+        ClientOperation::OpenRemoteProject {
+            host_id,
+            first_root_name,
+        } => {
+            let active_call = cx.read(ActiveCall::global);
+            let project = active_call
+                .update(cx, |call, cx| {
+                    let room = call.room().cloned()?;
+                    let participant = room
+                        .read(cx)
+                        .remote_participants()
+                        .get(&host_id.to_proto())?;
+                    let project_id = participant
+                        .projects
+                        .iter()
+                        .find(|project| project.worktree_root_names[0] == first_root_name)?
+                        .id;
+                    Some(room.update(cx, |room, cx| {
+                        room.join_project(
+                            project_id,
+                            client.language_registry.clone(),
+                            FakeFs::new(cx.background().clone()),
+                            cx,
+                        )
+                    }))
+                })
+                .ok_or(TestError::Inapplicable)?;
+
+            log::info!(
+                "{}: joining remote project of user {}, root name {}",
+                client.username,
+                host_id,
+                first_root_name,
+            );
+
+            let project = project.await?;
+            client.remote_projects_mut().push(project.clone());
+        }
+
+        ClientOperation::CreateWorktreeEntry {
+            project_root_name,
+            is_local,
+            full_path,
+            is_dir,
+        } => {
+            let project = project_for_root_name(client, &project_root_name, cx)
+                .ok_or(TestError::Inapplicable)?;
+            let project_path = project_path_for_full_path(&project, &full_path, cx)
+                .ok_or(TestError::Inapplicable)?;
+
+            log::info!(
+                "{}: creating {} at path {:?} in {} project {}",
+                client.username,
+                if is_dir { "dir" } else { "file" },
+                full_path,
+                if is_local { "local" } else { "remote" },
+                project_root_name,
+            );
+
+            ensure_project_shared(&project, client, cx).await;
+            project
+                .update(cx, |p, cx| p.create_entry(project_path, is_dir, cx))
+                .unwrap()
+                .await?;
+        }
+
+        ClientOperation::OpenBuffer {
+            project_root_name,
+            is_local,
+            full_path,
+        } => {
+            let project = project_for_root_name(client, &project_root_name, cx)
+                .ok_or(TestError::Inapplicable)?;
+            let project_path = project_path_for_full_path(&project, &full_path, cx)
+                .ok_or(TestError::Inapplicable)?;
+
+            log::info!(
+                "{}: opening buffer {:?} in {} project {}",
+                client.username,
+                full_path,
+                if is_local { "local" } else { "remote" },
+                project_root_name,
+            );
+
+            ensure_project_shared(&project, client, cx).await;
+            let buffer = project
+                .update(cx, |project, cx| project.open_buffer(project_path, cx))
+                .await?;
+            client.buffers_for_project(&project).insert(buffer);
+        }
+
+        ClientOperation::EditBuffer {
+            project_root_name,
+            is_local,
+            full_path,
+            edits,
+        } => {
+            let project = project_for_root_name(client, &project_root_name, cx)
+                .ok_or(TestError::Inapplicable)?;
+            let buffer = buffer_for_full_path(client, &project, &full_path, cx)
+                .ok_or(TestError::Inapplicable)?;
+
+            log::info!(
+                "{}: editing buffer {:?} in {} project {} with {:?}",
+                client.username,
+                full_path,
+                if is_local { "local" } else { "remote" },
+                project_root_name,
+                edits
+            );
+
+            ensure_project_shared(&project, client, cx).await;
+            buffer.update(cx, |buffer, cx| {
+                let snapshot = buffer.snapshot();
+                buffer.edit(
+                    edits.into_iter().map(|(range, text)| {
+                        let start = snapshot.clip_offset(range.start, Bias::Left);
+                        let end = snapshot.clip_offset(range.end, Bias::Right);
+                        (start..end, text)
+                    }),
+                    None,
+                    cx,
+                );
+            });
+        }
+
+        ClientOperation::CloseBuffer {
+            project_root_name,
+            is_local,
+            full_path,
+        } => {
+            let project = project_for_root_name(client, &project_root_name, cx)
+                .ok_or(TestError::Inapplicable)?;
+            let buffer = buffer_for_full_path(client, &project, &full_path, cx)
+                .ok_or(TestError::Inapplicable)?;
+
+            log::info!(
+                "{}: closing buffer {:?} in {} project {}",
+                client.username,
+                full_path,
+                if is_local { "local" } else { "remote" },
+                project_root_name
+            );
+
+            ensure_project_shared(&project, client, cx).await;
+            cx.update(|_| {
+                client.buffers_for_project(&project).remove(&buffer);
+                drop(buffer);
+            });
+        }
+
+        ClientOperation::SaveBuffer {
+            project_root_name,
+            is_local,
+            full_path,
+            detach,
+        } => {
+            let project = project_for_root_name(client, &project_root_name, cx)
+                .ok_or(TestError::Inapplicable)?;
+            let buffer = buffer_for_full_path(client, &project, &full_path, cx)
+                .ok_or(TestError::Inapplicable)?;
+
+            log::info!(
+                "{}: saving buffer {:?} in {} project {}, {}",
+                client.username,
+                full_path,
+                if is_local { "local" } else { "remote" },
+                project_root_name,
+                if detach { "detaching" } else { "awaiting" }
+            );
+
+            ensure_project_shared(&project, client, cx).await;
+            let requested_version = buffer.read_with(cx, |buffer, _| buffer.version());
+            let save = project.update(cx, |project, cx| project.save_buffer(buffer, cx));
+            let save = cx.background().spawn(async move {
+                let (saved_version, _, _) = save
                     .await
-                    .unwrap();
-                assert_eq!(stale_room_ids, vec![]);
+                    .map_err(|err| anyhow!("save request failed: {:?}", err))?;
+                assert!(saved_version.observed_all(&requested_version));
+                anyhow::Ok(())
+            });
+            if detach {
+                cx.update(|cx| save.detach_and_log_err(cx));
+            } else {
+                save.await?;
             }
+        }
 
-            _ if !op_start_signals.is_empty() => {
-                while operations < max_operations && rng.lock().gen_bool(0.7) {
-                    op_start_signals
-                        .choose(&mut *rng.lock())
-                        .unwrap()
-                        .unbounded_send(())
-                        .unwrap();
-                    operations += 1;
-                }
+        ClientOperation::RequestLspDataInBuffer {
+            project_root_name,
+            is_local,
+            full_path,
+            offset,
+            kind,
+            detach,
+        } => {
+            let project = project_for_root_name(client, &project_root_name, cx)
+                .ok_or(TestError::Inapplicable)?;
+            let buffer = buffer_for_full_path(client, &project, &full_path, cx)
+                .ok_or(TestError::Inapplicable)?;
 
-                if rng.lock().gen_bool(0.8) {
-                    deterministic.run_until_parked();
+            log::info!(
+                "{}: request LSP {:?} for buffer {:?} in {} project {}, {}",
+                client.username,
+                kind,
+                full_path,
+                if is_local { "local" } else { "remote" },
+                project_root_name,
+                if detach { "detaching" } else { "awaiting" }
+            );
+
+            use futures::{FutureExt as _, TryFutureExt as _};
+            let offset = buffer.read_with(cx, |b, _| b.clip_offset(offset, Bias::Left));
+            let request = cx.foreground().spawn(project.update(cx, |project, cx| {
+                match kind {
+                    LspRequestKind::Rename => project
+                        .prepare_rename(buffer, offset, cx)
+                        .map_ok(|_| ())
+                        .boxed(),
+                    LspRequestKind::Completion => project
+                        .completions(&buffer, offset, cx)
+                        .map_ok(|_| ())
+                        .boxed(),
+                    LspRequestKind::CodeAction => project
+                        .code_actions(&buffer, offset..offset, cx)
+                        .map_ok(|_| ())
+                        .boxed(),
+                    LspRequestKind::Definition => project
+                        .definition(&buffer, offset, cx)
+                        .map_ok(|_| ())
+                        .boxed(),
+                    LspRequestKind::Highlights => project
+                        .document_highlights(&buffer, offset, cx)
+                        .map_ok(|_| ())
+                        .boxed(),
                 }
+            }));
+            if detach {
+                request.detach();
+            } else {
+                request.await?;
             }
-            _ => {}
         }
-    }
 
-    drop(op_start_signals);
-    deterministic.start_waiting();
-    let clients = futures::future::join_all(clients).await;
-    deterministic.finish_waiting();
-    deterministic.run_until_parked();
+        ClientOperation::SearchProject {
+            project_root_name,
+            is_local,
+            query,
+            detach,
+        } => {
+            let project = project_for_root_name(client, &project_root_name, cx)
+                .ok_or(TestError::Inapplicable)?;
+
+            log::info!(
+                "{}: search {} project {} for {:?}, {}",
+                client.username,
+                if is_local { "local" } else { "remote" },
+                project_root_name,
+                query,
+                if detach { "detaching" } else { "awaiting" }
+            );
 
-    for (client, client_cx) in &clients {
-        for guest_project in &client.remote_projects {
+            let search = project.update(cx, |project, cx| {
+                project.search(SearchQuery::text(query, false, false), cx)
+            });
+            drop(project);
+            let search = cx.background().spawn(async move {
+                search
+                    .await
+                    .map_err(|err| anyhow!("search request failed: {:?}", err))
+            });
+            if detach {
+                cx.update(|cx| search.detach_and_log_err(cx));
+            } else {
+                search.await?;
+            }
+        }
+
+        ClientOperation::WriteFsEntry {
+            path,
+            is_dir,
+            content,
+        } => {
+            if !client
+                .fs
+                .directories()
+                .contains(&path.parent().unwrap().to_owned())
+            {
+                return Err(TestError::Inapplicable);
+            }
+
+            if is_dir {
+                log::info!("{}: creating dir at {:?}", client.username, path);
+                client.fs.create_dir(&path).await.unwrap();
+            } else {
+                let exists = client.fs.metadata(&path).await?.is_some();
+                let verb = if exists { "updating" } else { "creating" };
+                log::info!("{}: {} file at {:?}", verb, client.username, path);
+
+                client
+                    .fs
+                    .save(&path, &content.as_str().into(), fs::LineEnding::Unix)
+                    .await
+                    .unwrap();
+            }
+        }
+
+        ClientOperation::WriteGitIndex {
+            repo_path,
+            contents,
+        } => {
+            if !client.fs.directories().contains(&repo_path) {
+                return Err(TestError::Inapplicable);
+            }
+
+            log::info!(
+                "{}: writing git index for repo {:?}: {:?}",
+                client.username,
+                repo_path,
+                contents
+            );
+
+            let dot_git_dir = repo_path.join(".git");
+            let contents = contents
+                .iter()
+                .map(|(path, contents)| (path.as_path(), contents.clone()))
+                .collect::<Vec<_>>();
+            if client.fs.metadata(&dot_git_dir).await?.is_none() {
+                client.fs.create_dir(&dot_git_dir).await?;
+            }
+            client.fs.set_index_for_repo(&dot_git_dir, &contents).await;
+        }
+    }
+    Ok(())
+}
+
+fn check_consistency_between_clients(clients: &[(Rc<TestClient>, TestAppContext)]) {
+    for (client, client_cx) in clients {
+        for guest_project in client.remote_projects().iter() {
             guest_project.read_with(client_cx, |guest_project, cx| {
                 let host_project = clients.iter().find_map(|(client, cx)| {
-                    let project = client.local_projects.iter().find(|host_project| {
-                        host_project.read_with(cx, |host_project, _| {
-                            host_project.remote_id() == guest_project.remote_id()
-                        })
-                    })?;
+                    let project = client
+                        .local_projects()
+                        .iter()
+                        .find(|host_project| {
+                            host_project.read_with(cx, |host_project, _| {
+                                host_project.remote_id() == guest_project.remote_id()
+                            })
+                        })?
+                        .clone();
                     Some((project, cx))
                 });
 
@@ -275,10 +827,10 @@ async fn test_random_collaboration(
                             .collect::<BTreeMap<_, _>>();
 
                         assert_eq!(
-                            guest_worktree_snapshots.keys().collect::<Vec<_>>(),
-                            host_worktree_snapshots.keys().collect::<Vec<_>>(),
-                            "{} has different worktrees than the host",
-                            client.username
+                            guest_worktree_snapshots.values().map(|w| w.abs_path()).collect::<Vec<_>>(),
+                            host_worktree_snapshots.values().map(|w| w.abs_path()).collect::<Vec<_>>(),
+                            "{} has different worktrees than the host for project {:?}",
+                            client.username, guest_project.remote_id(),
                         );
 
                         for (id, host_snapshot) in &host_worktree_snapshots {
@@ -286,36 +838,53 @@ async fn test_random_collaboration(
                             assert_eq!(
                                 guest_snapshot.root_name(),
                                 host_snapshot.root_name(),
-                                "{} has different root name than the host for worktree {}",
+                                "{} has different root name than the host for worktree {}, project {:?}",
                                 client.username,
-                                id
+                                id,
+                                guest_project.remote_id(),
                             );
                             assert_eq!(
                                 guest_snapshot.abs_path(),
                                 host_snapshot.abs_path(),
-                                "{} has different abs path than the host for worktree {}",
+                                "{} has different abs path than the host for worktree {}, project: {:?}",
                                 client.username,
-                                id
+                                id,
+                                guest_project.remote_id(),
                             );
                             assert_eq!(
                                 guest_snapshot.entries(false).collect::<Vec<_>>(),
                                 host_snapshot.entries(false).collect::<Vec<_>>(),
-                                "{} has different snapshot than the host for worktree {} ({:?}) and project {:?}",
+                                "{} has different snapshot than the host for worktree {:?} and project {:?}",
+                                client.username,
+                                host_snapshot.abs_path(),
+                                guest_project.remote_id(),
+                            );
+                            assert_eq!(guest_snapshot.scan_id(), host_snapshot.scan_id(),
+                                "{} has different scan id than the host for worktree {:?} and project {:?}",
                                 client.username,
-                                id,
                                 host_snapshot.abs_path(),
-                                host_project.read_with(host_cx, |project, _| project.remote_id())
+                                guest_project.remote_id(),
                             );
-                            assert_eq!(guest_snapshot.scan_id(), host_snapshot.scan_id());
                         }
                     }
                 }
 
-                guest_project.check_invariants(cx);
+                for buffer in guest_project.opened_buffers(cx) {
+                    let buffer = buffer.read(cx);
+                    assert_eq!(
+                        buffer.deferred_ops_len(),
+                        0,
+                        "{} has deferred operations for buffer {:?} in project {:?}",
+                        client.username,
+                        buffer.file().unwrap().full_path(cx),
+                        guest_project.remote_id(),
+                    );
+                }
             });
         }
 
-        for (guest_project, guest_buffers) in &client.buffers {
+        let buffers = client.buffers().clone();
+        for (guest_project, guest_buffers) in &buffers {
             let project_id = if guest_project.read_with(client_cx, |project, _| {
                 project.is_local() || project.is_read_only()
             }) {

crates/collab_ui/src/collab_titlebar_item.rs 🔗

@@ -395,10 +395,10 @@ impl CollabTitlebarItem {
         let icon;
         let tooltip;
         if room.read(cx).is_screen_sharing() {
-            icon = "icons/disable_screen_sharing_12.svg";
+            icon = "icons/enable_screen_sharing_12.svg";
             tooltip = "Stop Sharing Screen"
         } else {
-            icon = "icons/enable_screen_sharing_12.svg";
+            icon = "icons/disable_screen_sharing_12.svg";
             tooltip = "Share Screen";
         }
 
@@ -15,12 +15,9 @@ pub struct BlinkManager {
 
 impl BlinkManager {
     pub fn new(blink_interval: Duration, cx: &mut ModelContext<Self>) -> Self {
-        let weak_handle = cx.weak_handle();
-        cx.observe_global::<Settings, _>(move |_, cx| {
-            if let Some(this) = weak_handle.upgrade(cx) {
-                // Make sure we blink the cursors if the setting is re-enabled
-                this.update(cx, |this, cx| this.blink_cursors(this.blink_epoch, cx));
-            }
+        cx.observe_global::<Settings, _>(move |this, cx| {
+            // Make sure we blink the cursors if the setting is re-enabled
+            this.blink_cursors(this.blink_epoch, cx)
         })
         .detach();
 

crates/editor/src/editor.rs 🔗

@@ -1040,7 +1040,8 @@ impl CopilotState {
         let completion = self.completions.get(self.active_completion_index)?;
         let excerpt_id = self.excerpt_id?;
         let completion_buffer = buffer.buffer_for_excerpt(excerpt_id)?;
-        if !completion.range.start.is_valid(completion_buffer)
+        if excerpt_id != cursor.excerpt_id
+            || !completion.range.start.is_valid(completion_buffer)
             || !completion.range.end.is_valid(completion_buffer)
         {
             return None;
@@ -6619,13 +6620,15 @@ impl Editor {
                 .as_singleton()
                 .and_then(|b| b.read(cx).file()),
         ) {
+            let settings = cx.global::<Settings>();
+
             let extension = Path::new(file.file_name(cx))
                 .extension()
                 .and_then(|e| e.to_str());
             project.read(cx).client().report_event(
                 name,
-                json!({ "File Extension": extension }),
-                cx.global::<Settings>().telemetry(),
+                json!({ "File Extension": extension, "Vim Mode": settings.vim_mode  }),
+                settings.telemetry(),
             );
         }
     }

crates/editor/src/editor_tests.rs 🔗

@@ -6163,6 +6163,110 @@ async fn test_copilot_completion_invalidation(
     });
 }
 
+#[gpui::test]
+async fn test_copilot_multibuffer(
+    deterministic: Arc<Deterministic>,
+    cx: &mut gpui::TestAppContext,
+) {
+    let (copilot, copilot_lsp) = Copilot::fake(cx);
+    cx.update(|cx| {
+        cx.set_global(Settings::test(cx));
+        cx.set_global(copilot)
+    });
+
+    let buffer_1 = cx.add_model(|cx| Buffer::new(0, "a = 1\nb = 2\n", cx));
+    let buffer_2 = cx.add_model(|cx| Buffer::new(0, "c = 3\nd = 4\n", cx));
+    let multibuffer = cx.add_model(|cx| {
+        let mut multibuffer = MultiBuffer::new(0);
+        multibuffer.push_excerpts(
+            buffer_1.clone(),
+            [ExcerptRange {
+                context: Point::new(0, 0)..Point::new(2, 0),
+                primary: None,
+            }],
+            cx,
+        );
+        multibuffer.push_excerpts(
+            buffer_2.clone(),
+            [ExcerptRange {
+                context: Point::new(0, 0)..Point::new(2, 0),
+                primary: None,
+            }],
+            cx,
+        );
+        multibuffer
+    });
+    let (_, editor) = cx.add_window(|cx| build_editor(multibuffer, cx));
+
+    handle_copilot_completion_request(
+        &copilot_lsp,
+        vec![copilot::request::Completion {
+            text: "b = 2 + a".into(),
+            range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(1, 5)),
+            ..Default::default()
+        }],
+        vec![],
+    );
+    editor.update(cx, |editor, cx| {
+        // Ensure copilot suggestions are shown for the first excerpt.
+        editor.change_selections(None, cx, |s| {
+            s.select_ranges([Point::new(1, 5)..Point::new(1, 5)])
+        });
+        editor.next_copilot_suggestion(&Default::default(), cx);
+    });
+    deterministic.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
+    editor.update(cx, |editor, cx| {
+        assert!(editor.has_active_copilot_suggestion(cx));
+        assert_eq!(
+            editor.display_text(cx),
+            "\n\na = 1\nb = 2 + a\n\n\n\nc = 3\nd = 4\n"
+        );
+        assert_eq!(editor.text(cx), "a = 1\nb = 2\n\nc = 3\nd = 4\n");
+    });
+
+    handle_copilot_completion_request(
+        &copilot_lsp,
+        vec![copilot::request::Completion {
+            text: "d = 4 + c".into(),
+            range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(1, 6)),
+            ..Default::default()
+        }],
+        vec![],
+    );
+    editor.update(cx, |editor, cx| {
+        // Move to another excerpt, ensuring the suggestion gets cleared.
+        editor.change_selections(None, cx, |s| {
+            s.select_ranges([Point::new(4, 5)..Point::new(4, 5)])
+        });
+        assert!(!editor.has_active_copilot_suggestion(cx));
+        assert_eq!(
+            editor.display_text(cx),
+            "\n\na = 1\nb = 2\n\n\n\nc = 3\nd = 4\n"
+        );
+        assert_eq!(editor.text(cx), "a = 1\nb = 2\n\nc = 3\nd = 4\n");
+
+        // Type a character, ensuring we don't even try to interpolate the previous suggestion.
+        editor.handle_input(" ", cx);
+        assert!(!editor.has_active_copilot_suggestion(cx));
+        assert_eq!(
+            editor.display_text(cx),
+            "\n\na = 1\nb = 2\n\n\n\nc = 3\nd = 4 \n"
+        );
+        assert_eq!(editor.text(cx), "a = 1\nb = 2\n\nc = 3\nd = 4 \n");
+    });
+
+    // Ensure the new suggestion is displayed when the debounce timeout expires.
+    deterministic.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
+    editor.update(cx, |editor, cx| {
+        assert!(editor.has_active_copilot_suggestion(cx));
+        assert_eq!(
+            editor.display_text(cx),
+            "\n\na = 1\nb = 2\n\n\n\nc = 3\nd = 4 + c\n"
+        );
+        assert_eq!(editor.text(cx), "a = 1\nb = 2\n\nc = 3\nd = 4 \n");
+    });
+}
+
 fn empty_range(row: usize, column: usize) -> Range<DisplayPoint> {
     let point = DisplayPoint::new(row as u32, column as u32);
     point..point

crates/fs/src/fs.rs 🔗

@@ -5,7 +5,7 @@ use fsevent::EventStream;
 use futures::{future::BoxFuture, Stream, StreamExt};
 use git2::Repository as LibGitRepository;
 use lazy_static::lazy_static;
-use parking_lot::Mutex as SyncMutex;
+use parking_lot::Mutex;
 use regex::Regex;
 use repository::GitRepository;
 use rope::Rope;
@@ -27,8 +27,6 @@ use util::ResultExt;
 #[cfg(any(test, feature = "test-support"))]
 use collections::{btree_map, BTreeMap};
 #[cfg(any(test, feature = "test-support"))]
-use futures::lock::Mutex;
-#[cfg(any(test, feature = "test-support"))]
 use repository::FakeGitRepositoryState;
 #[cfg(any(test, feature = "test-support"))]
 use std::sync::Weak;
@@ -117,7 +115,7 @@ pub trait Fs: Send + Sync {
         path: &Path,
         latency: Duration,
     ) -> Pin<Box<dyn Send + Stream<Item = Vec<fsevent::Event>>>>;
-    fn open_repo(&self, abs_dot_git: &Path) -> Option<Arc<SyncMutex<dyn GitRepository>>>;
+    fn open_repo(&self, abs_dot_git: &Path) -> Option<Arc<Mutex<dyn GitRepository>>>;
     fn is_fake(&self) -> bool;
     #[cfg(any(test, feature = "test-support"))]
     fn as_fake(&self) -> &FakeFs;
@@ -350,11 +348,11 @@ impl Fs for RealFs {
         })))
     }
 
-    fn open_repo(&self, dotgit_path: &Path) -> Option<Arc<SyncMutex<dyn GitRepository>>> {
+    fn open_repo(&self, dotgit_path: &Path) -> Option<Arc<Mutex<dyn GitRepository>>> {
         LibGitRepository::open(&dotgit_path)
             .log_err()
-            .and_then::<Arc<SyncMutex<dyn GitRepository>>, _>(|libgit_repository| {
-                Some(Arc::new(SyncMutex::new(libgit_repository)))
+            .and_then::<Arc<Mutex<dyn GitRepository>>, _>(|libgit_repository| {
+                Some(Arc::new(Mutex::new(libgit_repository)))
             })
     }
 
@@ -396,7 +394,7 @@ enum FakeFsEntry {
         inode: u64,
         mtime: SystemTime,
         entries: BTreeMap<String, Arc<Mutex<FakeFsEntry>>>,
-        git_repo_state: Option<Arc<SyncMutex<repository::FakeGitRepositoryState>>>,
+        git_repo_state: Option<Arc<Mutex<repository::FakeGitRepositoryState>>>,
     },
     Symlink {
         target: PathBuf,
@@ -405,18 +403,14 @@ enum FakeFsEntry {
 
 #[cfg(any(test, feature = "test-support"))]
 impl FakeFsState {
-    async fn read_path<'a>(&'a self, target: &Path) -> Result<Arc<Mutex<FakeFsEntry>>> {
+    fn read_path<'a>(&'a self, target: &Path) -> Result<Arc<Mutex<FakeFsEntry>>> {
         Ok(self
             .try_read_path(target)
-            .await
             .ok_or_else(|| anyhow!("path does not exist: {}", target.display()))?
             .0)
     }
 
-    async fn try_read_path<'a>(
-        &'a self,
-        target: &Path,
-    ) -> Option<(Arc<Mutex<FakeFsEntry>>, PathBuf)> {
+    fn try_read_path<'a>(&'a self, target: &Path) -> Option<(Arc<Mutex<FakeFsEntry>>, PathBuf)> {
         let mut path = target.to_path_buf();
         let mut real_path = PathBuf::new();
         let mut entry_stack = Vec::new();
@@ -438,10 +432,10 @@ impl FakeFsState {
                     }
                     Component::Normal(name) => {
                         let current_entry = entry_stack.last().cloned()?;
-                        let current_entry = current_entry.lock().await;
+                        let current_entry = current_entry.lock();
                         if let FakeFsEntry::Dir { entries, .. } = &*current_entry {
                             let entry = entries.get(name.to_str().unwrap()).cloned()?;
-                            let _entry = entry.lock().await;
+                            let _entry = entry.lock();
                             if let FakeFsEntry::Symlink { target, .. } = &*_entry {
                                 let mut target = target.clone();
                                 target.extend(path_components);
@@ -462,7 +456,7 @@ impl FakeFsState {
         entry_stack.pop().map(|entry| (entry, real_path))
     }
 
-    async fn write_path<Fn, T>(&self, path: &Path, callback: Fn) -> Result<T>
+    fn write_path<Fn, T>(&self, path: &Path, callback: Fn) -> Result<T>
     where
         Fn: FnOnce(btree_map::Entry<String, Arc<Mutex<FakeFsEntry>>>) -> Result<T>,
     {
@@ -472,8 +466,8 @@ impl FakeFsState {
             .ok_or_else(|| anyhow!("cannot overwrite the root"))?;
         let parent_path = path.parent().unwrap();
 
-        let parent = self.read_path(parent_path).await?;
-        let mut parent = parent.lock().await;
+        let parent = self.read_path(parent_path)?;
+        let mut parent = parent.lock();
         let new_entry = parent
             .dir_entries(parent_path)?
             .entry(filename.to_str().unwrap().into());
@@ -529,7 +523,7 @@ impl FakeFs {
     }
 
     pub async fn insert_file(&self, path: impl AsRef<Path>, content: String) {
-        let mut state = self.state.lock().await;
+        let mut state = self.state.lock();
         let path = path.as_ref();
         let inode = state.next_inode;
         let mtime = state.next_mtime;
@@ -552,13 +546,12 @@ impl FakeFs {
                 }
                 Ok(())
             })
-            .await
             .unwrap();
         state.emit_event(&[path]);
     }
 
     pub async fn insert_symlink(&self, path: impl AsRef<Path>, target: PathBuf) {
-        let mut state = self.state.lock().await;
+        let mut state = self.state.lock();
         let path = path.as_ref();
         let file = Arc::new(Mutex::new(FakeFsEntry::Symlink { target }));
         state
@@ -572,21 +565,20 @@ impl FakeFs {
                     Ok(())
                 }
             })
-            .await
             .unwrap();
         state.emit_event(&[path]);
     }
 
     pub async fn pause_events(&self) {
-        self.state.lock().await.events_paused = true;
+        self.state.lock().events_paused = true;
     }
 
     pub async fn buffered_event_count(&self) -> usize {
-        self.state.lock().await.buffered_events.len()
+        self.state.lock().buffered_events.len()
     }
 
     pub async fn flush_events(&self, count: usize) {
-        self.state.lock().await.flush_events(count);
+        self.state.lock().flush_events(count);
     }
 
     #[must_use]
@@ -625,9 +617,9 @@ impl FakeFs {
     }
 
     pub async fn set_index_for_repo(&self, dot_git: &Path, head_state: &[(&Path, String)]) {
-        let mut state = self.state.lock().await;
-        let entry = state.read_path(dot_git).await.unwrap();
-        let mut entry = entry.lock().await;
+        let mut state = self.state.lock();
+        let entry = state.read_path(dot_git).unwrap();
+        let mut entry = entry.lock();
 
         if let FakeFsEntry::Dir { git_repo_state, .. } = &mut *entry {
             let repo_state = git_repo_state.get_or_insert_with(Default::default);
@@ -646,12 +638,12 @@ impl FakeFs {
         }
     }
 
-    pub async fn paths(&self) -> Vec<PathBuf> {
+    pub fn paths(&self) -> Vec<PathBuf> {
         let mut result = Vec::new();
         let mut queue = collections::VecDeque::new();
-        queue.push_back((PathBuf::from("/"), self.state.lock().await.root.clone()));
+        queue.push_back((PathBuf::from("/"), self.state.lock().root.clone()));
         while let Some((path, entry)) = queue.pop_front() {
-            if let FakeFsEntry::Dir { entries, .. } = &*entry.lock().await {
+            if let FakeFsEntry::Dir { entries, .. } = &*entry.lock() {
                 for (name, entry) in entries {
                     queue.push_back((path.join(name), entry.clone()));
                 }
@@ -661,12 +653,12 @@ impl FakeFs {
         result
     }
 
-    pub async fn directories(&self) -> Vec<PathBuf> {
+    pub fn directories(&self) -> Vec<PathBuf> {
         let mut result = Vec::new();
         let mut queue = collections::VecDeque::new();
-        queue.push_back((PathBuf::from("/"), self.state.lock().await.root.clone()));
+        queue.push_back((PathBuf::from("/"), self.state.lock().root.clone()));
         while let Some((path, entry)) = queue.pop_front() {
-            if let FakeFsEntry::Dir { entries, .. } = &*entry.lock().await {
+            if let FakeFsEntry::Dir { entries, .. } = &*entry.lock() {
                 for (name, entry) in entries {
                     queue.push_back((path.join(name), entry.clone()));
                 }
@@ -676,12 +668,12 @@ impl FakeFs {
         result
     }
 
-    pub async fn files(&self) -> Vec<PathBuf> {
+    pub fn files(&self) -> Vec<PathBuf> {
         let mut result = Vec::new();
         let mut queue = collections::VecDeque::new();
-        queue.push_back((PathBuf::from("/"), self.state.lock().await.root.clone()));
+        queue.push_back((PathBuf::from("/"), self.state.lock().root.clone()));
         while let Some((path, entry)) = queue.pop_front() {
-            let e = entry.lock().await;
+            let e = entry.lock();
             match &*e {
                 FakeFsEntry::File { .. } => result.push(path),
                 FakeFsEntry::Dir { entries, .. } => {
@@ -745,11 +737,11 @@ impl FakeFsEntry {
 impl Fs for FakeFs {
     async fn create_dir(&self, path: &Path) -> Result<()> {
         self.simulate_random_delay().await;
-        let mut state = self.state.lock().await;
 
         let mut created_dirs = Vec::new();
         let mut cur_path = PathBuf::new();
         for component in path.components() {
+            let mut state = self.state.lock();
             cur_path.push(component);
             if cur_path == Path::new("/") {
                 continue;
@@ -759,29 +751,27 @@ impl Fs for FakeFs {
             let mtime = state.next_mtime;
             state.next_mtime += Duration::from_nanos(1);
             state.next_inode += 1;
-            state
-                .write_path(&cur_path, |entry| {
-                    entry.or_insert_with(|| {
-                        created_dirs.push(cur_path.clone());
-                        Arc::new(Mutex::new(FakeFsEntry::Dir {
-                            inode,
-                            mtime,
-                            entries: Default::default(),
-                            git_repo_state: None,
-                        }))
-                    });
-                    Ok(())
-                })
-                .await?;
+            state.write_path(&cur_path, |entry| {
+                entry.or_insert_with(|| {
+                    created_dirs.push(cur_path.clone());
+                    Arc::new(Mutex::new(FakeFsEntry::Dir {
+                        inode,
+                        mtime,
+                        entries: Default::default(),
+                        git_repo_state: None,
+                    }))
+                });
+                Ok(())
+            })?
         }
 
-        state.emit_event(&created_dirs);
+        self.state.lock().emit_event(&created_dirs);
         Ok(())
     }
 
     async fn create_file(&self, path: &Path, options: CreateOptions) -> Result<()> {
         self.simulate_random_delay().await;
-        let mut state = self.state.lock().await;
+        let mut state = self.state.lock();
         let inode = state.next_inode;
         let mtime = state.next_mtime;
         state.next_mtime += Duration::from_nanos(1);
@@ -791,108 +781,106 @@ impl Fs for FakeFs {
             mtime,
             content: String::new(),
         }));
-        state
-            .write_path(path, |entry| {
-                match entry {
-                    btree_map::Entry::Occupied(mut e) => {
-                        if options.overwrite {
-                            *e.get_mut() = file;
-                        } else if !options.ignore_if_exists {
-                            return Err(anyhow!("path already exists: {}", path.display()));
-                        }
-                    }
-                    btree_map::Entry::Vacant(e) => {
-                        e.insert(file);
+        state.write_path(path, |entry| {
+            match entry {
+                btree_map::Entry::Occupied(mut e) => {
+                    if options.overwrite {
+                        *e.get_mut() = file;
+                    } else if !options.ignore_if_exists {
+                        return Err(anyhow!("path already exists: {}", path.display()));
                     }
                 }
-                Ok(())
-            })
-            .await?;
+                btree_map::Entry::Vacant(e) => {
+                    e.insert(file);
+                }
+            }
+            Ok(())
+        })?;
         state.emit_event(&[path]);
         Ok(())
     }
 
     async fn rename(&self, old_path: &Path, new_path: &Path, options: RenameOptions) -> Result<()> {
+        self.simulate_random_delay().await;
+
         let old_path = normalize_path(old_path);
         let new_path = normalize_path(new_path);
-        let mut state = self.state.lock().await;
-        let moved_entry = state
-            .write_path(&old_path, |e| {
-                if let btree_map::Entry::Occupied(e) = e {
-                    Ok(e.remove())
-                } else {
-                    Err(anyhow!("path does not exist: {}", &old_path.display()))
-                }
-            })
-            .await?;
-        state
-            .write_path(&new_path, |e| {
-                match e {
-                    btree_map::Entry::Occupied(mut e) => {
-                        if options.overwrite {
-                            *e.get_mut() = moved_entry;
-                        } else if !options.ignore_if_exists {
-                            return Err(anyhow!("path already exists: {}", new_path.display()));
-                        }
-                    }
-                    btree_map::Entry::Vacant(e) => {
-                        e.insert(moved_entry);
+        let mut state = self.state.lock();
+        let moved_entry = state.write_path(&old_path, |e| {
+            if let btree_map::Entry::Occupied(e) = e {
+                Ok(e.remove())
+            } else {
+                Err(anyhow!("path does not exist: {}", &old_path.display()))
+            }
+        })?;
+        state.write_path(&new_path, |e| {
+            match e {
+                btree_map::Entry::Occupied(mut e) => {
+                    if options.overwrite {
+                        *e.get_mut() = moved_entry;
+                    } else if !options.ignore_if_exists {
+                        return Err(anyhow!("path already exists: {}", new_path.display()));
                     }
                 }
-                Ok(())
-            })
-            .await?;
+                btree_map::Entry::Vacant(e) => {
+                    e.insert(moved_entry);
+                }
+            }
+            Ok(())
+        })?;
         state.emit_event(&[old_path, new_path]);
         Ok(())
     }
 
     async fn copy_file(&self, source: &Path, target: &Path, options: CopyOptions) -> Result<()> {
+        self.simulate_random_delay().await;
+
         let source = normalize_path(source);
         let target = normalize_path(target);
-        let mut state = self.state.lock().await;
+        let mut state = self.state.lock();
         let mtime = state.next_mtime;
         let inode = util::post_inc(&mut state.next_inode);
         state.next_mtime += Duration::from_nanos(1);
-        let source_entry = state.read_path(&source).await?;
-        let content = source_entry.lock().await.file_content(&source)?.clone();
-        let entry = state
-            .write_path(&target, |e| match e {
-                btree_map::Entry::Occupied(e) => {
-                    if options.overwrite {
-                        Ok(Some(e.get().clone()))
-                    } else if !options.ignore_if_exists {
-                        return Err(anyhow!("{target:?} already exists"));
-                    } else {
-                        Ok(None)
-                    }
+        let source_entry = state.read_path(&source)?;
+        let content = source_entry.lock().file_content(&source)?.clone();
+        let entry = state.write_path(&target, |e| match e {
+            btree_map::Entry::Occupied(e) => {
+                if options.overwrite {
+                    Ok(Some(e.get().clone()))
+                } else if !options.ignore_if_exists {
+                    return Err(anyhow!("{target:?} already exists"));
+                } else {
+                    Ok(None)
                 }
-                btree_map::Entry::Vacant(e) => Ok(Some(
-                    e.insert(Arc::new(Mutex::new(FakeFsEntry::File {
-                        inode,
-                        mtime,
-                        content: String::new(),
-                    })))
-                    .clone(),
-                )),
-            })
-            .await?;
+            }
+            btree_map::Entry::Vacant(e) => Ok(Some(
+                e.insert(Arc::new(Mutex::new(FakeFsEntry::File {
+                    inode,
+                    mtime,
+                    content: String::new(),
+                })))
+                .clone(),
+            )),
+        })?;
         if let Some(entry) = entry {
-            entry.lock().await.set_file_content(&target, content)?;
+            entry.lock().set_file_content(&target, content)?;
         }
         state.emit_event(&[target]);
         Ok(())
     }
 
     async fn remove_dir(&self, path: &Path, options: RemoveOptions) -> Result<()> {
+        self.simulate_random_delay().await;
+
         let path = normalize_path(path);
         let parent_path = path
             .parent()
             .ok_or_else(|| anyhow!("cannot remove the root"))?;
         let base_name = path.file_name().unwrap();
 
-        let mut state = self.state.lock().await;
-        let parent_entry = state.read_path(parent_path).await?;
-        let mut parent_entry = parent_entry.lock().await;
+        let mut state = self.state.lock();
+        let parent_entry = state.read_path(parent_path)?;
+        let mut parent_entry = parent_entry.lock();
         let entry = parent_entry
             .dir_entries(parent_path)?
             .entry(base_name.to_str().unwrap().into());
@@ -905,7 +893,7 @@ impl Fs for FakeFs {
             }
             btree_map::Entry::Occupied(e) => {
                 {
-                    let mut entry = e.get().lock().await;
+                    let mut entry = e.get().lock();
                     let children = entry.dir_entries(&path)?;
                     if !options.recursive && !children.is_empty() {
                         return Err(anyhow!("{path:?} is not empty"));
@@ -919,14 +907,16 @@ impl Fs for FakeFs {
     }
 
     async fn remove_file(&self, path: &Path, options: RemoveOptions) -> Result<()> {
+        self.simulate_random_delay().await;
+
         let path = normalize_path(path);
         let parent_path = path
             .parent()
             .ok_or_else(|| anyhow!("cannot remove the root"))?;
         let base_name = path.file_name().unwrap();
-        let mut state = self.state.lock().await;
-        let parent_entry = state.read_path(parent_path).await?;
-        let mut parent_entry = parent_entry.lock().await;
+        let mut state = self.state.lock();
+        let parent_entry = state.read_path(parent_path)?;
+        let mut parent_entry = parent_entry.lock();
         let entry = parent_entry
             .dir_entries(parent_path)?
             .entry(base_name.to_str().unwrap().into());
@@ -937,7 +927,7 @@ impl Fs for FakeFs {
                 }
             }
             btree_map::Entry::Occupied(e) => {
-                e.get().lock().await.file_content(&path)?;
+                e.get().lock().file_content(&path)?;
                 e.remove();
             }
         }
@@ -953,9 +943,9 @@ impl Fs for FakeFs {
     async fn load(&self, path: &Path) -> Result<String> {
         let path = normalize_path(path);
         self.simulate_random_delay().await;
-        let state = self.state.lock().await;
-        let entry = state.read_path(&path).await?;
-        let entry = entry.lock().await;
+        let state = self.state.lock();
+        let entry = state.read_path(&path)?;
+        let entry = entry.lock();
         entry.file_content(&path).cloned()
     }
 
@@ -978,8 +968,8 @@ impl Fs for FakeFs {
     async fn canonicalize(&self, path: &Path) -> Result<PathBuf> {
         let path = normalize_path(path);
         self.simulate_random_delay().await;
-        let state = self.state.lock().await;
-        if let Some((_, real_path)) = state.try_read_path(&path).await {
+        let state = self.state.lock();
+        if let Some((_, real_path)) = state.try_read_path(&path) {
             Ok(real_path)
         } else {
             Err(anyhow!("path does not exist: {}", path.display()))
@@ -989,9 +979,9 @@ impl Fs for FakeFs {
     async fn is_file(&self, path: &Path) -> bool {
         let path = normalize_path(path);
         self.simulate_random_delay().await;
-        let state = self.state.lock().await;
-        if let Some((entry, _)) = state.try_read_path(&path).await {
-            entry.lock().await.is_file()
+        let state = self.state.lock();
+        if let Some((entry, _)) = state.try_read_path(&path) {
+            entry.lock().is_file()
         } else {
             false
         }
@@ -1000,9 +990,9 @@ impl Fs for FakeFs {
     async fn metadata(&self, path: &Path) -> Result<Option<Metadata>> {
         self.simulate_random_delay().await;
         let path = normalize_path(path);
-        let state = self.state.lock().await;
-        if let Some((entry, real_path)) = state.try_read_path(&path).await {
-            let entry = entry.lock().await;
+        let state = self.state.lock();
+        if let Some((entry, real_path)) = state.try_read_path(&path) {
+            let entry = entry.lock();
             let is_symlink = real_path != path;
 
             Ok(Some(match &*entry {
@@ -1031,9 +1021,9 @@ impl Fs for FakeFs {
     ) -> Result<Pin<Box<dyn Send + Stream<Item = Result<PathBuf>>>>> {
         self.simulate_random_delay().await;
         let path = normalize_path(path);
-        let state = self.state.lock().await;
-        let entry = state.read_path(&path).await?;
-        let mut entry = entry.lock().await;
+        let state = self.state.lock();
+        let entry = state.read_path(&path)?;
+        let mut entry = entry.lock();
         let children = entry.dir_entries(&path)?;
         let paths = children
             .keys()
@@ -1047,10 +1037,9 @@ impl Fs for FakeFs {
         path: &Path,
         _: Duration,
     ) -> Pin<Box<dyn Send + Stream<Item = Vec<fsevent::Event>>>> {
-        let mut state = self.state.lock().await;
         self.simulate_random_delay().await;
         let (tx, rx) = smol::channel::unbounded();
-        state.event_txs.push(tx);
+        self.state.lock().event_txs.push(tx);
         let path = path.to_path_buf();
         let executor = self.executor.clone();
         Box::pin(futures::StreamExt::filter(rx, move |events| {
@@ -1065,22 +1054,18 @@ impl Fs for FakeFs {
         }))
     }
 
-    fn open_repo(&self, abs_dot_git: &Path) -> Option<Arc<SyncMutex<dyn GitRepository>>> {
-        smol::block_on(async move {
-            let state = self.state.lock().await;
-            let entry = state.read_path(abs_dot_git).await.unwrap();
-            let mut entry = entry.lock().await;
-            if let FakeFsEntry::Dir { git_repo_state, .. } = &mut *entry {
-                let state = git_repo_state
-                    .get_or_insert_with(|| {
-                        Arc::new(SyncMutex::new(FakeGitRepositoryState::default()))
-                    })
-                    .clone();
-                Some(repository::FakeGitRepository::open(state))
-            } else {
-                None
-            }
-        })
+    fn open_repo(&self, abs_dot_git: &Path) -> Option<Arc<Mutex<dyn GitRepository>>> {
+        let state = self.state.lock();
+        let entry = state.read_path(abs_dot_git).unwrap();
+        let mut entry = entry.lock();
+        if let FakeFsEntry::Dir { git_repo_state, .. } = &mut *entry {
+            let state = git_repo_state
+                .get_or_insert_with(|| Arc::new(Mutex::new(FakeGitRepositoryState::default())))
+                .clone();
+            Some(repository::FakeGitRepository::open(state))
+        } else {
+            None
+        }
     }
 
     fn is_fake(&self) -> bool {
@@ -1213,7 +1198,7 @@ mod tests {
         .await;
 
         assert_eq!(
-            fs.files().await,
+            fs.files(),
             vec![
                 PathBuf::from("/root/dir1/a"),
                 PathBuf::from("/root/dir1/b"),

crates/gpui/src/app.rs 🔗

@@ -2086,7 +2086,7 @@ impl UpgradeModelHandle for AppContext {
         &self,
         handle: &WeakModelHandle<T>,
     ) -> Option<ModelHandle<T>> {
-        if self.models.contains_key(&handle.model_id) {
+        if self.ref_counts.lock().is_entity_alive(handle.model_id) {
             Some(ModelHandle::new(handle.model_id, &self.ref_counts))
         } else {
             None
@@ -2094,11 +2094,11 @@ impl UpgradeModelHandle for AppContext {
     }
 
     fn model_handle_is_upgradable<T: Entity>(&self, handle: &WeakModelHandle<T>) -> bool {
-        self.models.contains_key(&handle.model_id)
+        self.ref_counts.lock().is_entity_alive(handle.model_id)
     }
 
     fn upgrade_any_model_handle(&self, handle: &AnyWeakModelHandle) -> Option<AnyModelHandle> {
-        if self.models.contains_key(&handle.model_id) {
+        if self.ref_counts.lock().is_entity_alive(handle.model_id) {
             Some(AnyModelHandle::new(
                 handle.model_id,
                 handle.model_type,

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

@@ -31,6 +31,7 @@ use super::{
     ref_counts::LeakDetector, window_input_handler::WindowInputHandler, AsyncAppContext, RefCounts,
 };
 
+#[derive(Clone)]
 pub struct TestAppContext {
     cx: Rc<RefCell<AppContext>>,
     foreground_platform: Rc<platform::test::ForegroundPlatform>,

crates/gpui/src/executor.rs 🔗

@@ -829,6 +829,16 @@ impl Background {
         }
     }
 
+    #[cfg(any(test, feature = "test-support"))]
+    pub fn rng<'a>(&'a self) -> impl 'a + std::ops::DerefMut<Target = rand::prelude::StdRng> {
+        match self {
+            Self::Deterministic { executor, .. } => {
+                parking_lot::lock_api::MutexGuard::map(executor.state.lock(), |s| &mut s.rng)
+            }
+            _ => panic!("this method can only be called on a deterministic executor"),
+        }
+    }
+
     #[cfg(any(test, feature = "test-support"))]
     pub async fn simulate_random_delay(&self) {
         match self {

crates/gpui/src/test.rs 🔗

@@ -46,6 +46,7 @@ pub fn run_test(
         Arc<executor::Deterministic>,
         u64,
     )),
+    on_fail_fn: Option<fn()>,
     fn_name: String,
 ) {
     // let _profiler = dhat::Profiler::new_heap();
@@ -178,6 +179,7 @@ pub fn run_test(
                     if is_randomized {
                         eprintln!("failing seed: {}", atomic_seed.load(SeqCst));
                     }
+                    on_fail_fn.map(|f| f());
                     panic::resume_unwind(error);
                 }
             }

crates/gpui_macros/src/gpui_macros.rs 🔗

@@ -1,4 +1,5 @@
 use proc_macro::TokenStream;
+use proc_macro2::Ident;
 use quote::{format_ident, quote};
 use std::mem;
 use syn::{
@@ -15,6 +16,7 @@ pub fn test(args: TokenStream, function: TokenStream) -> TokenStream {
     let mut num_iterations = 1;
     let mut starting_seed = 0;
     let mut detect_nondeterminism = false;
+    let mut on_failure_fn_name = quote!(None);
 
     for arg in args {
         match arg {
@@ -33,6 +35,20 @@ pub fn test(args: TokenStream, function: TokenStream) -> TokenStream {
                         Some("retries") => max_retries = parse_int(&meta.lit)?,
                         Some("iterations") => num_iterations = parse_int(&meta.lit)?,
                         Some("seed") => starting_seed = parse_int(&meta.lit)?,
+                        Some("on_failure") => {
+                            if let Lit::Str(name) = meta.lit {
+                                let ident = Ident::new(&name.value(), name.span());
+                                on_failure_fn_name = quote!(Some(#ident));
+                            } else {
+                                return Err(TokenStream::from(
+                                    syn::Error::new(
+                                        meta.lit.span(),
+                                        "on_failure argument must be a string",
+                                    )
+                                    .into_compile_error(),
+                                ));
+                            }
+                        }
                         _ => {
                             return Err(TokenStream::from(
                                 syn::Error::new(meta.path.span(), "invalid argument")
@@ -152,6 +168,7 @@ pub fn test(args: TokenStream, function: TokenStream) -> TokenStream {
                         cx.foreground().run(#inner_fn_name(#inner_fn_args));
                         #cx_teardowns
                     },
+                    #on_failure_fn_name,
                     stringify!(#outer_fn_name).to_string(),
                 );
             }
@@ -187,6 +204,7 @@ pub fn test(args: TokenStream, function: TokenStream) -> TokenStream {
                     #max_retries,
                     #detect_nondeterminism,
                     &mut |cx, _, _, seed| #inner_fn_name(#inner_fn_args),
+                    #on_failure_fn_name,
                     stringify!(#outer_fn_name).to_string(),
                 );
             }

crates/language/src/buffer.rs 🔗

@@ -377,7 +377,7 @@ impl Buffer {
             rpc::proto::LineEnding::from_i32(message.line_ending)
                 .ok_or_else(|| anyhow!("missing line_ending"))?,
         ));
-        this.saved_version = proto::deserialize_version(message.saved_version);
+        this.saved_version = proto::deserialize_version(&message.saved_version);
         this.saved_version_fingerprint =
             proto::deserialize_fingerprint(&message.saved_version_fingerprint)?;
         this.saved_mtime = message
@@ -1309,21 +1309,25 @@ impl Buffer {
     pub fn wait_for_edits(
         &mut self,
         edit_ids: impl IntoIterator<Item = clock::Local>,
-    ) -> impl Future<Output = ()> {
+    ) -> impl Future<Output = Result<()>> {
         self.text.wait_for_edits(edit_ids)
     }
 
     pub fn wait_for_anchors<'a>(
         &mut self,
         anchors: impl IntoIterator<Item = &'a Anchor>,
-    ) -> impl Future<Output = ()> {
+    ) -> impl Future<Output = Result<()>> {
         self.text.wait_for_anchors(anchors)
     }
 
-    pub fn wait_for_version(&mut self, version: clock::Global) -> impl Future<Output = ()> {
+    pub fn wait_for_version(&mut self, version: clock::Global) -> impl Future<Output = Result<()>> {
         self.text.wait_for_version(version)
     }
 
+    pub fn give_up_waiting(&mut self) {
+        self.text.give_up_waiting();
+    }
+
     pub fn set_active_selections(
         &mut self,
         selections: Arc<[Selection<Anchor>]>,

crates/language/src/proto.rs 🔗

@@ -220,7 +220,7 @@ pub fn deserialize_operation(message: proto::Operation) -> Result<crate::Operati
                             replica_id: undo.replica_id as ReplicaId,
                             value: undo.local_timestamp,
                         },
-                        version: deserialize_version(undo.version),
+                        version: deserialize_version(&undo.version),
                         counts: undo
                             .counts
                             .into_iter()
@@ -294,7 +294,7 @@ pub fn deserialize_edit_operation(edit: proto::operation::Edit) -> EditOperation
             local: edit.local_timestamp,
             lamport: edit.lamport_timestamp,
         },
-        version: deserialize_version(edit.version),
+        version: deserialize_version(&edit.version),
         ranges: edit.ranges.into_iter().map(deserialize_range).collect(),
         new_text: edit.new_text.into_iter().map(Arc::from).collect(),
     }
@@ -509,7 +509,7 @@ pub fn deserialize_transaction(transaction: proto::Transaction) -> Result<Transa
             .into_iter()
             .map(deserialize_local_timestamp)
             .collect(),
-        start: deserialize_version(transaction.start),
+        start: deserialize_version(&transaction.start),
     })
 }
 
@@ -538,7 +538,7 @@ pub fn deserialize_range(range: proto::Range) -> Range<FullOffset> {
     FullOffset(range.start as usize)..FullOffset(range.end as usize)
 }
 
-pub fn deserialize_version(message: Vec<proto::VectorClockEntry>) -> clock::Global {
+pub fn deserialize_version(message: &[proto::VectorClockEntry]) -> clock::Global {
     let mut version = clock::Global::new();
     for entry in message {
         version.observe(clock::Local {

crates/project/src/lsp_command.rs 🔗

@@ -4,11 +4,13 @@ use crate::{
 use anyhow::{anyhow, Result};
 use async_trait::async_trait;
 use client::proto::{self, PeerId};
+use fs::LineEnding;
 use gpui::{AppContext, AsyncAppContext, ModelHandle};
 use language::{
     point_from_lsp, point_to_lsp,
     proto::{deserialize_anchor, deserialize_version, serialize_anchor, serialize_version},
-    range_from_lsp, Anchor, Bias, Buffer, CachedLspAdapter, PointUtf16, ToPointUtf16,
+    range_from_lsp, range_to_lsp, Anchor, Bias, Buffer, CachedLspAdapter, CharKind, CodeAction,
+    Completion, OffsetRangeExt, PointUtf16, ToOffset, ToPointUtf16, Unclipped,
 };
 use lsp::{DocumentHighlightKind, LanguageServer, ServerCapabilities};
 use pulldown_cmark::{CodeBlockKind, Event, Options, Parser, Tag};
@@ -27,6 +29,8 @@ pub(crate) trait LspCommand: 'static + Sized {
     fn to_lsp(
         &self,
         path: &Path,
+        buffer: &Buffer,
+        language_server: &Arc<LanguageServer>,
         cx: &AppContext,
     ) -> <Self::LspRequest as lsp::request::Request>::Params;
     async fn response_from_lsp(
@@ -49,7 +53,7 @@ pub(crate) trait LspCommand: 'static + Sized {
         project: &mut Project,
         peer_id: PeerId,
         buffer_version: &clock::Global,
-        cx: &AppContext,
+        cx: &mut AppContext,
     ) -> <Self::ProtoRequest as proto::RequestMessage>::Response;
     async fn response_from_proto(
         self,
@@ -91,6 +95,14 @@ pub(crate) struct GetHover {
     pub position: PointUtf16,
 }
 
+pub(crate) struct GetCompletions {
+    pub position: PointUtf16,
+}
+
+pub(crate) struct GetCodeActions {
+    pub range: Range<Anchor>,
+}
+
 #[async_trait(?Send)]
 impl LspCommand for PrepareRename {
     type Response = Option<Range<Anchor>>;
@@ -105,7 +117,13 @@ impl LspCommand for PrepareRename {
         }
     }
 
-    fn to_lsp(&self, path: &Path, _: &AppContext) -> lsp::TextDocumentPositionParams {
+    fn to_lsp(
+        &self,
+        path: &Path,
+        _: &Buffer,
+        _: &Arc<LanguageServer>,
+        _: &AppContext,
+    ) -> lsp::TextDocumentPositionParams {
         lsp::TextDocumentPositionParams {
             text_document: lsp::TextDocumentIdentifier {
                 uri: lsp::Url::from_file_path(path).unwrap(),
@@ -161,9 +179,9 @@ impl LspCommand for PrepareRename {
             .ok_or_else(|| anyhow!("invalid position"))?;
         buffer
             .update(&mut cx, |buffer, _| {
-                buffer.wait_for_version(deserialize_version(message.version))
+                buffer.wait_for_version(deserialize_version(&message.version))
             })
-            .await;
+            .await?;
 
         Ok(Self {
             position: buffer.read_with(&cx, |buffer, _| position.to_point_utf16(buffer)),
@@ -175,7 +193,7 @@ impl LspCommand for PrepareRename {
         _: &mut Project,
         _: PeerId,
         buffer_version: &clock::Global,
-        _: &AppContext,
+        _: &mut AppContext,
     ) -> proto::PrepareRenameResponse {
         proto::PrepareRenameResponse {
             can_rename: range.is_some(),
@@ -199,9 +217,9 @@ impl LspCommand for PrepareRename {
         if message.can_rename {
             buffer
                 .update(&mut cx, |buffer, _| {
-                    buffer.wait_for_version(deserialize_version(message.version))
+                    buffer.wait_for_version(deserialize_version(&message.version))
                 })
-                .await;
+                .await?;
             let start = message.start.and_then(deserialize_anchor);
             let end = message.end.and_then(deserialize_anchor);
             Ok(start.zip(end).map(|(start, end)| start..end))
@@ -221,7 +239,13 @@ impl LspCommand for PerformRename {
     type LspRequest = lsp::request::Rename;
     type ProtoRequest = proto::PerformRename;
 
-    fn to_lsp(&self, path: &Path, _: &AppContext) -> lsp::RenameParams {
+    fn to_lsp(
+        &self,
+        path: &Path,
+        _: &Buffer,
+        _: &Arc<LanguageServer>,
+        _: &AppContext,
+    ) -> lsp::RenameParams {
         lsp::RenameParams {
             text_document_position: lsp::TextDocumentPositionParams {
                 text_document: lsp::TextDocumentIdentifier {
@@ -281,9 +305,9 @@ impl LspCommand for PerformRename {
             .ok_or_else(|| anyhow!("invalid position"))?;
         buffer
             .update(&mut cx, |buffer, _| {
-                buffer.wait_for_version(deserialize_version(message.version))
+                buffer.wait_for_version(deserialize_version(&message.version))
             })
-            .await;
+            .await?;
         Ok(Self {
             position: buffer.read_with(&cx, |buffer, _| position.to_point_utf16(buffer)),
             new_name: message.new_name,
@@ -296,7 +320,7 @@ impl LspCommand for PerformRename {
         project: &mut Project,
         peer_id: PeerId,
         _: &clock::Global,
-        cx: &AppContext,
+        cx: &mut AppContext,
     ) -> proto::PerformRenameResponse {
         let transaction = project.serialize_project_transaction_for_peer(response, peer_id, cx);
         proto::PerformRenameResponse {
@@ -332,7 +356,13 @@ impl LspCommand for GetDefinition {
     type LspRequest = lsp::request::GotoDefinition;
     type ProtoRequest = proto::GetDefinition;
 
-    fn to_lsp(&self, path: &Path, _: &AppContext) -> lsp::GotoDefinitionParams {
+    fn to_lsp(
+        &self,
+        path: &Path,
+        _: &Buffer,
+        _: &Arc<LanguageServer>,
+        _: &AppContext,
+    ) -> lsp::GotoDefinitionParams {
         lsp::GotoDefinitionParams {
             text_document_position_params: lsp::TextDocumentPositionParams {
                 text_document: lsp::TextDocumentIdentifier {
@@ -378,9 +408,9 @@ impl LspCommand for GetDefinition {
             .ok_or_else(|| anyhow!("invalid position"))?;
         buffer
             .update(&mut cx, |buffer, _| {
-                buffer.wait_for_version(deserialize_version(message.version))
+                buffer.wait_for_version(deserialize_version(&message.version))
             })
-            .await;
+            .await?;
         Ok(Self {
             position: buffer.read_with(&cx, |buffer, _| position.to_point_utf16(buffer)),
         })
@@ -391,7 +421,7 @@ impl LspCommand for GetDefinition {
         project: &mut Project,
         peer_id: PeerId,
         _: &clock::Global,
-        cx: &AppContext,
+        cx: &mut AppContext,
     ) -> proto::GetDefinitionResponse {
         let links = location_links_to_proto(response, project, peer_id, cx);
         proto::GetDefinitionResponse { links }
@@ -418,7 +448,13 @@ impl LspCommand for GetTypeDefinition {
     type LspRequest = lsp::request::GotoTypeDefinition;
     type ProtoRequest = proto::GetTypeDefinition;
 
-    fn to_lsp(&self, path: &Path, _: &AppContext) -> lsp::GotoTypeDefinitionParams {
+    fn to_lsp(
+        &self,
+        path: &Path,
+        _: &Buffer,
+        _: &Arc<LanguageServer>,
+        _: &AppContext,
+    ) -> lsp::GotoTypeDefinitionParams {
         lsp::GotoTypeDefinitionParams {
             text_document_position_params: lsp::TextDocumentPositionParams {
                 text_document: lsp::TextDocumentIdentifier {
@@ -464,9 +500,9 @@ impl LspCommand for GetTypeDefinition {
             .ok_or_else(|| anyhow!("invalid position"))?;
         buffer
             .update(&mut cx, |buffer, _| {
-                buffer.wait_for_version(deserialize_version(message.version))
+                buffer.wait_for_version(deserialize_version(&message.version))
             })
-            .await;
+            .await?;
         Ok(Self {
             position: buffer.read_with(&cx, |buffer, _| position.to_point_utf16(buffer)),
         })
@@ -477,7 +513,7 @@ impl LspCommand for GetTypeDefinition {
         project: &mut Project,
         peer_id: PeerId,
         _: &clock::Global,
-        cx: &AppContext,
+        cx: &mut AppContext,
     ) -> proto::GetTypeDefinitionResponse {
         let links = location_links_to_proto(response, project, peer_id, cx);
         proto::GetTypeDefinitionResponse { links }
@@ -537,7 +573,7 @@ async fn location_links_from_proto(
                     .ok_or_else(|| anyhow!("missing origin end"))?;
                 buffer
                     .update(&mut cx, |buffer, _| buffer.wait_for_anchors([&start, &end]))
-                    .await;
+                    .await?;
                 Some(Location {
                     buffer,
                     range: start..end,
@@ -562,7 +598,7 @@ async fn location_links_from_proto(
             .ok_or_else(|| anyhow!("missing target end"))?;
         buffer
             .update(&mut cx, |buffer, _| buffer.wait_for_anchors([&start, &end]))
-            .await;
+            .await?;
         let target = Location {
             buffer,
             range: start..end,
@@ -658,7 +694,7 @@ fn location_links_to_proto(
     links: Vec<LocationLink>,
     project: &mut Project,
     peer_id: PeerId,
-    cx: &AppContext,
+    cx: &mut AppContext,
 ) -> Vec<proto::LocationLink> {
     links
         .into_iter()
@@ -693,7 +729,13 @@ impl LspCommand for GetReferences {
     type LspRequest = lsp::request::References;
     type ProtoRequest = proto::GetReferences;
 
-    fn to_lsp(&self, path: &Path, _: &AppContext) -> lsp::ReferenceParams {
+    fn to_lsp(
+        &self,
+        path: &Path,
+        _: &Buffer,
+        _: &Arc<LanguageServer>,
+        _: &AppContext,
+    ) -> lsp::ReferenceParams {
         lsp::ReferenceParams {
             text_document_position: lsp::TextDocumentPositionParams {
                 text_document: lsp::TextDocumentIdentifier {
@@ -774,9 +816,9 @@ impl LspCommand for GetReferences {
             .ok_or_else(|| anyhow!("invalid position"))?;
         buffer
             .update(&mut cx, |buffer, _| {
-                buffer.wait_for_version(deserialize_version(message.version))
+                buffer.wait_for_version(deserialize_version(&message.version))
             })
-            .await;
+            .await?;
         Ok(Self {
             position: buffer.read_with(&cx, |buffer, _| position.to_point_utf16(buffer)),
         })
@@ -787,7 +829,7 @@ impl LspCommand for GetReferences {
         project: &mut Project,
         peer_id: PeerId,
         _: &clock::Global,
-        cx: &AppContext,
+        cx: &mut AppContext,
     ) -> proto::GetReferencesResponse {
         let locations = response
             .into_iter()
@@ -827,7 +869,7 @@ impl LspCommand for GetReferences {
                 .ok_or_else(|| anyhow!("missing target end"))?;
             target_buffer
                 .update(&mut cx, |buffer, _| buffer.wait_for_anchors([&start, &end]))
-                .await;
+                .await?;
             locations.push(Location {
                 buffer: target_buffer,
                 range: start..end,
@@ -851,7 +893,13 @@ impl LspCommand for GetDocumentHighlights {
         capabilities.document_highlight_provider.is_some()
     }
 
-    fn to_lsp(&self, path: &Path, _: &AppContext) -> lsp::DocumentHighlightParams {
+    fn to_lsp(
+        &self,
+        path: &Path,
+        _: &Buffer,
+        _: &Arc<LanguageServer>,
+        _: &AppContext,
+    ) -> lsp::DocumentHighlightParams {
         lsp::DocumentHighlightParams {
             text_document_position_params: lsp::TextDocumentPositionParams {
                 text_document: lsp::TextDocumentIdentifier {
@@ -915,9 +963,9 @@ impl LspCommand for GetDocumentHighlights {
             .ok_or_else(|| anyhow!("invalid position"))?;
         buffer
             .update(&mut cx, |buffer, _| {
-                buffer.wait_for_version(deserialize_version(message.version))
+                buffer.wait_for_version(deserialize_version(&message.version))
             })
-            .await;
+            .await?;
         Ok(Self {
             position: buffer.read_with(&cx, |buffer, _| position.to_point_utf16(buffer)),
         })
@@ -928,7 +976,7 @@ impl LspCommand for GetDocumentHighlights {
         _: &mut Project,
         _: PeerId,
         _: &clock::Global,
-        _: &AppContext,
+        _: &mut AppContext,
     ) -> proto::GetDocumentHighlightsResponse {
         let highlights = response
             .into_iter()
@@ -965,7 +1013,7 @@ impl LspCommand for GetDocumentHighlights {
                 .ok_or_else(|| anyhow!("missing target end"))?;
             buffer
                 .update(&mut cx, |buffer, _| buffer.wait_for_anchors([&start, &end]))
-                .await;
+                .await?;
             let kind = match proto::document_highlight::Kind::from_i32(highlight.kind) {
                 Some(proto::document_highlight::Kind::Text) => DocumentHighlightKind::TEXT,
                 Some(proto::document_highlight::Kind::Read) => DocumentHighlightKind::READ,
@@ -991,7 +1039,13 @@ impl LspCommand for GetHover {
     type LspRequest = lsp::request::HoverRequest;
     type ProtoRequest = proto::GetHover;
 
-    fn to_lsp(&self, path: &Path, _: &AppContext) -> lsp::HoverParams {
+    fn to_lsp(
+        &self,
+        path: &Path,
+        _: &Buffer,
+        _: &Arc<LanguageServer>,
+        _: &AppContext,
+    ) -> lsp::HoverParams {
         lsp::HoverParams {
             text_document_position_params: lsp::TextDocumentPositionParams {
                 text_document: lsp::TextDocumentIdentifier {
@@ -1117,9 +1171,9 @@ impl LspCommand for GetHover {
             .ok_or_else(|| anyhow!("invalid position"))?;
         buffer
             .update(&mut cx, |buffer, _| {
-                buffer.wait_for_version(deserialize_version(message.version))
+                buffer.wait_for_version(deserialize_version(&message.version))
             })
-            .await;
+            .await?;
         Ok(Self {
             position: buffer.read_with(&cx, |buffer, _| position.to_point_utf16(buffer)),
         })
@@ -1130,7 +1184,7 @@ impl LspCommand for GetHover {
         _: &mut Project,
         _: PeerId,
         _: &clock::Global,
-        _: &AppContext,
+        _: &mut AppContext,
     ) -> proto::GetHoverResponse {
         if let Some(response) = response {
             let (start, end) = if let Some(range) = response.range {
@@ -1199,3 +1253,342 @@ impl LspCommand for GetHover {
         message.buffer_id
     }
 }
+
+#[async_trait(?Send)]
+impl LspCommand for GetCompletions {
+    type Response = Vec<Completion>;
+    type LspRequest = lsp::request::Completion;
+    type ProtoRequest = proto::GetCompletions;
+
+    fn to_lsp(
+        &self,
+        path: &Path,
+        _: &Buffer,
+        _: &Arc<LanguageServer>,
+        _: &AppContext,
+    ) -> lsp::CompletionParams {
+        lsp::CompletionParams {
+            text_document_position: lsp::TextDocumentPositionParams::new(
+                lsp::TextDocumentIdentifier::new(lsp::Url::from_file_path(path).unwrap()),
+                point_to_lsp(self.position),
+            ),
+            context: Default::default(),
+            work_done_progress_params: Default::default(),
+            partial_result_params: Default::default(),
+        }
+    }
+
+    async fn response_from_lsp(
+        self,
+        completions: Option<lsp::CompletionResponse>,
+        _: ModelHandle<Project>,
+        buffer: ModelHandle<Buffer>,
+        cx: AsyncAppContext,
+    ) -> Result<Vec<Completion>> {
+        let completions = if let Some(completions) = completions {
+            match completions {
+                lsp::CompletionResponse::Array(completions) => completions,
+                lsp::CompletionResponse::List(list) => list.items,
+            }
+        } else {
+            Default::default()
+        };
+
+        let completions = buffer.read_with(&cx, |buffer, _| {
+            let language = buffer.language().cloned();
+            let snapshot = buffer.snapshot();
+            let clipped_position = buffer.clip_point_utf16(Unclipped(self.position), Bias::Left);
+            let mut range_for_token = None;
+            completions
+                .into_iter()
+                .filter_map(move |mut lsp_completion| {
+                    // For now, we can only handle additional edits if they are returned
+                    // when resolving the completion, not if they are present initially.
+                    if lsp_completion
+                        .additional_text_edits
+                        .as_ref()
+                        .map_or(false, |edits| !edits.is_empty())
+                    {
+                        return None;
+                    }
+
+                    let (old_range, mut new_text) = match lsp_completion.text_edit.as_ref() {
+                        // If the language server provides a range to overwrite, then
+                        // check that the range is valid.
+                        Some(lsp::CompletionTextEdit::Edit(edit)) => {
+                            let range = range_from_lsp(edit.range);
+                            let start = snapshot.clip_point_utf16(range.start, Bias::Left);
+                            let end = snapshot.clip_point_utf16(range.end, Bias::Left);
+                            if start != range.start.0 || end != range.end.0 {
+                                log::info!("completion out of expected range");
+                                return None;
+                            }
+                            (
+                                snapshot.anchor_before(start)..snapshot.anchor_after(end),
+                                edit.new_text.clone(),
+                            )
+                        }
+                        // If the language server does not provide a range, then infer
+                        // the range based on the syntax tree.
+                        None => {
+                            if self.position != clipped_position {
+                                log::info!("completion out of expected range");
+                                return None;
+                            }
+                            let Range { start, end } = range_for_token
+                                .get_or_insert_with(|| {
+                                    let offset = self.position.to_offset(&snapshot);
+                                    let (range, kind) = snapshot.surrounding_word(offset);
+                                    if kind == Some(CharKind::Word) {
+                                        range
+                                    } else {
+                                        offset..offset
+                                    }
+                                })
+                                .clone();
+                            let text = lsp_completion
+                                .insert_text
+                                .as_ref()
+                                .unwrap_or(&lsp_completion.label)
+                                .clone();
+                            (
+                                snapshot.anchor_before(start)..snapshot.anchor_after(end),
+                                text,
+                            )
+                        }
+                        Some(lsp::CompletionTextEdit::InsertAndReplace(_)) => {
+                            log::info!("unsupported insert/replace completion");
+                            return None;
+                        }
+                    };
+
+                    let language = language.clone();
+                    LineEnding::normalize(&mut new_text);
+                    Some(async move {
+                        let mut label = None;
+                        if let Some(language) = language {
+                            language.process_completion(&mut lsp_completion).await;
+                            label = language.label_for_completion(&lsp_completion).await;
+                        }
+                        Completion {
+                            old_range,
+                            new_text,
+                            label: label.unwrap_or_else(|| {
+                                language::CodeLabel::plain(
+                                    lsp_completion.label.clone(),
+                                    lsp_completion.filter_text.as_deref(),
+                                )
+                            }),
+                            lsp_completion,
+                        }
+                    })
+                })
+        });
+
+        Ok(futures::future::join_all(completions).await)
+    }
+
+    fn to_proto(&self, project_id: u64, buffer: &Buffer) -> proto::GetCompletions {
+        let anchor = buffer.anchor_after(self.position);
+        proto::GetCompletions {
+            project_id,
+            buffer_id: buffer.remote_id(),
+            position: Some(language::proto::serialize_anchor(&anchor)),
+            version: serialize_version(&buffer.version()),
+        }
+    }
+
+    async fn from_proto(
+        message: proto::GetCompletions,
+        _: ModelHandle<Project>,
+        buffer: ModelHandle<Buffer>,
+        mut cx: AsyncAppContext,
+    ) -> Result<Self> {
+        let version = deserialize_version(&message.version);
+        buffer
+            .update(&mut cx, |buffer, _| buffer.wait_for_version(version))
+            .await?;
+        let position = message
+            .position
+            .and_then(language::proto::deserialize_anchor)
+            .map(|p| {
+                buffer.read_with(&cx, |buffer, _| {
+                    buffer.clip_point_utf16(Unclipped(p.to_point_utf16(buffer)), Bias::Left)
+                })
+            })
+            .ok_or_else(|| anyhow!("invalid position"))?;
+        Ok(Self { position })
+    }
+
+    fn response_to_proto(
+        completions: Vec<Completion>,
+        _: &mut Project,
+        _: PeerId,
+        buffer_version: &clock::Global,
+        _: &mut AppContext,
+    ) -> proto::GetCompletionsResponse {
+        proto::GetCompletionsResponse {
+            completions: completions
+                .iter()
+                .map(language::proto::serialize_completion)
+                .collect(),
+            version: serialize_version(&buffer_version),
+        }
+    }
+
+    async fn response_from_proto(
+        self,
+        message: proto::GetCompletionsResponse,
+        _: ModelHandle<Project>,
+        buffer: ModelHandle<Buffer>,
+        mut cx: AsyncAppContext,
+    ) -> Result<Vec<Completion>> {
+        buffer
+            .update(&mut cx, |buffer, _| {
+                buffer.wait_for_version(deserialize_version(&message.version))
+            })
+            .await?;
+
+        let language = buffer.read_with(&cx, |buffer, _| buffer.language().cloned());
+        let completions = message.completions.into_iter().map(|completion| {
+            language::proto::deserialize_completion(completion, language.clone())
+        });
+        futures::future::try_join_all(completions).await
+    }
+
+    fn buffer_id_from_proto(message: &proto::GetCompletions) -> u64 {
+        message.buffer_id
+    }
+}
+
+#[async_trait(?Send)]
+impl LspCommand for GetCodeActions {
+    type Response = Vec<CodeAction>;
+    type LspRequest = lsp::request::CodeActionRequest;
+    type ProtoRequest = proto::GetCodeActions;
+
+    fn check_capabilities(&self, capabilities: &ServerCapabilities) -> bool {
+        capabilities.code_action_provider.is_some()
+    }
+
+    fn to_lsp(
+        &self,
+        path: &Path,
+        buffer: &Buffer,
+        language_server: &Arc<LanguageServer>,
+        _: &AppContext,
+    ) -> lsp::CodeActionParams {
+        let relevant_diagnostics = buffer
+            .snapshot()
+            .diagnostics_in_range::<_, usize>(self.range.clone(), false)
+            .map(|entry| entry.to_lsp_diagnostic_stub())
+            .collect();
+        lsp::CodeActionParams {
+            text_document: lsp::TextDocumentIdentifier::new(
+                lsp::Url::from_file_path(path).unwrap(),
+            ),
+            range: range_to_lsp(self.range.to_point_utf16(buffer)),
+            work_done_progress_params: Default::default(),
+            partial_result_params: Default::default(),
+            context: lsp::CodeActionContext {
+                diagnostics: relevant_diagnostics,
+                only: language_server.code_action_kinds(),
+            },
+        }
+    }
+
+    async fn response_from_lsp(
+        self,
+        actions: Option<lsp::CodeActionResponse>,
+        _: ModelHandle<Project>,
+        _: ModelHandle<Buffer>,
+        _: AsyncAppContext,
+    ) -> Result<Vec<CodeAction>> {
+        Ok(actions
+            .unwrap_or_default()
+            .into_iter()
+            .filter_map(|entry| {
+                if let lsp::CodeActionOrCommand::CodeAction(lsp_action) = entry {
+                    Some(CodeAction {
+                        range: self.range.clone(),
+                        lsp_action,
+                    })
+                } else {
+                    None
+                }
+            })
+            .collect())
+    }
+
+    fn to_proto(&self, project_id: u64, buffer: &Buffer) -> proto::GetCodeActions {
+        proto::GetCodeActions {
+            project_id,
+            buffer_id: buffer.remote_id(),
+            start: Some(language::proto::serialize_anchor(&self.range.start)),
+            end: Some(language::proto::serialize_anchor(&self.range.end)),
+            version: serialize_version(&buffer.version()),
+        }
+    }
+
+    async fn from_proto(
+        message: proto::GetCodeActions,
+        _: ModelHandle<Project>,
+        buffer: ModelHandle<Buffer>,
+        mut cx: AsyncAppContext,
+    ) -> Result<Self> {
+        let start = message
+            .start
+            .and_then(language::proto::deserialize_anchor)
+            .ok_or_else(|| anyhow!("invalid start"))?;
+        let end = message
+            .end
+            .and_then(language::proto::deserialize_anchor)
+            .ok_or_else(|| anyhow!("invalid end"))?;
+        buffer
+            .update(&mut cx, |buffer, _| {
+                buffer.wait_for_version(deserialize_version(&message.version))
+            })
+            .await?;
+
+        Ok(Self { range: start..end })
+    }
+
+    fn response_to_proto(
+        code_actions: Vec<CodeAction>,
+        _: &mut Project,
+        _: PeerId,
+        buffer_version: &clock::Global,
+        _: &mut AppContext,
+    ) -> proto::GetCodeActionsResponse {
+        proto::GetCodeActionsResponse {
+            actions: code_actions
+                .iter()
+                .map(language::proto::serialize_code_action)
+                .collect(),
+            version: serialize_version(&buffer_version),
+        }
+    }
+
+    async fn response_from_proto(
+        self,
+        message: proto::GetCodeActionsResponse,
+        _: ModelHandle<Project>,
+        buffer: ModelHandle<Buffer>,
+        mut cx: AsyncAppContext,
+    ) -> Result<Vec<CodeAction>> {
+        buffer
+            .update(&mut cx, |buffer, _| {
+                buffer.wait_for_version(deserialize_version(&message.version))
+            })
+            .await?;
+        message
+            .actions
+            .into_iter()
+            .map(language::proto::deserialize_code_action)
+            .collect()
+    }
+
+    fn buffer_id_from_proto(message: &proto::GetCodeActions) -> u64 {
+        message.buffer_id
+    }
+}

crates/project/src/project.rs 🔗

@@ -13,7 +13,7 @@ use client::{proto, Client, TypedEnvelope, UserStore};
 use clock::ReplicaId;
 use collections::{hash_map, BTreeMap, HashMap, HashSet};
 use futures::{
-    channel::{mpsc, oneshot},
+    channel::mpsc::{self, UnboundedReceiver},
     future::{try_join_all, Shared},
     AsyncWriteExt, Future, FutureExt, StreamExt, TryFutureExt,
 };
@@ -27,11 +27,11 @@ use language::{
         deserialize_anchor, deserialize_fingerprint, deserialize_line_ending, deserialize_version,
         serialize_anchor, serialize_version,
     },
-    range_from_lsp, range_to_lsp, Anchor, Bias, Buffer, CachedLspAdapter, CharKind, CodeAction,
-    CodeLabel, Completion, Diagnostic, DiagnosticEntry, DiagnosticSet, Diff, Event as BufferEvent,
-    File as _, Language, LanguageRegistry, LanguageServerName, LocalFile, OffsetRangeExt,
-    Operation, Patch, PointUtf16, RopeFingerprint, TextBufferSnapshot, ToOffset, ToPointUtf16,
-    Transaction, Unclipped,
+    range_from_lsp, range_to_lsp, Anchor, Bias, Buffer, CachedLspAdapter, CodeAction, CodeLabel,
+    Completion, Diagnostic, DiagnosticEntry, DiagnosticSet, Diff, Event as BufferEvent, File as _,
+    Language, LanguageRegistry, LanguageServerName, LocalFile, OffsetRangeExt, Operation, Patch,
+    PointUtf16, RopeFingerprint, TextBufferSnapshot, ToOffset, ToPointUtf16, Transaction,
+    Unclipped,
 };
 use lsp::{
     DiagnosticSeverity, DiagnosticTag, DidChangeWatchedFilesRegistrationOptions,
@@ -92,6 +92,7 @@ pub trait Item {
 pub struct Project {
     worktrees: Vec<WorktreeHandle>,
     active_entry: Option<ProjectEntryId>,
+    buffer_changes_tx: mpsc::UnboundedSender<BufferMessage>,
     languages: Arc<LanguageRegistry>,
     language_servers: HashMap<usize, LanguageServerState>,
     language_server_ids: HashMap<(WorktreeId, LanguageServerName), usize>,
@@ -100,6 +101,7 @@ pub struct Project {
     next_language_server_id: usize,
     client: Arc<client::Client>,
     next_entry_id: Arc<AtomicUsize>,
+    join_project_response_message_id: u32,
     next_diagnostic_group_id: usize,
     user_store: ModelHandle<UserStore>,
     fs: Arc<dyn Fs>,
@@ -129,6 +131,22 @@ pub struct Project {
     terminals: Terminals,
 }
 
+enum BufferMessage {
+    Operation {
+        buffer_id: u64,
+        operation: proto::Operation,
+    },
+    Resync,
+}
+
+enum LocalProjectUpdate {
+    WorktreesChanged,
+    CreateBufferForPeer {
+        peer_id: proto::PeerId,
+        buffer_id: u64,
+    },
+}
+
 enum OpenBuffer {
     Strong(ModelHandle<Buffer>),
     Weak(WeakModelHandle<Buffer>),
@@ -143,8 +161,8 @@ enum WorktreeHandle {
 enum ProjectClientState {
     Local {
         remote_id: u64,
-        metadata_changed: mpsc::UnboundedSender<oneshot::Sender<()>>,
-        _maintain_metadata: Task<()>,
+        updates_tx: mpsc::UnboundedSender<LocalProjectUpdate>,
+        _send_updates: Task<()>,
     },
     Remote {
         sharing_has_stopped: bool,
@@ -379,7 +397,7 @@ impl Project {
         client.add_model_message_handler(Self::handle_unshare_project);
         client.add_model_message_handler(Self::handle_create_buffer_for_peer);
         client.add_model_message_handler(Self::handle_update_buffer_file);
-        client.add_model_message_handler(Self::handle_update_buffer);
+        client.add_model_request_handler(Self::handle_update_buffer);
         client.add_model_message_handler(Self::handle_update_diagnostic_summary);
         client.add_model_message_handler(Self::handle_update_worktree);
         client.add_model_request_handler(Self::handle_create_project_entry);
@@ -391,8 +409,8 @@ impl Project {
         client.add_model_request_handler(Self::handle_reload_buffers);
         client.add_model_request_handler(Self::handle_synchronize_buffers);
         client.add_model_request_handler(Self::handle_format_buffers);
-        client.add_model_request_handler(Self::handle_get_code_actions);
-        client.add_model_request_handler(Self::handle_get_completions);
+        client.add_model_request_handler(Self::handle_lsp_command::<GetCodeActions>);
+        client.add_model_request_handler(Self::handle_lsp_command::<GetCompletions>);
         client.add_model_request_handler(Self::handle_lsp_command::<GetHover>);
         client.add_model_request_handler(Self::handle_lsp_command::<GetDefinition>);
         client.add_model_request_handler(Self::handle_lsp_command::<GetTypeDefinition>);
@@ -416,38 +434,45 @@ impl Project {
         fs: Arc<dyn Fs>,
         cx: &mut AppContext,
     ) -> ModelHandle<Self> {
-        cx.add_model(|cx: &mut ModelContext<Self>| Self {
-            worktrees: Default::default(),
-            collaborators: Default::default(),
-            opened_buffers: Default::default(),
-            shared_buffers: Default::default(),
-            incomplete_remote_buffers: Default::default(),
-            loading_buffers_by_path: Default::default(),
-            loading_local_worktrees: Default::default(),
-            buffer_snapshots: Default::default(),
-            client_state: None,
-            opened_buffer: watch::channel(),
-            client_subscriptions: Vec::new(),
-            _subscriptions: vec![cx.observe_global::<Settings, _>(Self::on_settings_changed)],
-            _maintain_buffer_languages: Self::maintain_buffer_languages(&languages, cx),
-            _maintain_workspace_config: Self::maintain_workspace_config(languages.clone(), cx),
-            active_entry: None,
-            languages,
-            client,
-            user_store,
-            fs,
-            next_entry_id: Default::default(),
-            next_diagnostic_group_id: Default::default(),
-            language_servers: Default::default(),
-            language_server_ids: Default::default(),
-            language_server_statuses: Default::default(),
-            last_workspace_edits_by_language_server: Default::default(),
-            buffers_being_formatted: Default::default(),
-            next_language_server_id: 0,
-            nonce: StdRng::from_entropy().gen(),
-            terminals: Terminals {
-                local_handles: Vec::new(),
-            },
+        cx.add_model(|cx: &mut ModelContext<Self>| {
+            let (tx, rx) = mpsc::unbounded();
+            cx.spawn_weak(|this, cx| Self::send_buffer_messages(this, rx, cx))
+                .detach();
+            Self {
+                worktrees: Default::default(),
+                buffer_changes_tx: tx,
+                collaborators: Default::default(),
+                opened_buffers: Default::default(),
+                shared_buffers: Default::default(),
+                incomplete_remote_buffers: Default::default(),
+                loading_buffers_by_path: Default::default(),
+                loading_local_worktrees: Default::default(),
+                buffer_snapshots: Default::default(),
+                join_project_response_message_id: 0,
+                client_state: None,
+                opened_buffer: watch::channel(),
+                client_subscriptions: Vec::new(),
+                _subscriptions: vec![cx.observe_global::<Settings, _>(Self::on_settings_changed)],
+                _maintain_buffer_languages: Self::maintain_buffer_languages(&languages, cx),
+                _maintain_workspace_config: Self::maintain_workspace_config(languages.clone(), cx),
+                active_entry: None,
+                languages,
+                client,
+                user_store,
+                fs,
+                next_entry_id: Default::default(),
+                next_diagnostic_group_id: Default::default(),
+                language_servers: Default::default(),
+                language_server_ids: Default::default(),
+                language_server_statuses: Default::default(),
+                last_workspace_edits_by_language_server: Default::default(),
+                buffers_being_formatted: Default::default(),
+                next_language_server_id: 0,
+                nonce: StdRng::from_entropy().gen(),
+                terminals: Terminals {
+                    local_handles: Vec::new(),
+                },
+            }
         })
     }
 
@@ -461,25 +486,29 @@ impl Project {
     ) -> Result<ModelHandle<Self>> {
         client.authenticate_and_connect(true, &cx).await?;
 
-        let subscription = client.subscribe_to_entity(remote_id);
+        let subscription = client.subscribe_to_entity(remote_id)?;
         let response = client
-            .request(proto::JoinProject {
+            .request_envelope(proto::JoinProject {
                 project_id: remote_id,
             })
             .await?;
         let this = cx.add_model(|cx| {
-            let replica_id = response.replica_id as ReplicaId;
+            let replica_id = response.payload.replica_id as ReplicaId;
 
             let mut worktrees = Vec::new();
-            for worktree in response.worktrees {
+            for worktree in response.payload.worktrees {
                 let worktree = cx.update(|cx| {
                     Worktree::remote(remote_id, replica_id, worktree, client.clone(), cx)
                 });
                 worktrees.push(worktree);
             }
 
+            let (tx, rx) = mpsc::unbounded();
+            cx.spawn_weak(|this, cx| Self::send_buffer_messages(this, rx, cx))
+                .detach();
             let mut this = Self {
                 worktrees: Vec::new(),
+                buffer_changes_tx: tx,
                 loading_buffers_by_path: Default::default(),
                 opened_buffer: watch::channel(),
                 shared_buffers: Default::default(),
@@ -487,6 +516,7 @@ impl Project {
                 loading_local_worktrees: Default::default(),
                 active_entry: None,
                 collaborators: Default::default(),
+                join_project_response_message_id: response.message_id,
                 _maintain_buffer_languages: Self::maintain_buffer_languages(&languages, cx),
                 _maintain_workspace_config: Self::maintain_workspace_config(languages.clone(), cx),
                 languages,
@@ -505,6 +535,7 @@ impl Project {
                 language_servers: Default::default(),
                 language_server_ids: Default::default(),
                 language_server_statuses: response
+                    .payload
                     .language_servers
                     .into_iter()
                     .map(|server| {
@@ -537,6 +568,7 @@ impl Project {
         let subscription = subscription.set_model(&this, &mut cx);
 
         let user_ids = response
+            .payload
             .collaborators
             .iter()
             .map(|peer| peer.user_id)
@@ -546,7 +578,7 @@ impl Project {
             .await?;
 
         this.update(&mut cx, |this, cx| {
-            this.set_collaborators_from_proto(response.collaborators, cx)?;
+            this.set_collaborators_from_proto(response.payload.collaborators, cx)?;
             this.client_subscriptions.push(subscription);
             anyhow::Ok(())
         })?;
@@ -654,37 +686,11 @@ impl Project {
     }
 
     #[cfg(any(test, feature = "test-support"))]
-    pub fn check_invariants(&self, cx: &AppContext) {
-        if self.is_local() {
-            let mut worktree_root_paths = HashMap::default();
-            for worktree in self.worktrees(cx) {
-                let worktree = worktree.read(cx);
-                let abs_path = worktree.as_local().unwrap().abs_path().clone();
-                let prev_worktree_id = worktree_root_paths.insert(abs_path.clone(), worktree.id());
-                assert_eq!(
-                    prev_worktree_id,
-                    None,
-                    "abs path {:?} for worktree {:?} is not unique ({:?} was already registered with the same path)",
-                    abs_path,
-                    worktree.id(),
-                    prev_worktree_id
-                )
-            }
-        } else {
-            let replica_id = self.replica_id();
-            for buffer in self.opened_buffers.values() {
-                if let Some(buffer) = buffer.upgrade(cx) {
-                    let buffer = buffer.read(cx);
-                    assert_eq!(
-                        buffer.deferred_ops_len(),
-                        0,
-                        "replica {}, buffer {} has deferred operations",
-                        replica_id,
-                        buffer.remote_id()
-                    );
-                }
-            }
-        }
+    pub fn opened_buffers(&self, cx: &AppContext) -> Vec<ModelHandle<Buffer>> {
+        self.opened_buffers
+            .values()
+            .filter_map(|b| b.upgrade(cx))
+            .collect()
     }
 
     #[cfg(any(test, feature = "test-support"))]
@@ -724,22 +730,13 @@ impl Project {
         }
     }
 
-    fn metadata_changed(&mut self, cx: &mut ModelContext<Self>) -> impl Future<Output = ()> {
-        let (tx, rx) = oneshot::channel();
-        if let Some(ProjectClientState::Local {
-            metadata_changed, ..
-        }) = &mut self.client_state
-        {
-            let _ = metadata_changed.unbounded_send(tx);
+    fn metadata_changed(&mut self, cx: &mut ModelContext<Self>) {
+        if let Some(ProjectClientState::Local { updates_tx, .. }) = &mut self.client_state {
+            updates_tx
+                .unbounded_send(LocalProjectUpdate::WorktreesChanged)
+                .ok();
         }
         cx.notify();
-
-        async move {
-            // If the project is shared, this will resolve when the `_maintain_metadata` task has
-            // a chance to update the metadata. Otherwise, it will resolve right away because `tx`
-            // will get dropped.
-            let _ = rx.await;
-        }
     }
 
     pub fn collaborators(&self) -> &HashMap<proto::PeerId, Collaborator> {
@@ -984,6 +981,11 @@ impl Project {
         if self.client_state.is_some() {
             return Err(anyhow!("project was already shared"));
         }
+        self.client_subscriptions.push(
+            self.client
+                .subscribe_to_entity(project_id)?
+                .set_model(&cx.handle(), &mut cx.to_async()),
+        );
 
         for open_buffer in self.opened_buffers.values_mut() {
             match open_buffer {
@@ -1020,52 +1022,96 @@ impl Project {
                 .log_err();
         }
 
-        self.client_subscriptions.push(
-            self.client
-                .subscribe_to_entity(project_id)
-                .set_model(&cx.handle(), &mut cx.to_async()),
-        );
-
-        let (metadata_changed_tx, mut metadata_changed_rx) = mpsc::unbounded();
+        let (updates_tx, mut updates_rx) = mpsc::unbounded();
+        let client = self.client.clone();
         self.client_state = Some(ProjectClientState::Local {
             remote_id: project_id,
-            metadata_changed: metadata_changed_tx,
-            _maintain_metadata: cx.spawn_weak(move |this, mut cx| async move {
-                let mut txs = Vec::new();
-                while let Some(tx) = metadata_changed_rx.next().await {
-                    txs.push(tx);
-                    while let Ok(Some(next_tx)) = metadata_changed_rx.try_next() {
-                        txs.push(next_tx);
-                    }
-
+            updates_tx,
+            _send_updates: cx.spawn_weak(move |this, mut cx| async move {
+                while let Some(update) = updates_rx.next().await {
                     let Some(this) = this.upgrade(&cx) else { break };
-                    let worktrees =
-                        this.read_with(&cx, |this, cx| this.worktrees(cx).collect::<Vec<_>>());
-                    let update_project = this
-                        .read_with(&cx, |this, cx| {
-                            this.client.request(proto::UpdateProject {
-                                project_id,
-                                worktrees: this.worktree_metadata_protos(cx),
-                            })
-                        })
-                        .await;
-                    if update_project.is_ok() {
-                        for worktree in worktrees {
-                            worktree.update(&mut cx, |worktree, cx| {
-                                let worktree = worktree.as_local_mut().unwrap();
-                                worktree.share(project_id, cx).detach_and_log_err(cx)
-                            });
+
+                    match update {
+                        LocalProjectUpdate::WorktreesChanged => {
+                            let worktrees = this
+                                .read_with(&cx, |this, cx| this.worktrees(cx).collect::<Vec<_>>());
+                            let update_project = this
+                                .read_with(&cx, |this, cx| {
+                                    this.client.request(proto::UpdateProject {
+                                        project_id,
+                                        worktrees: this.worktree_metadata_protos(cx),
+                                    })
+                                })
+                                .await;
+                            if update_project.is_ok() {
+                                for worktree in worktrees {
+                                    worktree.update(&mut cx, |worktree, cx| {
+                                        let worktree = worktree.as_local_mut().unwrap();
+                                        worktree.share(project_id, cx).detach_and_log_err(cx)
+                                    });
+                                }
+                            }
                         }
-                    }
+                        LocalProjectUpdate::CreateBufferForPeer { peer_id, buffer_id } => {
+                            let buffer = this.update(&mut cx, |this, _| {
+                                let buffer = this.opened_buffers.get(&buffer_id).unwrap();
+                                let shared_buffers =
+                                    this.shared_buffers.entry(peer_id).or_default();
+                                if shared_buffers.insert(buffer_id) {
+                                    if let OpenBuffer::Strong(buffer) = buffer {
+                                        Some(buffer.clone())
+                                    } else {
+                                        None
+                                    }
+                                } else {
+                                    None
+                                }
+                            });
+
+                            let Some(buffer) = buffer else { continue };
+                            let operations =
+                                buffer.read_with(&cx, |b, cx| b.serialize_ops(None, cx));
+                            let operations = operations.await;
+                            let state = buffer.read_with(&cx, |buffer, _| buffer.to_proto());
 
-                    for tx in txs.drain(..) {
-                        let _ = tx.send(());
+                            let initial_state = proto::CreateBufferForPeer {
+                                project_id,
+                                peer_id: Some(peer_id),
+                                variant: Some(proto::create_buffer_for_peer::Variant::State(state)),
+                            };
+                            if client.send(initial_state).log_err().is_some() {
+                                let client = client.clone();
+                                cx.background()
+                                    .spawn(async move {
+                                        let mut chunks = split_operations(operations).peekable();
+                                        while let Some(chunk) = chunks.next() {
+                                            let is_last = chunks.peek().is_none();
+                                            client.send(proto::CreateBufferForPeer {
+                                                project_id,
+                                                peer_id: Some(peer_id),
+                                                variant: Some(
+                                                    proto::create_buffer_for_peer::Variant::Chunk(
+                                                        proto::BufferChunk {
+                                                            buffer_id,
+                                                            operations: chunk,
+                                                            is_last,
+                                                        },
+                                                    ),
+                                                ),
+                                            })?;
+                                        }
+                                        anyhow::Ok(())
+                                    })
+                                    .await
+                                    .log_err();
+                            }
+                        }
                     }
                 }
             }),
         });
 
-        let _ = self.metadata_changed(cx);
+        self.metadata_changed(cx);
         cx.emit(Event::RemoteIdChanged(Some(project_id)));
         cx.notify();
         Ok(())
@@ -1076,16 +1122,19 @@ impl Project {
         message: proto::ResharedProject,
         cx: &mut ModelContext<Self>,
     ) -> Result<()> {
+        self.shared_buffers.clear();
         self.set_collaborators_from_proto(message.collaborators, cx)?;
-        let _ = self.metadata_changed(cx);
+        self.metadata_changed(cx);
         Ok(())
     }
 
     pub fn rejoined(
         &mut self,
         message: proto::RejoinedProject,
+        message_id: u32,
         cx: &mut ModelContext<Self>,
     ) -> Result<()> {
+        self.join_project_response_message_id = message_id;
         self.set_worktrees_from_proto(message.worktrees, cx)?;
         self.set_collaborators_from_proto(message.collaborators, cx)?;
         self.language_server_statuses = message
@@ -1103,13 +1152,21 @@ impl Project {
                 )
             })
             .collect();
-        self.synchronize_remote_buffers(cx).detach_and_log_err(cx);
-
+        self.buffer_changes_tx
+            .unbounded_send(BufferMessage::Resync)
+            .unwrap();
         cx.notify();
         Ok(())
     }
 
     pub fn unshare(&mut self, cx: &mut ModelContext<Self>) -> Result<()> {
+        self.unshare_internal(cx)?;
+        self.metadata_changed(cx);
+        cx.notify();
+        Ok(())
+    }
+
+    fn unshare_internal(&mut self, cx: &mut AppContext) -> Result<()> {
         if self.is_remote() {
             return Err(anyhow!("attempted to unshare a remote project"));
         }
@@ -1132,13 +1189,16 @@ impl Project {
             }
 
             for open_buffer in self.opened_buffers.values_mut() {
+                // Wake up any tasks waiting for peers' edits to this buffer.
+                if let Some(buffer) = open_buffer.upgrade(cx) {
+                    buffer.update(cx, |buffer, _| buffer.give_up_waiting());
+                }
+
                 if let OpenBuffer::Strong(buffer) = open_buffer {
                     *open_buffer = OpenBuffer::Weak(buffer.downgrade());
                 }
             }
 
-            let _ = self.metadata_changed(cx);
-            cx.notify();
             self.client.send(proto::UnshareProject {
                 project_id: remote_id,
             })?;
@@ -1150,13 +1210,21 @@ impl Project {
     }
 
     pub fn disconnected_from_host(&mut self, cx: &mut ModelContext<Self>) {
+        self.disconnected_from_host_internal(cx);
+        cx.emit(Event::DisconnectedFromHost);
+        cx.notify();
+    }
+
+    fn disconnected_from_host_internal(&mut self, cx: &mut AppContext) {
         if let Some(ProjectClientState::Remote {
             sharing_has_stopped,
             ..
         }) = &mut self.client_state
         {
             *sharing_has_stopped = true;
+
             self.collaborators.clear();
+
             for worktree in &self.worktrees {
                 if let Some(worktree) = worktree.upgrade(cx) {
                     worktree.update(cx, |worktree, _| {
@@ -1166,8 +1234,17 @@ impl Project {
                     });
                 }
             }
-            cx.emit(Event::DisconnectedFromHost);
-            cx.notify();
+
+            for open_buffer in self.opened_buffers.values_mut() {
+                // Wake up any tasks waiting for peers' edits to this buffer.
+                if let Some(buffer) = open_buffer.upgrade(cx) {
+                    buffer.update(cx, |buffer, _| buffer.give_up_waiting());
+                }
+
+                if let OpenBuffer::Strong(buffer) = open_buffer {
+                    *open_buffer = OpenBuffer::Weak(buffer.downgrade());
+                }
+            }
 
             // Wake up all futures currently waiting on a buffer to get opened,
             // to give them a chance to fail now that we've disconnected.
@@ -1507,32 +1584,29 @@ impl Project {
         });
 
         let remote_id = buffer.read(cx).remote_id();
-        let open_buffer = if self.is_remote() || self.is_shared() {
+        let is_remote = self.is_remote();
+        let open_buffer = if is_remote || self.is_shared() {
             OpenBuffer::Strong(buffer.clone())
         } else {
             OpenBuffer::Weak(buffer.downgrade())
         };
 
-        match self.opened_buffers.insert(remote_id, open_buffer) {
-            None => {}
-            Some(OpenBuffer::Operations(operations)) => {
-                buffer.update(cx, |buffer, cx| buffer.apply_ops(operations, cx))?
+        match self.opened_buffers.entry(remote_id) {
+            hash_map::Entry::Vacant(entry) => {
+                entry.insert(open_buffer);
             }
-            Some(OpenBuffer::Weak(existing_handle)) => {
-                if existing_handle.upgrade(cx).is_some() {
-                    debug_panic!("already registered buffer with remote id {}", remote_id);
-                    Err(anyhow!(
-                        "already registered buffer with remote id {}",
-                        remote_id
-                    ))?
+            hash_map::Entry::Occupied(mut entry) => {
+                if let OpenBuffer::Operations(operations) = entry.get_mut() {
+                    buffer.update(cx, |b, cx| b.apply_ops(operations.drain(..), cx))?;
+                } else if entry.get().upgrade(cx).is_some() {
+                    if is_remote {
+                        return Ok(());
+                    } else {
+                        debug_panic!("buffer {} was already registered", remote_id);
+                        Err(anyhow!("buffer {} was already registered", remote_id))?;
+                    }
                 }
-            }
-            Some(OpenBuffer::Strong(_)) => {
-                debug_panic!("already registered buffer with remote id {}", remote_id);
-                Err(anyhow!(
-                    "already registered buffer with remote id {}",
-                    remote_id
-                ))?
+                entry.insert(open_buffer);
             }
         }
         cx.subscribe(buffer, |this, buffer, event, cx| {
@@ -1657,6 +1731,53 @@ impl Project {
         });
     }
 
+    async fn send_buffer_messages(
+        this: WeakModelHandle<Self>,
+        mut rx: UnboundedReceiver<BufferMessage>,
+        mut cx: AsyncAppContext,
+    ) {
+        let mut needs_resync_with_host = false;
+        while let Some(change) = rx.next().await {
+            if let Some(this) = this.upgrade(&mut cx) {
+                let is_local = this.read_with(&cx, |this, _| this.is_local());
+                match change {
+                    BufferMessage::Operation {
+                        buffer_id,
+                        operation,
+                    } => {
+                        if needs_resync_with_host {
+                            continue;
+                        }
+                        let request = this.read_with(&cx, |this, _| {
+                            let project_id = this.remote_id()?;
+                            Some(this.client.request(proto::UpdateBuffer {
+                                buffer_id,
+                                project_id,
+                                operations: vec![operation],
+                            }))
+                        });
+                        if let Some(request) = request {
+                            if request.await.is_err() && !is_local {
+                                needs_resync_with_host = true;
+                            }
+                        }
+                    }
+                    BufferMessage::Resync => {
+                        if this
+                            .update(&mut cx, |this, cx| this.synchronize_remote_buffers(cx))
+                            .await
+                            .is_ok()
+                        {
+                            needs_resync_with_host = false;
+                        }
+                    }
+                }
+            } else {
+                break;
+            }
+        }
+    }
+
     fn on_buffer_event(
         &mut self,
         buffer: ModelHandle<Buffer>,
@@ -1665,14 +1786,12 @@ impl Project {
     ) -> Option<()> {
         match event {
             BufferEvent::Operation(operation) => {
-                if let Some(project_id) = self.remote_id() {
-                    let request = self.client.request(proto::UpdateBuffer {
-                        project_id,
+                self.buffer_changes_tx
+                    .unbounded_send(BufferMessage::Operation {
                         buffer_id: buffer.read(cx).remote_id(),
-                        operations: vec![language::proto::serialize_operation(operation)],
-                    });
-                    cx.background().spawn(request).detach_and_log_err(cx);
-                }
+                        operation: language::proto::serialize_operation(operation),
+                    })
+                    .ok();
             }
             BufferEvent::Edited { .. } => {
                 let language_server = self
@@ -3477,188 +3596,12 @@ impl Project {
 
     pub fn completions<T: ToPointUtf16>(
         &self,
-        source_buffer_handle: &ModelHandle<Buffer>,
+        buffer: &ModelHandle<Buffer>,
         position: T,
         cx: &mut ModelContext<Self>,
     ) -> Task<Result<Vec<Completion>>> {
-        let source_buffer_handle = source_buffer_handle.clone();
-        let source_buffer = source_buffer_handle.read(cx);
-        let buffer_id = source_buffer.remote_id();
-        let language = source_buffer.language().cloned();
-        let worktree;
-        let buffer_abs_path;
-        if let Some(file) = File::from_dyn(source_buffer.file()) {
-            worktree = file.worktree.clone();
-            buffer_abs_path = file.as_local().map(|f| f.abs_path(cx));
-        } else {
-            return Task::ready(Ok(Default::default()));
-        };
-
-        let position = Unclipped(position.to_point_utf16(source_buffer));
-        let anchor = source_buffer.anchor_after(position);
-
-        if worktree.read(cx).as_local().is_some() {
-            let buffer_abs_path = buffer_abs_path.unwrap();
-            let lang_server =
-                if let Some((_, server)) = self.language_server_for_buffer(source_buffer, cx) {
-                    server.clone()
-                } else {
-                    return Task::ready(Ok(Default::default()));
-                };
-
-            cx.spawn(|_, cx| async move {
-                let completions = lang_server
-                    .request::<lsp::request::Completion>(lsp::CompletionParams {
-                        text_document_position: lsp::TextDocumentPositionParams::new(
-                            lsp::TextDocumentIdentifier::new(
-                                lsp::Url::from_file_path(buffer_abs_path).unwrap(),
-                            ),
-                            point_to_lsp(position.0),
-                        ),
-                        context: Default::default(),
-                        work_done_progress_params: Default::default(),
-                        partial_result_params: Default::default(),
-                    })
-                    .await
-                    .context("lsp completion request failed")?;
-
-                let completions = if let Some(completions) = completions {
-                    match completions {
-                        lsp::CompletionResponse::Array(completions) => completions,
-                        lsp::CompletionResponse::List(list) => list.items,
-                    }
-                } else {
-                    Default::default()
-                };
-
-                let completions = source_buffer_handle.read_with(&cx, |this, _| {
-                    let snapshot = this.snapshot();
-                    let clipped_position = this.clip_point_utf16(position, Bias::Left);
-                    let mut range_for_token = None;
-                    completions
-                        .into_iter()
-                        .filter_map(move |mut lsp_completion| {
-                            // For now, we can only handle additional edits if they are returned
-                            // when resolving the completion, not if they are present initially.
-                            if lsp_completion
-                                .additional_text_edits
-                                .as_ref()
-                                .map_or(false, |edits| !edits.is_empty())
-                            {
-                                return None;
-                            }
-
-                            let (old_range, mut new_text) = match lsp_completion.text_edit.as_ref()
-                            {
-                                // If the language server provides a range to overwrite, then
-                                // check that the range is valid.
-                                Some(lsp::CompletionTextEdit::Edit(edit)) => {
-                                    let range = range_from_lsp(edit.range);
-                                    let start = snapshot.clip_point_utf16(range.start, Bias::Left);
-                                    let end = snapshot.clip_point_utf16(range.end, Bias::Left);
-                                    if start != range.start.0 || end != range.end.0 {
-                                        log::info!("completion out of expected range");
-                                        return None;
-                                    }
-                                    (
-                                        snapshot.anchor_before(start)..snapshot.anchor_after(end),
-                                        edit.new_text.clone(),
-                                    )
-                                }
-                                // If the language server does not provide a range, then infer
-                                // the range based on the syntax tree.
-                                None => {
-                                    if position.0 != clipped_position {
-                                        log::info!("completion out of expected range");
-                                        return None;
-                                    }
-                                    let Range { start, end } = range_for_token
-                                        .get_or_insert_with(|| {
-                                            let offset = position.to_offset(&snapshot);
-                                            let (range, kind) = snapshot.surrounding_word(offset);
-                                            if kind == Some(CharKind::Word) {
-                                                range
-                                            } else {
-                                                offset..offset
-                                            }
-                                        })
-                                        .clone();
-                                    let text = lsp_completion
-                                        .insert_text
-                                        .as_ref()
-                                        .unwrap_or(&lsp_completion.label)
-                                        .clone();
-                                    (
-                                        snapshot.anchor_before(start)..snapshot.anchor_after(end),
-                                        text,
-                                    )
-                                }
-                                Some(lsp::CompletionTextEdit::InsertAndReplace(_)) => {
-                                    log::info!("unsupported insert/replace completion");
-                                    return None;
-                                }
-                            };
-
-                            LineEnding::normalize(&mut new_text);
-                            let language = language.clone();
-                            Some(async move {
-                                let mut label = None;
-                                if let Some(language) = language {
-                                    language.process_completion(&mut lsp_completion).await;
-                                    label = language.label_for_completion(&lsp_completion).await;
-                                }
-                                Completion {
-                                    old_range,
-                                    new_text,
-                                    label: label.unwrap_or_else(|| {
-                                        CodeLabel::plain(
-                                            lsp_completion.label.clone(),
-                                            lsp_completion.filter_text.as_deref(),
-                                        )
-                                    }),
-                                    lsp_completion,
-                                }
-                            })
-                        })
-                });
-
-                Ok(futures::future::join_all(completions).await)
-            })
-        } else if let Some(project_id) = self.remote_id() {
-            let rpc = self.client.clone();
-            let message = proto::GetCompletions {
-                project_id,
-                buffer_id,
-                position: Some(language::proto::serialize_anchor(&anchor)),
-                version: serialize_version(&source_buffer.version()),
-            };
-            cx.spawn_weak(|this, mut cx| async move {
-                let response = rpc.request(message).await?;
-
-                if this
-                    .upgrade(&cx)
-                    .ok_or_else(|| anyhow!("project was dropped"))?
-                    .read_with(&cx, |this, _| this.is_read_only())
-                {
-                    return Err(anyhow!(
-                        "failed to get completions: project was disconnected"
-                    ));
-                } else {
-                    source_buffer_handle
-                        .update(&mut cx, |buffer, _| {
-                            buffer.wait_for_version(deserialize_version(response.version))
-                        })
-                        .await;
-
-                    let completions = response.completions.into_iter().map(|completion| {
-                        language::proto::deserialize_completion(completion, language.clone())
-                    });
-                    futures::future::try_join_all(completions).await
-                }
-            })
-        } else {
-            Task::ready(Ok(Default::default()))
-        }
+        let position = position.to_point_utf16(buffer.read(cx));
+        self.request_lsp(buffer.clone(), GetCompletions { position }, cx)
     }
 
     pub fn apply_additional_edits_for_completion(
@@ -3739,7 +3682,7 @@ impl Project {
                         .update(&mut cx, |buffer, _| {
                             buffer.wait_for_edits(transaction.edit_ids.iter().copied())
                         })
-                        .await;
+                        .await?;
                     if push_to_history {
                         buffer_handle.update(&mut cx, |buffer, _| {
                             buffer.push_transaction(transaction.clone(), Instant::now());
@@ -3761,106 +3704,9 @@ impl Project {
         range: Range<T>,
         cx: &mut ModelContext<Self>,
     ) -> Task<Result<Vec<CodeAction>>> {
-        let buffer_handle = buffer_handle.clone();
         let buffer = buffer_handle.read(cx);
-        let snapshot = buffer.snapshot();
-        let relevant_diagnostics = snapshot
-            .diagnostics_in_range::<usize, usize>(range.to_offset(&snapshot), false)
-            .map(|entry| entry.to_lsp_diagnostic_stub())
-            .collect();
-        let buffer_id = buffer.remote_id();
-        let worktree;
-        let buffer_abs_path;
-        if let Some(file) = File::from_dyn(buffer.file()) {
-            worktree = file.worktree.clone();
-            buffer_abs_path = file.as_local().map(|f| f.abs_path(cx));
-        } else {
-            return Task::ready(Ok(Vec::new()));
-        };
         let range = buffer.anchor_before(range.start)..buffer.anchor_before(range.end);
-
-        if worktree.read(cx).as_local().is_some() {
-            let buffer_abs_path = buffer_abs_path.unwrap();
-            let lang_server = if let Some((_, server)) = self.language_server_for_buffer(buffer, cx)
-            {
-                server.clone()
-            } else {
-                return Task::ready(Ok(Vec::new()));
-            };
-
-            let lsp_range = range_to_lsp(range.to_point_utf16(buffer));
-            cx.foreground().spawn(async move {
-                if lang_server.capabilities().code_action_provider.is_none() {
-                    return Ok(Vec::new());
-                }
-
-                Ok(lang_server
-                    .request::<lsp::request::CodeActionRequest>(lsp::CodeActionParams {
-                        text_document: lsp::TextDocumentIdentifier::new(
-                            lsp::Url::from_file_path(buffer_abs_path).unwrap(),
-                        ),
-                        range: lsp_range,
-                        work_done_progress_params: Default::default(),
-                        partial_result_params: Default::default(),
-                        context: lsp::CodeActionContext {
-                            diagnostics: relevant_diagnostics,
-                            only: lang_server.code_action_kinds(),
-                        },
-                    })
-                    .await?
-                    .unwrap_or_default()
-                    .into_iter()
-                    .filter_map(|entry| {
-                        if let lsp::CodeActionOrCommand::CodeAction(lsp_action) = entry {
-                            Some(CodeAction {
-                                range: range.clone(),
-                                lsp_action,
-                            })
-                        } else {
-                            None
-                        }
-                    })
-                    .collect())
-            })
-        } else if let Some(project_id) = self.remote_id() {
-            let rpc = self.client.clone();
-            let version = buffer.version();
-            cx.spawn_weak(|this, mut cx| async move {
-                let response = rpc
-                    .request(proto::GetCodeActions {
-                        project_id,
-                        buffer_id,
-                        start: Some(language::proto::serialize_anchor(&range.start)),
-                        end: Some(language::proto::serialize_anchor(&range.end)),
-                        version: serialize_version(&version),
-                    })
-                    .await?;
-
-                if this
-                    .upgrade(&cx)
-                    .ok_or_else(|| anyhow!("project was dropped"))?
-                    .read_with(&cx, |this, _| this.is_read_only())
-                {
-                    return Err(anyhow!(
-                        "failed to get code actions: project was disconnected"
-                    ));
-                } else {
-                    buffer_handle
-                        .update(&mut cx, |buffer, _| {
-                            buffer.wait_for_version(deserialize_version(response.version))
-                        })
-                        .await;
-
-                    response
-                        .actions
-                        .into_iter()
-                        .map(language::proto::deserialize_code_action)
-                        .collect()
-                }
-            })
-        } else {
-            Task::ready(Ok(Default::default()))
-        }
+        self.request_lsp(buffer_handle.clone(), GetCodeActions { range }, cx)
     }
 
     pub fn apply_code_action(
@@ -4345,7 +4191,7 @@ impl Project {
                 self.language_server_for_buffer(buffer, cx)
                     .map(|(_, server)| server.clone()),
             ) {
-                let lsp_params = request.to_lsp(&file.abs_path(cx), cx);
+                let lsp_params = request.to_lsp(&file.abs_path(cx), buffer, &language_server, cx);
                 return cx.spawn(|this, cx| async move {
                     if !request.check_capabilities(language_server.capabilities()) {
                         return Ok(Default::default());

crates/project/src/project_tests.rs 🔗

@@ -2183,7 +2183,7 @@ async fn test_apply_code_actions_with_commands(cx: &mut gpui::TestAppContext) {
     });
 }
 
-#[gpui::test]
+#[gpui::test(iterations = 10)]
 async fn test_save_file(cx: &mut gpui::TestAppContext) {
     let fs = FakeFs::new(cx.background());
     fs.insert_tree(

crates/project/src/worktree.rs 🔗

@@ -12,7 +12,9 @@ use futures::{
         mpsc::{self, UnboundedSender},
         oneshot,
     },
-    select_biased, Stream, StreamExt,
+    select_biased,
+    task::Poll,
+    Stream, StreamExt,
 };
 use fuzzy::CharBag;
 use git::{DOT_GIT, GITIGNORE};
@@ -41,11 +43,11 @@ use std::{
     mem,
     ops::{Deref, DerefMut},
     path::{Path, PathBuf},
+    pin::Pin,
     sync::{
         atomic::{AtomicUsize, Ordering::SeqCst},
         Arc,
     },
-    task::Poll,
     time::{Duration, SystemTime},
 };
 use sum_tree::{Bias, Edit, SeekTarget, SumTree, TreeMap, TreeSet};
@@ -154,20 +156,12 @@ impl DerefMut for LocalSnapshot {
 }
 
 enum ScanState {
-    /// The worktree is performing its initial scan of the filesystem.
-    Initializing {
-        snapshot: LocalSnapshot,
-        barrier: Option<barrier::Sender>,
-    },
-    Initialized {
-        snapshot: LocalSnapshot,
-    },
-    /// The worktree is updating in response to filesystem events.
-    Updating,
+    Started,
     Updated {
         snapshot: LocalSnapshot,
         changes: HashMap<Arc<Path>, PathChange>,
         barrier: Option<barrier::Sender>,
+        scanning: bool,
     },
 }
 
@@ -221,7 +215,7 @@ impl Worktree {
                     root_char_bag: root_name.chars().map(|c| c.to_ascii_lowercase()).collect(),
                     entries_by_path: Default::default(),
                     entries_by_id: Default::default(),
-                    scan_id: 0,
+                    scan_id: 1,
                     completed_scan_id: 0,
                 },
             };
@@ -244,9 +238,24 @@ impl Worktree {
             cx.spawn_weak(|this, mut cx| async move {
                 while let Some((state, this)) = scan_states_rx.next().await.zip(this.upgrade(&cx)) {
                     this.update(&mut cx, |this, cx| {
-                        this.as_local_mut()
-                            .unwrap()
-                            .background_scanner_updated(state, cx);
+                        let this = this.as_local_mut().unwrap();
+                        match state {
+                            ScanState::Started => {
+                                *this.is_scanning.0.borrow_mut() = true;
+                            }
+                            ScanState::Updated {
+                                snapshot,
+                                changes,
+                                barrier,
+                                scanning,
+                            } => {
+                                *this.is_scanning.0.borrow_mut() = scanning;
+                                this.set_snapshot(snapshot, cx);
+                                cx.emit(Event::UpdatedEntries(changes));
+                                drop(barrier);
+                            }
+                        }
+                        cx.notify();
                     });
                 }
             })
@@ -258,9 +267,15 @@ impl Worktree {
                 let background = cx.background().clone();
                 async move {
                     let events = fs.watch(&abs_path, Duration::from_millis(100)).await;
-                    BackgroundScanner::new(snapshot, scan_states_tx, fs, background)
-                        .run(events, path_changes_rx)
-                        .await;
+                    BackgroundScanner::new(
+                        snapshot,
+                        fs,
+                        scan_states_tx,
+                        background,
+                        path_changes_rx,
+                    )
+                    .run(events)
+                    .await;
                 }
             });
 
@@ -298,7 +313,7 @@ impl Worktree {
                     .collect(),
                 entries_by_path: Default::default(),
                 entries_by_id: Default::default(),
-                scan_id: 0,
+                scan_id: 1,
                 completed_scan_id: 0,
             };
 
@@ -533,38 +548,6 @@ impl LocalWorktree {
         Ok(updated)
     }
 
-    fn background_scanner_updated(
-        &mut self,
-        scan_state: ScanState,
-        cx: &mut ModelContext<Worktree>,
-    ) {
-        match scan_state {
-            ScanState::Initializing { snapshot, barrier } => {
-                *self.is_scanning.0.borrow_mut() = true;
-                self.set_snapshot(snapshot, cx);
-                drop(barrier);
-            }
-            ScanState::Initialized { snapshot } => {
-                *self.is_scanning.0.borrow_mut() = false;
-                self.set_snapshot(snapshot, cx);
-            }
-            ScanState::Updating => {
-                *self.is_scanning.0.borrow_mut() = true;
-            }
-            ScanState::Updated {
-                snapshot,
-                changes,
-                barrier,
-            } => {
-                *self.is_scanning.0.borrow_mut() = false;
-                cx.emit(Event::UpdatedEntries(changes));
-                self.set_snapshot(snapshot, cx);
-                drop(barrier);
-            }
-        }
-        cx.notify();
-    }
-
     fn set_snapshot(&mut self, new_snapshot: LocalSnapshot, cx: &mut ModelContext<Worktree>) {
         let updated_repos = Self::changed_repos(
             &self.snapshot.git_repositories,
@@ -838,8 +821,7 @@ impl LocalWorktree {
                     .unwrap()
                     .path_changes_tx
                     .try_send((vec![abs_path], tx))
-                    .unwrap();
-            });
+            })?;
             rx.recv().await;
             Ok(())
         }))
@@ -930,7 +912,7 @@ impl LocalWorktree {
             }
 
             let (tx, mut rx) = barrier::channel();
-            path_changes_tx.try_send((paths, tx)).unwrap();
+            path_changes_tx.try_send((paths, tx))?;
             rx.recv().await;
             this.upgrade(&cx)
                 .ok_or_else(|| anyhow!("worktree was dropped"))?
@@ -1064,7 +1046,7 @@ impl RemoteWorktree {
                     version: serialize_version(&version),
                 })
                 .await?;
-            let version = deserialize_version(response.version);
+            let version = deserialize_version(&response.version);
             let fingerprint = deserialize_fingerprint(&response.fingerprint)?;
             let mtime = response
                 .mtime
@@ -1224,11 +1206,10 @@ impl Snapshot {
         let mut entries_by_path_edits = Vec::new();
         let mut entries_by_id_edits = Vec::new();
         for entry_id in update.removed_entries {
-            let entry = self
-                .entry_for_id(ProjectEntryId::from_proto(entry_id))
-                .ok_or_else(|| anyhow!("unknown entry {}", entry_id))?;
-            entries_by_path_edits.push(Edit::Remove(PathKey(entry.path.clone())));
-            entries_by_id_edits.push(Edit::Remove(entry.id));
+            if let Some(entry) = self.entry_for_id(ProjectEntryId::from_proto(entry_id)) {
+                entries_by_path_edits.push(Edit::Remove(PathKey(entry.path.clone())));
+                entries_by_id_edits.push(Edit::Remove(entry.id));
+            }
         }
 
         for entry in update.updated_entries {
@@ -1339,14 +1320,6 @@ impl Snapshot {
         &self.root_name
     }
 
-    pub fn scan_started(&mut self) {
-        self.scan_id += 1;
-    }
-
-    pub fn scan_completed(&mut self) {
-        self.completed_scan_id = self.scan_id;
-    }
-
     pub fn scan_id(&self) -> usize {
         self.scan_id
     }
@@ -1541,17 +1514,20 @@ impl LocalSnapshot {
             return;
         };
 
+        match parent_entry.kind {
+            EntryKind::PendingDir => {
+                parent_entry.kind = EntryKind::Dir;
+            }
+            EntryKind::Dir => {}
+            _ => return,
+        }
+
         if let Some(ignore) = ignore {
             self.ignores_by_parent_abs_path.insert(
                 self.abs_path.join(&parent_path).into(),
                 (ignore, self.scan_id),
             );
         }
-        if matches!(parent_entry.kind, EntryKind::PendingDir) {
-            parent_entry.kind = EntryKind::Dir;
-        } else {
-            unreachable!();
-        }
 
         if parent_path.file_name() == Some(&DOT_GIT) {
             let abs_path = self.abs_path.join(&parent_path);
@@ -2137,53 +2113,47 @@ impl<'a> sum_tree::Dimension<'a, EntrySummary> for PathKey {
 }
 
 struct BackgroundScanner {
-    fs: Arc<dyn Fs>,
     snapshot: Mutex<LocalSnapshot>,
-    notify: UnboundedSender<ScanState>,
+    fs: Arc<dyn Fs>,
+    status_updates_tx: UnboundedSender<ScanState>,
     executor: Arc<executor::Background>,
+    refresh_requests_rx: channel::Receiver<(Vec<PathBuf>, barrier::Sender)>,
+    prev_state: Mutex<(Snapshot, Vec<Arc<Path>>)>,
+    finished_initial_scan: bool,
 }
 
 impl BackgroundScanner {
     fn new(
         snapshot: LocalSnapshot,
-        notify: UnboundedSender<ScanState>,
         fs: Arc<dyn Fs>,
+        status_updates_tx: UnboundedSender<ScanState>,
         executor: Arc<executor::Background>,
+        refresh_requests_rx: channel::Receiver<(Vec<PathBuf>, barrier::Sender)>,
     ) -> Self {
         Self {
             fs,
-            snapshot: Mutex::new(snapshot),
-            notify,
+            status_updates_tx,
             executor,
+            refresh_requests_rx,
+            prev_state: Mutex::new((snapshot.snapshot.clone(), Vec::new())),
+            snapshot: Mutex::new(snapshot),
+            finished_initial_scan: false,
         }
     }
 
-    fn abs_path(&self) -> Arc<Path> {
-        self.snapshot.lock().abs_path.clone()
-    }
-
     async fn run(
-        self,
-        events_rx: impl Stream<Item = Vec<fsevent::Event>>,
-        mut changed_paths: channel::Receiver<(Vec<PathBuf>, barrier::Sender)>,
+        &mut self,
+        mut events_rx: Pin<Box<dyn Send + Stream<Item = Vec<fsevent::Event>>>>,
     ) {
         use futures::FutureExt as _;
 
-        // Retrieve the basic properties of the root node.
-        let root_char_bag;
-        let root_abs_path;
-        let root_inode;
-        let root_is_dir;
-        let next_entry_id;
-        {
-            let mut snapshot = self.snapshot.lock();
-            snapshot.scan_started();
-            root_char_bag = snapshot.root_char_bag;
-            root_abs_path = snapshot.abs_path.clone();
-            root_inode = snapshot.root_entry().map(|e| e.inode);
-            root_is_dir = snapshot.root_entry().map_or(false, |e| e.is_dir());
-            next_entry_id = snapshot.next_entry_id.clone();
-        }
+        let (root_abs_path, root_inode) = {
+            let snapshot = self.snapshot.lock();
+            (
+                snapshot.abs_path.clone(),
+                snapshot.root_entry().map(|e| e.inode),
+            )
+        };
 
         // Populate ignores above the root.
         let ignore_stack;
@@ -2207,198 +2177,220 @@ impl BackgroundScanner {
             }
         };
 
-        if root_is_dir {
-            let mut ancestor_inodes = TreeSet::default();
-            if let Some(root_inode) = root_inode {
-                ancestor_inodes.insert(root_inode);
+        // Perform an initial scan of the directory.
+        let (scan_job_tx, scan_job_rx) = channel::unbounded();
+        smol::block_on(scan_job_tx.send(ScanJob {
+            abs_path: root_abs_path,
+            path: Arc::from(Path::new("")),
+            ignore_stack,
+            ancestor_inodes: TreeSet::from_ordered_entries(root_inode),
+            scan_queue: scan_job_tx.clone(),
+        }))
+        .unwrap();
+        drop(scan_job_tx);
+        self.scan_dirs(true, scan_job_rx).await;
+        self.send_status_update(false, None);
+
+        // Process any any FS events that occurred while performing the initial scan.
+        // For these events, update events cannot be as precise, because we didn't
+        // have the previous state loaded yet.
+        if let Poll::Ready(Some(events)) = futures::poll!(events_rx.next()) {
+            let mut paths = events.into_iter().map(|e| e.path).collect::<Vec<_>>();
+            while let Poll::Ready(Some(more_events)) = futures::poll!(events_rx.next()) {
+                paths.extend(more_events.into_iter().map(|e| e.path));
             }
+            self.process_events(paths).await;
+            self.send_status_update(false, None);
+        }
 
-            let (tx, rx) = channel::unbounded();
-            self.executor
-                .block(tx.send(ScanJob {
-                    abs_path: root_abs_path.to_path_buf(),
-                    path: Arc::from(Path::new("")),
-                    ignore_stack,
-                    ancestor_inodes,
-                    scan_queue: tx.clone(),
-                }))
-                .unwrap();
-            drop(tx);
+        self.finished_initial_scan = true;
 
-            let progress_update_count = AtomicUsize::new(0);
-            self.executor
-                .scoped(|scope| {
-                    for _ in 0..self.executor.num_cpus() {
-                        scope.spawn(async {
-                            let mut last_progress_update_count = 0;
-                            let progress_update_timer = self.pause_between_progress_updates().fuse();
-                            futures::pin_mut!(progress_update_timer);
-                            loop {
-                                select_biased! {
-                                    // Send periodic progress updates to the worktree. Use an atomic counter
-                                    // to ensure that only one of the workers sends a progress update after
-                                    // the update interval elapses.
-                                    _ = progress_update_timer => {
-                                        match progress_update_count.compare_exchange(
-                                            last_progress_update_count,
-                                            last_progress_update_count + 1,
-                                            SeqCst,
-                                            SeqCst
-                                        ) {
-                                            Ok(_) => {
-                                                last_progress_update_count += 1;
-                                                if self
-                                                    .notify
-                                                    .unbounded_send(ScanState::Initializing {
-                                                        snapshot: self.snapshot.lock().clone(),
-                                                        barrier: None,
-                                                    })
-                                                    .is_err()
-                                                {
-                                                    break;
-                                                }
-                                            }
-                                            Err(current_count) => last_progress_update_count = current_count,
-                                        }
-                                        progress_update_timer.set(self.pause_between_progress_updates().fuse());
-                                    }
+        // Continue processing events until the worktree is dropped.
+        loop {
+            select_biased! {
+                // Process any path refresh requests from the worktree. Prioritize
+                // these before handling changes reported by the filesystem.
+                request = self.refresh_requests_rx.recv().fuse() => {
+                    let Ok((paths, barrier)) = request else { break };
+                    self.reload_entries_for_paths(paths, None).await;
+                    if !self.send_status_update(false, Some(barrier)) {
+                        break;
+                    }
+                }
 
-                                    // Refresh any paths requested by the main thread.
-                                    job = changed_paths.recv().fuse() => {
-                                        let Ok((abs_paths, barrier)) = job else { break };
-                                        self.update_entries_for_paths(abs_paths, None).await;
-                                        if self
-                                            .notify
-                                            .unbounded_send(ScanState::Initializing {
-                                                snapshot: self.snapshot.lock().clone(),
-                                                barrier: Some(barrier),
-                                            })
-                                            .is_err()
-                                        {
-                                            break;
-                                        }
-                                    }
+                events = events_rx.next().fuse() => {
+                    let Some(events) = events else { break };
+                    let mut paths = events.into_iter().map(|e| e.path).collect::<Vec<_>>();
+                    while let Poll::Ready(Some(more_events)) = futures::poll!(events_rx.next()) {
+                        paths.extend(more_events.into_iter().map(|e| e.path));
+                    }
+                    self.process_events(paths).await;
+                    self.send_status_update(false, None);
+                }
+            }
+        }
+    }
 
-                                    // Recursively load directories from the file system.
-                                    job = rx.recv().fuse() => {
-                                        let Ok(job) = job else { break };
-                                        if let Err(err) = self
-                                            .scan_dir(root_char_bag, next_entry_id.clone(), &job)
-                                            .await
-                                        {
-                                            log::error!("error scanning {:?}: {}", job.abs_path, err);
-                                        }
+    async fn process_events(&mut self, paths: Vec<PathBuf>) {
+        use futures::FutureExt as _;
+
+        let (scan_job_tx, scan_job_rx) = channel::unbounded();
+        if let Some(mut paths) = self
+            .reload_entries_for_paths(paths, Some(scan_job_tx.clone()))
+            .await
+        {
+            paths.sort_unstable();
+            util::extend_sorted(&mut self.prev_state.lock().1, paths, usize::MAX, Ord::cmp);
+        }
+        drop(scan_job_tx);
+        self.scan_dirs(false, scan_job_rx).await;
+
+        let (ignore_queue_tx, ignore_queue_rx) = channel::unbounded();
+        let snapshot = self.update_ignore_statuses(ignore_queue_tx);
+        self.executor
+            .scoped(|scope| {
+                for _ in 0..self.executor.num_cpus() {
+                    scope.spawn(async {
+                        loop {
+                            select_biased! {
+                                // Process any path refresh requests before moving on to process
+                                // the queue of ignore statuses.
+                                request = self.refresh_requests_rx.recv().fuse() => {
+                                    let Ok((paths, barrier)) = request else { break };
+                                    self.reload_entries_for_paths(paths, None).await;
+                                    if !self.send_status_update(false, Some(barrier)) {
+                                        return;
                                     }
                                 }
+
+                                // Recursively process directories whose ignores have changed.
+                                job = ignore_queue_rx.recv().fuse() => {
+                                    let Ok(job) = job else { break };
+                                    self.update_ignore_status(job, &snapshot).await;
+                                }
                             }
-                        });
-                    }
-                })
-                .await;
-        }
+                        }
+                    });
+                }
+            })
+            .await;
+
+        let mut snapshot = self.snapshot.lock();
+        let mut git_repositories = mem::take(&mut snapshot.git_repositories);
+        git_repositories.retain(|repo| snapshot.entry_for_path(&repo.git_dir_path).is_some());
+        snapshot.git_repositories = git_repositories;
+        snapshot.removed_entry_ids.clear();
+        snapshot.completed_scan_id = snapshot.scan_id;
+    }
 
-        self.snapshot.lock().scan_completed();
+    async fn scan_dirs(
+        &self,
+        enable_progress_updates: bool,
+        scan_jobs_rx: channel::Receiver<ScanJob>,
+    ) {
+        use futures::FutureExt as _;
 
         if self
-            .notify
-            .unbounded_send(ScanState::Initialized {
-                snapshot: self.snapshot.lock().clone(),
-            })
+            .status_updates_tx
+            .unbounded_send(ScanState::Started)
             .is_err()
         {
             return;
         }
 
-        // Process any events that occurred while performing the initial scan. These
-        // events can't be reported as precisely, because there is no snapshot of the
-        // worktree before they occurred.
-        futures::pin_mut!(events_rx);
-        if let Poll::Ready(Some(mut events)) = futures::poll!(events_rx.next()) {
-            while let Poll::Ready(Some(additional_events)) = futures::poll!(events_rx.next()) {
-                events.extend(additional_events);
-            }
-            let abs_paths = events.into_iter().map(|e| e.path).collect();
-            if self.notify.unbounded_send(ScanState::Updating).is_err() {
-                return;
-            }
-            if let Some(changes) = self.process_events(abs_paths, true).await {
-                if self
-                    .notify
-                    .unbounded_send(ScanState::Updated {
-                        snapshot: self.snapshot.lock().clone(),
-                        changes,
-                        barrier: None,
-                    })
-                    .is_err()
-                {
-                    return;
-                }
-            } else {
-                return;
-            }
-        }
+        let progress_update_count = AtomicUsize::new(0);
+        self.executor
+            .scoped(|scope| {
+                for _ in 0..self.executor.num_cpus() {
+                    scope.spawn(async {
+                        let mut last_progress_update_count = 0;
+                        let progress_update_timer = self.progress_timer(enable_progress_updates).fuse();
+                        futures::pin_mut!(progress_update_timer);
+
+                        loop {
+                            select_biased! {
+                                // Process any path refresh requests before moving on to process
+                                // the scan queue, so that user operations are prioritized.
+                                request = self.refresh_requests_rx.recv().fuse() => {
+                                    let Ok((paths, barrier)) = request else { break };
+                                    self.reload_entries_for_paths(paths, None).await;
+                                    if !self.send_status_update(false, Some(barrier)) {
+                                        return;
+                                    }
+                                }
 
-        // Continue processing events until the worktree is dropped.
-        loop {
-            let barrier;
-            let abs_paths;
-            select_biased! {
-                request = changed_paths.next().fuse() => {
-                    let Some((paths, b)) = request else { break };
-                    abs_paths = paths;
-                    barrier = Some(b);
-                }
-                events = events_rx.next().fuse() => {
-                    let Some(events) = events else { break };
-                    abs_paths = events.into_iter().map(|e| e.path).collect();
-                    barrier = None;
-                }
-            }
+                                // Send periodic progress updates to the worktree. Use an atomic counter
+                                // to ensure that only one of the workers sends a progress update after
+                                // the update interval elapses.
+                                _ = progress_update_timer => {
+                                    match progress_update_count.compare_exchange(
+                                        last_progress_update_count,
+                                        last_progress_update_count + 1,
+                                        SeqCst,
+                                        SeqCst
+                                    ) {
+                                        Ok(_) => {
+                                            last_progress_update_count += 1;
+                                            self.send_status_update(true, None);
+                                        }
+                                        Err(count) => {
+                                            last_progress_update_count = count;
+                                        }
+                                    }
+                                    progress_update_timer.set(self.progress_timer(enable_progress_updates).fuse());
+                                }
 
-            if self.notify.unbounded_send(ScanState::Updating).is_err() {
-                return;
-            }
-            if let Some(changes) = self.process_events(abs_paths, false).await {
-                if self
-                    .notify
-                    .unbounded_send(ScanState::Updated {
-                        snapshot: self.snapshot.lock().clone(),
-                        changes,
-                        barrier,
+                                // Recursively load directories from the file system.
+                                job = scan_jobs_rx.recv().fuse() => {
+                                    let Ok(job) = job else { break };
+                                    if let Err(err) = self.scan_dir(&job).await {
+                                        if job.path.as_ref() != Path::new("") {
+                                            log::error!("error scanning directory {:?}: {}", job.abs_path, err);
+                                        }
+                                    }
+                                }
+                            }
+                        }
                     })
-                    .is_err()
-                {
-                    return;
                 }
-            } else {
-                return;
-            }
-        }
+            })
+            .await;
     }
 
-    async fn pause_between_progress_updates(&self) {
-        #[cfg(any(test, feature = "test-support"))]
-        if self.fs.is_fake() {
-            return self.executor.simulate_random_delay().await;
-        }
-        smol::Timer::after(Duration::from_millis(100)).await;
+    fn send_status_update(&self, scanning: bool, barrier: Option<barrier::Sender>) -> bool {
+        let mut prev_state = self.prev_state.lock();
+        let snapshot = self.snapshot.lock().clone();
+        let mut old_snapshot = snapshot.snapshot.clone();
+        mem::swap(&mut old_snapshot, &mut prev_state.0);
+        let changed_paths = mem::take(&mut prev_state.1);
+        let changes = self.build_change_set(&old_snapshot, &snapshot.snapshot, changed_paths);
+        self.status_updates_tx
+            .unbounded_send(ScanState::Updated {
+                snapshot,
+                changes,
+                scanning,
+                barrier,
+            })
+            .is_ok()
     }
 
-    async fn scan_dir(
-        &self,
-        root_char_bag: CharBag,
-        next_entry_id: Arc<AtomicUsize>,
-        job: &ScanJob,
-    ) -> Result<()> {
+    async fn scan_dir(&self, job: &ScanJob) -> Result<()> {
         let mut new_entries: Vec<Entry> = Vec::new();
         let mut new_jobs: Vec<Option<ScanJob>> = Vec::new();
         let mut ignore_stack = job.ignore_stack.clone();
         let mut new_ignore = None;
-
+        let (root_abs_path, root_char_bag, next_entry_id) = {
+            let snapshot = self.snapshot.lock();
+            (
+                snapshot.abs_path().clone(),
+                snapshot.root_char_bag,
+                snapshot.next_entry_id.clone(),
+            )
+        };
         let mut child_paths = self.fs.read_dir(&job.abs_path).await?;
         while let Some(child_abs_path) = child_paths.next().await {
-            let child_abs_path = match child_abs_path {
-                Ok(child_abs_path) => child_abs_path,
+            let child_abs_path: Arc<Path> = match child_abs_path {
+                Ok(child_abs_path) => child_abs_path.into(),
                 Err(error) => {
                     log::error!("error processing entry {:?}", error);
                     continue;
@@ -2421,8 +2413,7 @@ impl BackgroundScanner {
                 match build_gitignore(&child_abs_path, self.fs.as_ref()).await {
                     Ok(ignore) => {
                         let ignore = Arc::new(ignore);
-                        ignore_stack =
-                            ignore_stack.append(job.abs_path.as_path().into(), ignore.clone());
+                        ignore_stack = ignore_stack.append(job.abs_path.clone(), ignore.clone());
                         new_ignore = Some(ignore);
                     }
                     Err(error) => {
@@ -2440,7 +2431,7 @@ impl BackgroundScanner {
                 // new jobs as well.
                 let mut new_jobs = new_jobs.iter_mut();
                 for entry in &mut new_entries {
-                    let entry_abs_path = self.abs_path().join(&entry.path);
+                    let entry_abs_path = root_abs_path.join(&entry.path);
                     entry.is_ignored =
                         ignore_stack.is_abs_path_ignored(&entry_abs_path, entry.is_dir());
 
@@ -2509,69 +2500,18 @@ impl BackgroundScanner {
         Ok(())
     }
 
-    async fn process_events(
-        &self,
-        abs_paths: Vec<PathBuf>,
-        received_before_initialized: bool,
-    ) -> Option<HashMap<Arc<Path>, PathChange>> {
-        let (scan_queue_tx, scan_queue_rx) = channel::unbounded();
-
-        let prev_snapshot = {
-            let mut snapshot = self.snapshot.lock();
-            snapshot.scan_started();
-            snapshot.clone()
-        };
-
-        let event_paths = self
-            .update_entries_for_paths(abs_paths, Some(scan_queue_tx))
-            .await?;
-
-        // Scan any directories that were created as part of this event batch.
-        self.executor
-            .scoped(|scope| {
-                for _ in 0..self.executor.num_cpus() {
-                    scope.spawn(async {
-                        while let Ok(job) = scan_queue_rx.recv().await {
-                            if let Err(err) = self
-                                .scan_dir(
-                                    prev_snapshot.root_char_bag,
-                                    prev_snapshot.next_entry_id.clone(),
-                                    &job,
-                                )
-                                .await
-                            {
-                                log::error!("error scanning {:?}: {}", job.abs_path, err);
-                            }
-                        }
-                    });
-                }
-            })
-            .await;
-
-        // Attempt to detect renames only over a single batch of file-system events.
-        self.snapshot.lock().removed_entry_ids.clear();
-
-        self.update_ignore_statuses().await;
-        self.update_git_repositories();
-        let changes = self.build_change_set(
-            prev_snapshot.snapshot,
-            event_paths,
-            received_before_initialized,
-        );
-        self.snapshot.lock().scan_completed();
-        Some(changes)
-    }
-
-    async fn update_entries_for_paths(
+    async fn reload_entries_for_paths(
         &self,
         mut abs_paths: Vec<PathBuf>,
         scan_queue_tx: Option<Sender<ScanJob>>,
     ) -> Option<Vec<Arc<Path>>> {
+        let doing_recursive_update = scan_queue_tx.is_some();
+
         abs_paths.sort_unstable();
         abs_paths.dedup_by(|a, b| a.starts_with(&b));
 
         let root_abs_path = self.snapshot.lock().abs_path.clone();
-        let root_canonical_path = self.fs.canonicalize(&root_abs_path).await.ok()?;
+        let root_canonical_path = self.fs.canonicalize(&root_abs_path).await.log_err()?;
         let metadata = futures::future::join_all(
             abs_paths
                 .iter()
@@ -2581,29 +2521,35 @@ impl BackgroundScanner {
         .await;
 
         let mut snapshot = self.snapshot.lock();
-        if scan_queue_tx.is_some() {
-            for abs_path in &abs_paths {
-                if let Ok(path) = abs_path.strip_prefix(&root_canonical_path) {
+
+        if snapshot.completed_scan_id == snapshot.scan_id {
+            snapshot.scan_id += 1;
+            if !doing_recursive_update {
+                snapshot.completed_scan_id = snapshot.scan_id;
+            }
+        }
+
+        // Remove any entries for paths that no longer exist or are being recursively
+        // refreshed. Do this before adding any new entries, so that renames can be
+        // detected regardless of the order of the paths.
+        let mut event_paths = Vec::<Arc<Path>>::with_capacity(abs_paths.len());
+        for (abs_path, metadata) in abs_paths.iter().zip(metadata.iter()) {
+            if let Ok(path) = abs_path.strip_prefix(&root_canonical_path) {
+                if matches!(metadata, Ok(None)) || doing_recursive_update {
                     snapshot.remove_path(path);
                 }
+                event_paths.push(path.into());
+            } else {
+                log::error!(
+                    "unexpected event {:?} for root path {:?}",
+                    abs_path,
+                    root_canonical_path
+                );
             }
         }
 
-        let mut event_paths = Vec::with_capacity(abs_paths.len());
-        for (abs_path, metadata) in abs_paths.into_iter().zip(metadata.into_iter()) {
-            let path: Arc<Path> = match abs_path.strip_prefix(&root_canonical_path) {
-                Ok(path) => Arc::from(path.to_path_buf()),
-                Err(_) => {
-                    log::error!(
-                        "unexpected event {:?} for root path {:?}",
-                        abs_path,
-                        root_canonical_path
-                    );
-                    continue;
-                }
-            };
-            event_paths.push(path.clone());
-            let abs_path = root_abs_path.join(&path);
+        for (path, metadata) in event_paths.iter().cloned().zip(metadata.into_iter()) {
+            let abs_path: Arc<Path> = root_abs_path.join(&path).into();
 
             match metadata {
                 Ok(Some(metadata)) => {
@@ -2628,15 +2574,14 @@ impl BackgroundScanner {
                         let mut ancestor_inodes = snapshot.ancestor_inodes_for_path(&path);
                         if metadata.is_dir && !ancestor_inodes.contains(&metadata.inode) {
                             ancestor_inodes.insert(metadata.inode);
-                            self.executor
-                                .block(scan_queue_tx.send(ScanJob {
-                                    abs_path,
-                                    path,
-                                    ignore_stack,
-                                    ancestor_inodes,
-                                    scan_queue: scan_queue_tx.clone(),
-                                }))
-                                .unwrap();
+                            smol::block_on(scan_queue_tx.send(ScanJob {
+                                abs_path,
+                                path,
+                                ignore_stack,
+                                ancestor_inodes,
+                                scan_queue: scan_queue_tx.clone(),
+                            }))
+                            .unwrap();
                         }
                     }
                 }
@@ -2651,7 +2596,10 @@ impl BackgroundScanner {
         Some(event_paths)
     }
 
-    async fn update_ignore_statuses(&self) {
+    fn update_ignore_statuses(
+        &self,
+        ignore_queue_tx: Sender<UpdateIgnoreStatusJob>,
+    ) -> LocalSnapshot {
         let mut snapshot = self.snapshot.lock().clone();
         let mut ignores_to_update = Vec::new();
         let mut ignores_to_delete = Vec::new();
@@ -2676,7 +2624,6 @@ impl BackgroundScanner {
                 .remove(&parent_abs_path);
         }
 
-        let (ignore_queue_tx, ignore_queue_rx) = channel::unbounded();
         ignores_to_update.sort_unstable();
         let mut ignores_to_update = ignores_to_update.into_iter().peekable();
         while let Some(parent_abs_path) = ignores_to_update.next() {
@@ -2688,35 +2635,15 @@ impl BackgroundScanner {
             }
 
             let ignore_stack = snapshot.ignore_stack_for_abs_path(&parent_abs_path, true);
-            ignore_queue_tx
-                .send(UpdateIgnoreStatusJob {
-                    abs_path: parent_abs_path,
-                    ignore_stack,
-                    ignore_queue: ignore_queue_tx.clone(),
-                })
-                .await
-                .unwrap();
+            smol::block_on(ignore_queue_tx.send(UpdateIgnoreStatusJob {
+                abs_path: parent_abs_path,
+                ignore_stack,
+                ignore_queue: ignore_queue_tx.clone(),
+            }))
+            .unwrap();
         }
-        drop(ignore_queue_tx);
 
-        self.executor
-            .scoped(|scope| {
-                for _ in 0..self.executor.num_cpus() {
-                    scope.spawn(async {
-                        while let Ok(job) = ignore_queue_rx.recv().await {
-                            self.update_ignore_status(job, &snapshot).await;
-                        }
-                    });
-                }
-            })
-            .await;
-    }
-
-    fn update_git_repositories(&self) {
-        let mut snapshot = self.snapshot.lock();
-        let mut git_repositories = mem::take(&mut snapshot.git_repositories);
-        git_repositories.retain(|repo| snapshot.entry_for_path(&repo.git_dir_path).is_some());
-        snapshot.git_repositories = git_repositories;
+        snapshot
     }
 
     async fn update_ignore_status(&self, job: UpdateIgnoreStatusJob, snapshot: &LocalSnapshot) {
@@ -2730,7 +2657,7 @@ impl BackgroundScanner {
         let path = job.abs_path.strip_prefix(&snapshot.abs_path).unwrap();
         for mut entry in snapshot.child_entries(path).cloned() {
             let was_ignored = entry.is_ignored;
-            let abs_path = self.abs_path().join(&entry.path);
+            let abs_path = snapshot.abs_path().join(&entry.path);
             entry.is_ignored = ignore_stack.is_abs_path_ignored(&abs_path, entry.is_dir());
             if entry.is_dir() {
                 let child_ignore_stack = if entry.is_ignored {
@@ -2764,16 +2691,16 @@ impl BackgroundScanner {
 
     fn build_change_set(
         &self,
-        old_snapshot: Snapshot,
+        old_snapshot: &Snapshot,
+        new_snapshot: &Snapshot,
         event_paths: Vec<Arc<Path>>,
-        received_before_initialized: bool,
     ) -> HashMap<Arc<Path>, PathChange> {
         use PathChange::{Added, AddedOrUpdated, Removed, Updated};
 
-        let new_snapshot = self.snapshot.lock();
         let mut changes = HashMap::default();
         let mut old_paths = old_snapshot.entries_by_path.cursor::<PathKey>();
         let mut new_paths = new_snapshot.entries_by_path.cursor::<PathKey>();
+        let received_before_initialized = !self.finished_initial_scan;
 
         for path in event_paths {
             let path = PathKey(path);
@@ -2801,9 +2728,9 @@ impl BackgroundScanner {
                                     // If the worktree was not fully initialized when this event was generated,
                                     // we can't know whether this entry was added during the scan or whether
                                     // it was merely updated.
-                                    changes.insert(old_entry.path.clone(), AddedOrUpdated);
+                                    changes.insert(new_entry.path.clone(), AddedOrUpdated);
                                 } else if old_entry.mtime != new_entry.mtime {
-                                    changes.insert(old_entry.path.clone(), Updated);
+                                    changes.insert(new_entry.path.clone(), Updated);
                                 }
                                 old_paths.next(&());
                                 new_paths.next(&());
@@ -2828,6 +2755,19 @@ impl BackgroundScanner {
         }
         changes
     }
+
+    async fn progress_timer(&self, running: bool) {
+        if !running {
+            return futures::future::pending().await;
+        }
+
+        #[cfg(any(test, feature = "test-support"))]
+        if self.fs.is_fake() {
+            return self.executor.simulate_random_delay().await;
+        }
+
+        smol::Timer::after(Duration::from_millis(100)).await;
+    }
 }
 
 fn char_bag_for_path(root_char_bag: CharBag, path: &Path) -> CharBag {
@@ -2841,7 +2781,7 @@ fn char_bag_for_path(root_char_bag: CharBag, path: &Path) -> CharBag {
 }
 
 struct ScanJob {
-    abs_path: PathBuf,
+    abs_path: Arc<Path>,
     path: Arc<Path>,
     ignore_stack: Arc<IgnoreStack>,
     scan_queue: Sender<ScanJob>,
@@ -3526,7 +3466,7 @@ mod tests {
 
         let fs = FakeFs::new(cx.background());
         fs.insert_tree(
-            "/a",
+            "/root",
             json!({
                 "b": {},
                 "c": {},

crates/rpc/src/peer.rs 🔗

@@ -7,7 +7,7 @@ use collections::HashMap;
 use futures::{
     channel::{mpsc, oneshot},
     stream::BoxStream,
-    FutureExt, SinkExt, StreamExt,
+    FutureExt, SinkExt, StreamExt, TryFutureExt,
 };
 use parking_lot::{Mutex, RwLock};
 use serde::{ser::SerializeStruct, Serialize};
@@ -71,6 +71,7 @@ impl<T> Clone for Receipt<T> {
 
 impl<T> Copy for Receipt<T> {}
 
+#[derive(Clone, Debug)]
 pub struct TypedEnvelope<T> {
     pub sender_id: ConnectionId,
     pub original_sender_id: Option<PeerId>,
@@ -370,6 +371,15 @@ impl Peer {
         receiver_id: ConnectionId,
         request: T,
     ) -> impl Future<Output = Result<T::Response>> {
+        self.request_internal(None, receiver_id, request)
+            .map_ok(|envelope| envelope.payload)
+    }
+
+    pub fn request_envelope<T: RequestMessage>(
+        &self,
+        receiver_id: ConnectionId,
+        request: T,
+    ) -> impl Future<Output = Result<TypedEnvelope<T::Response>>> {
         self.request_internal(None, receiver_id, request)
     }
 
@@ -380,6 +390,7 @@ impl Peer {
         request: T,
     ) -> impl Future<Output = Result<T::Response>> {
         self.request_internal(Some(sender_id), receiver_id, request)
+            .map_ok(|envelope| envelope.payload)
     }
 
     pub fn request_internal<T: RequestMessage>(
@@ -387,7 +398,7 @@ impl Peer {
         original_sender_id: Option<ConnectionId>,
         receiver_id: ConnectionId,
         request: T,
-    ) -> impl Future<Output = Result<T::Response>> {
+    ) -> impl Future<Output = Result<TypedEnvelope<T::Response>>> {
         let (tx, rx) = oneshot::channel();
         let send = self.connection_state(receiver_id).and_then(|connection| {
             let message_id = connection.next_message_id.fetch_add(1, SeqCst);
@@ -410,6 +421,7 @@ impl Peer {
         async move {
             send?;
             let (response, _barrier) = rx.await.map_err(|_| anyhow!("connection was closed"))?;
+
             if let Some(proto::envelope::Payload::Error(error)) = &response.payload {
                 Err(anyhow!(
                     "RPC request {} failed - {}",
@@ -417,8 +429,13 @@ impl Peer {
                     error.message
                 ))
             } else {
-                T::Response::from_envelope(response)
-                    .ok_or_else(|| anyhow!("received response of the wrong type"))
+                Ok(TypedEnvelope {
+                    message_id: response.id,
+                    sender_id: receiver_id,
+                    original_sender_id: response.original_sender_id,
+                    payload: T::Response::from_envelope(response)
+                        .ok_or_else(|| anyhow!("received response of the wrong type"))?,
+                })
             }
         }
     }

crates/rpc/src/proto.rs 🔗

@@ -233,7 +233,7 @@ messages!(
     (UpdateProject, Foreground),
     (UpdateProjectCollaborator, Foreground),
     (UpdateWorktree, Foreground),
-    (UpdateDiffBase, Background),
+    (UpdateDiffBase, Foreground),
     (GetPrivateUserInfo, Foreground),
     (GetPrivateUserInfoResponse, Foreground),
 );

crates/rpc/src/rpc.rs 🔗

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

crates/sum_tree/src/tree_map.rs 🔗

@@ -154,6 +154,12 @@ impl<K> TreeSet<K>
 where
     K: Clone + Debug + Default + Ord,
 {
+    pub fn from_ordered_entries(entries: impl IntoIterator<Item = K>) -> Self {
+        Self(TreeMap::from_ordered_entries(
+            entries.into_iter().map(|key| (key, ())),
+        ))
+    }
+
     pub fn insert(&mut self, key: K) {
         self.0.insert(key, ());
     }

crates/text/src/text.rs 🔗

@@ -11,14 +11,14 @@ mod tests;
 mod undo_map;
 
 pub use anchor::*;
-use anyhow::Result;
+use anyhow::{anyhow, Result};
 use clock::ReplicaId;
 use collections::{HashMap, HashSet};
 use fs::LineEnding;
 use locator::Locator;
 use operation_queue::OperationQueue;
 pub use patch::Patch;
-use postage::{barrier, oneshot, prelude::*};
+use postage::{oneshot, prelude::*};
 
 pub use rope::*;
 pub use selection::*;
@@ -52,7 +52,7 @@ pub struct Buffer {
     pub lamport_clock: clock::Lamport,
     subscriptions: Topic,
     edit_id_resolvers: HashMap<clock::Local, Vec<oneshot::Sender<()>>>,
-    version_barriers: Vec<(clock::Global, barrier::Sender)>,
+    wait_for_version_txs: Vec<(clock::Global, oneshot::Sender<()>)>,
 }
 
 #[derive(Clone)]
@@ -522,7 +522,7 @@ impl Buffer {
             lamport_clock,
             subscriptions: Default::default(),
             edit_id_resolvers: Default::default(),
-            version_barriers: Default::default(),
+            wait_for_version_txs: Default::default(),
         }
     }
 
@@ -793,8 +793,14 @@ impl Buffer {
                 }
             }
         }
-        self.version_barriers
-            .retain(|(version, _)| !self.snapshot.version().observed_all(version));
+        self.wait_for_version_txs.retain_mut(|(version, tx)| {
+            if self.snapshot.version().observed_all(version) {
+                tx.try_send(()).ok();
+                false
+            } else {
+                true
+            }
+        });
         Ok(())
     }
 
@@ -1305,7 +1311,7 @@ impl Buffer {
     pub fn wait_for_edits(
         &mut self,
         edit_ids: impl IntoIterator<Item = clock::Local>,
-    ) -> impl 'static + Future<Output = ()> {
+    ) -> impl 'static + Future<Output = Result<()>> {
         let mut futures = Vec::new();
         for edit_id in edit_ids {
             if !self.version.observed(edit_id) {
@@ -1317,15 +1323,18 @@ impl Buffer {
 
         async move {
             for mut future in futures {
-                future.recv().await;
+                if future.recv().await.is_none() {
+                    Err(anyhow!("gave up waiting for edits"))?;
+                }
             }
+            Ok(())
         }
     }
 
     pub fn wait_for_anchors<'a>(
         &mut self,
         anchors: impl IntoIterator<Item = &'a Anchor>,
-    ) -> impl 'static + Future<Output = ()> {
+    ) -> impl 'static + Future<Output = Result<()>> {
         let mut futures = Vec::new();
         for anchor in anchors {
             if !self.version.observed(anchor.timestamp)
@@ -1343,21 +1352,36 @@ impl Buffer {
 
         async move {
             for mut future in futures {
-                future.recv().await;
+                if future.recv().await.is_none() {
+                    Err(anyhow!("gave up waiting for anchors"))?;
+                }
             }
+            Ok(())
         }
     }
 
-    pub fn wait_for_version(&mut self, version: clock::Global) -> impl Future<Output = ()> {
-        let (tx, mut rx) = barrier::channel();
+    pub fn wait_for_version(&mut self, version: clock::Global) -> impl Future<Output = Result<()>> {
+        let mut rx = None;
         if !self.snapshot.version.observed_all(&version) {
-            self.version_barriers.push((version, tx));
+            let channel = oneshot::channel();
+            self.wait_for_version_txs.push((version, channel.0));
+            rx = Some(channel.1);
         }
         async move {
-            rx.recv().await;
+            if let Some(mut rx) = rx {
+                if rx.recv().await.is_none() {
+                    Err(anyhow!("gave up waiting for version"))?;
+                }
+            }
+            Ok(())
         }
     }
 
+    pub fn give_up_waiting(&mut self) {
+        self.edit_id_resolvers.clear();
+        self.wait_for_version_txs.clear();
+    }
+
     fn resolve_edit(&mut self, edit_id: clock::Local) {
         for mut tx in self
             .edit_id_resolvers
@@ -1365,7 +1389,7 @@ impl Buffer {
             .into_iter()
             .flatten()
         {
-            let _ = tx.try_send(());
+            tx.try_send(()).ok();
         }
     }
 }
@@ -1480,12 +1504,11 @@ impl Buffer {
         start..end
     }
 
-    #[allow(clippy::type_complexity)]
-    pub fn randomly_edit<T>(
-        &mut self,
+    pub fn get_random_edits<T>(
+        &self,
         rng: &mut T,
         edit_count: usize,
-    ) -> (Vec<(Range<usize>, Arc<str>)>, Operation)
+    ) -> Vec<(Range<usize>, Arc<str>)>
     where
         T: rand::Rng,
     {
@@ -1504,8 +1527,21 @@ impl Buffer {
 
             edits.push((range, new_text.into()));
         }
+        edits
+    }
 
+    #[allow(clippy::type_complexity)]
+    pub fn randomly_edit<T>(
+        &mut self,
+        rng: &mut T,
+        edit_count: usize,
+    ) -> (Vec<(Range<usize>, Arc<str>)>, Operation)
+    where
+        T: rand::Rng,
+    {
+        let mut edits = self.get_random_edits(rng, edit_count);
         log::info!("mutating buffer {} with {:?}", self.replica_id, edits);
+
         let op = self.edit(edits.iter().cloned());
         if let Operation::Edit(edit) = &op {
             assert_eq!(edits.len(), edit.new_text.len());

crates/util/src/github.rs 🔗

@@ -9,13 +9,13 @@ pub struct GitHubLspBinaryVersion {
     pub url: String,
 }
 
-#[derive(Deserialize)]
+#[derive(Deserialize, Debug)]
 pub struct GithubRelease {
     pub name: String,
     pub assets: Vec<GithubReleaseAsset>,
 }
 
-#[derive(Deserialize)]
+#[derive(Deserialize, Debug)]
 pub struct GithubReleaseAsset {
     pub name: String,
     pub browser_download_url: String,
@@ -40,7 +40,13 @@ pub async fn latest_github_release(
         .await
         .context("error reading latest release")?;
 
-    let release: GithubRelease =
-        serde_json::from_slice(body.as_slice()).context("error deserializing latest release")?;
-    Ok(release)
+    let release = serde_json::from_slice::<GithubRelease>(body.as_slice());
+    if release.is_err() {
+        log::error!(
+            "Github API response text: {:?}",
+            String::from_utf8_lossy(body.as_slice())
+        );
+    }
+
+    release.context("error deserializing latest release")
 }

crates/workspace/src/item.rs 🔗

@@ -417,7 +417,7 @@ impl<T: Item> ItemHandle for ViewHandle<T> {
                     for item_event in T::to_item_events(event).into_iter() {
                         match item_event {
                             ItemEvent::CloseItem => {
-                                Pane::close_item(workspace, pane, item.id(), cx)
+                                Pane::close_item_by_id(workspace, pane, item.id(), cx)
                                     .detach_and_log_err(cx);
                                 return;
                             }

crates/workspace/src/pane.rs 🔗

@@ -23,8 +23,8 @@ use gpui::{
     impl_actions, impl_internal_actions,
     keymap_matcher::KeymapContext,
     platform::{CursorStyle, MouseButton, NavigationDirection, PromptLevel},
-    Action, AnyViewHandle, AnyWeakViewHandle, AppContext, AsyncAppContext, Entity, EventContext,
-    ModelHandle, MouseRegion, Quad, Task, View, ViewContext, ViewHandle, WeakViewHandle,
+    Action, AnyViewHandle, AnyWeakViewHandle, AppContext, AsyncAppContext, Entity, ModelHandle,
+    MouseRegion, Quad, Task, View, ViewContext, ViewHandle, WeakViewHandle,
 };
 use project::{Project, ProjectEntryId, ProjectPath};
 use serde::Deserialize;
@@ -36,6 +36,24 @@ use util::ResultExt;
 #[derive(Clone, Deserialize, PartialEq)]
 pub struct ActivateItem(pub usize);
 
+#[derive(Clone, PartialEq)]
+pub struct CloseItemById {
+    pub item_id: usize,
+    pub pane: WeakViewHandle<Pane>,
+}
+
+#[derive(Clone, PartialEq)]
+pub struct CloseItemsToTheLeftById {
+    pub item_id: usize,
+    pub pane: WeakViewHandle<Pane>,
+}
+
+#[derive(Clone, PartialEq)]
+pub struct CloseItemsToTheRightById {
+    pub item_id: usize,
+    pub pane: WeakViewHandle<Pane>,
+}
+
 actions!(
     pane,
     [
@@ -56,12 +74,6 @@ actions!(
     ]
 );
 
-#[derive(Clone, PartialEq)]
-pub struct CloseItem {
-    pub item_id: usize,
-    pub pane: WeakViewHandle<Pane>,
-}
-
 #[derive(Clone, PartialEq)]
 pub struct MoveItem {
     pub item_id: usize,
@@ -91,11 +103,21 @@ pub struct DeployDockMenu;
 #[derive(Clone, PartialEq)]
 pub struct DeployNewMenu;
 
+#[derive(Clone, PartialEq)]
+pub struct DeployTabContextMenu {
+    pub position: Vector2F,
+    pub item_id: usize,
+    pub pane: WeakViewHandle<Pane>,
+}
+
 impl_actions!(pane, [GoBack, GoForward, ActivateItem]);
 impl_internal_actions!(
     pane,
     [
-        CloseItem,
+        CloseItemById,
+        CloseItemsToTheLeftById,
+        CloseItemsToTheRightById,
+        DeployTabContextMenu,
         DeploySplitMenu,
         DeployNewMenu,
         DeployDockMenu,
@@ -126,14 +148,34 @@ pub fn init(cx: &mut AppContext) {
     cx.add_async_action(Pane::close_items_to_the_left);
     cx.add_async_action(Pane::close_items_to_the_right);
     cx.add_async_action(Pane::close_all_items);
-    cx.add_async_action(|workspace: &mut Workspace, action: &CloseItem, cx| {
+    cx.add_async_action(|workspace: &mut Workspace, action: &CloseItemById, cx| {
         let pane = action.pane.upgrade(cx)?;
-        let task = Pane::close_item(workspace, pane, action.item_id, cx);
+        let task = Pane::close_item_by_id(workspace, pane, action.item_id, cx);
         Some(cx.foreground().spawn(async move {
             task.await?;
             Ok(())
         }))
     });
+    cx.add_async_action(
+        |workspace: &mut Workspace, action: &CloseItemsToTheLeftById, cx| {
+            let pane = action.pane.upgrade(cx)?;
+            let task = Pane::close_items_to_the_left_by_id(workspace, pane, action.item_id, cx);
+            Some(cx.foreground().spawn(async move {
+                task.await?;
+                Ok(())
+            }))
+        },
+    );
+    cx.add_async_action(
+        |workspace: &mut Workspace, action: &CloseItemsToTheRightById, cx| {
+            let pane = action.pane.upgrade(cx)?;
+            let task = Pane::close_items_to_the_right_by_id(workspace, pane, action.item_id, cx);
+            Some(cx.foreground().spawn(async move {
+                task.await?;
+                Ok(())
+            }))
+        },
+    );
     cx.add_action(
         |workspace,
          MoveItem {
@@ -167,6 +209,7 @@ pub fn init(cx: &mut AppContext) {
     cx.add_action(Pane::deploy_split_menu);
     cx.add_action(Pane::deploy_dock_menu);
     cx.add_action(Pane::deploy_new_menu);
+    cx.add_action(Pane::deploy_tab_context_menu);
     cx.add_action(|workspace: &mut Workspace, _: &ReopenClosedItem, cx| {
         Pane::reopen_closed_item(workspace, cx).detach();
     });
@@ -213,6 +256,7 @@ pub struct Pane {
     nav_history: Rc<RefCell<NavHistory>>,
     toolbar: ViewHandle<Toolbar>,
     tab_bar_context_menu: TabBarContextMenu,
+    tab_context_menu: ViewHandle<ContextMenu>,
     docked: Option<DockAnchor>,
     _background_actions: BackgroundActions,
     _workspace_id: usize,
@@ -318,6 +362,7 @@ impl Pane {
                 kind: TabBarContextMenuKind::New,
                 handle: context_menu,
             },
+            tab_context_menu: cx.add_view(ContextMenu::new),
             docked,
             _background_actions: background_actions,
             _workspace_id: workspace_id,
@@ -741,9 +786,7 @@ impl Pane {
         let pane = pane_handle.read(cx);
         let active_item_id = pane.items[pane.active_item_index].id();
 
-        let task = Self::close_items(workspace, pane_handle, cx, move |item_id| {
-            item_id == active_item_id
-        });
+        let task = Self::close_item_by_id(workspace, pane_handle, active_item_id, cx);
 
         Some(cx.foreground().spawn(async move {
             task.await?;
@@ -751,6 +794,17 @@ impl Pane {
         }))
     }
 
+    pub fn close_item_by_id(
+        workspace: &mut Workspace,
+        pane: ViewHandle<Pane>,
+        item_id_to_close: usize,
+        cx: &mut ViewContext<Workspace>,
+    ) -> Task<Result<()>> {
+        Self::close_items(workspace, pane, cx, move |view_id| {
+            view_id == item_id_to_close
+        })
+    }
+
     pub fn close_inactive_items(
         workspace: &mut Workspace,
         _: &CloseInactiveItems,
@@ -803,20 +857,35 @@ impl Pane {
         let pane = pane_handle.read(cx);
         let active_item_id = pane.items[pane.active_item_index].id();
 
+        let task = Self::close_items_to_the_left_by_id(workspace, pane_handle, active_item_id, cx);
+
+        Some(cx.foreground().spawn(async move {
+            task.await?;
+            Ok(())
+        }))
+    }
+
+    pub fn close_items_to_the_left_by_id(
+        workspace: &mut Workspace,
+        pane: ViewHandle<Pane>,
+        item_id: usize,
+        cx: &mut ViewContext<Workspace>,
+    ) -> Task<Result<()>> {
         let item_ids: Vec<_> = pane
+            .read(cx)
             .items()
-            .take_while(|item| item.id() != active_item_id)
+            .take_while(|item| item.id() != item_id)
             .map(|item| item.id())
             .collect();
 
-        let task = Self::close_items(workspace, pane_handle, cx, move |item_id| {
+        let task = Self::close_items(workspace, pane, cx, move |item_id| {
             item_ids.contains(&item_id)
         });
 
-        Some(cx.foreground().spawn(async move {
+        cx.foreground().spawn(async move {
             task.await?;
             Ok(())
-        }))
+        })
     }
 
     pub fn close_items_to_the_right(
@@ -828,21 +897,36 @@ impl Pane {
         let pane = pane_handle.read(cx);
         let active_item_id = pane.items[pane.active_item_index].id();
 
+        let task = Self::close_items_to_the_right_by_id(workspace, pane_handle, active_item_id, cx);
+
+        Some(cx.foreground().spawn(async move {
+            task.await?;
+            Ok(())
+        }))
+    }
+
+    pub fn close_items_to_the_right_by_id(
+        workspace: &mut Workspace,
+        pane: ViewHandle<Pane>,
+        item_id: usize,
+        cx: &mut ViewContext<Workspace>,
+    ) -> Task<Result<()>> {
         let item_ids: Vec<_> = pane
+            .read(cx)
             .items()
             .rev()
-            .take_while(|item| item.id() != active_item_id)
+            .take_while(|item| item.id() != item_id)
             .map(|item| item.id())
             .collect();
 
-        let task = Self::close_items(workspace, pane_handle, cx, move |item_id| {
+        let task = Self::close_items(workspace, pane, cx, move |item_id| {
             item_ids.contains(&item_id)
         });
 
-        Some(cx.foreground().spawn(async move {
+        cx.foreground().spawn(async move {
             task.await?;
             Ok(())
-        }))
+        })
     }
 
     pub fn close_all_items(
@@ -860,17 +944,6 @@ impl Pane {
         }))
     }
 
-    pub fn close_item(
-        workspace: &mut Workspace,
-        pane: ViewHandle<Pane>,
-        item_id_to_close: usize,
-        cx: &mut ViewContext<Workspace>,
-    ) -> Task<Result<()>> {
-        Self::close_items(workspace, pane, cx, move |view_id| {
-            view_id == item_id_to_close
-        })
-    }
-
     pub fn close_items(
         workspace: &mut Workspace,
         pane: ViewHandle<Pane>,
@@ -1206,6 +1279,65 @@ impl Pane {
         self.tab_bar_context_menu.kind = TabBarContextMenuKind::New;
     }
 
+    fn deploy_tab_context_menu(
+        &mut self,
+        action: &DeployTabContextMenu,
+        cx: &mut ViewContext<Self>,
+    ) {
+        let target_item_id = action.item_id;
+        let target_pane = action.pane.clone();
+        let active_item_id = self.items[self.active_item_index].id();
+        let is_active_item = target_item_id == active_item_id;
+
+        // The `CloseInactiveItems` action should really be called "CloseOthers" and the behaviour should be dynamically based on the tab the action is ran on.  Currenlty, this is a weird action because you can run it on a non-active tab and it will close everything by the actual active tab
+
+        self.tab_context_menu.update(cx, |menu, cx| {
+            menu.show(
+                action.position,
+                AnchorCorner::TopLeft,
+                if is_active_item {
+                    vec![
+                        ContextMenuItem::item("Close Active Item", CloseActiveItem),
+                        ContextMenuItem::item("Close Inactive Items", CloseInactiveItems),
+                        ContextMenuItem::item("Close Clean Items", CloseCleanItems),
+                        ContextMenuItem::item("Close Items To The Left", CloseItemsToTheLeft),
+                        ContextMenuItem::item("Close Items To The Right", CloseItemsToTheRight),
+                        ContextMenuItem::item("Close All Items", CloseAllItems),
+                    ]
+                } else {
+                    // In the case of the user right clicking on a non-active tab, for some item-closing commands, we need to provide the id of the tab, for the others, we can reuse the existing command.
+                    vec![
+                        ContextMenuItem::item(
+                            "Close Inactive Item",
+                            CloseItemById {
+                                item_id: target_item_id,
+                                pane: target_pane.clone(),
+                            },
+                        ),
+                        ContextMenuItem::item("Close Inactive Items", CloseInactiveItems),
+                        ContextMenuItem::item("Close Clean Items", CloseCleanItems),
+                        ContextMenuItem::item(
+                            "Close Items To The Left",
+                            CloseItemsToTheLeftById {
+                                item_id: target_item_id,
+                                pane: target_pane.clone(),
+                            },
+                        ),
+                        ContextMenuItem::item(
+                            "Close Items To The Right",
+                            CloseItemsToTheRightById {
+                                item_id: target_item_id,
+                                pane: target_pane.clone(),
+                            },
+                        ),
+                        ContextMenuItem::item("Close All Items", CloseAllItems),
+                    ]
+                },
+                cx,
+            );
+        });
+    }
+
     pub fn toolbar(&self) -> &ViewHandle<Toolbar> {
         &self.toolbar
     }
@@ -1276,13 +1408,22 @@ impl Pane {
                             })
                             .on_click(MouseButton::Middle, {
                                 let item = item.clone();
-                                move |_, _, cx: &mut EventContext<Self>| {
-                                    cx.dispatch_action(CloseItem {
+                                let pane = pane.clone();
+                                move |_, _, cx| {
+                                    cx.dispatch_action(CloseItemById {
                                         item_id: item.id(),
                                         pane: pane.clone(),
                                     })
                                 }
                             })
+                            .on_down(MouseButton::Right, move |e, _, cx| {
+                                let item = item.clone();
+                                cx.dispatch_action(DeployTabContextMenu {
+                                    position: e.position,
+                                    item_id: item.id(),
+                                    pane: pane.clone(),
+                                });
+                            })
                             .boxed()
                         }
                     });
@@ -1457,7 +1598,7 @@ impl Pane {
                         .on_click(MouseButton::Left, {
                             let pane = pane.clone();
                             move |_, _, cx| {
-                                cx.dispatch_action(CloseItem {
+                                cx.dispatch_action(CloseItemById {
                                     item_id,
                                     pane: pane.clone(),
                                 })
@@ -1532,11 +1673,7 @@ impl Pane {
             .boxed()
     }
 
-    fn render_blank_pane(
-        &mut self,
-        theme: &Theme,
-        _cx: &mut ViewContext<Self>,
-    ) -> Element<Self> {
+    fn render_blank_pane(&mut self, theme: &Theme, _cx: &mut ViewContext<Self>) -> Element<Self> {
         let background = theme.workspace.background;
         Empty::new()
             .contained()
@@ -1635,6 +1772,7 @@ impl View for Pane {
                                 .flex(1., true)
                                 .boxed()
                             })
+                            .with_child(ChildView::new(&self.tab_context_menu, cx).boxed())
                             .boxed()
                     } else {
                         enum EmptyPane {}
@@ -2237,14 +2375,14 @@ mod tests {
         let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
         let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
 
-        add_labled_item(&workspace, &pane, "A", cx);
-        add_labled_item(&workspace, &pane, "B", cx);
-        add_labled_item(&workspace, &pane, "C", cx);
-        add_labled_item(&workspace, &pane, "D", cx);
+        add_labeled_item(&workspace, &pane, "A", false, cx);
+        add_labeled_item(&workspace, &pane, "B", false, cx);
+        add_labeled_item(&workspace, &pane, "C", false, cx);
+        add_labeled_item(&workspace, &pane, "D", false, cx);
         assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
 
         pane.update(cx, |pane, cx| pane.activate_item(1, false, false, cx));
-        add_labled_item(&workspace, &pane, "1", cx);
+        add_labeled_item(&workspace, &pane, "1", false, cx);
         assert_item_labels(&pane, ["A", "B", "1*", "C", "D"], cx);
 
         workspace.update(cx, |workspace, cx| {
@@ -2275,14 +2413,125 @@ mod tests {
         assert_item_labels(&pane, ["A*"], cx);
     }
 
-    fn add_labled_item(
+    #[gpui::test]
+    async fn test_close_inactive_items(deterministic: Arc<Deterministic>, cx: &mut TestAppContext) {
+        Settings::test_async(cx);
+        let fs = FakeFs::new(cx.background());
+
+        let project = Project::test(fs, None, cx).await;
+        let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
+        let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
+
+        set_labeled_items(&workspace, &pane, ["A", "B", "C*", "D", "E"], cx);
+
+        workspace.update(cx, |workspace, cx| {
+            Pane::close_inactive_items(workspace, &CloseInactiveItems, cx);
+        });
+
+        deterministic.run_until_parked();
+        assert_item_labels(&pane, ["C*"], cx);
+    }
+
+    #[gpui::test]
+    async fn test_close_clean_items(deterministic: Arc<Deterministic>, cx: &mut TestAppContext) {
+        Settings::test_async(cx);
+        let fs = FakeFs::new(cx.background());
+
+        let project = Project::test(fs, None, cx).await;
+        let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
+        let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
+
+        add_labeled_item(&workspace, &pane, "A", true, cx);
+        add_labeled_item(&workspace, &pane, "B", false, cx);
+        add_labeled_item(&workspace, &pane, "C", true, cx);
+        add_labeled_item(&workspace, &pane, "D", false, cx);
+        add_labeled_item(&workspace, &pane, "E", false, cx);
+        assert_item_labels(&pane, ["A^", "B", "C^", "D", "E*"], cx);
+
+        workspace.update(cx, |workspace, cx| {
+            Pane::close_clean_items(workspace, &CloseCleanItems, cx);
+        });
+
+        deterministic.run_until_parked();
+        assert_item_labels(&pane, ["A^", "C*^"], cx);
+    }
+
+    #[gpui::test]
+    async fn test_close_items_to_the_left(
+        deterministic: Arc<Deterministic>,
+        cx: &mut TestAppContext,
+    ) {
+        Settings::test_async(cx);
+        let fs = FakeFs::new(cx.background());
+
+        let project = Project::test(fs, None, cx).await;
+        let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
+        let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
+
+        set_labeled_items(&workspace, &pane, ["A", "B", "C*", "D", "E"], cx);
+
+        workspace.update(cx, |workspace, cx| {
+            Pane::close_items_to_the_left(workspace, &CloseItemsToTheLeft, cx);
+        });
+
+        deterministic.run_until_parked();
+        assert_item_labels(&pane, ["C*", "D", "E"], cx);
+    }
+
+    #[gpui::test]
+    async fn test_close_items_to_the_right(
+        deterministic: Arc<Deterministic>,
+        cx: &mut TestAppContext,
+    ) {
+        Settings::test_async(cx);
+        let fs = FakeFs::new(cx.background());
+
+        let project = Project::test(fs, None, cx).await;
+        let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
+        let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
+
+        set_labeled_items(&workspace, &pane, ["A", "B", "C*", "D", "E"], cx);
+
+        workspace.update(cx, |workspace, cx| {
+            Pane::close_items_to_the_right(workspace, &CloseItemsToTheRight, cx);
+        });
+
+        deterministic.run_until_parked();
+        assert_item_labels(&pane, ["A", "B", "C*"], cx);
+    }
+
+    #[gpui::test]
+    async fn test_close_all_items(deterministic: Arc<Deterministic>, cx: &mut TestAppContext) {
+        Settings::test_async(cx);
+        let fs = FakeFs::new(cx.background());
+
+        let project = Project::test(fs, None, cx).await;
+        let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
+        let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
+
+        add_labeled_item(&workspace, &pane, "A", false, cx);
+        add_labeled_item(&workspace, &pane, "B", false, cx);
+        add_labeled_item(&workspace, &pane, "C", false, cx);
+        assert_item_labels(&pane, ["A", "B", "C*"], cx);
+
+        workspace.update(cx, |workspace, cx| {
+            Pane::close_all_items(workspace, &CloseAllItems, cx);
+        });
+
+        deterministic.run_until_parked();
+        assert_item_labels(&pane, [], cx);
+    }
+
+    fn add_labeled_item(
         workspace: &ViewHandle<Workspace>,
         pane: &ViewHandle<Pane>,
         label: &str,
+        is_dirty: bool,
         cx: &mut TestAppContext,
     ) -> Box<ViewHandle<TestItem>> {
         workspace.update(cx, |workspace, cx| {
-            let labeled_item = Box::new(cx.add_view(|_| TestItem::new().with_label(label)));
+            let labeled_item =
+                Box::new(cx.add_view(|_| TestItem::new().with_label(label).with_dirty(is_dirty)));
 
             Pane::add_item(
                 workspace,
@@ -2362,6 +2611,9 @@ mod tests {
                     if ix == pane.active_item_index {
                         state.push('*');
                     }
+                    if item.is_dirty(cx) {
+                        state.push('^');
+                    }
                     state
                 })
                 .collect::<Vec<_>>();

crates/workspace/src/workspace.rs 🔗

@@ -1304,10 +1304,8 @@ impl Workspace {
         RemoveWorktreeFromProject(worktree_id): &RemoveWorktreeFromProject,
         cx: &mut ViewContext<Self>,
     ) {
-        let future = self
-            .project
+        self.project
             .update(cx, |project, cx| project.remove_worktree(*worktree_id, cx));
-        cx.foreground().spawn(future).detach();
     }
 
     fn project_path_for_path(
@@ -3274,9 +3272,7 @@ mod tests {
         );
 
         // Remove a project folder
-        project
-            .update(cx, |project, cx| project.remove_worktree(worktree_id, cx))
-            .await;
+        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")

crates/zed/Cargo.toml 🔗

@@ -3,7 +3,7 @@ authors = ["Nathan Sobo <nathansobo@gmail.com>"]
 description = "The fast, collaborative code editor."
 edition = "2021"
 name = "zed"
-version = "0.82.0"
+version = "0.83.0"
 publish = false
 
 [lib]

crates/zed/src/main.rs 🔗

@@ -336,7 +336,7 @@ fn init_panic_hook(app_version: String) {
         let message = match info.location() {
             Some(location) => {
                 format!(
-                    "thread '{}' panicked at '{}': {}:{}\n{:?}",
+                    "thread '{}' panicked at '{}'\n{}:{}\n{:?}",
                     thread,
                     payload,
                     location.file(),

crates/zed/src/zed.rs 🔗

@@ -1541,7 +1541,7 @@ mod tests {
             .update(cx, |workspace, cx| {
                 let editor3_id = editor3.id();
                 drop(editor3);
-                Pane::close_item(workspace, workspace.active_pane().clone(), editor3_id, cx)
+                Pane::close_item_by_id(workspace, workspace.active_pane().clone(), editor3_id, cx)
             })
             .await
             .unwrap();
@@ -1574,7 +1574,7 @@ mod tests {
             .update(cx, |workspace, cx| {
                 let editor2_id = editor2.id();
                 drop(editor2);
-                Pane::close_item(workspace, workspace.active_pane().clone(), editor2_id, cx)
+                Pane::close_item_by_id(workspace, workspace.active_pane().clone(), editor2_id, cx)
             })
             .await
             .unwrap();
@@ -1724,7 +1724,7 @@ mod tests {
         // Close all the pane items in some arbitrary order.
         workspace
             .update(cx, |workspace, cx| {
-                Pane::close_item(workspace, pane.clone(), file1_item_id, cx)
+                Pane::close_item_by_id(workspace, pane.clone(), file1_item_id, cx)
             })
             .await
             .unwrap();
@@ -1732,7 +1732,7 @@ mod tests {
 
         workspace
             .update(cx, |workspace, cx| {
-                Pane::close_item(workspace, pane.clone(), file4_item_id, cx)
+                Pane::close_item_by_id(workspace, pane.clone(), file4_item_id, cx)
             })
             .await
             .unwrap();
@@ -1740,7 +1740,7 @@ mod tests {
 
         workspace
             .update(cx, |workspace, cx| {
-                Pane::close_item(workspace, pane.clone(), file2_item_id, cx)
+                Pane::close_item_by_id(workspace, pane.clone(), file2_item_id, cx)
             })
             .await
             .unwrap();
@@ -1748,7 +1748,7 @@ mod tests {
 
         workspace
             .update(cx, |workspace, cx| {
-                Pane::close_item(workspace, pane.clone(), file3_item_id, cx)
+                Pane::close_item_by_id(workspace, pane.clone(), file3_item_id, cx)
             })
             .await
             .unwrap();

styles/package-lock.json 🔗

@@ -11,6 +11,7 @@
             "dependencies": {
                 "@types/chroma-js": "^2.4.0",
                 "@types/node": "^18.14.1",
+                "ayu": "^8.0.1",
                 "bezier-easing": "^2.1.0",
                 "case-anything": "^2.1.10",
                 "chroma-js": "^2.4.2",
@@ -106,6 +107,16 @@
             "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz",
             "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA=="
         },
+        "node_modules/ayu": {
+            "version": "8.0.1",
+            "resolved": "https://registry.npmjs.org/ayu/-/ayu-8.0.1.tgz",
+            "integrity": "sha512-yuPZ2kZYQoYaPRQ/78F9rXDVx1rVGCJ1neBYithBoSprD6zPdIJdAKizUXG0jtTBu7nTFyAnVFFYuLnCS3cpDw==",
+            "dependencies": {
+                "@types/chroma-js": "^2.0.0",
+                "chroma-js": "^2.1.0",
+                "nonenumerable": "^1.1.1"
+            }
+        },
         "node_modules/bezier-easing": {
             "version": "2.1.0",
             "resolved": "https://registry.npmjs.org/bezier-easing/-/bezier-easing-2.1.0.tgz",
@@ -153,6 +164,11 @@
             "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz",
             "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw=="
         },
+        "node_modules/nonenumerable": {
+            "version": "1.1.1",
+            "resolved": "https://registry.npmjs.org/nonenumerable/-/nonenumerable-1.1.1.tgz",
+            "integrity": "sha512-ptUD9w9D8WqW6fuJJkZNCImkf+0vdbgUTbRK3i7jsy3olqtH96hYE6Q/S3Tx9NWbcB/ocAjYshXCAUP0lZ9B4Q=="
+        },
         "node_modules/toml": {
             "version": "3.0.0",
             "resolved": "https://registry.npmjs.org/toml/-/toml-3.0.0.tgz",
@@ -300,6 +316,16 @@
             "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz",
             "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA=="
         },
+        "ayu": {
+            "version": "8.0.1",
+            "resolved": "https://registry.npmjs.org/ayu/-/ayu-8.0.1.tgz",
+            "integrity": "sha512-yuPZ2kZYQoYaPRQ/78F9rXDVx1rVGCJ1neBYithBoSprD6zPdIJdAKizUXG0jtTBu7nTFyAnVFFYuLnCS3cpDw==",
+            "requires": {
+                "@types/chroma-js": "^2.0.0",
+                "chroma-js": "^2.1.0",
+                "nonenumerable": "^1.1.1"
+            }
+        },
         "bezier-easing": {
             "version": "2.1.0",
             "resolved": "https://registry.npmjs.org/bezier-easing/-/bezier-easing-2.1.0.tgz",
@@ -335,6 +361,11 @@
             "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz",
             "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw=="
         },
+        "nonenumerable": {
+            "version": "1.1.1",
+            "resolved": "https://registry.npmjs.org/nonenumerable/-/nonenumerable-1.1.1.tgz",
+            "integrity": "sha512-ptUD9w9D8WqW6fuJJkZNCImkf+0vdbgUTbRK3i7jsy3olqtH96hYE6Q/S3Tx9NWbcB/ocAjYshXCAUP0lZ9B4Q=="
+        },
         "toml": {
             "version": "3.0.0",
             "resolved": "https://registry.npmjs.org/toml/-/toml-3.0.0.tgz",

styles/package.json 🔗

@@ -12,6 +12,7 @@
     "dependencies": {
         "@types/chroma-js": "^2.4.0",
         "@types/node": "^18.14.1",
+        "ayu": "^8.0.1",
         "bezier-easing": "^2.1.0",
         "case-anything": "^2.1.10",
         "chroma-js": "^2.4.2",

styles/src/styleTree/components.ts 🔗

@@ -281,14 +281,18 @@ export function border(
     }
 }
 
-
-export function svg(color: string, asset: String, width: Number, height: Number) {
+export function svg(
+    color: string,
+    asset: String,
+    width: Number,
+    height: Number
+) {
     return {
         color,
         asset,
         dimensions: {
             width,
             height,
-        }
+        },
     }
 }

styles/src/styleTree/copilot.ts 🔗

@@ -1,13 +1,13 @@
 import { ColorScheme } from "../themes/common/colorScheme"
-import { background, border, foreground, svg, text } from "./components";
-
+import { background, border, foreground, svg, text } from "./components"
 
 export default function copilot(colorScheme: ColorScheme) {
-    let layer = colorScheme.middle;
+    let layer = colorScheme.middle
 
-    let content_width = 264;
+    let content_width = 264
 
-    let ctaButton = { // Copied from welcome screen. FIXME: Move this into a ZDS component
+    let ctaButton = {
+        // Copied from welcome screen. FIXME: Move this into a ZDS component
         background: background(layer),
         border: border(layer, "default"),
         cornerRadius: 4,
@@ -15,7 +15,7 @@ export default function copilot(colorScheme: ColorScheme) {
             top: 4,
             bottom: 4,
             left: 8,
-            right: 8
+            right: 8,
         },
         padding: {
             top: 3,
@@ -29,22 +29,32 @@ export default function copilot(colorScheme: ColorScheme) {
             background: background(layer, "hovered"),
             border: border(layer, "active"),
         },
-    };
+    }
 
     return {
         outLinkIcon: {
-            icon: svg(foreground(layer, "variant"), "icons/link_out_12.svg", 12, 12),
+            icon: svg(
+                foreground(layer, "variant"),
+                "icons/link_out_12.svg",
+                12,
+                12
+            ),
             container: {
                 cornerRadius: 6,
                 padding: { left: 6 },
             },
             hover: {
-                icon: svg(foreground(layer, "hovered"), "icons/link_out_12.svg", 12, 12)
+                icon: svg(
+                    foreground(layer, "hovered"),
+                    "icons/link_out_12.svg",
+                    12,
+                    12
+                ),
             },
         },
         modal: {
             titleText: {
-                ...text(layer, "sans", { size: "xs", "weight": "bold" })
+                ...text(layer, "sans", { size: "xs", weight: "bold" }),
             },
             titlebar: {
                 background: background(colorScheme.lowest),
@@ -54,7 +64,7 @@ export default function copilot(colorScheme: ColorScheme) {
                     bottom: 4,
                     left: 8,
                     right: 8,
-                }
+                },
             },
             container: {
                 background: background(colorScheme.lowest),
@@ -63,10 +73,15 @@ export default function copilot(colorScheme: ColorScheme) {
                     left: 0,
                     right: 0,
                     bottom: 8,
-                }
+                },
             },
             closeIcon: {
-                icon: svg(foreground(layer, "variant"), "icons/x_mark_8.svg", 8, 8),
+                icon: svg(
+                    foreground(layer, "variant"),
+                    "icons/x_mark_8.svg",
+                    8,
+                    8
+                ),
                 container: {
                     cornerRadius: 2,
                     padding: {
@@ -76,15 +91,25 @@ export default function copilot(colorScheme: ColorScheme) {
                         right: 4,
                     },
                     margin: {
-                        right: 0
-                    }
+                        right: 0,
+                    },
                 },
                 hover: {
-                    icon: svg(foreground(layer, "on"), "icons/x_mark_8.svg", 8, 8),
+                    icon: svg(
+                        foreground(layer, "on"),
+                        "icons/x_mark_8.svg",
+                        8,
+                        8
+                    ),
                 },
                 clicked: {
-                    icon: svg(foreground(layer, "base"), "icons/x_mark_8.svg", 8, 8),
-                }
+                    icon: svg(
+                        foreground(layer, "base"),
+                        "icons/x_mark_8.svg",
+                        8,
+                        8
+                    ),
+                },
             },
             dimensions: {
                 width: 280,
@@ -98,14 +123,19 @@ export default function copilot(colorScheme: ColorScheme) {
             ctaButton,
 
             header: {
-                icon: svg(foreground(layer, "default"), "icons/zed_plus_copilot_32.svg", 92, 32),
+                icon: svg(
+                    foreground(layer, "default"),
+                    "icons/zed_plus_copilot_32.svg",
+                    92,
+                    32
+                ),
                 container: {
                     margin: {
                         top: 35,
                         bottom: 5,
                         left: 0,
-                        right: 0
-                    }
+                        right: 0,
+                    },
                 },
             },
 
@@ -116,21 +146,20 @@ export default function copilot(colorScheme: ColorScheme) {
                         top: 6,
                         bottom: 12,
                         left: 0,
-                        right: 0
-                    }
+                        right: 0,
+                    },
                 },
 
                 hint: {
                     ...text(layer, "sans", { size: "xs", color: "#838994" }),
                     margin: {
                         top: 6,
-                        bottom: 2
-                    }
+                        bottom: 2,
+                    },
                 },
 
                 deviceCode: {
-                    text:
-                        text(layer, "mono", { size: "sm" }),
+                    text: text(layer, "mono", { size: "sm" }),
                     cta: {
                         ...ctaButton,
                         background: background(colorScheme.lowest),
@@ -144,7 +173,7 @@ export default function copilot(colorScheme: ColorScheme) {
                         margin: {
                             left: 16,
                             right: 16,
-                        }
+                        },
                     },
                     left: content_width / 2,
                     leftContainer: {
@@ -155,9 +184,14 @@ export default function copilot(colorScheme: ColorScheme) {
                             right: 6,
                         },
                     },
-                    right: content_width * 1 / 3,
+                    right: (content_width * 1) / 3,
                     rightContainer: {
-                        border: border(colorScheme.lowest, "inverted", { bottom: false, right: false, top: false, left: true }),
+                        border: border(colorScheme.lowest, "inverted", {
+                            bottom: false,
+                            right: false,
+                            top: false,
+                            left: true,
+                        }),
                         padding: {
                             top: 3,
                             bottom: 5,
@@ -165,9 +199,14 @@ export default function copilot(colorScheme: ColorScheme) {
                             right: 0,
                         },
                         hover: {
-                            border: border(layer, "active", { bottom: false, right: false, top: false, left: true }),
+                            border: border(layer, "active", {
+                                bottom: false,
+                                right: false,
+                                top: false,
+                                left: true,
+                            }),
                         },
-                    }
+                    },
                 },
             },
 
@@ -179,12 +218,15 @@ export default function copilot(colorScheme: ColorScheme) {
                         top: 16,
                         bottom: 16,
                         left: 0,
-                        right: 0
-                    }
+                        right: 0,
+                    },
                 },
 
                 warning: {
-                    ...text(layer, "sans", { size: "xs", color: foreground(layer, "warning") }),
+                    ...text(layer, "sans", {
+                        size: "xs",
+                        color: foreground(layer, "warning"),
+                    }),
                     border: border(layer, "warning"),
                     background: background(layer, "warning"),
                     cornerRadius: 2,
@@ -197,8 +239,8 @@ export default function copilot(colorScheme: ColorScheme) {
                     margin: {
                         bottom: 16,
                         left: 8,
-                        right: 8
-                    }
+                        right: 8,
+                    },
                 },
             },
 
@@ -208,19 +250,18 @@ export default function copilot(colorScheme: ColorScheme) {
 
                     margin: {
                         top: 16,
-                        bottom: 16
-                    }
+                        bottom: 16,
+                    },
                 },
 
                 hint: {
                     ...text(layer, "sans", { size: "xs", color: "#838994" }),
                     margin: {
                         top: 24,
-                        bottom: 4
-                    }
+                        bottom: 4,
+                    },
                 },
-
             },
-        }
+        },
     }
 }

styles/src/styleTree/workspace.ts 🔗

@@ -1,6 +1,13 @@
 import { ColorScheme } from "../themes/common/colorScheme"
 import { withOpacity } from "../utils/color"
-import { background, border, borderColor, foreground, svg, text } from "./components"
+import {
+    background,
+    border,
+    borderColor,
+    foreground,
+    svg,
+    text,
+} from "./components"
 import statusBar from "./statusBar"
 import tabBar from "./tabBar"
 
@@ -46,14 +53,24 @@ export default function workspace(colorScheme: ColorScheme) {
                 width: 256,
                 height: 256,
             },
-            logo: svg(withOpacity("#000000", colorScheme.isLight ? 0.6 : 0.8), "icons/logo_96.svg", 256, 256),
+            logo: svg(
+                withOpacity("#000000", colorScheme.isLight ? 0.6 : 0.8),
+                "icons/logo_96.svg",
+                256,
+                256
+            ),
 
-            logoShadow: svg(withOpacity(
-                colorScheme.isLight
-                    ? "#FFFFFF"
-                    : colorScheme.lowest.base.default.background,
-                colorScheme.isLight ? 1 : 0.6
-            ), "icons/logo_96.svg", 256, 256),
+            logoShadow: svg(
+                withOpacity(
+                    colorScheme.isLight
+                        ? "#FFFFFF"
+                        : colorScheme.lowest.base.default.background,
+                    colorScheme.isLight ? 1 : 0.6
+                ),
+                "icons/logo_96.svg",
+                256,
+                256
+            ),
             keyboardHints: {
                 margin: {
                     top: 96,
@@ -273,11 +290,7 @@ export default function workspace(colorScheme: ColorScheme) {
             },
             hover: {
                 color: foreground(colorScheme.highest, "on", "hovered"),
-                background: background(
-                    colorScheme.highest,
-                    "on",
-                    "hovered"
-                ),
+                background: background(colorScheme.highest, "on", "hovered"),
             },
         },
         disconnectedOverlay: {

styles/src/themes/ayu-dark.ts 🔗

@@ -0,0 +1,17 @@
+import { createColorScheme } from "./common/ramps"
+import { ayu, meta as themeMeta, buildTheme } from "./common/ayu-common"
+
+export const meta = {
+    ...themeMeta,
+    name: `${themeMeta.name} Dark`
+}
+
+const variant = ayu.dark
+const theme = buildTheme(variant, false)
+
+export const dark = createColorScheme(
+    meta.name,
+    false,
+    theme.ramps,
+    theme.syntax
+)

styles/src/themes/ayu-light.ts 🔗

@@ -0,0 +1,17 @@
+import { createColorScheme } from "./common/ramps"
+import { ayu, meta as themeMeta, buildTheme } from "./common/ayu-common"
+
+export const meta = {
+    ...themeMeta,
+    name: `${themeMeta.name} Light`
+}
+
+const variant = ayu.light
+const theme = buildTheme(variant, true)
+
+export const light = createColorScheme(
+    meta.name,
+    true,
+    theme.ramps,
+    theme.syntax
+)

styles/src/themes/ayu-mirage.ts 🔗

@@ -0,0 +1,17 @@
+import { createColorScheme } from "./common/ramps"
+import { ayu, meta as themeMeta, buildTheme } from "./common/ayu-common"
+
+export const meta = {
+    ...themeMeta,
+    name: `${themeMeta.name} Mirage`
+}
+
+const variant = ayu.mirage
+const theme = buildTheme(variant, false)
+
+export const dark = createColorScheme(
+    meta.name,
+    false,
+    theme.ramps,
+    theme.syntax
+)

styles/src/themes/common/ayu-common.ts 🔗

@@ -0,0 +1,90 @@
+import { dark, light, mirage } from "ayu"
+import { ThemeSyntax } from "./syntax"
+import chroma from "chroma-js"
+import { colorRamp } from "./ramps"
+import { Meta } from "./colorScheme"
+
+export const ayu = {
+    dark,
+    light,
+    mirage,
+}
+
+export const buildTheme = (t: typeof dark, light: boolean) => {
+    const color = {
+        lightBlue: t.syntax.tag.hex(),
+        yellow: t.syntax.func.hex(),
+        blue: t.syntax.entity.hex(),
+        green: t.syntax.string.hex(),
+        teal: t.syntax.regexp.hex(),
+        red: t.syntax.markup.hex(),
+        orange: t.syntax.keyword.hex(),
+        lightYellow: t.syntax.special.hex(),
+        gray: t.syntax.comment.hex(),
+        purple: t.syntax.constant.hex(),
+    }
+
+    const syntax: ThemeSyntax = {
+        constant: { color: t.syntax.constant.hex() },
+        "string.regex": { color: t.syntax.regexp.hex() },
+        string: { color: t.syntax.string.hex() },
+        comment: { color: t.syntax.comment.hex() },
+        keyword: { color: t.syntax.keyword.hex() },
+        operator: { color: t.syntax.operator.hex() },
+        number: { color: t.syntax.constant.hex() },
+        type: { color: color.blue },
+        boolean: { color: color.purple },
+        "punctuation.special": { color: color.purple },
+        "string.special": { color: t.syntax.special.hex() },
+        function: { color: t.syntax.func.hex() },
+    }
+
+    return {
+        ramps: {
+            neutral: chroma.scale([
+                light ? t.editor.fg.hex() : t.editor.bg.hex(),
+                light ? t.editor.bg.hex() : t.editor.fg.hex(),
+            ]),
+            red: colorRamp(chroma(color.red)),
+            orange: colorRamp(chroma(color.orange)),
+            yellow: colorRamp(chroma(color.yellow)),
+            green: colorRamp(chroma(color.green)),
+            cyan: colorRamp(chroma(color.teal)),
+            blue: colorRamp(chroma(color.blue)),
+            violet: colorRamp(chroma(color.purple)),
+            magenta: colorRamp(chroma(color.lightBlue)),
+        },
+        syntax,
+    }
+}
+
+export const buildSyntax = (t: typeof dark): ThemeSyntax => {
+    return {
+        constant: { color: t.syntax.constant.hex() },
+        "string.regex": { color: t.syntax.regexp.hex() },
+        string: { color: t.syntax.string.hex() },
+        comment: { color: t.syntax.comment.hex() },
+        keyword: { color: t.syntax.keyword.hex() },
+        operator: { color: t.syntax.operator.hex() },
+        number: { color: t.syntax.constant.hex() },
+        type: { color: t.syntax.regexp.hex() },
+        "punctuation.special": { color: t.syntax.special.hex() },
+        "string.special": { color: t.syntax.special.hex() },
+        function: { color: t.syntax.func.hex() },
+    }
+}
+
+export const meta: Meta = {
+    name: "Ayu",
+    author: "dempfi",
+    license: {
+        SPDX: "MIT",
+        license_text: {
+            https_url:
+                "https://raw.githubusercontent.com/dempfi/ayu/master/LICENSE",
+            license_checksum:
+                "e0af0e0d1754c18ca075649d42f5c6d9a60f8bdc03c20dfd97105f2253a94173",
+        },
+    },
+    url: "https://github.com/dempfi/ayu",
+}

styles/src/themes/staff/ayu-mirage.ts 🔗

@@ -1,31 +0,0 @@
-import chroma from "chroma-js"
-import { colorRamp, createColorScheme } from "../common/ramps"
-
-const name = "Ayu"
-const author = "Konstantin Pschera <me@kons.ch>"
-const url = "https://github.com/ayu-theme/ayu-colors"
-const license = {
-    type: "MIT",
-    url: "https://github.com/ayu-theme/ayu-colors/blob/master/license",
-}
-
-export const dark = createColorScheme(`${name} Mirage`, false, {
-    neutral: chroma.scale([
-        "#171B24",
-        "#1F2430",
-        "#242936",
-        "#707A8C",
-        "#8A9199",
-        "#CCCAC2",
-        "#D9D7CE",
-        "#F3F4F5",
-    ]),
-    red: colorRamp(chroma("#F28779")),
-    orange: colorRamp(chroma("#FFAD66")),
-    yellow: colorRamp(chroma("#FFD173")),
-    green: colorRamp(chroma("#D5FF80")),
-    cyan: colorRamp(chroma("#95E6CB")),
-    blue: colorRamp(chroma("#5CCFE6")),
-    violet: colorRamp(chroma("#D4BFFF")),
-    magenta: colorRamp(chroma("#F29E74")),
-})

styles/src/themes/staff/ayu.ts 🔗

@@ -1,52 +0,0 @@
-import chroma from "chroma-js"
-import { colorRamp, createColorScheme } from "../common/ramps"
-
-const name = "Ayu"
-const author = "Konstantin Pschera <me@kons.ch>"
-const url = "https://github.com/ayu-theme/ayu-colors"
-const license = {
-    type: "MIT",
-    url: "https://github.com/ayu-theme/ayu-colors/blob/master/license",
-}
-
-export const dark = createColorScheme(`${name} Dark`, false, {
-    neutral: chroma.scale([
-        "#0F1419",
-        "#131721",
-        "#272D38",
-        "#3E4B59",
-        "#BFBDB6",
-        "#E6E1CF",
-        "#E6E1CF",
-        "#F3F4F5",
-    ]),
-    red: colorRamp(chroma("#F07178")),
-    orange: colorRamp(chroma("#FF8F40")),
-    yellow: colorRamp(chroma("#FFB454")),
-    green: colorRamp(chroma("#B8CC52")),
-    cyan: colorRamp(chroma("#95E6CB")),
-    blue: colorRamp(chroma("#59C2FF")),
-    violet: colorRamp(chroma("#D2A6FF")),
-    magenta: colorRamp(chroma("#E6B673")),
-})
-
-export const light = createColorScheme(`${name} Light`, true, {
-    neutral: chroma.scale([
-        "#1A1F29",
-        "#242936",
-        "#5C6773",
-        "#828C99",
-        "#ABB0B6",
-        "#F8F9FA",
-        "#F3F4F5",
-        "#FAFAFA",
-    ]),
-    red: colorRamp(chroma("#F07178")),
-    orange: colorRamp(chroma("#FA8D3E")),
-    yellow: colorRamp(chroma("#F2AE49")),
-    green: colorRamp(chroma("#86B300")),
-    cyan: colorRamp(chroma("#4CBF99")),
-    blue: colorRamp(chroma("#36A3D9")),
-    violet: colorRamp(chroma("#A37ACC")),
-    magenta: colorRamp(chroma("#E6BA7E")),
-})