Merge branch 'main' into storybook

Nathan Sobo created

Change summary

Cargo.lock                                                    |   72 
assets/keymaps/default.json                                   |   11 
assets/keymaps/vim.json                                       |    2 
crates/call/src/call.rs                                       |   28 
crates/channel/src/channel_buffer.rs                          |   48 
crates/channel/src/channel_store.rs                           |  168 
crates/client/src/client.rs                                   |    6 
crates/client/src/telemetry.rs                                |    2 
crates/collab/Cargo.toml                                      |    3 
crates/collab/src/db.rs                                       |   11 
crates/collab/src/db/queries/buffers.rs                       |  316 
crates/collab/src/db/queries/channels.rs                      |   14 
crates/collab/src/db/queries/rooms.rs                         |    2 
crates/collab/src/db/queries/servers.rs                       |   32 
crates/collab/src/rpc.rs                                      |   70 
crates/collab/src/tests.rs                                    |  557 -
crates/collab/src/tests/channel_buffer_tests.rs               |  242 
crates/collab/src/tests/random_channel_buffer_tests.rs        |  288 
crates/collab/src/tests/random_project_collaboration_tests.rs | 1585 +++
crates/collab/src/tests/randomized_integration_tests.rs       | 2199 -----
crates/collab/src/tests/randomized_test_helpers.rs            |  689 +
crates/collab/src/tests/test_server.rs                        |  558 +
crates/collab_ui/src/collab_panel.rs                          |   15 
crates/collab_ui/src/collab_ui.rs                             |    8 
crates/copilot/src/copilot.rs                                 |   13 
crates/editor/src/editor.rs                                   |   76 
crates/editor/src/editor_tests.rs                             |    2 
crates/editor/src/movement.rs                                 |  167 
crates/editor/src/scroll.rs                                   |    4 
crates/editor/src/scroll/scroll_amount.rs                     |   26 
crates/gpui/src/executor.rs                                   |    1 
crates/gpui_macros/src/gpui_macros.rs                         |   10 
crates/language/src/proto.rs                                  |   25 
crates/language_tools/src/lsp_log.rs                          |   55 
crates/live_kit_client/LiveKitBridge/Package.resolved         |    4 
crates/node_runtime/Cargo.toml                                |    1 
crates/node_runtime/src/node_runtime.rs                       |  151 
crates/project/src/project.rs                                 |   27 
crates/rpc/proto/zed.proto                                    |   33 
crates/rpc/src/proto.rs                                       |    7 
crates/search/src/project_search.rs                           |   42 
crates/semantic_index/Cargo.toml                              |    3 
crates/semantic_index/src/db.rs                               |  736 
crates/semantic_index/src/embedding.rs                        |  251 
crates/semantic_index/src/embedding_queue.rs                  |  165 
crates/semantic_index/src/parsing.rs                          |  129 
crates/semantic_index/src/semantic_index.rs                   | 1081 -
crates/semantic_index/src/semantic_index_tests.rs             |  274 
crates/theme/src/theme.rs                                     |    1 
crates/util/src/util.rs                                       |   35 
crates/vim/src/motion.rs                                      |   25 
crates/vim/src/normal.rs                                      |   11 
crates/vim/src/normal/change.rs                               |   19 
crates/vim/src/normal/scroll.rs                               |   62 
crates/vim/src/normal/substitute.rs                           |   97 
crates/vim/src/object.rs                                      |   31 
crates/vim/src/test.rs                                        |   18 
crates/vim/src/test/neovim_backed_test_context.rs             |    7 
crates/vim/src/test/neovim_connection.rs                      |    3 
crates/vim/test_data/test_end_of_word.json                    |   32 
crates/vim/test_data/test_substitute_line.json                |   29 
crates/vim/test_data/test_visual_word_object.json             |    6 
crates/vim/test_data/test_wrapped_lines.json                  |    5 
crates/zed/Cargo.toml                                         |    2 
crates/zed/src/languages.rs                                   |    2 
crates/zed/src/languages/css.rs                               |   12 
crates/zed/src/languages/html.rs                              |   12 
crates/zed/src/languages/json.rs                              |   12 
crates/zed/src/languages/php.rs                               |   12 
crates/zed/src/languages/python.rs                            |   12 
crates/zed/src/languages/svelte.rs                            |   12 
crates/zed/src/languages/tailwind.rs                          |   12 
crates/zed/src/languages/typescript.rs                        |   22 
crates/zed/src/languages/yaml.rs                              |   15 
crates/zed/src/main.rs                                        |    4 
crates/zed/src/zed.rs                                         |    5 
script/randomized-test-minimize                               |    3 
styles/src/component/text_button.ts                           |    6 
styles/src/style_tree/search.ts                               |    4 
styles/src/style_tree/toolbar.ts                              |   26 
80 files changed, 6,156 insertions(+), 4,607 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -1453,9 +1453,10 @@ dependencies = [
 
 [[package]]
 name = "collab"
-version = "0.19.0"
+version = "0.20.0"
 dependencies = [
  "anyhow",
+ "async-trait",
  "async-tungstenite",
  "audio",
  "axum",
@@ -3570,7 +3571,7 @@ dependencies = [
  "gif",
  "jpeg-decoder",
  "num-iter",
- "num-rational",
+ "num-rational 0.3.2",
  "num-traits",
  "png",
  "scoped_threadpool",
@@ -4613,6 +4614,7 @@ dependencies = [
  "anyhow",
  "async-compression",
  "async-tar",
+ "async-trait",
  "futures 0.3.28",
  "gpui",
  "log",
@@ -4662,6 +4664,31 @@ dependencies = [
  "winapi 0.3.9",
 ]
 
+[[package]]
+name = "num"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b8536030f9fea7127f841b45bb6243b27255787fb4eb83958aa1ef9d2fdc0c36"
+dependencies = [
+ "num-bigint 0.2.6",
+ "num-complex",
+ "num-integer",
+ "num-iter",
+ "num-rational 0.2.4",
+ "num-traits",
+]
+
+[[package]]
+name = "num-bigint"
+version = "0.2.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "090c7f9998ee0ff65aa5b723e4009f7b217707f1fb5ea551329cc4d6231fb304"
+dependencies = [
+ "autocfg",
+ "num-integer",
+ "num-traits",
+]
+
 [[package]]
 name = "num-bigint"
 version = "0.4.4"
@@ -4690,6 +4717,16 @@ dependencies = [
  "zeroize",
 ]
 
+[[package]]
+name = "num-complex"
+version = "0.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6b19411a9719e753aff12e5187b74d60d3dc449ec3f4dc21e3989c3f554bc95"
+dependencies = [
+ "autocfg",
+ "num-traits",
+]
+
 [[package]]
 name = "num-derive"
 version = "0.3.3"
@@ -4722,6 +4759,18 @@ dependencies = [
  "num-traits",
 ]
 
+[[package]]
+name = "num-rational"
+version = "0.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c000134b5dbf44adc5cb772486d335293351644b801551abe8f75c84cfa4aef"
+dependencies = [
+ "autocfg",
+ "num-bigint 0.2.6",
+ "num-integer",
+ "num-traits",
+]
+
 [[package]]
 name = "num-rational"
 version = "0.3.2"
@@ -5038,6 +5087,17 @@ dependencies = [
  "windows-targets 0.48.5",
 ]
 
+[[package]]
+name = "parse_duration"
+version = "2.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7037e5e93e0172a5a96874380bf73bc6ecef022e26fa25f2be26864d6b3ba95d"
+dependencies = [
+ "lazy_static",
+ "num",
+ "regex",
+]
+
 [[package]]
 name = "password-hash"
 version = "0.2.3"
@@ -6666,6 +6726,7 @@ dependencies = [
  "anyhow",
  "async-trait",
  "bincode",
+ "collections",
  "ctor",
  "editor",
  "env_logger 0.9.3",
@@ -6678,6 +6739,7 @@ dependencies = [
  "log",
  "matrixmultiply",
  "parking_lot 0.11.2",
+ "parse_duration",
  "picker",
  "postage",
  "pretty_assertions",
@@ -7009,7 +7071,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "8eb4ea60fb301dc81dfc113df680571045d375ab7345d171c5dc7d7e13107a80"
 dependencies = [
  "chrono",
- "num-bigint",
+ "num-bigint 0.4.4",
  "num-traits",
  "thiserror",
 ]
@@ -7241,7 +7303,7 @@ dependencies = [
  "log",
  "md-5",
  "memchr",
- "num-bigint",
+ "num-bigint 0.4.4",
  "once_cell",
  "paste",
  "percent-encoding",
@@ -9720,7 +9782,7 @@ dependencies = [
 
 [[package]]
 name = "zed"
-version = "0.103.0"
+version = "0.104.0"
 dependencies = [
  "activity_indicator",
  "ai",

assets/keymaps/default.json 🔗

@@ -515,6 +515,17 @@
       "enter": "editor::ConfirmCodeAction"
     }
   },
+  {
+    "context": "Editor && (showing_code_actions || showing_completions)",
+    "bindings": {
+      "up": "editor::ContextMenuPrev",
+      "ctrl-p": "editor::ContextMenuPrev",
+      "down": "editor::ContextMenuNext",
+      "ctrl-n": "editor::ContextMenuNext",
+      "pageup": "editor::ContextMenuFirst",
+      "pagedown": "editor::ContextMenuLast"
+    }
+  },
   // Custom bindings
   {
     "bindings": {

assets/keymaps/vim.json 🔗

@@ -371,6 +371,7 @@
         "Replace"
       ],
       "s": "vim::Substitute",
+      "shift-s": "vim::SubstituteLine",
       "> >": "editor::Indent",
       "< <": "editor::Outdent",
       "ctrl-pagedown": "pane::ActivateNextItem",
@@ -446,6 +447,7 @@
         }
       ],
       "s": "vim::Substitute",
+      "shift-s": "vim::SubstituteLine",
       "c": "vim::Substitute",
       "~": "vim::ChangeCase",
       "shift-i": [

crates/call/src/call.rs 🔗

@@ -273,7 +273,13 @@ impl ActiveCall {
             .borrow_mut()
             .take()
             .ok_or_else(|| anyhow!("no incoming call"))?;
-        Self::report_call_event_for_room("decline incoming", call.room_id, None, &self.client, cx);
+        Self::report_call_event_for_room(
+            "decline incoming",
+            Some(call.room_id),
+            None,
+            &self.client,
+            cx,
+        );
         self.client.send(proto::DeclineCall {
             room_id: call.room_id,
         })?;
@@ -404,21 +410,19 @@ impl ActiveCall {
     }
 
     fn report_call_event(&self, operation: &'static str, cx: &AppContext) {
-        if let Some(room) = self.room() {
-            let room = room.read(cx);
-            Self::report_call_event_for_room(
-                operation,
-                room.id(),
-                room.channel_id(),
-                &self.client,
-                cx,
-            )
-        }
+        let (room_id, channel_id) = match self.room() {
+            Some(room) => {
+                let room = room.read(cx);
+                (Some(room.id()), room.channel_id())
+            }
+            None => (None, None),
+        };
+        Self::report_call_event_for_room(operation, room_id, channel_id, &self.client, cx)
     }
 
     pub fn report_call_event_for_room(
         operation: &'static str,
-        room_id: u64,
+        room_id: Option<u64>,
         channel_id: Option<u64>,
         client: &Arc<Client>,
         cx: &AppContext,

crates/channel/src/channel_buffer.rs 🔗

@@ -10,6 +10,7 @@ pub(crate) fn init(client: &Arc<Client>) {
     client.add_model_message_handler(ChannelBuffer::handle_update_channel_buffer);
     client.add_model_message_handler(ChannelBuffer::handle_add_channel_buffer_collaborator);
     client.add_model_message_handler(ChannelBuffer::handle_remove_channel_buffer_collaborator);
+    client.add_model_message_handler(ChannelBuffer::handle_update_channel_buffer_collaborator);
 }
 
 pub struct ChannelBuffer {
@@ -17,6 +18,7 @@ pub struct ChannelBuffer {
     connected: bool,
     collaborators: Vec<proto::Collaborator>,
     buffer: ModelHandle<language::Buffer>,
+    buffer_epoch: u64,
     client: Arc<Client>,
     subscription: Option<client::Subscription>,
 }
@@ -73,6 +75,7 @@ impl ChannelBuffer {
 
             Self {
                 buffer,
+                buffer_epoch: response.epoch,
                 client,
                 connected: true,
                 collaborators,
@@ -82,6 +85,26 @@ impl ChannelBuffer {
         }))
     }
 
+    pub(crate) fn replace_collaborators(
+        &mut self,
+        collaborators: Vec<proto::Collaborator>,
+        cx: &mut ModelContext<Self>,
+    ) {
+        for old_collaborator in &self.collaborators {
+            if collaborators
+                .iter()
+                .any(|c| c.replica_id == old_collaborator.replica_id)
+            {
+                self.buffer.update(cx, |buffer, cx| {
+                    buffer.remove_peer(old_collaborator.replica_id as u16, cx)
+                });
+            }
+        }
+        self.collaborators = collaborators;
+        cx.emit(Event::CollaboratorsChanged);
+        cx.notify();
+    }
+
     async fn handle_update_channel_buffer(
         this: ModelHandle<Self>,
         update_channel_buffer: TypedEnvelope<proto::UpdateChannelBuffer>,
@@ -149,6 +172,26 @@ impl ChannelBuffer {
         Ok(())
     }
 
+    async fn handle_update_channel_buffer_collaborator(
+        this: ModelHandle<Self>,
+        message: TypedEnvelope<proto::UpdateChannelBufferCollaborator>,
+        _: Arc<Client>,
+        mut cx: AsyncAppContext,
+    ) -> Result<()> {
+        this.update(&mut cx, |this, cx| {
+            for collaborator in &mut this.collaborators {
+                if collaborator.peer_id == message.payload.old_peer_id {
+                    collaborator.peer_id = message.payload.new_peer_id;
+                    break;
+                }
+            }
+            cx.emit(Event::CollaboratorsChanged);
+            cx.notify();
+        });
+
+        Ok(())
+    }
+
     fn on_buffer_update(
         &mut self,
         _: ModelHandle<language::Buffer>,
@@ -166,6 +209,10 @@ impl ChannelBuffer {
         }
     }
 
+    pub fn epoch(&self) -> u64 {
+        self.buffer_epoch
+    }
+
     pub fn buffer(&self) -> ModelHandle<language::Buffer> {
         self.buffer.clone()
     }
@@ -179,6 +226,7 @@ impl ChannelBuffer {
     }
 
     pub(crate) fn disconnect(&mut self, cx: &mut ModelContext<Self>) {
+        log::info!("channel buffer {} disconnected", self.channel.id);
         if self.connected {
             self.connected = false;
             self.subscription.take();

crates/channel/src/channel_store.rs 🔗

@@ -1,13 +1,15 @@
 use crate::channel_buffer::ChannelBuffer;
 use anyhow::{anyhow, Result};
-use client::{Client, Status, Subscription, User, UserId, UserStore};
+use client::{Client, Subscription, User, UserId, UserStore};
 use collections::{hash_map, HashMap, HashSet};
 use futures::{channel::mpsc, future::Shared, Future, FutureExt, StreamExt};
-use gpui::{AsyncAppContext, Entity, ModelContext, ModelHandle, Task, WeakModelHandle};
+use gpui::{AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle, Task, WeakModelHandle};
 use rpc::{proto, TypedEnvelope};
-use std::sync::Arc;
+use std::{mem, sync::Arc, time::Duration};
 use util::ResultExt;
 
+pub const RECONNECT_TIMEOUT: Duration = Duration::from_secs(30);
+
 pub type ChannelId = u64;
 
 pub struct ChannelStore {
@@ -22,7 +24,8 @@ pub struct ChannelStore {
     client: Arc<Client>,
     user_store: ModelHandle<UserStore>,
     _rpc_subscription: Subscription,
-    _watch_connection_status: Task<()>,
+    _watch_connection_status: Task<Option<()>>,
+    disconnect_channel_buffers_task: Option<Task<()>>,
     _update_channels: Task<()>,
 }
 
@@ -67,24 +70,20 @@ impl ChannelStore {
         let rpc_subscription =
             client.add_message_handler(cx.handle(), Self::handle_update_channels);
 
-        let (update_channels_tx, mut update_channels_rx) = mpsc::unbounded();
         let mut connection_status = client.status();
+        let (update_channels_tx, mut update_channels_rx) = mpsc::unbounded();
         let watch_connection_status = cx.spawn_weak(|this, mut cx| async move {
             while let Some(status) = connection_status.next().await {
-                if !status.is_connected() {
-                    if let Some(this) = this.upgrade(&cx) {
-                        this.update(&mut cx, |this, cx| {
-                            if matches!(status, Status::ConnectionLost | Status::SignedOut) {
-                                this.handle_disconnect(cx);
-                            } else {
-                                this.disconnect_buffers(cx);
-                            }
-                        });
-                    } else {
-                        break;
-                    }
+                let this = this.upgrade(&cx)?;
+                if status.is_connected() {
+                    this.update(&mut cx, |this, cx| this.handle_connect(cx))
+                        .await
+                        .log_err()?;
+                } else {
+                    this.update(&mut cx, |this, cx| this.handle_disconnect(cx));
                 }
             }
+            Some(())
         });
 
         Self {
@@ -100,6 +99,7 @@ impl ChannelStore {
             user_store,
             _rpc_subscription: rpc_subscription,
             _watch_connection_status: watch_connection_status,
+            disconnect_channel_buffers_task: None,
             _update_channels: cx.spawn_weak(|this, mut cx| async move {
                 while let Some(update_channels) = update_channels_rx.next().await {
                     if let Some(this) = this.upgrade(&cx) {
@@ -152,6 +152,15 @@ impl ChannelStore {
         self.channels_by_id.get(&channel_id)
     }
 
+    pub fn has_open_channel_buffer(&self, channel_id: ChannelId, cx: &AppContext) -> bool {
+        if let Some(buffer) = self.opened_buffers.get(&channel_id) {
+            if let OpenedChannelBuffer::Open(buffer) = buffer {
+                return buffer.upgrade(cx).is_some();
+            }
+        }
+        false
+    }
+
     pub fn open_channel_buffer(
         &mut self,
         channel_id: ChannelId,
@@ -482,8 +491,106 @@ impl ChannelStore {
         Ok(())
     }
 
-    fn handle_disconnect(&mut self, cx: &mut ModelContext<'_, ChannelStore>) {
-        self.disconnect_buffers(cx);
+    fn handle_connect(&mut self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
+        self.disconnect_channel_buffers_task.take();
+
+        let mut buffer_versions = Vec::new();
+        for buffer in self.opened_buffers.values() {
+            if let OpenedChannelBuffer::Open(buffer) = buffer {
+                if let Some(buffer) = buffer.upgrade(cx) {
+                    let channel_buffer = buffer.read(cx);
+                    let buffer = channel_buffer.buffer().read(cx);
+                    buffer_versions.push(proto::ChannelBufferVersion {
+                        channel_id: channel_buffer.channel().id,
+                        epoch: channel_buffer.epoch(),
+                        version: language::proto::serialize_version(&buffer.version()),
+                    });
+                }
+            }
+        }
+
+        if buffer_versions.is_empty() {
+            return Task::ready(Ok(()));
+        }
+
+        let response = self.client.request(proto::RejoinChannelBuffers {
+            buffers: buffer_versions,
+        });
+
+        cx.spawn(|this, mut cx| async move {
+            let mut response = response.await?;
+
+            this.update(&mut cx, |this, cx| {
+                this.opened_buffers.retain(|_, buffer| match buffer {
+                    OpenedChannelBuffer::Open(channel_buffer) => {
+                        let Some(channel_buffer) = channel_buffer.upgrade(cx) else {
+                            return false;
+                        };
+
+                        channel_buffer.update(cx, |channel_buffer, cx| {
+                            let channel_id = channel_buffer.channel().id;
+                            if let Some(remote_buffer) = response
+                                .buffers
+                                .iter_mut()
+                                .find(|buffer| buffer.channel_id == channel_id)
+                            {
+                                let channel_id = channel_buffer.channel().id;
+                                let remote_version =
+                                    language::proto::deserialize_version(&remote_buffer.version);
+
+                                channel_buffer.replace_collaborators(
+                                    mem::take(&mut remote_buffer.collaborators),
+                                    cx,
+                                );
+
+                                let operations = channel_buffer
+                                    .buffer()
+                                    .update(cx, |buffer, cx| {
+                                        let outgoing_operations =
+                                            buffer.serialize_ops(Some(remote_version), cx);
+                                        let incoming_operations =
+                                            mem::take(&mut remote_buffer.operations)
+                                                .into_iter()
+                                                .map(language::proto::deserialize_operation)
+                                                .collect::<Result<Vec<_>>>()?;
+                                        buffer.apply_ops(incoming_operations, cx)?;
+                                        anyhow::Ok(outgoing_operations)
+                                    })
+                                    .log_err();
+
+                                if let Some(operations) = operations {
+                                    let client = this.client.clone();
+                                    cx.background()
+                                        .spawn(async move {
+                                            let operations = operations.await;
+                                            for chunk in
+                                                language::proto::split_operations(operations)
+                                            {
+                                                client
+                                                    .send(proto::UpdateChannelBuffer {
+                                                        channel_id,
+                                                        operations: chunk,
+                                                    })
+                                                    .ok();
+                                            }
+                                        })
+                                        .detach();
+                                    return true;
+                                }
+                            }
+
+                            channel_buffer.disconnect(cx);
+                            false
+                        })
+                    }
+                    OpenedChannelBuffer::Loading(_) => true,
+                });
+            });
+            anyhow::Ok(())
+        })
+    }
+
+    fn handle_disconnect(&mut self, cx: &mut ModelContext<Self>) {
         self.channels_by_id.clear();
         self.channel_invitations.clear();
         self.channel_participants.clear();
@@ -491,16 +598,23 @@ impl ChannelStore {
         self.channel_paths.clear();
         self.outgoing_invites.clear();
         cx.notify();
-    }
 
-    fn disconnect_buffers(&mut self, cx: &mut ModelContext<ChannelStore>) {
-        for (_, buffer) in self.opened_buffers.drain() {
-            if let OpenedChannelBuffer::Open(buffer) = buffer {
-                if let Some(buffer) = buffer.upgrade(cx) {
-                    buffer.update(cx, |buffer, cx| buffer.disconnect(cx));
+        self.disconnect_channel_buffers_task.get_or_insert_with(|| {
+            cx.spawn_weak(|this, mut cx| async move {
+                cx.background().timer(RECONNECT_TIMEOUT).await;
+                if let Some(this) = this.upgrade(&cx) {
+                    this.update(&mut cx, |this, cx| {
+                        for (_, buffer) in this.opened_buffers.drain() {
+                            if let OpenedChannelBuffer::Open(buffer) = buffer {
+                                if let Some(buffer) = buffer.upgrade(cx) {
+                                    buffer.update(cx, |buffer, cx| buffer.disconnect(cx));
+                                }
+                            }
+                        }
+                    });
                 }
-            }
-        }
+            })
+        });
     }
 
     pub(crate) fn update_channels(

crates/client/src/client.rs 🔗

@@ -1011,9 +1011,9 @@ impl Client {
         credentials: &Credentials,
         cx: &AsyncAppContext,
     ) -> Task<Result<Connection, EstablishConnectionError>> {
-        let is_preview = cx.read(|cx| {
+        let use_preview_server = cx.read(|cx| {
             if cx.has_global::<ReleaseChannel>() {
-                *cx.global::<ReleaseChannel>() == ReleaseChannel::Preview
+                *cx.global::<ReleaseChannel>() != ReleaseChannel::Stable
             } else {
                 false
             }
@@ -1028,7 +1028,7 @@ impl Client {
 
         let http = self.http.clone();
         cx.background().spawn(async move {
-            let mut rpc_url = Self::get_rpc_url(http, is_preview).await?;
+            let mut rpc_url = Self::get_rpc_url(http, use_preview_server).await?;
             let rpc_host = rpc_url
                 .host_str()
                 .zip(rpc_url.port_or_known_default())

crates/client/src/telemetry.rs 🔗

@@ -73,7 +73,7 @@ pub enum ClickhouseEvent {
     },
     Call {
         operation: &'static str,
-        room_id: u64,
+        room_id: Option<u64>,
         channel_id: Option<u64>,
     },
 }

crates/collab/Cargo.toml 🔗

@@ -3,7 +3,7 @@ authors = ["Nathan Sobo <nathan@zed.dev>"]
 default-run = "collab"
 edition = "2021"
 name = "collab"
-version = "0.19.0"
+version = "0.20.0"
 publish = false
 
 [[bin]]
@@ -80,6 +80,7 @@ theme = { path = "../theme" }
 workspace = { path = "../workspace", features = ["test-support"] }
 collab_ui = { path = "../collab_ui", features = ["test-support"] }
 
+async-trait.workspace = true
 ctor.workspace = true
 env_logger.workspace = true
 indoc.workspace = true

crates/collab/src/db.rs 🔗

@@ -435,6 +435,12 @@ pub struct ChannelsForUser {
     pub channels_with_admin_privileges: HashSet<ChannelId>,
 }
 
+#[derive(Debug)]
+pub struct RejoinedChannelBuffer {
+    pub buffer: proto::RejoinedChannelBuffer,
+    pub old_connection_id: ConnectionId,
+}
+
 #[derive(Clone)]
 pub struct JoinRoom {
     pub room: proto::Room,
@@ -498,6 +504,11 @@ pub struct RefreshedRoom {
     pub canceled_calls_to_user_ids: Vec<UserId>,
 }
 
+pub struct RefreshedChannelBuffer {
+    pub connection_ids: Vec<ConnectionId>,
+    pub removed_collaborators: Vec<proto::RemoveChannelBufferCollaborator>,
+}
+
 pub struct Project {
     pub collaborators: Vec<ProjectCollaborator>,
     pub worktrees: BTreeMap<u64, Worktree>,

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

@@ -10,8 +10,6 @@ impl Database {
         connection: ConnectionId,
     ) -> Result<proto::JoinChannelBufferResponse> {
         self.transaction(|tx| async move {
-            let tx = tx;
-
             self.check_user_is_channel_member(channel_id, user_id, &tx)
                 .await?;
 
@@ -70,7 +68,6 @@ impl Database {
             .await?;
             collaborators.push(collaborator);
 
-            // Assemble the buffer state
             let (base_text, operations) = self.get_buffer_state(&buffer, &tx).await?;
 
             Ok(proto::JoinChannelBufferResponse {
@@ -78,6 +75,7 @@ impl Database {
                 replica_id: replica_id.to_proto() as u32,
                 base_text,
                 operations,
+                epoch: buffer.epoch as u64,
                 collaborators: collaborators
                     .into_iter()
                     .map(|collaborator| proto::Collaborator {
@@ -91,6 +89,154 @@ impl Database {
         .await
     }
 
+    pub async fn rejoin_channel_buffers(
+        &self,
+        buffers: &[proto::ChannelBufferVersion],
+        user_id: UserId,
+        connection_id: ConnectionId,
+    ) -> Result<Vec<RejoinedChannelBuffer>> {
+        self.transaction(|tx| async move {
+            let mut results = Vec::new();
+            for client_buffer in buffers {
+                let channel_id = ChannelId::from_proto(client_buffer.channel_id);
+                if self
+                    .check_user_is_channel_member(channel_id, user_id, &*tx)
+                    .await
+                    .is_err()
+                {
+                    log::info!("user is not a member of channel");
+                    continue;
+                }
+
+                let buffer = self.get_channel_buffer(channel_id, &*tx).await?;
+                let mut collaborators = channel_buffer_collaborator::Entity::find()
+                    .filter(channel_buffer_collaborator::Column::ChannelId.eq(channel_id))
+                    .all(&*tx)
+                    .await?;
+
+                // If the buffer epoch hasn't changed since the client lost
+                // connection, then the client's buffer can be syncronized with
+                // the server's buffer.
+                if buffer.epoch as u64 != client_buffer.epoch {
+                    log::info!("can't rejoin buffer, epoch has changed");
+                    continue;
+                }
+
+                // Find the collaborator record for this user's previous lost
+                // connection. Update it with the new connection id.
+                let server_id = ServerId(connection_id.owner_id as i32);
+                let Some(self_collaborator) = collaborators.iter_mut().find(|c| {
+                    c.user_id == user_id
+                        && (c.connection_lost || c.connection_server_id != server_id)
+                }) else {
+                    log::info!("can't rejoin buffer, no previous collaborator found");
+                    continue;
+                };
+                let old_connection_id = self_collaborator.connection();
+                *self_collaborator = channel_buffer_collaborator::ActiveModel {
+                    id: ActiveValue::Unchanged(self_collaborator.id),
+                    connection_id: ActiveValue::Set(connection_id.id as i32),
+                    connection_server_id: ActiveValue::Set(ServerId(connection_id.owner_id as i32)),
+                    connection_lost: ActiveValue::Set(false),
+                    ..Default::default()
+                }
+                .update(&*tx)
+                .await?;
+
+                let client_version = version_from_wire(&client_buffer.version);
+                let serialization_version = self
+                    .get_buffer_operation_serialization_version(buffer.id, buffer.epoch, &*tx)
+                    .await?;
+
+                let mut rows = buffer_operation::Entity::find()
+                    .filter(
+                        buffer_operation::Column::BufferId
+                            .eq(buffer.id)
+                            .and(buffer_operation::Column::Epoch.eq(buffer.epoch)),
+                    )
+                    .stream(&*tx)
+                    .await?;
+
+                // Find the server's version vector and any operations
+                // that the client has not seen.
+                let mut server_version = clock::Global::new();
+                let mut operations = Vec::new();
+                while let Some(row) = rows.next().await {
+                    let row = row?;
+                    let timestamp = clock::Lamport {
+                        replica_id: row.replica_id as u16,
+                        value: row.lamport_timestamp as u32,
+                    };
+                    server_version.observe(timestamp);
+                    if !client_version.observed(timestamp) {
+                        operations.push(proto::Operation {
+                            variant: Some(operation_from_storage(row, serialization_version)?),
+                        })
+                    }
+                }
+
+                results.push(RejoinedChannelBuffer {
+                    old_connection_id,
+                    buffer: proto::RejoinedChannelBuffer {
+                        channel_id: client_buffer.channel_id,
+                        version: version_to_wire(&server_version),
+                        operations,
+                        collaborators: collaborators
+                            .into_iter()
+                            .map(|collaborator| proto::Collaborator {
+                                peer_id: Some(collaborator.connection().into()),
+                                user_id: collaborator.user_id.to_proto(),
+                                replica_id: collaborator.replica_id.0 as u32,
+                            })
+                            .collect(),
+                    },
+                });
+            }
+
+            Ok(results)
+        })
+        .await
+    }
+
+    pub async fn clear_stale_channel_buffer_collaborators(
+        &self,
+        channel_id: ChannelId,
+        server_id: ServerId,
+    ) -> Result<RefreshedChannelBuffer> {
+        self.transaction(|tx| async move {
+            let collaborators = channel_buffer_collaborator::Entity::find()
+                .filter(channel_buffer_collaborator::Column::ChannelId.eq(channel_id))
+                .all(&*tx)
+                .await?;
+
+            let mut connection_ids = Vec::new();
+            let mut removed_collaborators = Vec::new();
+            let mut collaborator_ids_to_remove = Vec::new();
+            for collaborator in &collaborators {
+                if !collaborator.connection_lost && collaborator.connection_server_id == server_id {
+                    connection_ids.push(collaborator.connection());
+                } else {
+                    removed_collaborators.push(proto::RemoveChannelBufferCollaborator {
+                        channel_id: channel_id.to_proto(),
+                        peer_id: Some(collaborator.connection().into()),
+                    });
+                    collaborator_ids_to_remove.push(collaborator.id);
+                }
+            }
+
+            channel_buffer_collaborator::Entity::delete_many()
+                .filter(channel_buffer_collaborator::Column::Id.is_in(collaborator_ids_to_remove))
+                .exec(&*tx)
+                .await?;
+
+            Ok(RefreshedChannelBuffer {
+                connection_ids,
+                removed_collaborators,
+            })
+        })
+        .await
+    }
+
     pub async fn leave_channel_buffer(
         &self,
         channel_id: ChannelId,
@@ -103,6 +249,39 @@ impl Database {
         .await
     }
 
+    pub async fn leave_channel_buffers(
+        &self,
+        connection: ConnectionId,
+    ) -> Result<Vec<(ChannelId, Vec<ConnectionId>)>> {
+        self.transaction(|tx| async move {
+            #[derive(Debug, Clone, Copy, EnumIter, DeriveColumn)]
+            enum QueryChannelIds {
+                ChannelId,
+            }
+
+            let channel_ids: Vec<ChannelId> = channel_buffer_collaborator::Entity::find()
+                .select_only()
+                .column(channel_buffer_collaborator::Column::ChannelId)
+                .filter(Condition::all().add(
+                    channel_buffer_collaborator::Column::ConnectionId.eq(connection.id as i32),
+                ))
+                .into_values::<_, QueryChannelIds>()
+                .all(&*tx)
+                .await?;
+
+            let mut result = Vec::new();
+            for channel_id in channel_ids {
+                let collaborators = self
+                    .leave_channel_buffer_internal(channel_id, connection, &*tx)
+                    .await?;
+                result.push((channel_id, collaborators));
+            }
+
+            Ok(result)
+        })
+        .await
+    }
+
     pub async fn leave_channel_buffer_internal(
         &self,
         channel_id: ChannelId,
@@ -143,45 +322,12 @@ impl Database {
         drop(rows);
 
         if connections.is_empty() {
-            self.snapshot_buffer(channel_id, &tx).await?;
+            self.snapshot_channel_buffer(channel_id, &tx).await?;
         }
 
         Ok(connections)
     }
 
-    pub async fn leave_channel_buffers(
-        &self,
-        connection: ConnectionId,
-    ) -> Result<Vec<(ChannelId, Vec<ConnectionId>)>> {
-        self.transaction(|tx| async move {
-            #[derive(Debug, Clone, Copy, EnumIter, DeriveColumn)]
-            enum QueryChannelIds {
-                ChannelId,
-            }
-
-            let channel_ids: Vec<ChannelId> = channel_buffer_collaborator::Entity::find()
-                .select_only()
-                .column(channel_buffer_collaborator::Column::ChannelId)
-                .filter(Condition::all().add(
-                    channel_buffer_collaborator::Column::ConnectionId.eq(connection.id as i32),
-                ))
-                .into_values::<_, QueryChannelIds>()
-                .all(&*tx)
-                .await?;
-
-            let mut result = Vec::new();
-            for channel_id in channel_ids {
-                let collaborators = self
-                    .leave_channel_buffer_internal(channel_id, connection, &*tx)
-                    .await?;
-                result.push((channel_id, collaborators));
-            }
-
-            Ok(result)
-        })
-        .await
-    }
-
     pub async fn get_channel_buffer_collaborators(
         &self,
         channel_id: ChannelId,
@@ -224,20 +370,9 @@ impl Database {
                 .await?
                 .ok_or_else(|| anyhow!("no such buffer"))?;
 
-            #[derive(Debug, Clone, Copy, EnumIter, DeriveColumn)]
-            enum QueryVersion {
-                OperationSerializationVersion,
-            }
-
-            let serialization_version: i32 = buffer
-                .find_related(buffer_snapshot::Entity)
-                .select_only()
-                .column(buffer_snapshot::Column::OperationSerializationVersion)
-                .filter(buffer_snapshot::Column::Epoch.eq(buffer.epoch))
-                .into_values::<_, QueryVersion>()
-                .one(&*tx)
-                .await?
-                .ok_or_else(|| anyhow!("missing buffer snapshot"))?;
+            let serialization_version = self
+                .get_buffer_operation_serialization_version(buffer.id, buffer.epoch, &*tx)
+                .await?;
 
             let operations = operations
                 .iter()
@@ -245,6 +380,16 @@ impl Database {
                 .collect::<Vec<_>>();
             if !operations.is_empty() {
                 buffer_operation::Entity::insert_many(operations)
+                    .on_conflict(
+                        OnConflict::columns([
+                            buffer_operation::Column::BufferId,
+                            buffer_operation::Column::Epoch,
+                            buffer_operation::Column::LamportTimestamp,
+                            buffer_operation::Column::ReplicaId,
+                        ])
+                        .do_nothing()
+                        .to_owned(),
+                    )
                     .exec(&*tx)
                     .await?;
             }
@@ -270,6 +415,38 @@ impl Database {
         .await
     }
 
+    async fn get_buffer_operation_serialization_version(
+        &self,
+        buffer_id: BufferId,
+        epoch: i32,
+        tx: &DatabaseTransaction,
+    ) -> Result<i32> {
+        Ok(buffer_snapshot::Entity::find()
+            .filter(buffer_snapshot::Column::BufferId.eq(buffer_id))
+            .filter(buffer_snapshot::Column::Epoch.eq(epoch))
+            .select_only()
+            .column(buffer_snapshot::Column::OperationSerializationVersion)
+            .into_values::<_, QueryOperationSerializationVersion>()
+            .one(&*tx)
+            .await?
+            .ok_or_else(|| anyhow!("missing buffer snapshot"))?)
+    }
+
+    async fn get_channel_buffer(
+        &self,
+        channel_id: ChannelId,
+        tx: &DatabaseTransaction,
+    ) -> Result<buffer::Model> {
+        Ok(channel::Model {
+            id: channel_id,
+            ..Default::default()
+        }
+        .find_related(buffer::Entity)
+        .one(&*tx)
+        .await?
+        .ok_or_else(|| anyhow!("no such buffer"))?)
+    }
+
     async fn get_buffer_state(
         &self,
         buffer: &buffer::Model,
@@ -303,27 +480,20 @@ impl Database {
             .await?;
         let mut operations = Vec::new();
         while let Some(row) = rows.next().await {
-            let row = row?;
-
-            let operation = operation_from_storage(row, version)?;
             operations.push(proto::Operation {
-                variant: Some(operation),
+                variant: Some(operation_from_storage(row?, version)?),
             })
         }
 
         Ok((base_text, operations))
     }
 
-    async fn snapshot_buffer(&self, channel_id: ChannelId, tx: &DatabaseTransaction) -> Result<()> {
-        let buffer = channel::Model {
-            id: channel_id,
-            ..Default::default()
-        }
-        .find_related(buffer::Entity)
-        .one(&*tx)
-        .await?
-        .ok_or_else(|| anyhow!("no such buffer"))?;
-
+    async fn snapshot_channel_buffer(
+        &self,
+        channel_id: ChannelId,
+        tx: &DatabaseTransaction,
+    ) -> Result<()> {
+        let buffer = self.get_channel_buffer(channel_id, tx).await?;
         let (base_text, operations) = self.get_buffer_state(&buffer, tx).await?;
         if operations.is_empty() {
             return Ok(());
@@ -527,6 +697,22 @@ fn version_from_wire(message: &[proto::VectorClockEntry]) -> clock::Global {
     version
 }
 
+fn version_to_wire(version: &clock::Global) -> Vec<proto::VectorClockEntry> {
+    let mut message = Vec::new();
+    for entry in version.iter() {
+        message.push(proto::VectorClockEntry {
+            replica_id: entry.replica_id as u32,
+            timestamp: entry.value,
+        });
+    }
+    message
+}
+
+#[derive(Debug, Clone, Copy, EnumIter, DeriveColumn)]
+enum QueryOperationSerializationVersion {
+    OperationSerializationVersion,
+}
+
 mod storage {
     #![allow(non_snake_case)]
     use prost::Message;

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

@@ -1,6 +1,20 @@
 use super::*;
 
 impl Database {
+    #[cfg(test)]
+    pub async fn all_channels(&self) -> Result<Vec<(ChannelId, String)>> {
+        self.transaction(move |tx| async move {
+            let mut channels = Vec::new();
+            let mut rows = channel::Entity::find().stream(&*tx).await?;
+            while let Some(row) = rows.next().await {
+                let row = row?;
+                channels.push((row.id, row.name));
+            }
+            Ok(channels)
+        })
+        .await
+    }
+
     pub async fn create_root_channel(
         &self,
         name: &str,

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

@@ -1,7 +1,7 @@
 use super::*;
 
 impl Database {
-    pub async fn refresh_room(
+    pub async fn clear_stale_room_participants(
         &self,
         room_id: RoomId,
         new_server_id: ServerId,

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

@@ -14,31 +14,49 @@ impl Database {
         .await
     }
 
-    pub async fn stale_room_ids(
+    pub async fn stale_server_resource_ids(
         &self,
         environment: &str,
         new_server_id: ServerId,
-    ) -> Result<Vec<RoomId>> {
+    ) -> Result<(Vec<RoomId>, Vec<ChannelId>)> {
         self.transaction(|tx| async move {
             #[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)]
-            enum QueryAs {
+            enum QueryRoomIds {
                 RoomId,
             }
 
+            #[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)]
+            enum QueryChannelIds {
+                ChannelId,
+            }
+
             let stale_server_epochs = self
                 .stale_server_ids(environment, new_server_id, &tx)
                 .await?;
-            Ok(room_participant::Entity::find()
+            let room_ids = room_participant::Entity::find()
                 .select_only()
                 .column(room_participant::Column::RoomId)
                 .distinct()
                 .filter(
                     room_participant::Column::AnsweringConnectionServerId
-                        .is_in(stale_server_epochs),
+                        .is_in(stale_server_epochs.iter().copied()),
                 )
-                .into_values::<_, QueryAs>()
+                .into_values::<_, QueryRoomIds>()
                 .all(&*tx)
-                .await?)
+                .await?;
+            let channel_ids = channel_buffer_collaborator::Entity::find()
+                .select_only()
+                .column(channel_buffer_collaborator::Column::ChannelId)
+                .distinct()
+                .filter(
+                    channel_buffer_collaborator::Column::ConnectionServerId
+                        .is_in(stale_server_epochs.iter().copied()),
+                )
+                .into_values::<_, QueryChannelIds>()
+                .all(&*tx)
+                .await?;
+
+            Ok((room_ids, channel_ids))
         })
         .await
     }

crates/collab/src/rpc.rs 🔗

@@ -251,6 +251,7 @@ impl Server {
             .add_request_handler(join_channel_buffer)
             .add_request_handler(leave_channel_buffer)
             .add_message_handler(update_channel_buffer)
+            .add_request_handler(rejoin_channel_buffers)
             .add_request_handler(get_channel_members)
             .add_request_handler(respond_to_channel_invite)
             .add_request_handler(join_channel)
@@ -277,13 +278,33 @@ impl Server {
                 tracing::info!("waiting for cleanup timeout");
                 timeout.await;
                 tracing::info!("cleanup timeout expired, retrieving stale rooms");
-                if let Some(room_ids) = app_state
+                if let Some((room_ids, channel_ids)) = app_state
                     .db
-                    .stale_room_ids(&app_state.config.zed_environment, server_id)
+                    .stale_server_resource_ids(&app_state.config.zed_environment, server_id)
                     .await
                     .trace_err()
                 {
                     tracing::info!(stale_room_count = room_ids.len(), "retrieved stale rooms");
+                    tracing::info!(
+                        stale_channel_buffer_count = channel_ids.len(),
+                        "retrieved stale channel buffers"
+                    );
+
+                    for channel_id in channel_ids {
+                        if let Some(refreshed_channel_buffer) = app_state
+                            .db
+                            .clear_stale_channel_buffer_collaborators(channel_id, server_id)
+                            .await
+                            .trace_err()
+                        {
+                            for connection_id in refreshed_channel_buffer.connection_ids {
+                                for message in &refreshed_channel_buffer.removed_collaborators {
+                                    peer.send(connection_id, message.clone()).trace_err();
+                                }
+                            }
+                        }
+                    }
+
                     for room_id in room_ids {
                         let mut contacts_to_update = HashSet::default();
                         let mut canceled_calls_to_user_ids = Vec::new();
@@ -292,7 +313,7 @@ impl Server {
 
                         if let Some(mut refreshed_room) = app_state
                             .db
-                            .refresh_room(room_id, server_id)
+                            .clear_stale_room_participants(room_id, server_id)
                             .await
                             .trace_err()
                         {
@@ -854,13 +875,13 @@ async fn connection_lost(
         .await
         .trace_err();
 
-    leave_channel_buffers_for_session(&session)
-        .await
-        .trace_err();
-
     futures::select_biased! {
         _ = executor.sleep(RECONNECT_TIMEOUT).fuse() => {
+            log::info!("connection lost, removing all resources for user:{}, connection:{:?}", session.user_id, session.connection_id);
             leave_room_for_session(&session).await.trace_err();
+            leave_channel_buffers_for_session(&session)
+                .await
+                .trace_err();
 
             if !session
                 .connection_pool()
@@ -2547,6 +2568,41 @@ async fn update_channel_buffer(
     Ok(())
 }
 
+async fn rejoin_channel_buffers(
+    request: proto::RejoinChannelBuffers,
+    response: Response<proto::RejoinChannelBuffers>,
+    session: Session,
+) -> Result<()> {
+    let db = session.db().await;
+    let buffers = db
+        .rejoin_channel_buffers(&request.buffers, session.user_id, session.connection_id)
+        .await?;
+
+    for buffer in &buffers {
+        let collaborators_to_notify = buffer
+            .buffer
+            .collaborators
+            .iter()
+            .filter_map(|c| Some(c.peer_id?.into()));
+        channel_buffer_updated(
+            session.connection_id,
+            collaborators_to_notify,
+            &proto::UpdateChannelBufferCollaborator {
+                channel_id: buffer.buffer.channel_id,
+                old_peer_id: Some(buffer.old_connection_id.into()),
+                new_peer_id: Some(session.connection_id.into()),
+            },
+            &session.peer,
+        );
+    }
+
+    response.send(proto::RejoinChannelBuffersResponse {
+        buffers: buffers.into_iter().map(|b| b.buffer).collect(),
+    })?;
+
+    Ok(())
+}
+
 async fn leave_channel_buffer(
     request: proto::LeaveChannelBuffer,
     response: Response<proto::LeaveChannelBuffer>,

crates/collab/src/tests.rs 🔗

@@ -1,555 +1,18 @@
-use crate::{
-    db::{tests::TestDb, NewUserParams, UserId},
-    executor::Executor,
-    rpc::{Server, CLEANUP_TIMEOUT},
-    AppState,
-};
-use anyhow::anyhow;
-use call::{ActiveCall, Room};
-use channel::ChannelStore;
-use client::{
-    self, proto::PeerId, Client, Connection, Credentials, EstablishConnectionError, UserStore,
-};
-use collections::{HashMap, HashSet};
-use fs::FakeFs;
-use futures::{channel::oneshot, StreamExt as _};
-use gpui::{executor::Deterministic, ModelHandle, Task, TestAppContext, WindowHandle};
-use language::LanguageRegistry;
-use parking_lot::Mutex;
-use project::{Project, WorktreeId};
-use settings::SettingsStore;
-use std::{
-    cell::{Ref, RefCell, RefMut},
-    env,
-    ops::{Deref, DerefMut},
-    path::Path,
-    sync::{
-        atomic::{AtomicBool, AtomicUsize, Ordering::SeqCst},
-        Arc,
-    },
-};
-use util::http::FakeHttpClient;
-use workspace::Workspace;
+use call::Room;
+use gpui::{ModelHandle, TestAppContext};
 
 mod channel_buffer_tests;
 mod channel_tests;
 mod integration_tests;
-mod randomized_integration_tests;
-
-struct TestServer {
-    app_state: Arc<AppState>,
-    server: Arc<Server>,
-    connection_killers: Arc<Mutex<HashMap<PeerId, Arc<AtomicBool>>>>,
-    forbid_connections: Arc<AtomicBool>,
-    _test_db: TestDb,
-    test_live_kit_server: Arc<live_kit_client::TestServer>,
-}
-
-impl TestServer {
-    async fn start(deterministic: &Arc<Deterministic>) -> Self {
-        static NEXT_LIVE_KIT_SERVER_ID: AtomicUsize = AtomicUsize::new(0);
-
-        let use_postgres = env::var("USE_POSTGRES").ok();
-        let use_postgres = use_postgres.as_deref();
-        let test_db = if use_postgres == Some("true") || use_postgres == Some("1") {
-            TestDb::postgres(deterministic.build_background())
-        } else {
-            TestDb::sqlite(deterministic.build_background())
-        };
-        let live_kit_server_id = NEXT_LIVE_KIT_SERVER_ID.fetch_add(1, SeqCst);
-        let live_kit_server = live_kit_client::TestServer::create(
-            format!("http://livekit.{}.test", live_kit_server_id),
-            format!("devkey-{}", live_kit_server_id),
-            format!("secret-{}", live_kit_server_id),
-            deterministic.build_background(),
-        )
-        .unwrap();
-        let app_state = Self::build_app_state(&test_db, &live_kit_server).await;
-        let epoch = app_state
-            .db
-            .create_server(&app_state.config.zed_environment)
-            .await
-            .unwrap();
-        let server = Server::new(
-            epoch,
-            app_state.clone(),
-            Executor::Deterministic(deterministic.build_background()),
-        );
-        server.start().await.unwrap();
-        // Advance clock to ensure the server's cleanup task is finished.
-        deterministic.advance_clock(CLEANUP_TIMEOUT);
-        Self {
-            app_state,
-            server,
-            connection_killers: Default::default(),
-            forbid_connections: Default::default(),
-            _test_db: test_db,
-            test_live_kit_server: live_kit_server,
-        }
-    }
-
-    async fn reset(&self) {
-        self.app_state.db.reset();
-        let epoch = self
-            .app_state
-            .db
-            .create_server(&self.app_state.config.zed_environment)
-            .await
-            .unwrap();
-        self.server.reset(epoch);
-    }
-
-    async fn create_client(&mut self, cx: &mut TestAppContext, name: &str) -> TestClient {
-        cx.update(|cx| {
-            if cx.has_global::<SettingsStore>() {
-                panic!("Same cx used to create two test clients")
-            }
-            cx.set_global(SettingsStore::test(cx));
-        });
-
-        let http = FakeHttpClient::with_404_response();
-        let user_id = if let Ok(Some(user)) = self.app_state.db.get_user_by_github_login(name).await
-        {
-            user.id
-        } else {
-            self.app_state
-                .db
-                .create_user(
-                    &format!("{name}@example.com"),
-                    false,
-                    NewUserParams {
-                        github_login: name.into(),
-                        github_user_id: 0,
-                        invite_count: 0,
-                    },
-                )
-                .await
-                .expect("creating user failed")
-                .user_id
-        };
-        let client_name = name.to_string();
-        let mut client = cx.read(|cx| Client::new(http.clone(), cx));
-        let server = self.server.clone();
-        let db = self.app_state.db.clone();
-        let connection_killers = self.connection_killers.clone();
-        let forbid_connections = self.forbid_connections.clone();
-
-        Arc::get_mut(&mut client)
-            .unwrap()
-            .set_id(user_id.0 as usize)
-            .override_authenticate(move |cx| {
-                cx.spawn(|_| async move {
-                    let access_token = "the-token".to_string();
-                    Ok(Credentials {
-                        user_id: user_id.0 as u64,
-                        access_token,
-                    })
-                })
-            })
-            .override_establish_connection(move |credentials, cx| {
-                assert_eq!(credentials.user_id, user_id.0 as u64);
-                assert_eq!(credentials.access_token, "the-token");
-
-                let server = server.clone();
-                let db = db.clone();
-                let connection_killers = connection_killers.clone();
-                let forbid_connections = forbid_connections.clone();
-                let client_name = client_name.clone();
-                cx.spawn(move |cx| async move {
-                    if forbid_connections.load(SeqCst) {
-                        Err(EstablishConnectionError::other(anyhow!(
-                            "server is forbidding connections"
-                        )))
-                    } else {
-                        let (client_conn, server_conn, killed) =
-                            Connection::in_memory(cx.background());
-                        let (connection_id_tx, connection_id_rx) = oneshot::channel();
-                        let user = db
-                            .get_user_by_id(user_id)
-                            .await
-                            .expect("retrieving user failed")
-                            .unwrap();
-                        cx.background()
-                            .spawn(server.handle_connection(
-                                server_conn,
-                                client_name,
-                                user,
-                                Some(connection_id_tx),
-                                Executor::Deterministic(cx.background()),
-                            ))
-                            .detach();
-                        let connection_id = connection_id_rx.await.unwrap();
-                        connection_killers
-                            .lock()
-                            .insert(connection_id.into(), killed);
-                        Ok(client_conn)
-                    }
-                })
-            });
-
-        let fs = FakeFs::new(cx.background());
-        let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http, cx));
-        let channel_store =
-            cx.add_model(|cx| ChannelStore::new(client.clone(), user_store.clone(), cx));
-        let app_state = Arc::new(workspace::AppState {
-            client: client.clone(),
-            user_store: user_store.clone(),
-            channel_store: channel_store.clone(),
-            languages: Arc::new(LanguageRegistry::test()),
-            fs: fs.clone(),
-            build_window_options: |_, _, _| Default::default(),
-            initialize_workspace: |_, _, _, _| Task::ready(Ok(())),
-            background_actions: || &[],
-        });
-
-        cx.update(|cx| {
-            theme::init((), cx);
-            Project::init(&client, cx);
-            client::init(&client, cx);
-            language::init(cx);
-            editor::init_settings(cx);
-            workspace::init(app_state.clone(), cx);
-            audio::init((), cx);
-            call::init(client.clone(), user_store.clone(), cx);
-            channel::init(&client);
-        });
-
-        client
-            .authenticate_and_connect(false, &cx.to_async())
-            .await
-            .unwrap();
-
-        let client = TestClient {
-            app_state,
-            username: name.to_string(),
-            state: Default::default(),
-        };
-        client.wait_for_current_user(cx).await;
-        client
-    }
-
-    fn disconnect_client(&self, peer_id: PeerId) {
-        self.connection_killers
-            .lock()
-            .remove(&peer_id)
-            .unwrap()
-            .store(true, SeqCst);
-    }
-
-    fn forbid_connections(&self) {
-        self.forbid_connections.store(true, SeqCst);
-    }
-
-    fn allow_connections(&self) {
-        self.forbid_connections.store(false, SeqCst);
-    }
-
-    async fn make_contacts(&self, clients: &mut [(&TestClient, &mut TestAppContext)]) {
-        for ix in 1..clients.len() {
-            let (left, right) = clients.split_at_mut(ix);
-            let (client_a, cx_a) = left.last_mut().unwrap();
-            for (client_b, cx_b) in right {
-                client_a
-                    .app_state
-                    .user_store
-                    .update(*cx_a, |store, cx| {
-                        store.request_contact(client_b.user_id().unwrap(), cx)
-                    })
-                    .await
-                    .unwrap();
-                cx_a.foreground().run_until_parked();
-                client_b
-                    .app_state
-                    .user_store
-                    .update(*cx_b, |store, cx| {
-                        store.respond_to_contact_request(client_a.user_id().unwrap(), true, cx)
-                    })
-                    .await
-                    .unwrap();
-            }
-        }
-    }
-
-    async fn make_channel(
-        &self,
-        channel: &str,
-        admin: (&TestClient, &mut TestAppContext),
-        members: &mut [(&TestClient, &mut TestAppContext)],
-    ) -> u64 {
-        let (admin_client, admin_cx) = admin;
-        let channel_id = admin_client
-            .app_state
-            .channel_store
-            .update(admin_cx, |channel_store, cx| {
-                channel_store.create_channel(channel, None, cx)
-            })
-            .await
-            .unwrap();
-
-        for (member_client, member_cx) in members {
-            admin_client
-                .app_state
-                .channel_store
-                .update(admin_cx, |channel_store, cx| {
-                    channel_store.invite_member(
-                        channel_id,
-                        member_client.user_id().unwrap(),
-                        false,
-                        cx,
-                    )
-                })
-                .await
-                .unwrap();
-
-            admin_cx.foreground().run_until_parked();
-
-            member_client
-                .app_state
-                .channel_store
-                .update(*member_cx, |channels, _| {
-                    channels.respond_to_channel_invite(channel_id, true)
-                })
-                .await
-                .unwrap();
-        }
-
-        channel_id
-    }
-
-    async fn create_room(&self, clients: &mut [(&TestClient, &mut TestAppContext)]) {
-        self.make_contacts(clients).await;
-
-        let (left, right) = clients.split_at_mut(1);
-        let (_client_a, cx_a) = &mut left[0];
-        let active_call_a = cx_a.read(ActiveCall::global);
-
-        for (client_b, cx_b) in right {
-            let user_id_b = client_b.current_user_id(*cx_b).to_proto();
-            active_call_a
-                .update(*cx_a, |call, cx| call.invite(user_id_b, None, cx))
-                .await
-                .unwrap();
-
-            cx_b.foreground().run_until_parked();
-            let active_call_b = cx_b.read(ActiveCall::global);
-            active_call_b
-                .update(*cx_b, |call, cx| call.accept_incoming(cx))
-                .await
-                .unwrap();
-        }
-    }
-
-    async fn build_app_state(
-        test_db: &TestDb,
-        fake_server: &live_kit_client::TestServer,
-    ) -> Arc<AppState> {
-        Arc::new(AppState {
-            db: test_db.db().clone(),
-            live_kit_client: Some(Arc::new(fake_server.create_api_client())),
-            config: Default::default(),
-        })
-    }
-}
-
-impl Deref for TestServer {
-    type Target = Server;
-
-    fn deref(&self) -> &Self::Target {
-        &self.server
-    }
-}
-
-impl Drop for TestServer {
-    fn drop(&mut self) {
-        self.server.teardown();
-        self.test_live_kit_server.teardown().unwrap();
-    }
-}
-
-struct TestClient {
-    username: String,
-    state: RefCell<TestClientState>,
-    app_state: Arc<workspace::AppState>,
-}
-
-#[derive(Default)]
-struct TestClientState {
-    local_projects: Vec<ModelHandle<Project>>,
-    remote_projects: Vec<ModelHandle<Project>>,
-    buffers: HashMap<ModelHandle<Project>, HashSet<ModelHandle<language::Buffer>>>,
-}
-
-impl Deref for TestClient {
-    type Target = Arc<Client>;
-
-    fn deref(&self) -> &Self::Target {
-        &self.app_state.client
-    }
-}
-
-struct ContactsSummary {
-    pub current: Vec<String>,
-    pub outgoing_requests: Vec<String>,
-    pub incoming_requests: Vec<String>,
-}
-
-impl TestClient {
-    pub fn fs(&self) -> &FakeFs {
-        self.app_state.fs.as_fake()
-    }
-
-    pub fn channel_store(&self) -> &ModelHandle<ChannelStore> {
-        &self.app_state.channel_store
-    }
-
-    pub fn user_store(&self) -> &ModelHandle<UserStore> {
-        &self.app_state.user_store
-    }
-
-    pub fn language_registry(&self) -> &Arc<LanguageRegistry> {
-        &self.app_state.languages
-    }
-
-    pub fn client(&self) -> &Arc<Client> {
-        &self.app_state.client
-    }
+mod random_channel_buffer_tests;
+mod random_project_collaboration_tests;
+mod randomized_test_helpers;
+mod test_server;
 
-    pub fn current_user_id(&self, cx: &TestAppContext) -> UserId {
-        UserId::from_proto(
-            self.app_state
-                .user_store
-                .read_with(cx, |user_store, _| user_store.current_user().unwrap().id),
-        )
-    }
-
-    async fn wait_for_current_user(&self, cx: &TestAppContext) {
-        let mut authed_user = self
-            .app_state
-            .user_store
-            .read_with(cx, |user_store, _| user_store.watch_current_user());
-        while authed_user.next().await.unwrap().is_none() {}
-    }
-
-    async fn clear_contacts(&self, cx: &mut TestAppContext) {
-        self.app_state
-            .user_store
-            .update(cx, |store, _| store.clear_contacts())
-            .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.app_state
-            .user_store
-            .read_with(cx, |store, _| ContactsSummary {
-                current: store
-                    .contacts()
-                    .iter()
-                    .map(|contact| contact.user.github_login.clone())
-                    .collect(),
-                outgoing_requests: store
-                    .outgoing_contact_requests()
-                    .iter()
-                    .map(|user| user.github_login.clone())
-                    .collect(),
-                incoming_requests: store
-                    .incoming_contact_requests()
-                    .iter()
-                    .map(|user| user.github_login.clone())
-                    .collect(),
-            })
-    }
-
-    async fn build_local_project(
-        &self,
-        root_path: impl AsRef<Path>,
-        cx: &mut TestAppContext,
-    ) -> (ModelHandle<Project>, WorktreeId) {
-        let project = cx.update(|cx| {
-            Project::local(
-                self.client().clone(),
-                self.app_state.user_store.clone(),
-                self.app_state.languages.clone(),
-                self.app_state.fs.clone(),
-                cx,
-            )
-        });
-        let (worktree, _) = project
-            .update(cx, |p, cx| {
-                p.find_or_create_local_worktree(root_path, true, cx)
-            })
-            .await
-            .unwrap();
-        worktree
-            .read_with(cx, |tree, _| tree.as_local().unwrap().scan_complete())
-            .await;
-        (project, worktree.read_with(cx, |tree, _| tree.id()))
-    }
-
-    async fn build_remote_project(
-        &self,
-        host_project_id: u64,
-        guest_cx: &mut TestAppContext,
-    ) -> ModelHandle<Project> {
-        let active_call = guest_cx.read(ActiveCall::global);
-        let room = active_call.read_with(guest_cx, |call, _| call.room().unwrap().clone());
-        room.update(guest_cx, |room, cx| {
-            room.join_project(
-                host_project_id,
-                self.app_state.languages.clone(),
-                self.app_state.fs.clone(),
-                cx,
-            )
-        })
-        .await
-        .unwrap()
-    }
-
-    fn build_workspace(
-        &self,
-        project: &ModelHandle<Project>,
-        cx: &mut TestAppContext,
-    ) -> WindowHandle<Workspace> {
-        cx.add_window(|cx| Workspace::new(0, project.clone(), self.app_state.clone(), cx))
-    }
-}
-
-impl Drop for TestClient {
-    fn drop(&mut self) {
-        self.app_state.client.teardown();
-    }
-}
+pub use randomized_test_helpers::{
+    run_randomized_test, save_randomized_test_plan, RandomizedTest, TestError, UserTestPlan,
+};
+pub use test_server::{TestClient, TestServer};
 
 #[derive(Debug, Eq, PartialEq)]
 struct RoomParticipants {

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

@@ -1,4 +1,7 @@
-use crate::{rpc::RECONNECT_TIMEOUT, tests::TestServer};
+use crate::{
+    rpc::{CLEANUP_TIMEOUT, RECONNECT_TIMEOUT},
+    tests::TestServer,
+};
 use call::ActiveCall;
 use channel::Channel;
 use client::UserId;
@@ -21,20 +24,19 @@ async fn test_core_channel_buffers(
     let client_a = server.create_client(cx_a, "user_a").await;
     let client_b = server.create_client(cx_b, "user_b").await;
 
-    let zed_id = server
+    let channel_id = server
         .make_channel("zed", (&client_a, cx_a), &mut [(&client_b, cx_b)])
         .await;
 
     // Client A joins the channel buffer
     let channel_buffer_a = client_a
         .channel_store()
-        .update(cx_a, |channel, cx| channel.open_channel_buffer(zed_id, cx))
+        .update(cx_a, |store, cx| store.open_channel_buffer(channel_id, cx))
         .await
         .unwrap();
 
     // Client A edits the buffer
     let buffer_a = channel_buffer_a.read_with(cx_a, |buffer, _| buffer.buffer());
-
     buffer_a.update(cx_a, |buffer, cx| {
         buffer.edit([(0..0, "hello world")], None, cx)
     });
@@ -45,17 +47,15 @@ async fn test_core_channel_buffers(
         buffer.edit([(0..5, "goodbye")], None, cx)
     });
     buffer_a.update(cx_a, |buffer, cx| buffer.undo(cx));
-    deterministic.run_until_parked();
-
     assert_eq!(buffer_text(&buffer_a, cx_a), "hello, cruel world");
+    deterministic.run_until_parked();
 
     // Client B joins the channel buffer
     let channel_buffer_b = client_b
         .channel_store()
-        .update(cx_b, |channel, cx| channel.open_channel_buffer(zed_id, cx))
+        .update(cx_b, |store, cx| store.open_channel_buffer(channel_id, cx))
         .await
         .unwrap();
-
     channel_buffer_b.read_with(cx_b, |buffer, _| {
         assert_collaborators(
             buffer.collaborators(),
@@ -91,9 +91,7 @@ async fn test_core_channel_buffers(
     // Client A rejoins the channel buffer
     let _channel_buffer_a = client_a
         .channel_store()
-        .update(cx_a, |channels, cx| {
-            channels.open_channel_buffer(zed_id, cx)
-        })
+        .update(cx_a, |store, cx| store.open_channel_buffer(channel_id, cx))
         .await
         .unwrap();
     deterministic.run_until_parked();
@@ -136,7 +134,7 @@ async fn test_channel_buffer_replica_ids(
 
     let channel_id = server
         .make_channel(
-            "zed",
+            "the-channel",
             (&client_a, cx_a),
             &mut [(&client_b, cx_b), (&client_c, cx_c)],
         )
@@ -160,23 +158,17 @@ async fn test_channel_buffer_replica_ids(
     // C first so that the replica IDs in the project and the channel buffer are different
     let channel_buffer_c = client_c
         .channel_store()
-        .update(cx_c, |channel, cx| {
-            channel.open_channel_buffer(channel_id, cx)
-        })
+        .update(cx_c, |store, cx| store.open_channel_buffer(channel_id, cx))
         .await
         .unwrap();
     let channel_buffer_b = client_b
         .channel_store()
-        .update(cx_b, |channel, cx| {
-            channel.open_channel_buffer(channel_id, cx)
-        })
+        .update(cx_b, |store, cx| store.open_channel_buffer(channel_id, cx))
         .await
         .unwrap();
     let channel_buffer_a = client_a
         .channel_store()
-        .update(cx_a, |channel, cx| {
-            channel.open_channel_buffer(channel_id, cx)
-        })
+        .update(cx_a, |store, cx| store.open_channel_buffer(channel_id, cx))
         .await
         .unwrap();
 
@@ -286,28 +278,30 @@ async fn test_reopen_channel_buffer(deterministic: Arc<Deterministic>, cx_a: &mu
     let mut server = TestServer::start(&deterministic).await;
     let client_a = server.create_client(cx_a, "user_a").await;
 
-    let zed_id = server.make_channel("zed", (&client_a, cx_a), &mut []).await;
+    let channel_id = server
+        .make_channel("the-channel", (&client_a, cx_a), &mut [])
+        .await;
 
     let channel_buffer_1 = client_a
         .channel_store()
-        .update(cx_a, |channel, cx| channel.open_channel_buffer(zed_id, cx));
+        .update(cx_a, |store, cx| store.open_channel_buffer(channel_id, cx));
     let channel_buffer_2 = client_a
         .channel_store()
-        .update(cx_a, |channel, cx| channel.open_channel_buffer(zed_id, cx));
+        .update(cx_a, |store, cx| store.open_channel_buffer(channel_id, cx));
     let channel_buffer_3 = client_a
         .channel_store()
-        .update(cx_a, |channel, cx| channel.open_channel_buffer(zed_id, cx));
+        .update(cx_a, |store, cx| store.open_channel_buffer(channel_id, cx));
 
     // All concurrent tasks for opening a channel buffer return the same model handle.
-    let (channel_buffer_1, channel_buffer_2, channel_buffer_3) =
+    let (channel_buffer, channel_buffer_2, channel_buffer_3) =
         future::try_join3(channel_buffer_1, channel_buffer_2, channel_buffer_3)
             .await
             .unwrap();
-    let model_id = channel_buffer_1.id();
-    assert_eq!(channel_buffer_1, channel_buffer_2);
-    assert_eq!(channel_buffer_1, channel_buffer_3);
+    let channel_buffer_model_id = channel_buffer.id();
+    assert_eq!(channel_buffer, channel_buffer_2);
+    assert_eq!(channel_buffer, channel_buffer_3);
 
-    channel_buffer_1.update(cx_a, |buffer, cx| {
+    channel_buffer.update(cx_a, |buffer, cx| {
         buffer.buffer().update(cx, |buffer, cx| {
             buffer.edit([(0..0, "hello")], None, cx);
         })
@@ -315,7 +309,7 @@ async fn test_reopen_channel_buffer(deterministic: Arc<Deterministic>, cx_a: &mu
     deterministic.run_until_parked();
 
     cx_a.update(|_| {
-        drop(channel_buffer_1);
+        drop(channel_buffer);
         drop(channel_buffer_2);
         drop(channel_buffer_3);
     });
@@ -324,10 +318,10 @@ async fn test_reopen_channel_buffer(deterministic: Arc<Deterministic>, cx_a: &mu
     // The channel buffer can be reopened after dropping it.
     let channel_buffer = client_a
         .channel_store()
-        .update(cx_a, |channel, cx| channel.open_channel_buffer(zed_id, cx))
+        .update(cx_a, |store, cx| store.open_channel_buffer(channel_id, cx))
         .await
         .unwrap();
-    assert_ne!(channel_buffer.id(), model_id);
+    assert_ne!(channel_buffer.id(), channel_buffer_model_id);
     channel_buffer.update(cx_a, |buffer, cx| {
         buffer.buffer().update(cx, |buffer, _| {
             assert_eq!(buffer.text(), "hello");
@@ -347,22 +341,17 @@ async fn test_channel_buffer_disconnect(
     let client_b = server.create_client(cx_b, "user_b").await;
 
     let channel_id = server
-        .make_channel("zed", (&client_a, cx_a), &mut [(&client_b, cx_b)])
+        .make_channel("the-channel", (&client_a, cx_a), &mut [(&client_b, cx_b)])
         .await;
 
     let channel_buffer_a = client_a
         .channel_store()
-        .update(cx_a, |channel, cx| {
-            channel.open_channel_buffer(channel_id, cx)
-        })
+        .update(cx_a, |store, cx| store.open_channel_buffer(channel_id, cx))
         .await
         .unwrap();
-
     let channel_buffer_b = client_b
         .channel_store()
-        .update(cx_b, |channel, cx| {
-            channel.open_channel_buffer(channel_id, cx)
-        })
+        .update(cx_b, |store, cx| store.open_channel_buffer(channel_id, cx))
         .await
         .unwrap();
 
@@ -375,7 +364,7 @@ async fn test_channel_buffer_disconnect(
             buffer.channel().as_ref(),
             &Channel {
                 id: channel_id,
-                name: "zed".to_string()
+                name: "the-channel".to_string()
             }
         );
         assert!(!buffer.is_connected());
@@ -403,13 +392,180 @@ async fn test_channel_buffer_disconnect(
             buffer.channel().as_ref(),
             &Channel {
                 id: channel_id,
-                name: "zed".to_string()
+                name: "the-channel".to_string()
             }
         );
         assert!(!buffer.is_connected());
     });
 }
 
+#[gpui::test]
+async fn test_rejoin_channel_buffer(
+    deterministic: Arc<Deterministic>,
+    cx_a: &mut TestAppContext,
+    cx_b: &mut TestAppContext,
+) {
+    deterministic.forbid_parking();
+    let mut server = TestServer::start(&deterministic).await;
+    let client_a = server.create_client(cx_a, "user_a").await;
+    let client_b = server.create_client(cx_b, "user_b").await;
+
+    let channel_id = server
+        .make_channel("the-channel", (&client_a, cx_a), &mut [(&client_b, cx_b)])
+        .await;
+
+    let channel_buffer_a = client_a
+        .channel_store()
+        .update(cx_a, |store, cx| store.open_channel_buffer(channel_id, cx))
+        .await
+        .unwrap();
+    let channel_buffer_b = client_b
+        .channel_store()
+        .update(cx_b, |store, cx| store.open_channel_buffer(channel_id, cx))
+        .await
+        .unwrap();
+
+    channel_buffer_a.update(cx_a, |buffer, cx| {
+        buffer.buffer().update(cx, |buffer, cx| {
+            buffer.edit([(0..0, "1")], None, cx);
+        })
+    });
+    deterministic.run_until_parked();
+
+    // Client A disconnects.
+    server.forbid_connections();
+    server.disconnect_client(client_a.peer_id().unwrap());
+
+    // Both clients make an edit.
+    channel_buffer_a.update(cx_a, |buffer, cx| {
+        buffer.buffer().update(cx, |buffer, cx| {
+            buffer.edit([(1..1, "2")], None, cx);
+        })
+    });
+    channel_buffer_b.update(cx_b, |buffer, cx| {
+        buffer.buffer().update(cx, |buffer, cx| {
+            buffer.edit([(0..0, "0")], None, cx);
+        })
+    });
+
+    // Both clients see their own edit.
+    deterministic.run_until_parked();
+    channel_buffer_a.read_with(cx_a, |buffer, cx| {
+        assert_eq!(buffer.buffer().read(cx).text(), "12");
+    });
+    channel_buffer_b.read_with(cx_b, |buffer, cx| {
+        assert_eq!(buffer.buffer().read(cx).text(), "01");
+    });
+
+    // Client A reconnects. Both clients see each other's edits, and see
+    // the same collaborators.
+    server.allow_connections();
+    deterministic.advance_clock(RECEIVE_TIMEOUT);
+    channel_buffer_a.read_with(cx_a, |buffer, cx| {
+        assert_eq!(buffer.buffer().read(cx).text(), "012");
+    });
+    channel_buffer_b.read_with(cx_b, |buffer, cx| {
+        assert_eq!(buffer.buffer().read(cx).text(), "012");
+    });
+
+    channel_buffer_a.read_with(cx_a, |buffer_a, _| {
+        channel_buffer_b.read_with(cx_b, |buffer_b, _| {
+            assert_eq!(buffer_a.collaborators(), buffer_b.collaborators());
+        });
+    });
+}
+
+#[gpui::test]
+async fn test_channel_buffers_and_server_restarts(
+    deterministic: Arc<Deterministic>,
+    cx_a: &mut TestAppContext,
+    cx_b: &mut TestAppContext,
+    cx_c: &mut TestAppContext,
+) {
+    deterministic.forbid_parking();
+    let mut server = TestServer::start(&deterministic).await;
+    let client_a = server.create_client(cx_a, "user_a").await;
+    let client_b = server.create_client(cx_b, "user_b").await;
+    let client_c = server.create_client(cx_c, "user_c").await;
+
+    let channel_id = server
+        .make_channel(
+            "the-channel",
+            (&client_a, cx_a),
+            &mut [(&client_b, cx_b), (&client_c, cx_c)],
+        )
+        .await;
+
+    let channel_buffer_a = client_a
+        .channel_store()
+        .update(cx_a, |store, cx| store.open_channel_buffer(channel_id, cx))
+        .await
+        .unwrap();
+    let channel_buffer_b = client_b
+        .channel_store()
+        .update(cx_b, |store, cx| store.open_channel_buffer(channel_id, cx))
+        .await
+        .unwrap();
+    let _channel_buffer_c = client_c
+        .channel_store()
+        .update(cx_c, |store, cx| store.open_channel_buffer(channel_id, cx))
+        .await
+        .unwrap();
+
+    channel_buffer_a.update(cx_a, |buffer, cx| {
+        buffer.buffer().update(cx, |buffer, cx| {
+            buffer.edit([(0..0, "1")], None, cx);
+        })
+    });
+    deterministic.run_until_parked();
+
+    // Client C can't reconnect.
+    client_c.override_establish_connection(|_, cx| cx.spawn(|_| future::pending()));
+
+    // Server stops.
+    server.reset().await;
+    deterministic.advance_clock(RECEIVE_TIMEOUT);
+
+    // While the server is down, both clients make an edit.
+    channel_buffer_a.update(cx_a, |buffer, cx| {
+        buffer.buffer().update(cx, |buffer, cx| {
+            buffer.edit([(1..1, "2")], None, cx);
+        })
+    });
+    channel_buffer_b.update(cx_b, |buffer, cx| {
+        buffer.buffer().update(cx, |buffer, cx| {
+            buffer.edit([(0..0, "0")], None, cx);
+        })
+    });
+
+    // Server restarts.
+    server.start().await.unwrap();
+    deterministic.advance_clock(CLEANUP_TIMEOUT);
+
+    // Clients reconnects. Clients A and B see each other's edits, and see
+    // that client C has disconnected.
+    channel_buffer_a.read_with(cx_a, |buffer, cx| {
+        assert_eq!(buffer.buffer().read(cx).text(), "012");
+    });
+    channel_buffer_b.read_with(cx_b, |buffer, cx| {
+        assert_eq!(buffer.buffer().read(cx).text(), "012");
+    });
+
+    channel_buffer_a.read_with(cx_a, |buffer_a, _| {
+        channel_buffer_b.read_with(cx_b, |buffer_b, _| {
+            assert_eq!(
+                buffer_a
+                    .collaborators()
+                    .iter()
+                    .map(|c| c.user_id)
+                    .collect::<Vec<_>>(),
+                vec![client_a.user_id().unwrap(), client_b.user_id().unwrap()]
+            );
+            assert_eq!(buffer_a.collaborators(), buffer_b.collaborators());
+        });
+    });
+}
+
 #[track_caller]
 fn assert_collaborators(collaborators: &[proto::Collaborator], ids: &[Option<UserId>]) {
     assert_eq!(

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

@@ -0,0 +1,288 @@
+use super::{run_randomized_test, RandomizedTest, TestClient, TestError, TestServer, UserTestPlan};
+use anyhow::Result;
+use async_trait::async_trait;
+use gpui::{executor::Deterministic, TestAppContext};
+use rand::prelude::*;
+use serde_derive::{Deserialize, Serialize};
+use std::{ops::Range, rc::Rc, sync::Arc};
+use text::Bias;
+
+#[gpui::test(
+    iterations = 100,
+    on_failure = "crate::tests::save_randomized_test_plan"
+)]
+async fn test_random_channel_buffers(
+    cx: &mut TestAppContext,
+    deterministic: Arc<Deterministic>,
+    rng: StdRng,
+) {
+    run_randomized_test::<RandomChannelBufferTest>(cx, deterministic, rng).await;
+}
+
+struct RandomChannelBufferTest;
+
+#[derive(Clone, Serialize, Deserialize)]
+enum ChannelBufferOperation {
+    JoinChannelNotes {
+        channel_name: String,
+    },
+    LeaveChannelNotes {
+        channel_name: String,
+    },
+    EditChannelNotes {
+        channel_name: String,
+        edits: Vec<(Range<usize>, Arc<str>)>,
+    },
+    Noop,
+}
+
+const CHANNEL_COUNT: usize = 3;
+
+#[async_trait(?Send)]
+impl RandomizedTest for RandomChannelBufferTest {
+    type Operation = ChannelBufferOperation;
+
+    async fn initialize(server: &mut TestServer, users: &[UserTestPlan]) {
+        let db = &server.app_state.db;
+        for ix in 0..CHANNEL_COUNT {
+            let id = db
+                .create_channel(
+                    &format!("channel-{ix}"),
+                    None,
+                    &format!("livekit-room-{ix}"),
+                    users[0].user_id,
+                )
+                .await
+                .unwrap();
+            for user in &users[1..] {
+                db.invite_channel_member(id, user.user_id, users[0].user_id, false)
+                    .await
+                    .unwrap();
+                db.respond_to_channel_invite(id, user.user_id, true)
+                    .await
+                    .unwrap();
+            }
+        }
+    }
+
+    fn generate_operation(
+        client: &TestClient,
+        rng: &mut StdRng,
+        _: &mut UserTestPlan,
+        cx: &TestAppContext,
+    ) -> ChannelBufferOperation {
+        let channel_store = client.channel_store().clone();
+        let channel_buffers = client.channel_buffers();
+
+        // When signed out, we can't do anything unless a channel buffer is
+        // already open.
+        if channel_buffers.is_empty()
+            && channel_store.read_with(cx, |store, _| store.channel_count() == 0)
+        {
+            return ChannelBufferOperation::Noop;
+        }
+
+        loop {
+            match rng.gen_range(0..100_u32) {
+                0..=29 => {
+                    let channel_name = client.channel_store().read_with(cx, |store, cx| {
+                        store.channels().find_map(|(_, channel)| {
+                            if store.has_open_channel_buffer(channel.id, cx) {
+                                None
+                            } else {
+                                Some(channel.name.clone())
+                            }
+                        })
+                    });
+                    if let Some(channel_name) = channel_name {
+                        break ChannelBufferOperation::JoinChannelNotes { channel_name };
+                    }
+                }
+
+                30..=40 => {
+                    if let Some(buffer) = channel_buffers.iter().choose(rng) {
+                        let channel_name = buffer.read_with(cx, |b, _| b.channel().name.clone());
+                        break ChannelBufferOperation::LeaveChannelNotes { channel_name };
+                    }
+                }
+
+                _ => {
+                    if let Some(buffer) = channel_buffers.iter().choose(rng) {
+                        break buffer.read_with(cx, |b, _| {
+                            let channel_name = b.channel().name.clone();
+                            let edits = b
+                                .buffer()
+                                .read_with(cx, |buffer, _| buffer.get_random_edits(rng, 3));
+                            ChannelBufferOperation::EditChannelNotes {
+                                channel_name,
+                                edits,
+                            }
+                        });
+                    }
+                }
+            }
+        }
+    }
+
+    async fn apply_operation(
+        client: &TestClient,
+        operation: ChannelBufferOperation,
+        cx: &mut TestAppContext,
+    ) -> Result<(), TestError> {
+        match operation {
+            ChannelBufferOperation::JoinChannelNotes { channel_name } => {
+                let buffer = client.channel_store().update(cx, |store, cx| {
+                    let channel_id = store
+                        .channels()
+                        .find(|(_, c)| c.name == channel_name)
+                        .unwrap()
+                        .1
+                        .id;
+                    if store.has_open_channel_buffer(channel_id, cx) {
+                        Err(TestError::Inapplicable)
+                    } else {
+                        Ok(store.open_channel_buffer(channel_id, cx))
+                    }
+                })?;
+
+                log::info!(
+                    "{}: opening notes for channel {channel_name}",
+                    client.username
+                );
+                client.channel_buffers().insert(buffer.await?);
+            }
+
+            ChannelBufferOperation::LeaveChannelNotes { channel_name } => {
+                let buffer = cx.update(|cx| {
+                    let mut left_buffer = Err(TestError::Inapplicable);
+                    client.channel_buffers().retain(|buffer| {
+                        if buffer.read(cx).channel().name == channel_name {
+                            left_buffer = Ok(buffer.clone());
+                            false
+                        } else {
+                            true
+                        }
+                    });
+                    left_buffer
+                })?;
+
+                log::info!(
+                    "{}: closing notes for channel {channel_name}",
+                    client.username
+                );
+                cx.update(|_| drop(buffer));
+            }
+
+            ChannelBufferOperation::EditChannelNotes {
+                channel_name,
+                edits,
+            } => {
+                let channel_buffer = cx
+                    .read(|cx| {
+                        client
+                            .channel_buffers()
+                            .iter()
+                            .find(|buffer| buffer.read(cx).channel().name == channel_name)
+                            .cloned()
+                    })
+                    .ok_or_else(|| TestError::Inapplicable)?;
+
+                log::info!(
+                    "{}: editing notes for channel {channel_name} with {:?}",
+                    client.username,
+                    edits
+                );
+
+                channel_buffer.update(cx, |buffer, cx| {
+                    let buffer = buffer.buffer();
+                    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,
+                        );
+                    });
+                });
+            }
+
+            ChannelBufferOperation::Noop => Err(TestError::Inapplicable)?,
+        }
+        Ok(())
+    }
+
+    async fn on_client_added(client: &Rc<TestClient>, cx: &mut TestAppContext) {
+        let channel_store = client.channel_store();
+        while channel_store.read_with(cx, |store, _| store.channel_count() == 0) {
+            channel_store.next_notification(cx).await;
+        }
+    }
+
+    async fn on_quiesce(server: &mut TestServer, clients: &mut [(Rc<TestClient>, TestAppContext)]) {
+        let channels = server.app_state.db.all_channels().await.unwrap();
+
+        for (client, client_cx) in clients.iter_mut() {
+            client_cx.update(|cx| {
+                client
+                    .channel_buffers()
+                    .retain(|b| b.read(cx).is_connected());
+            });
+        }
+
+        for (channel_id, channel_name) in channels {
+            let mut prev_text: Option<(u64, String)> = None;
+
+            let mut collaborator_user_ids = server
+                .app_state
+                .db
+                .get_channel_buffer_collaborators(channel_id)
+                .await
+                .unwrap()
+                .into_iter()
+                .map(|id| id.to_proto())
+                .collect::<Vec<_>>();
+            collaborator_user_ids.sort();
+
+            for (client, client_cx) in clients.iter() {
+                let user_id = client.user_id().unwrap();
+                client_cx.read(|cx| {
+                    if let Some(channel_buffer) = client
+                        .channel_buffers()
+                        .iter()
+                        .find(|b| b.read(cx).channel().id == channel_id.to_proto())
+                    {
+                        let channel_buffer = channel_buffer.read(cx);
+
+                        // Assert that channel buffer's text matches other clients' copies.
+                        let text = channel_buffer.buffer().read(cx).text();
+                        if let Some((prev_user_id, prev_text)) = &prev_text {
+                            assert_eq!(
+                                &text,
+                                prev_text,
+                                "client {user_id} has different text than client {prev_user_id} for channel {channel_name}",
+                            );
+                        } else {
+                            prev_text = Some((user_id, text.clone()));
+                        }
+
+                        // Assert that all clients and the server agree about who is present in the
+                        // channel buffer.
+                        let collaborators = channel_buffer.collaborators();
+                        let mut user_ids =
+                            collaborators.iter().map(|c| c.user_id).collect::<Vec<_>>();
+                        user_ids.sort();
+                        assert_eq!(
+                            user_ids,
+                            collaborator_user_ids,
+                            "client {user_id} has different user ids for channel {channel_name} than the server",
+                        );
+                    }
+                });
+            }
+        }
+    }
+}

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

@@ -0,0 +1,1585 @@
+use super::{run_randomized_test, RandomizedTest, TestClient, TestError, TestServer, UserTestPlan};
+use crate::db::UserId;
+use anyhow::{anyhow, Result};
+use async_trait::async_trait;
+use call::ActiveCall;
+use collections::{BTreeMap, HashMap};
+use editor::Bias;
+use fs::{repository::GitFileStatus, FakeFs, Fs as _};
+use futures::StreamExt;
+use gpui::{executor::Deterministic, ModelHandle, TestAppContext};
+use language::{range_to_lsp, FakeLspAdapter, Language, LanguageConfig, PointUtf16};
+use lsp::FakeLanguageServer;
+use pretty_assertions::assert_eq;
+use project::{search::SearchQuery, Project, ProjectPath};
+use rand::{
+    distributions::{Alphanumeric, DistString},
+    prelude::*,
+};
+use serde::{Deserialize, Serialize};
+use std::{
+    ops::Range,
+    path::{Path, PathBuf},
+    rc::Rc,
+    sync::Arc,
+};
+use util::ResultExt;
+
+#[gpui::test(
+    iterations = 100,
+    on_failure = "crate::tests::save_randomized_test_plan"
+)]
+async fn test_random_project_collaboration(
+    cx: &mut TestAppContext,
+    deterministic: Arc<Deterministic>,
+    rng: StdRng,
+) {
+    run_randomized_test::<ProjectCollaborationTest>(cx, deterministic, rng).await;
+}
+
+#[derive(Clone, Debug, Serialize, Deserialize)]
+enum ClientOperation {
+    AcceptIncomingCall,
+    RejectIncomingCall,
+    LeaveCall,
+    InviteContactToCall {
+        user_id: UserId,
+    },
+    OpenLocalProject {
+        first_root_name: String,
+    },
+    OpenRemoteProject {
+        host_id: UserId,
+        first_root_name: String,
+    },
+    AddWorktreeToProject {
+        project_root_name: String,
+        new_root_path: PathBuf,
+    },
+    CloseRemoteProject {
+        project_root_name: String,
+    },
+    OpenBuffer {
+        project_root_name: String,
+        is_local: bool,
+        full_path: PathBuf,
+    },
+    SearchProject {
+        project_root_name: String,
+        is_local: bool,
+        query: String,
+        detach: bool,
+    },
+    EditBuffer {
+        project_root_name: String,
+        is_local: bool,
+        full_path: PathBuf,
+        edits: Vec<(Range<usize>, Arc<str>)>,
+    },
+    CloseBuffer {
+        project_root_name: String,
+        is_local: bool,
+        full_path: PathBuf,
+    },
+    SaveBuffer {
+        project_root_name: String,
+        is_local: bool,
+        full_path: PathBuf,
+        detach: bool,
+    },
+    RequestLspDataInBuffer {
+        project_root_name: String,
+        is_local: bool,
+        full_path: PathBuf,
+        offset: usize,
+        kind: LspRequestKind,
+        detach: bool,
+    },
+    CreateWorktreeEntry {
+        project_root_name: String,
+        is_local: bool,
+        full_path: PathBuf,
+        is_dir: bool,
+    },
+    WriteFsEntry {
+        path: PathBuf,
+        is_dir: bool,
+        content: String,
+    },
+    GitOperation {
+        operation: GitOperation,
+    },
+}
+
+#[derive(Clone, Debug, Serialize, Deserialize)]
+enum GitOperation {
+    WriteGitIndex {
+        repo_path: PathBuf,
+        contents: Vec<(PathBuf, String)>,
+    },
+    WriteGitBranch {
+        repo_path: PathBuf,
+        new_branch: Option<String>,
+    },
+    WriteGitStatuses {
+        repo_path: PathBuf,
+        statuses: Vec<(PathBuf, GitFileStatus)>,
+        git_operation: bool,
+    },
+}
+
+#[derive(Clone, Debug, Serialize, Deserialize)]
+enum LspRequestKind {
+    Rename,
+    Completion,
+    CodeAction,
+    Definition,
+    Highlights,
+}
+
+struct ProjectCollaborationTest;
+
+#[async_trait(?Send)]
+impl RandomizedTest for ProjectCollaborationTest {
+    type Operation = ClientOperation;
+
+    async fn initialize(server: &mut TestServer, users: &[UserTestPlan]) {
+        let db = &server.app_state.db;
+        for (ix, user_a) in users.iter().enumerate() {
+            for user_b in &users[ix + 1..] {
+                db.send_contact_request(user_a.user_id, user_b.user_id)
+                    .await
+                    .unwrap();
+                db.respond_to_contact_request(user_b.user_id, user_a.user_id, true)
+                    .await
+                    .unwrap();
+            }
+        }
+    }
+
+    fn generate_operation(
+        client: &TestClient,
+        rng: &mut StdRng,
+        plan: &mut UserTestPlan,
+        cx: &TestAppContext,
+    ) -> ClientOperation {
+        let call = cx.read(ActiveCall::global);
+        loop {
+            match rng.gen_range(0..100_u32) {
+                // Mutate the call
+                0..=29 => {
+                    // Respond to an incoming call
+                    if call.read_with(cx, |call, _| call.incoming().borrow().is_some()) {
+                        break if rng.gen_bool(0.7) {
+                            ClientOperation::AcceptIncomingCall
+                        } else {
+                            ClientOperation::RejectIncomingCall
+                        };
+                    }
+
+                    match rng.gen_range(0..100_u32) {
+                        // Invite a contact to the current call
+                        0..=70 => {
+                            let available_contacts =
+                                client.user_store().read_with(cx, |user_store, _| {
+                                    user_store
+                                        .contacts()
+                                        .iter()
+                                        .filter(|contact| contact.online && !contact.busy)
+                                        .cloned()
+                                        .collect::<Vec<_>>()
+                                });
+                            if !available_contacts.is_empty() {
+                                let contact = available_contacts.choose(rng).unwrap();
+                                break ClientOperation::InviteContactToCall {
+                                    user_id: UserId(contact.user.id as i32),
+                                };
+                            }
+                        }
+
+                        // Leave the current call
+                        71.. => {
+                            if plan.allow_client_disconnection
+                                && call.read_with(cx, |call, _| call.room().is_some())
+                            {
+                                break ClientOperation::LeaveCall;
+                            }
+                        }
+                    }
+                }
+
+                // Mutate projects
+                30..=59 => match rng.gen_range(0..100_u32) {
+                    // Open a new project
+                    0..=70 => {
+                        // Open a remote project
+                        if let Some(room) = call.read_with(cx, |call, _| call.room().cloned()) {
+                            let existing_remote_project_ids = cx.read(|cx| {
+                                client
+                                    .remote_projects()
+                                    .iter()
+                                    .map(|p| p.read(cx).remote_id().unwrap())
+                                    .collect::<Vec<_>>()
+                            });
+                            let new_remote_projects = room.read_with(cx, |room, _| {
+                                room.remote_participants()
+                                    .values()
+                                    .flat_map(|participant| {
+                                        participant.projects.iter().filter_map(|project| {
+                                            if existing_remote_project_ids.contains(&project.id) {
+                                                None
+                                            } else {
+                                                Some((
+                                                    UserId::from_proto(participant.user.id),
+                                                    project.worktree_root_names[0].clone(),
+                                                ))
+                                            }
+                                        })
+                                    })
+                                    .collect::<Vec<_>>()
+                            });
+                            if !new_remote_projects.is_empty() {
+                                let (host_id, first_root_name) =
+                                    new_remote_projects.choose(rng).unwrap().clone();
+                                break ClientOperation::OpenRemoteProject {
+                                    host_id,
+                                    first_root_name,
+                                };
+                            }
+                        }
+                        // Open a local project
+                        else {
+                            let first_root_name = plan.next_root_dir_name();
+                            break ClientOperation::OpenLocalProject { first_root_name };
+                        }
+                    }
+
+                    // Close a remote project
+                    71..=80 => {
+                        if !client.remote_projects().is_empty() {
+                            let project = client.remote_projects().choose(rng).unwrap().clone();
+                            let first_root_name = root_name_for_project(&project, cx);
+                            break ClientOperation::CloseRemoteProject {
+                                project_root_name: first_root_name,
+                            };
+                        }
+                    }
+
+                    // Mutate project worktrees
+                    81.. => match rng.gen_range(0..100_u32) {
+                        // Add a worktree to a local project
+                        0..=50 => {
+                            let Some(project) = client.local_projects().choose(rng).cloned() else {
+                                continue;
+                            };
+                            let project_root_name = root_name_for_project(&project, cx);
+                            let mut paths = client.fs().paths(false);
+                            paths.remove(0);
+                            let new_root_path = if paths.is_empty() || rng.gen() {
+                                Path::new("/").join(&plan.next_root_dir_name())
+                            } else {
+                                paths.choose(rng).unwrap().clone()
+                            };
+                            break ClientOperation::AddWorktreeToProject {
+                                project_root_name,
+                                new_root_path,
+                            };
+                        }
+
+                        // Add an entry to a worktree
+                        _ => {
+                            let Some(project) = choose_random_project(client, rng) else {
+                                continue;
+                            };
+                            let project_root_name = root_name_for_project(&project, cx);
+                            let is_local = project.read_with(cx, |project, _| project.is_local());
+                            let worktree = project.read_with(cx, |project, cx| {
+                                project
+                                    .worktrees(cx)
+                                    .filter(|worktree| {
+                                        let worktree = worktree.read(cx);
+                                        worktree.is_visible()
+                                            && worktree.entries(false).any(|e| e.is_file())
+                                            && worktree.root_entry().map_or(false, |e| e.is_dir())
+                                    })
+                                    .choose(rng)
+                            });
+                            let Some(worktree) = worktree else { continue };
+                            let is_dir = rng.gen::<bool>();
+                            let mut full_path =
+                                worktree.read_with(cx, |w, _| PathBuf::from(w.root_name()));
+                            full_path.push(gen_file_name(rng));
+                            if !is_dir {
+                                full_path.set_extension("rs");
+                            }
+                            break ClientOperation::CreateWorktreeEntry {
+                                project_root_name,
+                                is_local,
+                                full_path,
+                                is_dir,
+                            };
+                        }
+                    },
+                },
+
+                // Query and mutate buffers
+                60..=90 => {
+                    let Some(project) = choose_random_project(client, rng) else {
+                        continue;
+                    };
+                    let project_root_name = root_name_for_project(&project, cx);
+                    let is_local = project.read_with(cx, |project, _| project.is_local());
+
+                    match rng.gen_range(0..100_u32) {
+                        // Manipulate an existing buffer
+                        0..=70 => {
+                            let Some(buffer) = client
+                                .buffers_for_project(&project)
+                                .iter()
+                                .choose(rng)
+                                .cloned()
+                            else {
+                                continue;
+                            };
+
+                            let full_path = buffer
+                                .read_with(cx, |buffer, cx| buffer.file().unwrap().full_path(cx));
+
+                            match rng.gen_range(0..100_u32) {
+                                // Close the buffer
+                                0..=15 => {
+                                    break ClientOperation::CloseBuffer {
+                                        project_root_name,
+                                        is_local,
+                                        full_path,
+                                    };
+                                }
+                                // Save the buffer
+                                16..=29 if buffer.read_with(cx, |b, _| b.is_dirty()) => {
+                                    let detach = rng.gen_bool(0.3);
+                                    break ClientOperation::SaveBuffer {
+                                        project_root_name,
+                                        is_local,
+                                        full_path,
+                                        detach,
+                                    };
+                                }
+                                // Edit the buffer
+                                30..=69 => {
+                                    let edits = buffer
+                                        .read_with(cx, |buffer, _| buffer.get_random_edits(rng, 3));
+                                    break ClientOperation::EditBuffer {
+                                        project_root_name,
+                                        is_local,
+                                        full_path,
+                                        edits,
+                                    };
+                                }
+                                // Make an LSP request
+                                _ => {
+                                    let offset = buffer.read_with(cx, |buffer, _| {
+                                        buffer.clip_offset(
+                                            rng.gen_range(0..=buffer.len()),
+                                            language::Bias::Left,
+                                        )
+                                    });
+                                    let detach = rng.gen();
+                                    break ClientOperation::RequestLspDataInBuffer {
+                                        project_root_name,
+                                        full_path,
+                                        offset,
+                                        is_local,
+                                        kind: match rng.gen_range(0..5_u32) {
+                                            0 => LspRequestKind::Rename,
+                                            1 => LspRequestKind::Highlights,
+                                            2 => LspRequestKind::Definition,
+                                            3 => LspRequestKind::CodeAction,
+                                            4.. => LspRequestKind::Completion,
+                                        },
+                                        detach,
+                                    };
+                                }
+                            }
+                        }
+
+                        71..=80 => {
+                            let query = rng.gen_range('a'..='z').to_string();
+                            let detach = rng.gen_bool(0.3);
+                            break ClientOperation::SearchProject {
+                                project_root_name,
+                                is_local,
+                                query,
+                                detach,
+                            };
+                        }
+
+                        // Open a buffer
+                        81.. => {
+                            let worktree = project.read_with(cx, |project, cx| {
+                                project
+                                    .worktrees(cx)
+                                    .filter(|worktree| {
+                                        let worktree = worktree.read(cx);
+                                        worktree.is_visible()
+                                            && worktree.entries(false).any(|e| e.is_file())
+                                    })
+                                    .choose(rng)
+                            });
+                            let Some(worktree) = worktree else { continue };
+                            let full_path = worktree.read_with(cx, |worktree, _| {
+                                let entry = worktree
+                                    .entries(false)
+                                    .filter(|e| e.is_file())
+                                    .choose(rng)
+                                    .unwrap();
+                                if entry.path.as_ref() == Path::new("") {
+                                    Path::new(worktree.root_name()).into()
+                                } else {
+                                    Path::new(worktree.root_name()).join(&entry.path)
+                                }
+                            });
+                            break ClientOperation::OpenBuffer {
+                                project_root_name,
+                                is_local,
+                                full_path,
+                            };
+                        }
+                    }
+                }
+
+                // Update a git related action
+                91..=95 => {
+                    break ClientOperation::GitOperation {
+                        operation: generate_git_operation(rng, client),
+                    };
+                }
+
+                // Create or update a file or directory
+                96.. => {
+                    let is_dir = rng.gen::<bool>();
+                    let content;
+                    let mut path;
+                    let dir_paths = client.fs().directories(false);
+
+                    if is_dir {
+                        content = String::new();
+                        path = dir_paths.choose(rng).unwrap().clone();
+                        path.push(gen_file_name(rng));
+                    } else {
+                        content = Alphanumeric.sample_string(rng, 16);
+
+                        // Create a new file or overwrite an existing file
+                        let file_paths = client.fs().files();
+                        if file_paths.is_empty() || rng.gen_bool(0.5) {
+                            path = dir_paths.choose(rng).unwrap().clone();
+                            path.push(gen_file_name(rng));
+                            path.set_extension("rs");
+                        } else {
+                            path = file_paths.choose(rng).unwrap().clone()
+                        };
+                    }
+                    break ClientOperation::WriteFsEntry {
+                        path,
+                        is_dir,
+                        content,
+                    };
+                }
+            }
+        }
+    }
+
+    async fn apply_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, cx| call.decline_incoming(cx))?;
+            }
+
+            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(false).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.clone(), cx));
+                let save = cx.spawn(|cx| async move {
+                    save.await
+                        .map_err(|err| anyhow!("save request failed: {:?}", err))?;
+                    assert!(buffer
+                        .read_with(&cx, |buffer, _| { buffer.saved_version().to_owned() })
+                        .observed_all(&requested_version));
+                    anyhow::Ok(())
+                });
+                if detach {
+                    cx.update(|cx| save.detach_and_log_err(cx));
+                } else {
+                    save.await?;
+                }
+            }
+
+            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)?;
+
+                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?;
+                }
+            }
+
+            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" }
+                );
+
+                let mut search = project.update(cx, |project, cx| {
+                    project.search(
+                        SearchQuery::text(query, false, false, Vec::new(), Vec::new()),
+                        cx,
+                    )
+                });
+                drop(project);
+                let search = cx.background().spawn(async move {
+                    let mut results = HashMap::default();
+                    while let Some((buffer, ranges)) = search.next().await {
+                        results.entry(buffer).or_insert(ranges);
+                    }
+                    results
+                });
+                search.await;
+            }
+
+            ClientOperation::WriteFsEntry {
+                path,
+                is_dir,
+                content,
+            } => {
+                if !client
+                    .fs()
+                    .directories(false)
+                    .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(), text::LineEnding::Unix)
+                        .await
+                        .unwrap();
+                }
+            }
+
+            ClientOperation::GitOperation { operation } => match operation {
+                GitOperation::WriteGitIndex {
+                    repo_path,
+                    contents,
+                } => {
+                    if !client.fs().directories(false).contains(&repo_path) {
+                        return Err(TestError::Inapplicable);
+                    }
+
+                    for (path, _) in contents.iter() {
+                        if !client.fs().files().contains(&repo_path.join(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);
+                }
+                GitOperation::WriteGitBranch {
+                    repo_path,
+                    new_branch,
+                } => {
+                    if !client.fs().directories(false).contains(&repo_path) {
+                        return Err(TestError::Inapplicable);
+                    }
+
+                    log::info!(
+                        "{}: writing git branch for repo {:?}: {:?}",
+                        client.username,
+                        repo_path,
+                        new_branch
+                    );
+
+                    let dot_git_dir = repo_path.join(".git");
+                    if client.fs().metadata(&dot_git_dir).await?.is_none() {
+                        client.fs().create_dir(&dot_git_dir).await?;
+                    }
+                    client
+                        .fs()
+                        .set_branch_name(&dot_git_dir, new_branch.clone());
+                }
+                GitOperation::WriteGitStatuses {
+                    repo_path,
+                    statuses,
+                    git_operation,
+                } => {
+                    if !client.fs().directories(false).contains(&repo_path) {
+                        return Err(TestError::Inapplicable);
+                    }
+                    for (path, _) in statuses.iter() {
+                        if !client.fs().files().contains(&repo_path.join(path)) {
+                            return Err(TestError::Inapplicable);
+                        }
+                    }
+
+                    log::info!(
+                        "{}: writing git statuses for repo {:?}: {:?}",
+                        client.username,
+                        repo_path,
+                        statuses
+                    );
+
+                    let dot_git_dir = repo_path.join(".git");
+
+                    let statuses = statuses
+                        .iter()
+                        .map(|(path, val)| (path.as_path(), val.clone()))
+                        .collect::<Vec<_>>();
+
+                    if client.fs().metadata(&dot_git_dir).await?.is_none() {
+                        client.fs().create_dir(&dot_git_dir).await?;
+                    }
+
+                    if git_operation {
+                        client.fs().set_status_for_repo_via_git_operation(
+                            &dot_git_dir,
+                            statuses.as_slice(),
+                        );
+                    } else {
+                        client.fs().set_status_for_repo_via_working_copy_change(
+                            &dot_git_dir,
+                            statuses.as_slice(),
+                        );
+                    }
+                }
+            },
+        }
+        Ok(())
+    }
+
+    async fn on_client_added(client: &Rc<TestClient>, _: &mut TestAppContext) {
+        let mut language = Language::new(
+            LanguageConfig {
+                name: "Rust".into(),
+                path_suffixes: vec!["rs".to_string()],
+                ..Default::default()
+            },
+            None,
+        );
+        language
+            .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
+                name: "the-fake-language-server",
+                capabilities: lsp::LanguageServer::full_capabilities(),
+                initializer: Some(Box::new({
+                    let fs = client.app_state.fs.clone();
+                    move |fake_server: &mut FakeLanguageServer| {
+                        fake_server.handle_request::<lsp::request::Completion, _, _>(
+                            |_, _| async move {
+                                Ok(Some(lsp::CompletionResponse::Array(vec![
+                                    lsp::CompletionItem {
+                                        text_edit: Some(lsp::CompletionTextEdit::Edit(
+                                            lsp::TextEdit {
+                                                range: lsp::Range::new(
+                                                    lsp::Position::new(0, 0),
+                                                    lsp::Position::new(0, 0),
+                                                ),
+                                                new_text: "the-new-text".to_string(),
+                                            },
+                                        )),
+                                        ..Default::default()
+                                    },
+                                ])))
+                            },
+                        );
+
+                        fake_server.handle_request::<lsp::request::CodeActionRequest, _, _>(
+                            |_, _| async move {
+                                Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
+                                    lsp::CodeAction {
+                                        title: "the-code-action".to_string(),
+                                        ..Default::default()
+                                    },
+                                )]))
+                            },
+                        );
+
+                        fake_server.handle_request::<lsp::request::PrepareRenameRequest, _, _>(
+                            |params, _| async move {
+                                Ok(Some(lsp::PrepareRenameResponse::Range(lsp::Range::new(
+                                    params.position,
+                                    params.position,
+                                ))))
+                            },
+                        );
+
+                        fake_server.handle_request::<lsp::request::GotoDefinition, _, _>({
+                            let fs = fs.clone();
+                            move |_, cx| {
+                                let background = cx.background();
+                                let mut rng = background.rng();
+                                let count = rng.gen_range::<usize, _>(1..3);
+                                let files = fs.as_fake().files();
+                                let files = (0..count)
+                                    .map(|_| files.choose(&mut *rng).unwrap().clone())
+                                    .collect::<Vec<_>>();
+                                async move {
+                                    log::info!("LSP: Returning definitions in files {:?}", &files);
+                                    Ok(Some(lsp::GotoDefinitionResponse::Array(
+                                        files
+                                            .into_iter()
+                                            .map(|file| lsp::Location {
+                                                uri: lsp::Url::from_file_path(file).unwrap(),
+                                                range: Default::default(),
+                                            })
+                                            .collect(),
+                                    )))
+                                }
+                            }
+                        });
+
+                        fake_server.handle_request::<lsp::request::DocumentHighlightRequest, _, _>(
+                            move |_, cx| {
+                                let mut highlights = Vec::new();
+                                let background = cx.background();
+                                let mut rng = background.rng();
+
+                                let highlight_count = rng.gen_range(1..=5);
+                                for _ in 0..highlight_count {
+                                    let start_row = rng.gen_range(0..100);
+                                    let start_column = rng.gen_range(0..100);
+                                    let end_row = rng.gen_range(0..100);
+                                    let end_column = rng.gen_range(0..100);
+                                    let start = PointUtf16::new(start_row, start_column);
+                                    let end = PointUtf16::new(end_row, end_column);
+                                    let range = if start > end { end..start } else { start..end };
+                                    highlights.push(lsp::DocumentHighlight {
+                                        range: range_to_lsp(range.clone()),
+                                        kind: Some(lsp::DocumentHighlightKind::READ),
+                                    });
+                                }
+                                highlights.sort_unstable_by_key(|highlight| {
+                                    (highlight.range.start, highlight.range.end)
+                                });
+                                async move { Ok(Some(highlights)) }
+                            },
+                        );
+                    }
+                })),
+                ..Default::default()
+            }))
+            .await;
+        client.app_state.languages.add(Arc::new(language));
+    }
+
+    async fn on_quiesce(_: &mut TestServer, clients: &mut [(Rc<TestClient>, TestAppContext)]) {
+        for (client, client_cx) in clients.iter() {
+            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()
+                                    })
+                                })?
+                                .clone();
+                            Some((project, cx))
+                        });
+
+                        if !guest_project.is_read_only() {
+                            if let Some((host_project, host_cx)) = host_project {
+                                let host_worktree_snapshots =
+                                    host_project.read_with(host_cx, |host_project, cx| {
+                                        host_project
+                                            .worktrees(cx)
+                                            .map(|worktree| {
+                                                let worktree = worktree.read(cx);
+                                                (worktree.id(), worktree.snapshot())
+                                            })
+                                            .collect::<BTreeMap<_, _>>()
+                                    });
+                                let guest_worktree_snapshots = guest_project
+                                    .worktrees(cx)
+                                    .map(|worktree| {
+                                        let worktree = worktree.read(cx);
+                                        (worktree.id(), worktree.snapshot())
+                                    })
+                                    .collect::<BTreeMap<_, _>>();
+
+                                assert_eq!(
+                                    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 {
+                                    let guest_snapshot = &guest_worktree_snapshots[id];
+                                    assert_eq!(
+                                        guest_snapshot.root_name(),
+                                        host_snapshot.root_name(),
+                                        "{} has different root name than the host for worktree {}, project {:?}",
+                                        client.username,
+                                        id,
+                                        guest_project.remote_id(),
+                                    );
+                                    assert_eq!(
+                                        guest_snapshot.abs_path(),
+                                        host_snapshot.abs_path(),
+                                        "{} has different abs path than the host for worktree {}, project: {:?}",
+                                        client.username,
+                                        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 {:?}",
+                                        client.username,
+                                        host_snapshot.abs_path(),
+                                        id,
+                                        guest_project.remote_id(),
+                                    );
+                                    assert_eq!(guest_snapshot.repositories().collect::<Vec<_>>(), host_snapshot.repositories().collect::<Vec<_>>(),
+                                        "{} has different repositories 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,
+                                        host_snapshot.abs_path(),
+                                        guest_project.remote_id(),
+                                    );
+                                }
+                            }
+                        }
+
+                        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(),
+                            );
+                        }
+                    });
+            }
+
+            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()
+                }) {
+                    continue;
+                } else {
+                    guest_project
+                        .read_with(client_cx, |project, _| project.remote_id())
+                        .unwrap()
+                };
+                let guest_user_id = client.user_id().unwrap();
+
+                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() == Some(project_id)
+                            })
+                        })?
+                        .clone();
+                    Some((client.user_id().unwrap(), project, cx))
+                });
+
+                let (host_user_id, host_project, host_cx) =
+                    if let Some((host_user_id, host_project, host_cx)) = host_project {
+                        (host_user_id, host_project, host_cx)
+                    } else {
+                        continue;
+                    };
+
+                for guest_buffer in guest_buffers {
+                    let buffer_id =
+                        guest_buffer.read_with(client_cx, |buffer, _| buffer.remote_id());
+                    let host_buffer = host_project.read_with(host_cx, |project, cx| {
+                        project.buffer_for_id(buffer_id, cx).unwrap_or_else(|| {
+                            panic!(
+                                "host does not have buffer for guest:{}, peer:{:?}, id:{}",
+                                client.username,
+                                client.peer_id(),
+                                buffer_id
+                            )
+                        })
+                    });
+                    let path = host_buffer
+                        .read_with(host_cx, |buffer, cx| buffer.file().unwrap().full_path(cx));
+
+                    assert_eq!(
+                        guest_buffer.read_with(client_cx, |buffer, _| buffer.deferred_ops_len()),
+                        0,
+                        "{}, buffer {}, path {:?} has deferred operations",
+                        client.username,
+                        buffer_id,
+                        path,
+                    );
+                    assert_eq!(
+                        guest_buffer.read_with(client_cx, |buffer, _| buffer.text()),
+                        host_buffer.read_with(host_cx, |buffer, _| buffer.text()),
+                        "{}, buffer {}, path {:?}, differs from the host's buffer",
+                        client.username,
+                        buffer_id,
+                        path
+                    );
+
+                    let host_file = host_buffer.read_with(host_cx, |b, _| b.file().cloned());
+                    let guest_file = guest_buffer.read_with(client_cx, |b, _| b.file().cloned());
+                    match (host_file, guest_file) {
+                        (Some(host_file), Some(guest_file)) => {
+                            assert_eq!(guest_file.path(), host_file.path());
+                            assert_eq!(guest_file.is_deleted(), host_file.is_deleted());
+                            assert_eq!(
+                                guest_file.mtime(),
+                                host_file.mtime(),
+                                "guest {} mtime does not match host {} for path {:?} in project {}",
+                                guest_user_id,
+                                host_user_id,
+                                guest_file.path(),
+                                project_id,
+                            );
+                        }
+                        (None, None) => {}
+                        (None, _) => panic!("host's file is None, guest's isn't"),
+                        (_, None) => panic!("guest's file is None, hosts's isn't"),
+                    }
+
+                    let host_diff_base = host_buffer
+                        .read_with(host_cx, |b, _| b.diff_base().map(ToString::to_string));
+                    let guest_diff_base = guest_buffer
+                        .read_with(client_cx, |b, _| b.diff_base().map(ToString::to_string));
+                    assert_eq!(
+                            guest_diff_base, host_diff_base,
+                            "guest {} diff base does not match host's for path {path:?} in project {project_id}",
+                            client.username
+                        );
+
+                    let host_saved_version =
+                        host_buffer.read_with(host_cx, |b, _| b.saved_version().clone());
+                    let guest_saved_version =
+                        guest_buffer.read_with(client_cx, |b, _| b.saved_version().clone());
+                    assert_eq!(
+                            guest_saved_version, host_saved_version,
+                            "guest {} saved version does not match host's for path {path:?} in project {project_id}",
+                            client.username
+                        );
+
+                    let host_saved_version_fingerprint =
+                        host_buffer.read_with(host_cx, |b, _| b.saved_version_fingerprint());
+                    let guest_saved_version_fingerprint =
+                        guest_buffer.read_with(client_cx, |b, _| b.saved_version_fingerprint());
+                    assert_eq!(
+                            guest_saved_version_fingerprint, host_saved_version_fingerprint,
+                            "guest {} saved fingerprint does not match host's for path {path:?} in project {project_id}",
+                            client.username
+                        );
+
+                    let host_saved_mtime = host_buffer.read_with(host_cx, |b, _| b.saved_mtime());
+                    let guest_saved_mtime =
+                        guest_buffer.read_with(client_cx, |b, _| b.saved_mtime());
+                    assert_eq!(
+                            guest_saved_mtime, host_saved_mtime,
+                            "guest {} saved mtime does not match host's for path {path:?} in project {project_id}",
+                            client.username
+                        );
+
+                    let host_is_dirty = host_buffer.read_with(host_cx, |b, _| b.is_dirty());
+                    let guest_is_dirty = guest_buffer.read_with(client_cx, |b, _| b.is_dirty());
+                    assert_eq!(guest_is_dirty, host_is_dirty,
+                            "guest {} dirty status does not match host's for path {path:?} in project {project_id}",
+                            client.username
+                        );
+
+                    let host_has_conflict = host_buffer.read_with(host_cx, |b, _| b.has_conflict());
+                    let guest_has_conflict =
+                        guest_buffer.read_with(client_cx, |b, _| b.has_conflict());
+                    assert_eq!(guest_has_conflict, host_has_conflict,
+                            "guest {} conflict status does not match host's for path {path:?} in project {project_id}",
+                            client.username
+                        );
+                }
+            }
+        }
+    }
+}
+
+fn generate_git_operation(rng: &mut StdRng, client: &TestClient) -> GitOperation {
+    fn generate_file_paths(
+        repo_path: &Path,
+        rng: &mut StdRng,
+        client: &TestClient,
+    ) -> Vec<PathBuf> {
+        let mut paths = client
+            .fs()
+            .files()
+            .into_iter()
+            .filter(|path| path.starts_with(repo_path))
+            .collect::<Vec<_>>();
+
+        let count = rng.gen_range(0..=paths.len());
+        paths.shuffle(rng);
+        paths.truncate(count);
+
+        paths
+            .iter()
+            .map(|path| path.strip_prefix(repo_path).unwrap().to_path_buf())
+            .collect::<Vec<_>>()
+    }
+
+    let repo_path = client.fs().directories(false).choose(rng).unwrap().clone();
+
+    match rng.gen_range(0..100_u32) {
+        0..=25 => {
+            let file_paths = generate_file_paths(&repo_path, rng, client);
+
+            let contents = file_paths
+                .into_iter()
+                .map(|path| (path, Alphanumeric.sample_string(rng, 16)))
+                .collect();
+
+            GitOperation::WriteGitIndex {
+                repo_path,
+                contents,
+            }
+        }
+        26..=63 => {
+            let new_branch = (rng.gen_range(0..10) > 3).then(|| Alphanumeric.sample_string(rng, 8));
+
+            GitOperation::WriteGitBranch {
+                repo_path,
+                new_branch,
+            }
+        }
+        64..=100 => {
+            let file_paths = generate_file_paths(&repo_path, rng, client);
+
+            let statuses = file_paths
+                .into_iter()
+                .map(|paths| {
+                    (
+                        paths,
+                        match rng.gen_range(0..3_u32) {
+                            0 => GitFileStatus::Added,
+                            1 => GitFileStatus::Modified,
+                            2 => GitFileStatus::Conflict,
+                            _ => unreachable!(),
+                        },
+                    )
+                })
+                .collect::<Vec<_>>();
+
+            let git_operation = rng.gen::<bool>();
+
+            GitOperation::WriteGitStatuses {
+                repo_path,
+                statuses,
+                git_operation,
+            }
+        }
+        _ => unreachable!(),
+    }
+}
+
+fn buffer_for_full_path(
+    client: &TestClient,
+    project: &ModelHandle<Project>,
+    full_path: &PathBuf,
+    cx: &TestAppContext,
+) -> Option<ModelHandle<language::Buffer>> {
+    client
+        .buffers_for_project(project)
+        .iter()
+        .find(|buffer| {
+            buffer.read_with(cx, |buffer, cx| {
+                buffer.file().unwrap().full_path(cx) == *full_path
+            })
+        })
+        .cloned()
+}
+
+fn project_for_root_name(
+    client: &TestClient,
+    root_name: &str,
+    cx: &TestAppContext,
+) -> Option<ModelHandle<Project>> {
+    if let Some(ix) = project_ix_for_root_name(&*client.local_projects(), root_name, cx) {
+        return Some(client.local_projects()[ix].clone());
+    }
+    if let Some(ix) = project_ix_for_root_name(&*client.remote_projects(), root_name, cx) {
+        return Some(client.remote_projects()[ix].clone());
+    }
+    None
+}
+
+fn project_ix_for_root_name(
+    projects: &[ModelHandle<Project>],
+    root_name: &str,
+    cx: &TestAppContext,
+) -> Option<usize> {
+    projects.iter().position(|project| {
+        project.read_with(cx, |project, cx| {
+            let worktree = project.visible_worktrees(cx).next().unwrap();
+            worktree.read(cx).root_name() == root_name
+        })
+    })
+}
+
+fn root_name_for_project(project: &ModelHandle<Project>, cx: &TestAppContext) -> String {
+    project.read_with(cx, |project, cx| {
+        project
+            .visible_worktrees(cx)
+            .next()
+            .unwrap()
+            .read(cx)
+            .root_name()
+            .to_string()
+    })
+}
+
+fn project_path_for_full_path(
+    project: &ModelHandle<Project>,
+    full_path: &Path,
+    cx: &TestAppContext,
+) -> Option<ProjectPath> {
+    let mut components = full_path.components();
+    let root_name = components.next().unwrap().as_os_str().to_str().unwrap();
+    let path = components.as_path().into();
+    let worktree_id = project.read_with(cx, |project, cx| {
+        project.worktrees(cx).find_map(|worktree| {
+            let worktree = worktree.read(cx);
+            if worktree.root_name() == root_name {
+                Some(worktree.id())
+            } else {
+                None
+            }
+        })
+    })?;
+    Some(ProjectPath { worktree_id, path })
+}
+
+async fn ensure_project_shared(
+    project: &ModelHandle<Project>,
+    client: &TestClient,
+    cx: &mut TestAppContext,
+) {
+    let first_root_name = root_name_for_project(project, cx);
+    let active_call = cx.read(ActiveCall::global);
+    if active_call.read_with(cx, |call, _| call.room().is_some())
+        && project.read_with(cx, |project, _| project.is_local() && !project.is_shared())
+    {
+        match active_call
+            .update(cx, |call, cx| call.share_project(project.clone(), cx))
+            .await
+        {
+            Ok(project_id) => {
+                log::info!(
+                    "{}: shared project {} with id {}",
+                    client.username,
+                    first_root_name,
+                    project_id
+                );
+            }
+            Err(error) => {
+                log::error!(
+                    "{}: error sharing project {}: {:?}",
+                    client.username,
+                    first_root_name,
+                    error
+                );
+            }
+        }
+    }
+}
+
+fn choose_random_project(client: &TestClient, rng: &mut StdRng) -> Option<ModelHandle<Project>> {
+    client
+        .local_projects()
+        .iter()
+        .chain(client.remote_projects().iter())
+        .choose(rng)
+        .cloned()
+}
+
+fn gen_file_name(rng: &mut StdRng) -> String {
+    let mut name = String::new();
+    for _ in 0..10 {
+        let letter = rng.gen_range('a'..='z');
+        name.push(letter);
+    }
+    name
+}

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

@@ -1,2199 +0,0 @@
-use crate::{
-    db::{self, NewUserParams, UserId},
-    rpc::{CLEANUP_TIMEOUT, RECONNECT_TIMEOUT},
-    tests::{TestClient, TestServer},
-};
-use anyhow::{anyhow, Result};
-use call::ActiveCall;
-use client::RECEIVE_TIMEOUT;
-use collections::{BTreeMap, HashMap};
-use editor::Bias;
-use fs::{repository::GitFileStatus, FakeFs, Fs as _};
-use futures::StreamExt as _;
-use gpui::{executor::Deterministic, ModelHandle, Task, TestAppContext};
-use language::{range_to_lsp, FakeLspAdapter, Language, LanguageConfig, PointUtf16};
-use lsp::FakeLanguageServer;
-use parking_lot::Mutex;
-use pretty_assertions::assert_eq;
-use project::{search::SearchQuery, Project, ProjectPath};
-use rand::{
-    distributions::{Alphanumeric, DistString},
-    prelude::*,
-};
-use serde::{Deserialize, Serialize};
-use settings::SettingsStore;
-use std::{
-    env,
-    ops::Range,
-    path::{Path, PathBuf},
-    rc::Rc,
-    sync::{
-        atomic::{AtomicBool, Ordering::SeqCst},
-        Arc,
-    },
-};
-use util::ResultExt;
-
-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 LOADED_PLAN_JSON: Mutex<Option<Vec<u8>>> = Mutex::new(None);
-static PLAN: Mutex<Option<Arc<Mutex<TestPlan>>>> = Mutex::new(None);
-
-#[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 max_peers = env::var("MAX_PEERS")
-        .map(|i| i.parse().expect("invalid `MAX_PEERS` variable"))
-        .unwrap_or(3);
-    let max_operations = env::var("OPERATIONS")
-        .map(|i| i.parse().expect("invalid `OPERATIONS` variable"))
-        .unwrap_or(10);
-
-    let mut server = TestServer::start(&deterministic).await;
-    let db = server.app_state.db.clone();
-
-    let mut users = Vec::new();
-    for ix in 0..max_peers {
-        let username = format!("user-{}", ix + 1);
-        let user_id = db
-            .create_user(
-                &format!("{username}@example.com"),
-                false,
-                NewUserParams {
-                    github_login: username.clone(),
-                    github_user_id: (ix + 1) as i32,
-                    invite_count: 0,
-                },
-            )
-            .await
-            .unwrap()
-            .user_id;
-        users.push(UserTestPlan {
-            user_id,
-            username,
-            online: false,
-            next_root_id: 0,
-            operation_ix: 0,
-        });
-    }
-
-    for (ix, user_a) in users.iter().enumerate() {
-        for user_b in &users[ix + 1..] {
-            server
-                .app_state
-                .db
-                .send_contact_request(user_a.user_id, user_b.user_id)
-                .await
-                .unwrap();
-            server
-                .app_state
-                .db
-                .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 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);
-        }
-    }
-
-    drop(operation_channels);
-    deterministic.start_waiting();
-    futures::future::join_all(client_tasks).await;
-    deterministic.finish_waiting();
-    deterministic.run_until_parked();
-
-    check_consistency_between_clients(&clients);
-
-    for (client, mut cx) in clients {
-        cx.update(|cx| {
-            let store = cx.remove_global::<SettingsStore>();
-            cx.clear_globals();
-            cx.set_global(store);
-            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 user = plan.user(user_id);
-                if user.online {
-                    return false;
-                }
-                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);
-            plan.lock().user(removed_user_id).online = false;
-            client_cx.update(|cx| {
-                cx.clear_globals();
-                drop(client);
-            });
-        }
-
-        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);
-        }
-
-        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}");
-                }
-            }
-
-            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, cx| call.decline_incoming(cx))?;
-        }
-
-        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(false).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.clone(), cx));
-            let save = cx.spawn(|cx| async move {
-                save.await
-                    .map_err(|err| anyhow!("save request failed: {:?}", err))?;
-                assert!(buffer
-                    .read_with(&cx, |buffer, _| { buffer.saved_version().to_owned() })
-                    .observed_all(&requested_version));
-                anyhow::Ok(())
-            });
-            if detach {
-                cx.update(|cx| save.detach_and_log_err(cx));
-            } else {
-                save.await?;
-            }
-        }
-
-        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)?;
-
-            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?;
-            }
-        }
-
-        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" }
-            );
-
-            let mut search = project.update(cx, |project, cx| {
-                project.search(
-                    SearchQuery::text(query, false, false, Vec::new(), Vec::new()),
-                    cx,
-                )
-            });
-            drop(project);
-            let search = cx.background().spawn(async move {
-                let mut results = HashMap::default();
-                while let Some((buffer, ranges)) = search.next().await {
-                    results.entry(buffer).or_insert(ranges);
-                }
-                results
-            });
-            search.await;
-        }
-
-        ClientOperation::WriteFsEntry {
-            path,
-            is_dir,
-            content,
-        } => {
-            if !client
-                .fs()
-                .directories(false)
-                .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(), text::LineEnding::Unix)
-                    .await
-                    .unwrap();
-            }
-        }
-
-        ClientOperation::GitOperation { operation } => match operation {
-            GitOperation::WriteGitIndex {
-                repo_path,
-                contents,
-            } => {
-                if !client.fs().directories(false).contains(&repo_path) {
-                    return Err(TestError::Inapplicable);
-                }
-
-                for (path, _) in contents.iter() {
-                    if !client.fs().files().contains(&repo_path.join(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);
-            }
-            GitOperation::WriteGitBranch {
-                repo_path,
-                new_branch,
-            } => {
-                if !client.fs().directories(false).contains(&repo_path) {
-                    return Err(TestError::Inapplicable);
-                }
-
-                log::info!(
-                    "{}: writing git branch for repo {:?}: {:?}",
-                    client.username,
-                    repo_path,
-                    new_branch
-                );
-
-                let dot_git_dir = repo_path.join(".git");
-                if client.fs().metadata(&dot_git_dir).await?.is_none() {
-                    client.fs().create_dir(&dot_git_dir).await?;
-                }
-                client.fs().set_branch_name(&dot_git_dir, new_branch);
-            }
-            GitOperation::WriteGitStatuses {
-                repo_path,
-                statuses,
-                git_operation,
-            } => {
-                if !client.fs().directories(false).contains(&repo_path) {
-                    return Err(TestError::Inapplicable);
-                }
-                for (path, _) in statuses.iter() {
-                    if !client.fs().files().contains(&repo_path.join(path)) {
-                        return Err(TestError::Inapplicable);
-                    }
-                }
-
-                log::info!(
-                    "{}: writing git statuses for repo {:?}: {:?}",
-                    client.username,
-                    repo_path,
-                    statuses
-                );
-
-                let dot_git_dir = repo_path.join(".git");
-
-                let statuses = statuses
-                    .iter()
-                    .map(|(path, val)| (path.as_path(), val.clone()))
-                    .collect::<Vec<_>>();
-
-                if client.fs().metadata(&dot_git_dir).await?.is_none() {
-                    client.fs().create_dir(&dot_git_dir).await?;
-                }
-
-                if git_operation {
-                    client
-                        .fs()
-                        .set_status_for_repo_via_git_operation(&dot_git_dir, statuses.as_slice());
-                } else {
-                    client.fs().set_status_for_repo_via_working_copy_change(
-                        &dot_git_dir,
-                        statuses.as_slice(),
-                    );
-                }
-            }
-        },
-    }
-    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()
-                            })
-                        })?
-                        .clone();
-                    Some((project, cx))
-                });
-
-                if !guest_project.is_read_only() {
-                    if let Some((host_project, host_cx)) = host_project {
-                        let host_worktree_snapshots =
-                            host_project.read_with(host_cx, |host_project, cx| {
-                                host_project
-                                    .worktrees(cx)
-                                    .map(|worktree| {
-                                        let worktree = worktree.read(cx);
-                                        (worktree.id(), worktree.snapshot())
-                                    })
-                                    .collect::<BTreeMap<_, _>>()
-                            });
-                        let guest_worktree_snapshots = guest_project
-                            .worktrees(cx)
-                            .map(|worktree| {
-                                let worktree = worktree.read(cx);
-                                (worktree.id(), worktree.snapshot())
-                            })
-                            .collect::<BTreeMap<_, _>>();
-
-                        assert_eq!(
-                            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 {
-                            let guest_snapshot = &guest_worktree_snapshots[id];
-                            assert_eq!(
-                                guest_snapshot.root_name(),
-                                host_snapshot.root_name(),
-                                "{} has different root name than the host for worktree {}, project {:?}",
-                                client.username,
-                                id,
-                                guest_project.remote_id(),
-                            );
-                            assert_eq!(
-                                guest_snapshot.abs_path(),
-                                host_snapshot.abs_path(),
-                                "{} has different abs path than the host for worktree {}, project: {:?}",
-                                client.username,
-                                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 {:?}",
-                                client.username,
-                                host_snapshot.abs_path(),
-                                id,
-                                guest_project.remote_id(),
-                            );
-                            assert_eq!(guest_snapshot.repositories().collect::<Vec<_>>(), host_snapshot.repositories().collect::<Vec<_>>(),
-                                "{} has different repositories 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,
-                                host_snapshot.abs_path(),
-                                guest_project.remote_id(),
-                            );
-                        }
-                    }
-                }
-
-                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(),
-                    );
-                }
-            });
-        }
-
-        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()
-            }) {
-                continue;
-            } else {
-                guest_project
-                    .read_with(client_cx, |project, _| project.remote_id())
-                    .unwrap()
-            };
-            let guest_user_id = client.user_id().unwrap();
-
-            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() == Some(project_id)
-                        })
-                    })?
-                    .clone();
-                Some((client.user_id().unwrap(), project, cx))
-            });
-
-            let (host_user_id, host_project, host_cx) =
-                if let Some((host_user_id, host_project, host_cx)) = host_project {
-                    (host_user_id, host_project, host_cx)
-                } else {
-                    continue;
-                };
-
-            for guest_buffer in guest_buffers {
-                let buffer_id = guest_buffer.read_with(client_cx, |buffer, _| buffer.remote_id());
-                let host_buffer = host_project.read_with(host_cx, |project, cx| {
-                    project.buffer_for_id(buffer_id, cx).unwrap_or_else(|| {
-                        panic!(
-                            "host does not have buffer for guest:{}, peer:{:?}, id:{}",
-                            client.username,
-                            client.peer_id(),
-                            buffer_id
-                        )
-                    })
-                });
-                let path = host_buffer
-                    .read_with(host_cx, |buffer, cx| buffer.file().unwrap().full_path(cx));
-
-                assert_eq!(
-                    guest_buffer.read_with(client_cx, |buffer, _| buffer.deferred_ops_len()),
-                    0,
-                    "{}, buffer {}, path {:?} has deferred operations",
-                    client.username,
-                    buffer_id,
-                    path,
-                );
-                assert_eq!(
-                    guest_buffer.read_with(client_cx, |buffer, _| buffer.text()),
-                    host_buffer.read_with(host_cx, |buffer, _| buffer.text()),
-                    "{}, buffer {}, path {:?}, differs from the host's buffer",
-                    client.username,
-                    buffer_id,
-                    path
-                );
-
-                let host_file = host_buffer.read_with(host_cx, |b, _| b.file().cloned());
-                let guest_file = guest_buffer.read_with(client_cx, |b, _| b.file().cloned());
-                match (host_file, guest_file) {
-                    (Some(host_file), Some(guest_file)) => {
-                        assert_eq!(guest_file.path(), host_file.path());
-                        assert_eq!(guest_file.is_deleted(), host_file.is_deleted());
-                        assert_eq!(
-                            guest_file.mtime(),
-                            host_file.mtime(),
-                            "guest {} mtime does not match host {} for path {:?} in project {}",
-                            guest_user_id,
-                            host_user_id,
-                            guest_file.path(),
-                            project_id,
-                        );
-                    }
-                    (None, None) => {}
-                    (None, _) => panic!("host's file is None, guest's isn't"),
-                    (_, None) => panic!("guest's file is None, hosts's isn't"),
-                }
-
-                let host_diff_base =
-                    host_buffer.read_with(host_cx, |b, _| b.diff_base().map(ToString::to_string));
-                let guest_diff_base = guest_buffer
-                    .read_with(client_cx, |b, _| b.diff_base().map(ToString::to_string));
-                assert_eq!(
-                    guest_diff_base, host_diff_base,
-                    "guest {} diff base does not match host's for path {path:?} in project {project_id}",
-                    client.username
-                );
-
-                let host_saved_version =
-                    host_buffer.read_with(host_cx, |b, _| b.saved_version().clone());
-                let guest_saved_version =
-                    guest_buffer.read_with(client_cx, |b, _| b.saved_version().clone());
-                assert_eq!(
-                    guest_saved_version, host_saved_version,
-                    "guest {} saved version does not match host's for path {path:?} in project {project_id}",
-                    client.username
-                );
-
-                let host_saved_version_fingerprint =
-                    host_buffer.read_with(host_cx, |b, _| b.saved_version_fingerprint());
-                let guest_saved_version_fingerprint =
-                    guest_buffer.read_with(client_cx, |b, _| b.saved_version_fingerprint());
-                assert_eq!(
-                    guest_saved_version_fingerprint, host_saved_version_fingerprint,
-                    "guest {} saved fingerprint does not match host's for path {path:?} in project {project_id}",
-                    client.username
-                );
-
-                let host_saved_mtime = host_buffer.read_with(host_cx, |b, _| b.saved_mtime());
-                let guest_saved_mtime = guest_buffer.read_with(client_cx, |b, _| b.saved_mtime());
-                assert_eq!(
-                    guest_saved_mtime, host_saved_mtime,
-                    "guest {} saved mtime does not match host's for path {path:?} in project {project_id}",
-                    client.username
-                );
-
-                let host_is_dirty = host_buffer.read_with(host_cx, |b, _| b.is_dirty());
-                let guest_is_dirty = guest_buffer.read_with(client_cx, |b, _| b.is_dirty());
-                assert_eq!(guest_is_dirty, host_is_dirty,
-                    "guest {} dirty status does not match host's for path {path:?} in project {project_id}",
-                    client.username
-                );
-
-                let host_has_conflict = host_buffer.read_with(host_cx, |b, _| b.has_conflict());
-                let guest_has_conflict = guest_buffer.read_with(client_cx, |b, _| b.has_conflict());
-                assert_eq!(guest_has_conflict, host_has_conflict,
-                    "guest {} conflict status does not match host's for path {path:?} in project {project_id}",
-                    client.username
-                );
-            }
-        }
-    }
-}
-
-struct TestPlan {
-    rng: StdRng,
-    replay: bool,
-    stored_operations: Vec<(StoredOperation, Arc<AtomicBool>)>,
-    max_operations: usize,
-    operation_ix: usize,
-    users: Vec<UserTestPlan>,
-    next_batch_id: usize,
-    allow_server_restarts: bool,
-    allow_client_reconnection: bool,
-    allow_client_disconnection: bool,
-}
-
-struct UserTestPlan {
-    user_id: UserId,
-    username: String,
-    next_root_id: usize,
-    operation_ix: usize,
-    online: bool,
-}
-
-#[derive(Clone, Debug, Serialize, Deserialize)]
-#[serde(untagged)]
-enum StoredOperation {
-    Server(Operation),
-    Client {
-        user_id: UserId,
-        batch_id: usize,
-        operation: ClientOperation,
-    },
-}
-
-#[derive(Clone, Debug, Serialize, Deserialize)]
-enum Operation {
-    AddConnection {
-        user_id: UserId,
-    },
-    RemoveConnection {
-        user_id: UserId,
-    },
-    BounceConnection {
-        user_id: UserId,
-    },
-    RestartServer,
-    MutateClients {
-        batch_id: usize,
-        #[serde(skip_serializing)]
-        #[serde(skip_deserializing)]
-        user_ids: Vec<UserId>,
-        quiesce: bool,
-    },
-}
-
-#[derive(Clone, Debug, Serialize, Deserialize)]
-enum ClientOperation {
-    AcceptIncomingCall,
-    RejectIncomingCall,
-    LeaveCall,
-    InviteContactToCall {
-        user_id: UserId,
-    },
-    OpenLocalProject {
-        first_root_name: String,
-    },
-    OpenRemoteProject {
-        host_id: UserId,
-        first_root_name: String,
-    },
-    AddWorktreeToProject {
-        project_root_name: String,
-        new_root_path: PathBuf,
-    },
-    CloseRemoteProject {
-        project_root_name: String,
-    },
-    OpenBuffer {
-        project_root_name: String,
-        is_local: bool,
-        full_path: PathBuf,
-    },
-    SearchProject {
-        project_root_name: String,
-        is_local: bool,
-        query: String,
-        detach: bool,
-    },
-    EditBuffer {
-        project_root_name: String,
-        is_local: bool,
-        full_path: PathBuf,
-        edits: Vec<(Range<usize>, Arc<str>)>,
-    },
-    CloseBuffer {
-        project_root_name: String,
-        is_local: bool,
-        full_path: PathBuf,
-    },
-    SaveBuffer {
-        project_root_name: String,
-        is_local: bool,
-        full_path: PathBuf,
-        detach: bool,
-    },
-    RequestLspDataInBuffer {
-        project_root_name: String,
-        is_local: bool,
-        full_path: PathBuf,
-        offset: usize,
-        kind: LspRequestKind,
-        detach: bool,
-    },
-    CreateWorktreeEntry {
-        project_root_name: String,
-        is_local: bool,
-        full_path: PathBuf,
-        is_dir: bool,
-    },
-    WriteFsEntry {
-        path: PathBuf,
-        is_dir: bool,
-        content: String,
-    },
-    GitOperation {
-        operation: GitOperation,
-    },
-}
-
-#[derive(Clone, Debug, Serialize, Deserialize)]
-enum GitOperation {
-    WriteGitIndex {
-        repo_path: PathBuf,
-        contents: Vec<(PathBuf, String)>,
-    },
-    WriteGitBranch {
-        repo_path: PathBuf,
-        new_branch: Option<String>,
-    },
-    WriteGitStatuses {
-        repo_path: PathBuf,
-        statuses: Vec<(PathBuf, GitFileStatus)>,
-        git_operation: bool,
-    },
-}
-
-#[derive(Clone, Debug, Serialize, Deserialize)]
-enum LspRequestKind {
-    Rename,
-    Completion,
-    CodeAction,
-    Definition,
-    Highlights,
-}
-
-enum TestError {
-    Inapplicable,
-    Other(anyhow::Error),
-}
-
-impl From<anyhow::Error> for TestError {
-    fn from(value: anyhow::Error) -> Self {
-        Self::Other(value)
-    }
-}
-
-impl TestPlan {
-    fn new(mut rng: StdRng, users: Vec<UserTestPlan>, max_operations: usize) -> Self {
-        Self {
-            replay: false,
-            allow_server_restarts: rng.gen_bool(0.7),
-            allow_client_reconnection: rng.gen_bool(0.7),
-            allow_client_disconnection: rng.gen_bool(0.1),
-            stored_operations: Vec::new(),
-            operation_ix: 0,
-            next_batch_id: 0,
-            max_operations,
-            users,
-            rng,
-        }
-    }
-
-    fn deserialize(&mut self, json: Vec<u8>) {
-        let stored_operations: Vec<StoredOperation> = serde_json::from_slice(&json).unwrap();
-        self.replay = true;
-        self.stored_operations = stored_operations
-            .iter()
-            .cloned()
-            .enumerate()
-            .map(|(i, mut operation)| {
-                if let StoredOperation::Server(Operation::MutateClients {
-                    batch_id: current_batch_id,
-                    user_ids,
-                    ..
-                }) = &mut operation
-                {
-                    assert!(user_ids.is_empty());
-                    user_ids.extend(stored_operations[i + 1..].iter().filter_map(|operation| {
-                        if let StoredOperation::Client {
-                            user_id, batch_id, ..
-                        } = operation
-                        {
-                            if batch_id == current_batch_id {
-                                return Some(user_id);
-                            }
-                        }
-                        None
-                    }));
-                    user_ids.sort_unstable();
-                }
-                (operation, Arc::new(AtomicBool::new(false)))
-            })
-            .collect()
-    }
-
-    fn serialize(&mut self) -> Vec<u8> {
-        // Format each operation as one line
-        let mut json = Vec::new();
-        json.push(b'[');
-        for (operation, applied) in &self.stored_operations {
-            if !applied.load(SeqCst) {
-                continue;
-            }
-            if json.len() > 1 {
-                json.push(b',');
-            }
-            json.extend_from_slice(b"\n  ");
-            serde_json::to_writer(&mut json, operation).unwrap();
-        }
-        json.extend_from_slice(b"\n]\n");
-        json
-    }
-
-    fn next_server_operation(
-        &mut self,
-        clients: &[(Rc<TestClient>, TestAppContext)],
-    ) -> Option<(Operation, Arc<AtomicBool>)> {
-        if self.replay {
-            while let Some(stored_operation) = self.stored_operations.get(self.operation_ix) {
-                self.operation_ix += 1;
-                if let (StoredOperation::Server(operation), applied) = stored_operation {
-                    return Some((operation.clone(), applied.clone()));
-                }
-            }
-            None
-        } else {
-            let operation = self.generate_server_operation(clients)?;
-            let applied = Arc::new(AtomicBool::new(false));
-            self.stored_operations
-                .push((StoredOperation::Server(operation.clone()), applied.clone()));
-            Some((operation, applied))
-        }
-    }
-
-    fn next_client_operation(
-        &mut self,
-        client: &TestClient,
-        current_batch_id: usize,
-        cx: &TestAppContext,
-    ) -> Option<(ClientOperation, Arc<AtomicBool>)> {
-        let current_user_id = client.current_user_id(cx);
-        let user_ix = self
-            .users
-            .iter()
-            .position(|user| user.user_id == current_user_id)
-            .unwrap();
-        let user_plan = &mut self.users[user_ix];
-
-        if self.replay {
-            while let Some(stored_operation) = self.stored_operations.get(user_plan.operation_ix) {
-                user_plan.operation_ix += 1;
-                if let (
-                    StoredOperation::Client {
-                        user_id, operation, ..
-                    },
-                    applied,
-                ) = stored_operation
-                {
-                    if user_id == &current_user_id {
-                        return Some((operation.clone(), applied.clone()));
-                    }
-                }
-            }
-            None
-        } else {
-            let operation = self.generate_client_operation(current_user_id, client, cx)?;
-            let applied = Arc::new(AtomicBool::new(false));
-            self.stored_operations.push((
-                StoredOperation::Client {
-                    user_id: current_user_id,
-                    batch_id: current_batch_id,
-                    operation: operation.clone(),
-                },
-                applied.clone(),
-            ));
-            Some((operation, applied))
-        }
-    }
-
-    fn generate_server_operation(
-        &mut self,
-        clients: &[(Rc<TestClient>, TestAppContext)],
-    ) -> Option<Operation> {
-        if self.operation_ix == self.max_operations {
-            return None;
-        }
-
-        Some(loop {
-            break match self.rng.gen_range(0..100) {
-                0..=29 if clients.len() < self.users.len() => {
-                    let user = self
-                        .users
-                        .iter()
-                        .filter(|u| !u.online)
-                        .choose(&mut self.rng)
-                        .unwrap();
-                    self.operation_ix += 1;
-                    Operation::AddConnection {
-                        user_id: user.user_id,
-                    }
-                }
-                30..=34 if clients.len() > 1 && self.allow_client_disconnection => {
-                    let (client, cx) = &clients[self.rng.gen_range(0..clients.len())];
-                    let user_id = client.current_user_id(cx);
-                    self.operation_ix += 1;
-                    Operation::RemoveConnection { user_id }
-                }
-                35..=39 if clients.len() > 1 && self.allow_client_reconnection => {
-                    let (client, cx) = &clients[self.rng.gen_range(0..clients.len())];
-                    let user_id = client.current_user_id(cx);
-                    self.operation_ix += 1;
-                    Operation::BounceConnection { user_id }
-                }
-                40..=44 if self.allow_server_restarts && clients.len() > 1 => {
-                    self.operation_ix += 1;
-                    Operation::RestartServer
-                }
-                _ if !clients.is_empty() => {
-                    let count = self
-                        .rng
-                        .gen_range(1..10)
-                        .min(self.max_operations - self.operation_ix);
-                    let batch_id = util::post_inc(&mut self.next_batch_id);
-                    let mut user_ids = (0..count)
-                        .map(|_| {
-                            let ix = self.rng.gen_range(0..clients.len());
-                            let (client, cx) = &clients[ix];
-                            client.current_user_id(cx)
-                        })
-                        .collect::<Vec<_>>();
-                    user_ids.sort_unstable();
-                    Operation::MutateClients {
-                        user_ids,
-                        batch_id,
-                        quiesce: self.rng.gen_bool(0.7),
-                    }
-                }
-                _ => continue,
-            };
-        })
-    }
-
-    fn generate_client_operation(
-        &mut self,
-        user_id: UserId,
-        client: &TestClient,
-        cx: &TestAppContext,
-    ) -> Option<ClientOperation> {
-        if self.operation_ix == self.max_operations {
-            return None;
-        }
-
-        self.operation_ix += 1;
-        let call = cx.read(ActiveCall::global);
-        Some(loop {
-            match self.rng.gen_range(0..100_u32) {
-                // Mutate the call
-                0..=29 => {
-                    // Respond to an incoming call
-                    if call.read_with(cx, |call, _| call.incoming().borrow().is_some()) {
-                        break if self.rng.gen_bool(0.7) {
-                            ClientOperation::AcceptIncomingCall
-                        } else {
-                            ClientOperation::RejectIncomingCall
-                        };
-                    }
-
-                    match self.rng.gen_range(0..100_u32) {
-                        // Invite a contact to the current call
-                        0..=70 => {
-                            let available_contacts =
-                                client.user_store().read_with(cx, |user_store, _| {
-                                    user_store
-                                        .contacts()
-                                        .iter()
-                                        .filter(|contact| contact.online && !contact.busy)
-                                        .cloned()
-                                        .collect::<Vec<_>>()
-                                });
-                            if !available_contacts.is_empty() {
-                                let contact = available_contacts.choose(&mut self.rng).unwrap();
-                                break ClientOperation::InviteContactToCall {
-                                    user_id: UserId(contact.user.id as i32),
-                                };
-                            }
-                        }
-
-                        // Leave the current call
-                        71.. => {
-                            if self.allow_client_disconnection
-                                && call.read_with(cx, |call, _| call.room().is_some())
-                            {
-                                break ClientOperation::LeaveCall;
-                            }
-                        }
-                    }
-                }
-
-                // Mutate projects
-                30..=59 => match self.rng.gen_range(0..100_u32) {
-                    // Open a new project
-                    0..=70 => {
-                        // Open a remote project
-                        if let Some(room) = call.read_with(cx, |call, _| call.room().cloned()) {
-                            let existing_remote_project_ids = cx.read(|cx| {
-                                client
-                                    .remote_projects()
-                                    .iter()
-                                    .map(|p| p.read(cx).remote_id().unwrap())
-                                    .collect::<Vec<_>>()
-                            });
-                            let new_remote_projects = room.read_with(cx, |room, _| {
-                                room.remote_participants()
-                                    .values()
-                                    .flat_map(|participant| {
-                                        participant.projects.iter().filter_map(|project| {
-                                            if existing_remote_project_ids.contains(&project.id) {
-                                                None
-                                            } else {
-                                                Some((
-                                                    UserId::from_proto(participant.user.id),
-                                                    project.worktree_root_names[0].clone(),
-                                                ))
-                                            }
-                                        })
-                                    })
-                                    .collect::<Vec<_>>()
-                            });
-                            if !new_remote_projects.is_empty() {
-                                let (host_id, first_root_name) =
-                                    new_remote_projects.choose(&mut self.rng).unwrap().clone();
-                                break ClientOperation::OpenRemoteProject {
-                                    host_id,
-                                    first_root_name,
-                                };
-                            }
-                        }
-                        // Open a local project
-                        else {
-                            let first_root_name = self.next_root_dir_name(user_id);
-                            break ClientOperation::OpenLocalProject { first_root_name };
-                        }
-                    }
-
-                    // Close a remote project
-                    71..=80 => {
-                        if !client.remote_projects().is_empty() {
-                            let project = client
-                                .remote_projects()
-                                .choose(&mut self.rng)
-                                .unwrap()
-                                .clone();
-                            let first_root_name = root_name_for_project(&project, cx);
-                            break ClientOperation::CloseRemoteProject {
-                                project_root_name: first_root_name,
-                            };
-                        }
-                    }
-
-                    // Mutate project worktrees
-                    81.. => match self.rng.gen_range(0..100_u32) {
-                        // Add a worktree to a local project
-                        0..=50 => {
-                            let Some(project) =
-                                client.local_projects().choose(&mut self.rng).cloned()
-                            else {
-                                continue;
-                            };
-                            let project_root_name = root_name_for_project(&project, cx);
-                            let mut paths = client.fs().paths(false);
-                            paths.remove(0);
-                            let new_root_path = if paths.is_empty() || self.rng.gen() {
-                                Path::new("/").join(&self.next_root_dir_name(user_id))
-                            } else {
-                                paths.choose(&mut self.rng).unwrap().clone()
-                            };
-                            break ClientOperation::AddWorktreeToProject {
-                                project_root_name,
-                                new_root_path,
-                            };
-                        }
-
-                        // Add an entry to a worktree
-                        _ => {
-                            let Some(project) = choose_random_project(client, &mut self.rng) else {
-                                continue;
-                            };
-                            let project_root_name = root_name_for_project(&project, cx);
-                            let is_local = project.read_with(cx, |project, _| project.is_local());
-                            let worktree = project.read_with(cx, |project, cx| {
-                                project
-                                    .worktrees(cx)
-                                    .filter(|worktree| {
-                                        let worktree = worktree.read(cx);
-                                        worktree.is_visible()
-                                            && worktree.entries(false).any(|e| e.is_file())
-                                            && worktree.root_entry().map_or(false, |e| e.is_dir())
-                                    })
-                                    .choose(&mut self.rng)
-                            });
-                            let Some(worktree) = worktree else { continue };
-                            let is_dir = self.rng.gen::<bool>();
-                            let mut full_path =
-                                worktree.read_with(cx, |w, _| PathBuf::from(w.root_name()));
-                            full_path.push(gen_file_name(&mut self.rng));
-                            if !is_dir {
-                                full_path.set_extension("rs");
-                            }
-                            break ClientOperation::CreateWorktreeEntry {
-                                project_root_name,
-                                is_local,
-                                full_path,
-                                is_dir,
-                            };
-                        }
-                    },
-                },
-
-                // Query and mutate buffers
-                60..=90 => {
-                    let Some(project) = choose_random_project(client, &mut self.rng) else {
-                        continue;
-                    };
-                    let project_root_name = root_name_for_project(&project, cx);
-                    let is_local = project.read_with(cx, |project, _| project.is_local());
-
-                    match self.rng.gen_range(0..100_u32) {
-                        // Manipulate an existing buffer
-                        0..=70 => {
-                            let Some(buffer) = client
-                                .buffers_for_project(&project)
-                                .iter()
-                                .choose(&mut self.rng)
-                                .cloned()
-                            else {
-                                continue;
-                            };
-
-                            let full_path = buffer
-                                .read_with(cx, |buffer, cx| buffer.file().unwrap().full_path(cx));
-
-                            match self.rng.gen_range(0..100_u32) {
-                                // Close the buffer
-                                0..=15 => {
-                                    break ClientOperation::CloseBuffer {
-                                        project_root_name,
-                                        is_local,
-                                        full_path,
-                                    };
-                                }
-                                // Save the buffer
-                                16..=29 if buffer.read_with(cx, |b, _| b.is_dirty()) => {
-                                    let detach = self.rng.gen_bool(0.3);
-                                    break ClientOperation::SaveBuffer {
-                                        project_root_name,
-                                        is_local,
-                                        full_path,
-                                        detach,
-                                    };
-                                }
-                                // Edit the buffer
-                                30..=69 => {
-                                    let edits = buffer.read_with(cx, |buffer, _| {
-                                        buffer.get_random_edits(&mut self.rng, 3)
-                                    });
-                                    break ClientOperation::EditBuffer {
-                                        project_root_name,
-                                        is_local,
-                                        full_path,
-                                        edits,
-                                    };
-                                }
-                                // Make an LSP request
-                                _ => {
-                                    let offset = buffer.read_with(cx, |buffer, _| {
-                                        buffer.clip_offset(
-                                            self.rng.gen_range(0..=buffer.len()),
-                                            language::Bias::Left,
-                                        )
-                                    });
-                                    let detach = self.rng.gen();
-                                    break ClientOperation::RequestLspDataInBuffer {
-                                        project_root_name,
-                                        full_path,
-                                        offset,
-                                        is_local,
-                                        kind: match self.rng.gen_range(0..5_u32) {
-                                            0 => LspRequestKind::Rename,
-                                            1 => LspRequestKind::Highlights,
-                                            2 => LspRequestKind::Definition,
-                                            3 => LspRequestKind::CodeAction,
-                                            4.. => LspRequestKind::Completion,
-                                        },
-                                        detach,
-                                    };
-                                }
-                            }
-                        }
-
-                        71..=80 => {
-                            let query = self.rng.gen_range('a'..='z').to_string();
-                            let detach = self.rng.gen_bool(0.3);
-                            break ClientOperation::SearchProject {
-                                project_root_name,
-                                is_local,
-                                query,
-                                detach,
-                            };
-                        }
-
-                        // Open a buffer
-                        81.. => {
-                            let worktree = project.read_with(cx, |project, cx| {
-                                project
-                                    .worktrees(cx)
-                                    .filter(|worktree| {
-                                        let worktree = worktree.read(cx);
-                                        worktree.is_visible()
-                                            && worktree.entries(false).any(|e| e.is_file())
-                                    })
-                                    .choose(&mut self.rng)
-                            });
-                            let Some(worktree) = worktree else { continue };
-                            let full_path = worktree.read_with(cx, |worktree, _| {
-                                let entry = worktree
-                                    .entries(false)
-                                    .filter(|e| e.is_file())
-                                    .choose(&mut self.rng)
-                                    .unwrap();
-                                if entry.path.as_ref() == Path::new("") {
-                                    Path::new(worktree.root_name()).into()
-                                } else {
-                                    Path::new(worktree.root_name()).join(&entry.path)
-                                }
-                            });
-                            break ClientOperation::OpenBuffer {
-                                project_root_name,
-                                is_local,
-                                full_path,
-                            };
-                        }
-                    }
-                }
-
-                // Update a git related action
-                91..=95 => {
-                    break ClientOperation::GitOperation {
-                        operation: self.generate_git_operation(client),
-                    };
-                }
-
-                // Create or update a file or directory
-                96.. => {
-                    let is_dir = self.rng.gen::<bool>();
-                    let content;
-                    let mut path;
-                    let dir_paths = client.fs().directories(false);
-
-                    if is_dir {
-                        content = String::new();
-                        path = dir_paths.choose(&mut self.rng).unwrap().clone();
-                        path.push(gen_file_name(&mut self.rng));
-                    } else {
-                        content = Alphanumeric.sample_string(&mut self.rng, 16);
-
-                        // Create a new file or overwrite an existing file
-                        let file_paths = client.fs().files();
-                        if file_paths.is_empty() || self.rng.gen_bool(0.5) {
-                            path = dir_paths.choose(&mut self.rng).unwrap().clone();
-                            path.push(gen_file_name(&mut self.rng));
-                            path.set_extension("rs");
-                        } else {
-                            path = file_paths.choose(&mut self.rng).unwrap().clone()
-                        };
-                    }
-                    break ClientOperation::WriteFsEntry {
-                        path,
-                        is_dir,
-                        content,
-                    };
-                }
-            }
-        })
-    }
-
-    fn generate_git_operation(&mut self, client: &TestClient) -> GitOperation {
-        fn generate_file_paths(
-            repo_path: &Path,
-            rng: &mut StdRng,
-            client: &TestClient,
-        ) -> Vec<PathBuf> {
-            let mut paths = client
-                .fs()
-                .files()
-                .into_iter()
-                .filter(|path| path.starts_with(repo_path))
-                .collect::<Vec<_>>();
-
-            let count = rng.gen_range(0..=paths.len());
-            paths.shuffle(rng);
-            paths.truncate(count);
-
-            paths
-                .iter()
-                .map(|path| path.strip_prefix(repo_path).unwrap().to_path_buf())
-                .collect::<Vec<_>>()
-        }
-
-        let repo_path = client
-            .fs()
-            .directories(false)
-            .choose(&mut self.rng)
-            .unwrap()
-            .clone();
-
-        match self.rng.gen_range(0..100_u32) {
-            0..=25 => {
-                let file_paths = generate_file_paths(&repo_path, &mut self.rng, client);
-
-                let contents = file_paths
-                    .into_iter()
-                    .map(|path| (path, Alphanumeric.sample_string(&mut self.rng, 16)))
-                    .collect();
-
-                GitOperation::WriteGitIndex {
-                    repo_path,
-                    contents,
-                }
-            }
-            26..=63 => {
-                let new_branch = (self.rng.gen_range(0..10) > 3)
-                    .then(|| Alphanumeric.sample_string(&mut self.rng, 8));
-
-                GitOperation::WriteGitBranch {
-                    repo_path,
-                    new_branch,
-                }
-            }
-            64..=100 => {
-                let file_paths = generate_file_paths(&repo_path, &mut self.rng, client);
-
-                let statuses = file_paths
-                    .into_iter()
-                    .map(|paths| {
-                        (
-                            paths,
-                            match self.rng.gen_range(0..3_u32) {
-                                0 => GitFileStatus::Added,
-                                1 => GitFileStatus::Modified,
-                                2 => GitFileStatus::Conflict,
-                                _ => unreachable!(),
-                            },
-                        )
-                    })
-                    .collect::<Vec<_>>();
-
-                let git_operation = self.rng.gen::<bool>();
-
-                GitOperation::WriteGitStatuses {
-                    repo_path,
-                    statuses,
-                    git_operation,
-                }
-            }
-            _ => unreachable!(),
-        }
-    }
-
-    fn next_root_dir_name(&mut self, user_id: UserId) -> String {
-        let user_ix = self
-            .users
-            .iter()
-            .position(|user| user.user_id == user_id)
-            .unwrap();
-        let root_id = util::post_inc(&mut self.users[user_ix].next_root_id);
-        format!("dir-{user_id}-{root_id}")
-    }
-
-    fn user(&mut self, user_id: UserId) -> &mut UserTestPlan {
-        let ix = self
-            .users
-            .iter()
-            .position(|user| user.user_id == user_id)
-            .unwrap();
-        &mut self.users[ix]
-    }
-}
-
-async fn simulate_client(
-    client: Rc<TestClient>,
-    mut operation_rx: futures::channel::mpsc::UnboundedReceiver<usize>,
-    plan: Arc<Mutex<TestPlan>>,
-    mut cx: TestAppContext,
-) {
-    // Setup language server
-    let mut language = Language::new(
-        LanguageConfig {
-            name: "Rust".into(),
-            path_suffixes: vec!["rs".to_string()],
-            ..Default::default()
-        },
-        None,
-    );
-    let _fake_language_servers = language
-        .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
-            name: "the-fake-language-server",
-            capabilities: lsp::LanguageServer::full_capabilities(),
-            initializer: Some(Box::new({
-                let fs = client.app_state.fs.clone();
-                move |fake_server: &mut FakeLanguageServer| {
-                    fake_server.handle_request::<lsp::request::Completion, _, _>(
-                        |_, _| async move {
-                            Ok(Some(lsp::CompletionResponse::Array(vec![
-                                lsp::CompletionItem {
-                                    text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
-                                        range: lsp::Range::new(
-                                            lsp::Position::new(0, 0),
-                                            lsp::Position::new(0, 0),
-                                        ),
-                                        new_text: "the-new-text".to_string(),
-                                    })),
-                                    ..Default::default()
-                                },
-                            ])))
-                        },
-                    );
-
-                    fake_server.handle_request::<lsp::request::CodeActionRequest, _, _>(
-                        |_, _| async move {
-                            Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
-                                lsp::CodeAction {
-                                    title: "the-code-action".to_string(),
-                                    ..Default::default()
-                                },
-                            )]))
-                        },
-                    );
-
-                    fake_server.handle_request::<lsp::request::PrepareRenameRequest, _, _>(
-                        |params, _| async move {
-                            Ok(Some(lsp::PrepareRenameResponse::Range(lsp::Range::new(
-                                params.position,
-                                params.position,
-                            ))))
-                        },
-                    );
-
-                    fake_server.handle_request::<lsp::request::GotoDefinition, _, _>({
-                        let fs = fs.clone();
-                        move |_, cx| {
-                            let background = cx.background();
-                            let mut rng = background.rng();
-                            let count = rng.gen_range::<usize, _>(1..3);
-                            let files = fs.as_fake().files();
-                            let files = (0..count)
-                                .map(|_| files.choose(&mut *rng).unwrap().clone())
-                                .collect::<Vec<_>>();
-                            async move {
-                                log::info!("LSP: Returning definitions in files {:?}", &files);
-                                Ok(Some(lsp::GotoDefinitionResponse::Array(
-                                    files
-                                        .into_iter()
-                                        .map(|file| lsp::Location {
-                                            uri: lsp::Url::from_file_path(file).unwrap(),
-                                            range: Default::default(),
-                                        })
-                                        .collect(),
-                                )))
-                            }
-                        }
-                    });
-
-                    fake_server.handle_request::<lsp::request::DocumentHighlightRequest, _, _>(
-                        move |_, cx| {
-                            let mut highlights = Vec::new();
-                            let background = cx.background();
-                            let mut rng = background.rng();
-
-                            let highlight_count = rng.gen_range(1..=5);
-                            for _ in 0..highlight_count {
-                                let start_row = rng.gen_range(0..100);
-                                let start_column = rng.gen_range(0..100);
-                                let end_row = rng.gen_range(0..100);
-                                let end_column = rng.gen_range(0..100);
-                                let start = PointUtf16::new(start_row, start_column);
-                                let end = PointUtf16::new(end_row, end_column);
-                                let range = if start > end { end..start } else { start..end };
-                                highlights.push(lsp::DocumentHighlight {
-                                    range: range_to_lsp(range.clone()),
-                                    kind: Some(lsp::DocumentHighlightKind::READ),
-                                });
-                            }
-                            highlights.sort_unstable_by_key(|highlight| {
-                                (highlight.range.start, highlight.range.end)
-                            });
-                            async move { Ok(Some(highlights)) }
-                        },
-                    );
-                }
-            })),
-            ..Default::default()
-        }))
-        .await;
-    client.app_state.languages.add(Arc::new(language));
-
-    while let Some(batch_id) = operation_rx.next().await {
-        let Some((operation, applied)) = plan.lock().next_client_operation(&client, batch_id, &cx)
-        else {
-            break;
-        };
-        applied.store(true, SeqCst);
-        match apply_client_operation(&client, operation, &mut cx).await {
-            Ok(()) => {}
-            Err(TestError::Inapplicable) => {
-                applied.store(false, SeqCst);
-                log::info!("skipped operation");
-            }
-            Err(TestError::Other(error)) => {
-                log::error!("{} error: {}", client.username, error);
-            }
-        }
-        cx.background().simulate_random_delay().await;
-    }
-    log::info!("{}: done", client.username);
-}
-
-fn buffer_for_full_path(
-    client: &TestClient,
-    project: &ModelHandle<Project>,
-    full_path: &PathBuf,
-    cx: &TestAppContext,
-) -> Option<ModelHandle<language::Buffer>> {
-    client
-        .buffers_for_project(project)
-        .iter()
-        .find(|buffer| {
-            buffer.read_with(cx, |buffer, cx| {
-                buffer.file().unwrap().full_path(cx) == *full_path
-            })
-        })
-        .cloned()
-}
-
-fn project_for_root_name(
-    client: &TestClient,
-    root_name: &str,
-    cx: &TestAppContext,
-) -> Option<ModelHandle<Project>> {
-    if let Some(ix) = project_ix_for_root_name(&*client.local_projects(), root_name, cx) {
-        return Some(client.local_projects()[ix].clone());
-    }
-    if let Some(ix) = project_ix_for_root_name(&*client.remote_projects(), root_name, cx) {
-        return Some(client.remote_projects()[ix].clone());
-    }
-    None
-}
-
-fn project_ix_for_root_name(
-    projects: &[ModelHandle<Project>],
-    root_name: &str,
-    cx: &TestAppContext,
-) -> Option<usize> {
-    projects.iter().position(|project| {
-        project.read_with(cx, |project, cx| {
-            let worktree = project.visible_worktrees(cx).next().unwrap();
-            worktree.read(cx).root_name() == root_name
-        })
-    })
-}
-
-fn root_name_for_project(project: &ModelHandle<Project>, cx: &TestAppContext) -> String {
-    project.read_with(cx, |project, cx| {
-        project
-            .visible_worktrees(cx)
-            .next()
-            .unwrap()
-            .read(cx)
-            .root_name()
-            .to_string()
-    })
-}
-
-fn project_path_for_full_path(
-    project: &ModelHandle<Project>,
-    full_path: &Path,
-    cx: &TestAppContext,
-) -> Option<ProjectPath> {
-    let mut components = full_path.components();
-    let root_name = components.next().unwrap().as_os_str().to_str().unwrap();
-    let path = components.as_path().into();
-    let worktree_id = project.read_with(cx, |project, cx| {
-        project.worktrees(cx).find_map(|worktree| {
-            let worktree = worktree.read(cx);
-            if worktree.root_name() == root_name {
-                Some(worktree.id())
-            } else {
-                None
-            }
-        })
-    })?;
-    Some(ProjectPath { worktree_id, path })
-}
-
-async fn ensure_project_shared(
-    project: &ModelHandle<Project>,
-    client: &TestClient,
-    cx: &mut TestAppContext,
-) {
-    let first_root_name = root_name_for_project(project, cx);
-    let active_call = cx.read(ActiveCall::global);
-    if active_call.read_with(cx, |call, _| call.room().is_some())
-        && project.read_with(cx, |project, _| project.is_local() && !project.is_shared())
-    {
-        match active_call
-            .update(cx, |call, cx| call.share_project(project.clone(), cx))
-            .await
-        {
-            Ok(project_id) => {
-                log::info!(
-                    "{}: shared project {} with id {}",
-                    client.username,
-                    first_root_name,
-                    project_id
-                );
-            }
-            Err(error) => {
-                log::error!(
-                    "{}: error sharing project {}: {:?}",
-                    client.username,
-                    first_root_name,
-                    error
-                );
-            }
-        }
-    }
-}
-
-fn choose_random_project(client: &TestClient, rng: &mut StdRng) -> Option<ModelHandle<Project>> {
-    client
-        .local_projects()
-        .iter()
-        .chain(client.remote_projects().iter())
-        .choose(rng)
-        .cloned()
-}
-
-fn gen_file_name(rng: &mut StdRng) -> String {
-    let mut name = String::new();
-    for _ in 0..10 {
-        let letter = rng.gen_range('a'..='z');
-        name.push(letter);
-    }
-    name
-}
-
-fn path_env_var(name: &str) -> Option<PathBuf> {
-    let value = env::var(name).ok()?;
-    let mut path = PathBuf::from(value);
-    if path.is_relative() {
-        let mut abs_path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
-        abs_path.pop();
-        abs_path.pop();
-        abs_path.push(path);
-        path = abs_path
-    }
-    Some(path)
-}

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

@@ -0,0 +1,689 @@
+use crate::{
+    db::{self, NewUserParams, UserId},
+    rpc::{CLEANUP_TIMEOUT, RECONNECT_TIMEOUT},
+    tests::{TestClient, TestServer},
+};
+use async_trait::async_trait;
+use futures::StreamExt;
+use gpui::{executor::Deterministic, Task, TestAppContext};
+use parking_lot::Mutex;
+use rand::prelude::*;
+use rpc::RECEIVE_TIMEOUT;
+use serde::{de::DeserializeOwned, Deserialize, Serialize};
+use settings::SettingsStore;
+use std::{
+    env,
+    path::PathBuf,
+    rc::Rc,
+    sync::{
+        atomic::{AtomicBool, Ordering::SeqCst},
+        Arc,
+    },
+};
+
+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 MAX_PEERS: usize = env::var("MAX_PEERS")
+        .map(|i| i.parse().expect("invalid `MAX_PEERS` variable"))
+        .unwrap_or(3);
+    static ref MAX_OPERATIONS: usize = env::var("OPERATIONS")
+        .map(|i| i.parse().expect("invalid `OPERATIONS` variable"))
+        .unwrap_or(10);
+
+}
+
+static LOADED_PLAN_JSON: Mutex<Option<Vec<u8>>> = Mutex::new(None);
+static LAST_PLAN: Mutex<Option<Box<dyn Send + FnOnce() -> Vec<u8>>>> = Mutex::new(None);
+
+struct TestPlan<T: RandomizedTest> {
+    rng: StdRng,
+    replay: bool,
+    stored_operations: Vec<(StoredOperation<T::Operation>, Arc<AtomicBool>)>,
+    max_operations: usize,
+    operation_ix: usize,
+    users: Vec<UserTestPlan>,
+    next_batch_id: usize,
+    allow_server_restarts: bool,
+    allow_client_reconnection: bool,
+    allow_client_disconnection: bool,
+}
+
+pub struct UserTestPlan {
+    pub user_id: UserId,
+    pub username: String,
+    pub allow_client_reconnection: bool,
+    pub allow_client_disconnection: bool,
+    next_root_id: usize,
+    operation_ix: usize,
+    online: bool,
+}
+
+#[derive(Clone, Debug, Serialize, Deserialize)]
+#[serde(untagged)]
+enum StoredOperation<T> {
+    Server(ServerOperation),
+    Client {
+        user_id: UserId,
+        batch_id: usize,
+        operation: T,
+    },
+}
+
+#[derive(Clone, Debug, Serialize, Deserialize)]
+enum ServerOperation {
+    AddConnection {
+        user_id: UserId,
+    },
+    RemoveConnection {
+        user_id: UserId,
+    },
+    BounceConnection {
+        user_id: UserId,
+    },
+    RestartServer,
+    MutateClients {
+        batch_id: usize,
+        #[serde(skip_serializing)]
+        #[serde(skip_deserializing)]
+        user_ids: Vec<UserId>,
+        quiesce: bool,
+    },
+}
+
+pub enum TestError {
+    Inapplicable,
+    Other(anyhow::Error),
+}
+
+#[async_trait(?Send)]
+pub trait RandomizedTest: 'static + Sized {
+    type Operation: Send + Clone + Serialize + DeserializeOwned;
+
+    fn generate_operation(
+        client: &TestClient,
+        rng: &mut StdRng,
+        plan: &mut UserTestPlan,
+        cx: &TestAppContext,
+    ) -> Self::Operation;
+
+    async fn apply_operation(
+        client: &TestClient,
+        operation: Self::Operation,
+        cx: &mut TestAppContext,
+    ) -> Result<(), TestError>;
+
+    async fn initialize(server: &mut TestServer, users: &[UserTestPlan]);
+
+    async fn on_client_added(client: &Rc<TestClient>, cx: &mut TestAppContext);
+
+    async fn on_quiesce(server: &mut TestServer, client: &mut [(Rc<TestClient>, TestAppContext)]);
+}
+
+pub async fn run_randomized_test<T: RandomizedTest>(
+    cx: &mut TestAppContext,
+    deterministic: Arc<Deterministic>,
+    rng: StdRng,
+) {
+    deterministic.forbid_parking();
+    let mut server = TestServer::start(&deterministic).await;
+    let plan = TestPlan::<T>::new(&mut server, rng).await;
+
+    LAST_PLAN.lock().replace({
+        let plan = plan.clone();
+        Box::new(move || plan.lock().serialize())
+    });
+
+    let mut clients = Vec::new();
+    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 = TestPlan::apply_server_operation(
+            plan.clone(),
+            deterministic.clone(),
+            &mut server,
+            &mut clients,
+            &mut client_tasks,
+            &mut operation_channels,
+            next_operation,
+            cx,
+        )
+        .await;
+        if !did_apply {
+            applied.store(false, SeqCst);
+        }
+    }
+
+    drop(operation_channels);
+    deterministic.start_waiting();
+    futures::future::join_all(client_tasks).await;
+    deterministic.finish_waiting();
+
+    deterministic.run_until_parked();
+    T::on_quiesce(&mut server, &mut clients).await;
+
+    for (client, mut cx) in clients {
+        cx.update(|cx| {
+            let store = cx.remove_global::<SettingsStore>();
+            cx.clear_globals();
+            cx.set_global(store);
+            drop(client);
+        });
+    }
+    deterministic.run_until_parked();
+
+    if let Some(path) = &*PLAN_SAVE_PATH {
+        eprintln!("saved test plan to path {:?}", path);
+        std::fs::write(path, plan.lock().serialize()).unwrap();
+    }
+}
+
+pub fn save_randomized_test_plan() {
+    if let Some(serialize_plan) = LAST_PLAN.lock().take() {
+        if let Some(path) = &*PLAN_SAVE_PATH {
+            eprintln!("saved test plan to path {:?}", path);
+            std::fs::write(path, serialize_plan()).unwrap();
+        }
+    }
+}
+
+impl<T: RandomizedTest> TestPlan<T> {
+    pub async fn new(server: &mut TestServer, mut rng: StdRng) -> Arc<Mutex<Self>> {
+        let allow_server_restarts = rng.gen_bool(0.7);
+        let allow_client_reconnection = rng.gen_bool(0.7);
+        let allow_client_disconnection = rng.gen_bool(0.1);
+
+        let mut users = Vec::new();
+        for ix in 0..*MAX_PEERS {
+            let username = format!("user-{}", ix + 1);
+            let user_id = server
+                .app_state
+                .db
+                .create_user(
+                    &format!("{username}@example.com"),
+                    false,
+                    NewUserParams {
+                        github_login: username.clone(),
+                        github_user_id: (ix + 1) as i32,
+                        invite_count: 0,
+                    },
+                )
+                .await
+                .unwrap()
+                .user_id;
+            users.push(UserTestPlan {
+                user_id,
+                username,
+                online: false,
+                next_root_id: 0,
+                operation_ix: 0,
+                allow_client_disconnection,
+                allow_client_reconnection,
+            });
+        }
+
+        T::initialize(server, &users).await;
+
+        let plan = Arc::new(Mutex::new(Self {
+            replay: false,
+            allow_server_restarts,
+            allow_client_reconnection,
+            allow_client_disconnection,
+            stored_operations: Vec::new(),
+            operation_ix: 0,
+            next_batch_id: 0,
+            max_operations: *MAX_OPERATIONS,
+            users,
+            rng,
+        }));
+
+        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
+    }
+
+    fn deserialize(&mut self, json: Vec<u8>) {
+        let stored_operations: Vec<StoredOperation<T::Operation>> =
+            serde_json::from_slice(&json).unwrap();
+        self.replay = true;
+        self.stored_operations = stored_operations
+            .iter()
+            .cloned()
+            .enumerate()
+            .map(|(i, mut operation)| {
+                let did_apply = Arc::new(AtomicBool::new(false));
+                if let StoredOperation::Server(ServerOperation::MutateClients {
+                    batch_id: current_batch_id,
+                    user_ids,
+                    ..
+                }) = &mut operation
+                {
+                    assert!(user_ids.is_empty());
+                    user_ids.extend(stored_operations[i + 1..].iter().filter_map(|operation| {
+                        if let StoredOperation::Client {
+                            user_id, batch_id, ..
+                        } = operation
+                        {
+                            if batch_id == current_batch_id {
+                                return Some(user_id);
+                            }
+                        }
+                        None
+                    }));
+                    user_ids.sort_unstable();
+                }
+                (operation, did_apply)
+            })
+            .collect()
+    }
+
+    fn serialize(&mut self) -> Vec<u8> {
+        // Format each operation as one line
+        let mut json = Vec::new();
+        json.push(b'[');
+        for (operation, applied) in &self.stored_operations {
+            if !applied.load(SeqCst) {
+                continue;
+            }
+            if json.len() > 1 {
+                json.push(b',');
+            }
+            json.extend_from_slice(b"\n  ");
+            serde_json::to_writer(&mut json, operation).unwrap();
+        }
+        json.extend_from_slice(b"\n]\n");
+        json
+    }
+
+    fn next_server_operation(
+        &mut self,
+        clients: &[(Rc<TestClient>, TestAppContext)],
+    ) -> Option<(ServerOperation, Arc<AtomicBool>)> {
+        if self.replay {
+            while let Some(stored_operation) = self.stored_operations.get(self.operation_ix) {
+                self.operation_ix += 1;
+                if let (StoredOperation::Server(operation), applied) = stored_operation {
+                    return Some((operation.clone(), applied.clone()));
+                }
+            }
+            None
+        } else {
+            let operation = self.generate_server_operation(clients)?;
+            let applied = Arc::new(AtomicBool::new(false));
+            self.stored_operations
+                .push((StoredOperation::Server(operation.clone()), applied.clone()));
+            Some((operation, applied))
+        }
+    }
+
+    fn next_client_operation(
+        &mut self,
+        client: &TestClient,
+        current_batch_id: usize,
+        cx: &TestAppContext,
+    ) -> Option<(T::Operation, Arc<AtomicBool>)> {
+        let current_user_id = client.current_user_id(cx);
+        let user_ix = self
+            .users
+            .iter()
+            .position(|user| user.user_id == current_user_id)
+            .unwrap();
+        let user_plan = &mut self.users[user_ix];
+
+        if self.replay {
+            while let Some(stored_operation) = self.stored_operations.get(user_plan.operation_ix) {
+                user_plan.operation_ix += 1;
+                if let (
+                    StoredOperation::Client {
+                        user_id, operation, ..
+                    },
+                    applied,
+                ) = stored_operation
+                {
+                    if user_id == &current_user_id {
+                        return Some((operation.clone(), applied.clone()));
+                    }
+                }
+            }
+            None
+        } else {
+            if self.operation_ix == self.max_operations {
+                return None;
+            }
+            self.operation_ix += 1;
+            let operation = T::generate_operation(
+                client,
+                &mut self.rng,
+                self.users
+                    .iter_mut()
+                    .find(|user| user.user_id == current_user_id)
+                    .unwrap(),
+                cx,
+            );
+            let applied = Arc::new(AtomicBool::new(false));
+            self.stored_operations.push((
+                StoredOperation::Client {
+                    user_id: current_user_id,
+                    batch_id: current_batch_id,
+                    operation: operation.clone(),
+                },
+                applied.clone(),
+            ));
+            Some((operation, applied))
+        }
+    }
+
+    fn generate_server_operation(
+        &mut self,
+        clients: &[(Rc<TestClient>, TestAppContext)],
+    ) -> Option<ServerOperation> {
+        if self.operation_ix == self.max_operations {
+            return None;
+        }
+
+        Some(loop {
+            break match self.rng.gen_range(0..100) {
+                0..=29 if clients.len() < self.users.len() => {
+                    let user = self
+                        .users
+                        .iter()
+                        .filter(|u| !u.online)
+                        .choose(&mut self.rng)
+                        .unwrap();
+                    self.operation_ix += 1;
+                    ServerOperation::AddConnection {
+                        user_id: user.user_id,
+                    }
+                }
+                30..=34 if clients.len() > 1 && self.allow_client_disconnection => {
+                    let (client, cx) = &clients[self.rng.gen_range(0..clients.len())];
+                    let user_id = client.current_user_id(cx);
+                    self.operation_ix += 1;
+                    ServerOperation::RemoveConnection { user_id }
+                }
+                35..=39 if clients.len() > 1 && self.allow_client_reconnection => {
+                    let (client, cx) = &clients[self.rng.gen_range(0..clients.len())];
+                    let user_id = client.current_user_id(cx);
+                    self.operation_ix += 1;
+                    ServerOperation::BounceConnection { user_id }
+                }
+                40..=44 if self.allow_server_restarts && clients.len() > 1 => {
+                    self.operation_ix += 1;
+                    ServerOperation::RestartServer
+                }
+                _ if !clients.is_empty() => {
+                    let count = self
+                        .rng
+                        .gen_range(1..10)
+                        .min(self.max_operations - self.operation_ix);
+                    let batch_id = util::post_inc(&mut self.next_batch_id);
+                    let mut user_ids = (0..count)
+                        .map(|_| {
+                            let ix = self.rng.gen_range(0..clients.len());
+                            let (client, cx) = &clients[ix];
+                            client.current_user_id(cx)
+                        })
+                        .collect::<Vec<_>>();
+                    user_ids.sort_unstable();
+                    ServerOperation::MutateClients {
+                        user_ids,
+                        batch_id,
+                        quiesce: self.rng.gen_bool(0.7),
+                    }
+                }
+                _ => continue,
+            };
+        })
+    }
+
+    async fn apply_server_operation(
+        plan: Arc<Mutex<Self>>,
+        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>>,
+        operation: ServerOperation,
+        cx: &mut TestAppContext,
+    ) -> bool {
+        match operation {
+            ServerOperation::AddConnection { user_id } => {
+                let username;
+                {
+                    let mut plan = plan.lock();
+                    let user = plan.user(user_id);
+                    if user.online {
+                        return false;
+                    }
+                    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(Self::simulate_client(
+                    plan.clone(),
+                    client,
+                    operation_rx,
+                    client_cx,
+                )));
+
+                log::info!("added connection for {}", username);
+            }
+
+            ServerOperation::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);
+                plan.lock().user(removed_user_id).online = false;
+                client_cx.update(|cx| {
+                    cx.clear_globals();
+                    drop(client);
+                });
+            }
+
+            ServerOperation::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);
+            }
+
+            ServerOperation::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_server_resource_ids(environment, server.id())
+                    .await
+                    .unwrap();
+                assert_eq!(stale_room_ids, vec![]);
+            }
+
+            ServerOperation::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}");
+                    }
+                }
+
+                if quiesce && applied {
+                    deterministic.run_until_parked();
+                    T::on_quiesce(server, clients).await;
+                }
+
+                return applied;
+            }
+        }
+        true
+    }
+
+    async fn simulate_client(
+        plan: Arc<Mutex<Self>>,
+        client: Rc<TestClient>,
+        mut operation_rx: futures::channel::mpsc::UnboundedReceiver<usize>,
+        mut cx: TestAppContext,
+    ) {
+        T::on_client_added(&client, &mut cx).await;
+
+        while let Some(batch_id) = operation_rx.next().await {
+            let Some((operation, applied)) =
+                plan.lock().next_client_operation(&client, batch_id, &cx)
+            else {
+                break;
+            };
+            applied.store(true, SeqCst);
+            match T::apply_operation(&client, operation, &mut cx).await {
+                Ok(()) => {}
+                Err(TestError::Inapplicable) => {
+                    applied.store(false, SeqCst);
+                    log::info!("skipped operation");
+                }
+                Err(TestError::Other(error)) => {
+                    log::error!("{} error: {}", client.username, error);
+                }
+            }
+            cx.background().simulate_random_delay().await;
+        }
+        log::info!("{}: done", client.username);
+    }
+
+    fn user(&mut self, user_id: UserId) -> &mut UserTestPlan {
+        self.users
+            .iter_mut()
+            .find(|user| user.user_id == user_id)
+            .unwrap()
+    }
+}
+
+impl UserTestPlan {
+    pub fn next_root_dir_name(&mut self) -> String {
+        let user_id = self.user_id;
+        let root_id = util::post_inc(&mut self.next_root_id);
+        format!("dir-{user_id}-{root_id}")
+    }
+}
+
+impl From<anyhow::Error> for TestError {
+    fn from(value: anyhow::Error) -> Self {
+        Self::Other(value)
+    }
+}
+
+fn path_env_var(name: &str) -> Option<PathBuf> {
+    let value = env::var(name).ok()?;
+    let mut path = PathBuf::from(value);
+    if path.is_relative() {
+        let mut abs_path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
+        abs_path.pop();
+        abs_path.pop();
+        abs_path.push(path);
+        path = abs_path
+    }
+    Some(path)
+}

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

@@ -0,0 +1,558 @@
+use crate::{
+    db::{tests::TestDb, NewUserParams, UserId},
+    executor::Executor,
+    rpc::{Server, CLEANUP_TIMEOUT},
+    AppState,
+};
+use anyhow::anyhow;
+use call::ActiveCall;
+use channel::{channel_buffer::ChannelBuffer, ChannelStore};
+use client::{
+    self, proto::PeerId, Client, Connection, Credentials, EstablishConnectionError, UserStore,
+};
+use collections::{HashMap, HashSet};
+use fs::FakeFs;
+use futures::{channel::oneshot, StreamExt as _};
+use gpui::{executor::Deterministic, ModelHandle, Task, TestAppContext, WindowHandle};
+use language::LanguageRegistry;
+use parking_lot::Mutex;
+use project::{Project, WorktreeId};
+use settings::SettingsStore;
+use std::{
+    cell::{Ref, RefCell, RefMut},
+    env,
+    ops::{Deref, DerefMut},
+    path::Path,
+    sync::{
+        atomic::{AtomicBool, AtomicUsize, Ordering::SeqCst},
+        Arc,
+    },
+};
+use util::http::FakeHttpClient;
+use workspace::Workspace;
+
+pub struct TestServer {
+    pub app_state: Arc<AppState>,
+    pub test_live_kit_server: Arc<live_kit_client::TestServer>,
+    server: Arc<Server>,
+    connection_killers: Arc<Mutex<HashMap<PeerId, Arc<AtomicBool>>>>,
+    forbid_connections: Arc<AtomicBool>,
+    _test_db: TestDb,
+}
+
+pub struct TestClient {
+    pub username: String,
+    pub app_state: Arc<workspace::AppState>,
+    state: RefCell<TestClientState>,
+}
+
+#[derive(Default)]
+struct TestClientState {
+    local_projects: Vec<ModelHandle<Project>>,
+    remote_projects: Vec<ModelHandle<Project>>,
+    buffers: HashMap<ModelHandle<Project>, HashSet<ModelHandle<language::Buffer>>>,
+    channel_buffers: HashSet<ModelHandle<ChannelBuffer>>,
+}
+
+pub struct ContactsSummary {
+    pub current: Vec<String>,
+    pub outgoing_requests: Vec<String>,
+    pub incoming_requests: Vec<String>,
+}
+
+impl TestServer {
+    pub async fn start(deterministic: &Arc<Deterministic>) -> Self {
+        static NEXT_LIVE_KIT_SERVER_ID: AtomicUsize = AtomicUsize::new(0);
+
+        let use_postgres = env::var("USE_POSTGRES").ok();
+        let use_postgres = use_postgres.as_deref();
+        let test_db = if use_postgres == Some("true") || use_postgres == Some("1") {
+            TestDb::postgres(deterministic.build_background())
+        } else {
+            TestDb::sqlite(deterministic.build_background())
+        };
+        let live_kit_server_id = NEXT_LIVE_KIT_SERVER_ID.fetch_add(1, SeqCst);
+        let live_kit_server = live_kit_client::TestServer::create(
+            format!("http://livekit.{}.test", live_kit_server_id),
+            format!("devkey-{}", live_kit_server_id),
+            format!("secret-{}", live_kit_server_id),
+            deterministic.build_background(),
+        )
+        .unwrap();
+        let app_state = Self::build_app_state(&test_db, &live_kit_server).await;
+        let epoch = app_state
+            .db
+            .create_server(&app_state.config.zed_environment)
+            .await
+            .unwrap();
+        let server = Server::new(
+            epoch,
+            app_state.clone(),
+            Executor::Deterministic(deterministic.build_background()),
+        );
+        server.start().await.unwrap();
+        // Advance clock to ensure the server's cleanup task is finished.
+        deterministic.advance_clock(CLEANUP_TIMEOUT);
+        Self {
+            app_state,
+            server,
+            connection_killers: Default::default(),
+            forbid_connections: Default::default(),
+            _test_db: test_db,
+            test_live_kit_server: live_kit_server,
+        }
+    }
+
+    pub async fn reset(&self) {
+        self.app_state.db.reset();
+        let epoch = self
+            .app_state
+            .db
+            .create_server(&self.app_state.config.zed_environment)
+            .await
+            .unwrap();
+        self.server.reset(epoch);
+    }
+
+    pub async fn create_client(&mut self, cx: &mut TestAppContext, name: &str) -> TestClient {
+        cx.update(|cx| {
+            if cx.has_global::<SettingsStore>() {
+                panic!("Same cx used to create two test clients")
+            }
+            cx.set_global(SettingsStore::test(cx));
+        });
+
+        let http = FakeHttpClient::with_404_response();
+        let user_id = if let Ok(Some(user)) = self.app_state.db.get_user_by_github_login(name).await
+        {
+            user.id
+        } else {
+            self.app_state
+                .db
+                .create_user(
+                    &format!("{name}@example.com"),
+                    false,
+                    NewUserParams {
+                        github_login: name.into(),
+                        github_user_id: 0,
+                        invite_count: 0,
+                    },
+                )
+                .await
+                .expect("creating user failed")
+                .user_id
+        };
+        let client_name = name.to_string();
+        let mut client = cx.read(|cx| Client::new(http.clone(), cx));
+        let server = self.server.clone();
+        let db = self.app_state.db.clone();
+        let connection_killers = self.connection_killers.clone();
+        let forbid_connections = self.forbid_connections.clone();
+
+        Arc::get_mut(&mut client)
+            .unwrap()
+            .set_id(user_id.0 as usize)
+            .override_authenticate(move |cx| {
+                cx.spawn(|_| async move {
+                    let access_token = "the-token".to_string();
+                    Ok(Credentials {
+                        user_id: user_id.0 as u64,
+                        access_token,
+                    })
+                })
+            })
+            .override_establish_connection(move |credentials, cx| {
+                assert_eq!(credentials.user_id, user_id.0 as u64);
+                assert_eq!(credentials.access_token, "the-token");
+
+                let server = server.clone();
+                let db = db.clone();
+                let connection_killers = connection_killers.clone();
+                let forbid_connections = forbid_connections.clone();
+                let client_name = client_name.clone();
+                cx.spawn(move |cx| async move {
+                    if forbid_connections.load(SeqCst) {
+                        Err(EstablishConnectionError::other(anyhow!(
+                            "server is forbidding connections"
+                        )))
+                    } else {
+                        let (client_conn, server_conn, killed) =
+                            Connection::in_memory(cx.background());
+                        let (connection_id_tx, connection_id_rx) = oneshot::channel();
+                        let user = db
+                            .get_user_by_id(user_id)
+                            .await
+                            .expect("retrieving user failed")
+                            .unwrap();
+                        cx.background()
+                            .spawn(server.handle_connection(
+                                server_conn,
+                                client_name,
+                                user,
+                                Some(connection_id_tx),
+                                Executor::Deterministic(cx.background()),
+                            ))
+                            .detach();
+                        let connection_id = connection_id_rx.await.unwrap();
+                        connection_killers
+                            .lock()
+                            .insert(connection_id.into(), killed);
+                        Ok(client_conn)
+                    }
+                })
+            });
+
+        let fs = FakeFs::new(cx.background());
+        let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http, cx));
+        let channel_store =
+            cx.add_model(|cx| ChannelStore::new(client.clone(), user_store.clone(), cx));
+        let app_state = Arc::new(workspace::AppState {
+            client: client.clone(),
+            user_store: user_store.clone(),
+            channel_store: channel_store.clone(),
+            languages: Arc::new(LanguageRegistry::test()),
+            fs: fs.clone(),
+            build_window_options: |_, _, _| Default::default(),
+            initialize_workspace: |_, _, _, _| Task::ready(Ok(())),
+            background_actions: || &[],
+        });
+
+        cx.update(|cx| {
+            theme::init((), cx);
+            Project::init(&client, cx);
+            client::init(&client, cx);
+            language::init(cx);
+            editor::init_settings(cx);
+            workspace::init(app_state.clone(), cx);
+            audio::init((), cx);
+            call::init(client.clone(), user_store.clone(), cx);
+            channel::init(&client);
+        });
+
+        client
+            .authenticate_and_connect(false, &cx.to_async())
+            .await
+            .unwrap();
+
+        let client = TestClient {
+            app_state,
+            username: name.to_string(),
+            state: Default::default(),
+        };
+        client.wait_for_current_user(cx).await;
+        client
+    }
+
+    pub fn disconnect_client(&self, peer_id: PeerId) {
+        self.connection_killers
+            .lock()
+            .remove(&peer_id)
+            .unwrap()
+            .store(true, SeqCst);
+    }
+
+    pub fn forbid_connections(&self) {
+        self.forbid_connections.store(true, SeqCst);
+    }
+
+    pub fn allow_connections(&self) {
+        self.forbid_connections.store(false, SeqCst);
+    }
+
+    pub async fn make_contacts(&self, clients: &mut [(&TestClient, &mut TestAppContext)]) {
+        for ix in 1..clients.len() {
+            let (left, right) = clients.split_at_mut(ix);
+            let (client_a, cx_a) = left.last_mut().unwrap();
+            for (client_b, cx_b) in right {
+                client_a
+                    .app_state
+                    .user_store
+                    .update(*cx_a, |store, cx| {
+                        store.request_contact(client_b.user_id().unwrap(), cx)
+                    })
+                    .await
+                    .unwrap();
+                cx_a.foreground().run_until_parked();
+                client_b
+                    .app_state
+                    .user_store
+                    .update(*cx_b, |store, cx| {
+                        store.respond_to_contact_request(client_a.user_id().unwrap(), true, cx)
+                    })
+                    .await
+                    .unwrap();
+            }
+        }
+    }
+
+    pub async fn make_channel(
+        &self,
+        channel: &str,
+        admin: (&TestClient, &mut TestAppContext),
+        members: &mut [(&TestClient, &mut TestAppContext)],
+    ) -> u64 {
+        let (admin_client, admin_cx) = admin;
+        let channel_id = admin_client
+            .app_state
+            .channel_store
+            .update(admin_cx, |channel_store, cx| {
+                channel_store.create_channel(channel, None, cx)
+            })
+            .await
+            .unwrap();
+
+        for (member_client, member_cx) in members {
+            admin_client
+                .app_state
+                .channel_store
+                .update(admin_cx, |channel_store, cx| {
+                    channel_store.invite_member(
+                        channel_id,
+                        member_client.user_id().unwrap(),
+                        false,
+                        cx,
+                    )
+                })
+                .await
+                .unwrap();
+
+            admin_cx.foreground().run_until_parked();
+
+            member_client
+                .app_state
+                .channel_store
+                .update(*member_cx, |channels, _| {
+                    channels.respond_to_channel_invite(channel_id, true)
+                })
+                .await
+                .unwrap();
+        }
+
+        channel_id
+    }
+
+    pub async fn create_room(&self, clients: &mut [(&TestClient, &mut TestAppContext)]) {
+        self.make_contacts(clients).await;
+
+        let (left, right) = clients.split_at_mut(1);
+        let (_client_a, cx_a) = &mut left[0];
+        let active_call_a = cx_a.read(ActiveCall::global);
+
+        for (client_b, cx_b) in right {
+            let user_id_b = client_b.current_user_id(*cx_b).to_proto();
+            active_call_a
+                .update(*cx_a, |call, cx| call.invite(user_id_b, None, cx))
+                .await
+                .unwrap();
+
+            cx_b.foreground().run_until_parked();
+            let active_call_b = cx_b.read(ActiveCall::global);
+            active_call_b
+                .update(*cx_b, |call, cx| call.accept_incoming(cx))
+                .await
+                .unwrap();
+        }
+    }
+
+    pub async fn build_app_state(
+        test_db: &TestDb,
+        fake_server: &live_kit_client::TestServer,
+    ) -> Arc<AppState> {
+        Arc::new(AppState {
+            db: test_db.db().clone(),
+            live_kit_client: Some(Arc::new(fake_server.create_api_client())),
+            config: Default::default(),
+        })
+    }
+}
+
+impl Deref for TestServer {
+    type Target = Server;
+
+    fn deref(&self) -> &Self::Target {
+        &self.server
+    }
+}
+
+impl Drop for TestServer {
+    fn drop(&mut self) {
+        self.server.teardown();
+        self.test_live_kit_server.teardown().unwrap();
+    }
+}
+
+impl Deref for TestClient {
+    type Target = Arc<Client>;
+
+    fn deref(&self) -> &Self::Target {
+        &self.app_state.client
+    }
+}
+
+impl TestClient {
+    pub fn fs(&self) -> &FakeFs {
+        self.app_state.fs.as_fake()
+    }
+
+    pub fn channel_store(&self) -> &ModelHandle<ChannelStore> {
+        &self.app_state.channel_store
+    }
+
+    pub fn user_store(&self) -> &ModelHandle<UserStore> {
+        &self.app_state.user_store
+    }
+
+    pub fn language_registry(&self) -> &Arc<LanguageRegistry> {
+        &self.app_state.languages
+    }
+
+    pub fn client(&self) -> &Arc<Client> {
+        &self.app_state.client
+    }
+
+    pub fn current_user_id(&self, cx: &TestAppContext) -> UserId {
+        UserId::from_proto(
+            self.app_state
+                .user_store
+                .read_with(cx, |user_store, _| user_store.current_user().unwrap().id),
+        )
+    }
+
+    pub async fn wait_for_current_user(&self, cx: &TestAppContext) {
+        let mut authed_user = self
+            .app_state
+            .user_store
+            .read_with(cx, |user_store, _| user_store.watch_current_user());
+        while authed_user.next().await.unwrap().is_none() {}
+    }
+
+    pub async fn clear_contacts(&self, cx: &mut TestAppContext) {
+        self.app_state
+            .user_store
+            .update(cx, |store, _| store.clear_contacts())
+            .await;
+    }
+
+    pub fn local_projects<'a>(&'a self) -> impl Deref<Target = Vec<ModelHandle<Project>>> + 'a {
+        Ref::map(self.state.borrow(), |state| &state.local_projects)
+    }
+
+    pub fn remote_projects<'a>(&'a self) -> impl Deref<Target = Vec<ModelHandle<Project>>> + 'a {
+        Ref::map(self.state.borrow(), |state| &state.remote_projects)
+    }
+
+    pub 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)
+    }
+
+    pub 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)
+    }
+
+    pub 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()
+        })
+    }
+
+    pub 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)
+    }
+
+    pub fn channel_buffers<'a>(
+        &'a self,
+    ) -> impl DerefMut<Target = HashSet<ModelHandle<ChannelBuffer>>> + 'a {
+        RefMut::map(self.state.borrow_mut(), |state| &mut state.channel_buffers)
+    }
+
+    pub fn summarize_contacts(&self, cx: &TestAppContext) -> ContactsSummary {
+        self.app_state
+            .user_store
+            .read_with(cx, |store, _| ContactsSummary {
+                current: store
+                    .contacts()
+                    .iter()
+                    .map(|contact| contact.user.github_login.clone())
+                    .collect(),
+                outgoing_requests: store
+                    .outgoing_contact_requests()
+                    .iter()
+                    .map(|user| user.github_login.clone())
+                    .collect(),
+                incoming_requests: store
+                    .incoming_contact_requests()
+                    .iter()
+                    .map(|user| user.github_login.clone())
+                    .collect(),
+            })
+    }
+
+    pub async fn build_local_project(
+        &self,
+        root_path: impl AsRef<Path>,
+        cx: &mut TestAppContext,
+    ) -> (ModelHandle<Project>, WorktreeId) {
+        let project = cx.update(|cx| {
+            Project::local(
+                self.client().clone(),
+                self.app_state.user_store.clone(),
+                self.app_state.languages.clone(),
+                self.app_state.fs.clone(),
+                cx,
+            )
+        });
+        let (worktree, _) = project
+            .update(cx, |p, cx| {
+                p.find_or_create_local_worktree(root_path, true, cx)
+            })
+            .await
+            .unwrap();
+        worktree
+            .read_with(cx, |tree, _| tree.as_local().unwrap().scan_complete())
+            .await;
+        (project, worktree.read_with(cx, |tree, _| tree.id()))
+    }
+
+    pub async fn build_remote_project(
+        &self,
+        host_project_id: u64,
+        guest_cx: &mut TestAppContext,
+    ) -> ModelHandle<Project> {
+        let active_call = guest_cx.read(ActiveCall::global);
+        let room = active_call.read_with(guest_cx, |call, _| call.room().unwrap().clone());
+        room.update(guest_cx, |room, cx| {
+            room.join_project(
+                host_project_id,
+                self.app_state.languages.clone(),
+                self.app_state.fs.clone(),
+                cx,
+            )
+        })
+        .await
+        .unwrap()
+    }
+
+    pub fn build_workspace(
+        &self,
+        project: &ModelHandle<Project>,
+        cx: &mut TestAppContext,
+    ) -> WindowHandle<Workspace> {
+        cx.add_window(|cx| Workspace::new(0, project.clone(), self.app_state.clone(), cx))
+    }
+}
+
+impl Drop for TestClient {
+    fn drop(&mut self) {
+        self.app_state.client.teardown();
+    }
+}

crates/collab_ui/src/collab_panel.rs 🔗

@@ -2240,7 +2240,8 @@ impl CollabPanel {
     fn open_channel_buffer(&mut self, action: &OpenChannelBuffer, cx: &mut ViewContext<Self>) {
         if let Some(workspace) = self.workspace.upgrade(cx) {
             let pane = workspace.read(cx).active_pane().clone();
-            let channel_view = ChannelView::open(action.channel_id, pane.clone(), workspace, cx);
+            let channel_id = action.channel_id;
+            let channel_view = ChannelView::open(channel_id, pane.clone(), workspace, cx);
             cx.spawn(|_, mut cx| async move {
                 let channel_view = channel_view.await?;
                 pane.update(&mut cx, |pane, cx| {
@@ -2249,6 +2250,18 @@ impl CollabPanel {
                 anyhow::Ok(())
             })
             .detach();
+            let room_id = ActiveCall::global(cx)
+                .read(cx)
+                .room()
+                .map(|room| room.read(cx).id());
+
+            ActiveCall::report_call_event_for_room(
+                "open channel notes",
+                room_id,
+                Some(channel_id),
+                &self.client,
+                cx,
+            );
         }
     }
 

crates/collab_ui/src/collab_ui.rs 🔗

@@ -49,7 +49,7 @@ pub fn toggle_screen_sharing(_: &ToggleScreenSharing, cx: &mut AppContext) {
             if room.is_screen_sharing() {
                 ActiveCall::report_call_event_for_room(
                     "disable screen share",
-                    room.id(),
+                    Some(room.id()),
                     room.channel_id(),
                     &client,
                     cx,
@@ -58,7 +58,7 @@ pub fn toggle_screen_sharing(_: &ToggleScreenSharing, cx: &mut AppContext) {
             } else {
                 ActiveCall::report_call_event_for_room(
                     "enable screen share",
-                    room.id(),
+                    Some(room.id()),
                     room.channel_id(),
                     &client,
                     cx,
@@ -78,7 +78,7 @@ pub fn toggle_mute(_: &ToggleMute, cx: &mut AppContext) {
             if room.is_muted(cx) {
                 ActiveCall::report_call_event_for_room(
                     "enable microphone",
-                    room.id(),
+                    Some(room.id()),
                     room.channel_id(),
                     &client,
                     cx,
@@ -86,7 +86,7 @@ pub fn toggle_mute(_: &ToggleMute, cx: &mut AppContext) {
             } else {
                 ActiveCall::report_call_event_for_room(
                     "disable microphone",
-                    room.id(),
+                    Some(room.id()),
                     room.channel_id(),
                     &client,
                     cx,

crates/copilot/src/copilot.rs 🔗

@@ -41,7 +41,7 @@ actions!(
     [Suggest, NextSuggestion, PreviousSuggestion, Reinstall]
 );
 
-pub fn init(http: Arc<dyn HttpClient>, node_runtime: Arc<NodeRuntime>, cx: &mut AppContext) {
+pub fn init(http: Arc<dyn HttpClient>, node_runtime: Arc<dyn NodeRuntime>, cx: &mut AppContext) {
     let copilot = cx.add_model({
         let node_runtime = node_runtime.clone();
         move |cx| Copilot::start(http, node_runtime, cx)
@@ -265,7 +265,7 @@ pub struct Completion {
 
 pub struct Copilot {
     http: Arc<dyn HttpClient>,
-    node_runtime: Arc<NodeRuntime>,
+    node_runtime: Arc<dyn NodeRuntime>,
     server: CopilotServer,
     buffers: HashSet<WeakModelHandle<Buffer>>,
 }
@@ -299,7 +299,7 @@ impl Copilot {
 
     fn start(
         http: Arc<dyn HttpClient>,
-        node_runtime: Arc<NodeRuntime>,
+        node_runtime: Arc<dyn NodeRuntime>,
         cx: &mut ModelContext<Self>,
     ) -> Self {
         let mut this = Self {
@@ -335,12 +335,15 @@ impl Copilot {
 
     #[cfg(any(test, feature = "test-support"))]
     pub fn fake(cx: &mut gpui::TestAppContext) -> (ModelHandle<Self>, lsp::FakeLanguageServer) {
+        use node_runtime::FakeNodeRuntime;
+
         let (server, fake_server) =
             LanguageServer::fake("copilot".into(), Default::default(), cx.to_async());
         let http = util::http::FakeHttpClient::create(|_| async { unreachable!() });
+        let node_runtime = FakeNodeRuntime::new();
         let this = cx.add_model(|_| Self {
             http: http.clone(),
-            node_runtime: NodeRuntime::instance(http),
+            node_runtime,
             server: CopilotServer::Running(RunningCopilotServer {
                 lsp: Arc::new(server),
                 sign_in_status: SignInStatus::Authorized,
@@ -353,7 +356,7 @@ impl Copilot {
 
     fn start_language_server(
         http: Arc<dyn HttpClient>,
-        node_runtime: Arc<NodeRuntime>,
+        node_runtime: Arc<dyn NodeRuntime>,
         this: ModelHandle<Self>,
         mut cx: AsyncAppContext,
     ) -> impl Future<Output = ()> {

crates/editor/src/editor.rs 🔗

@@ -312,6 +312,10 @@ actions!(
         CopyPath,
         CopyRelativePath,
         CopyHighlightJson,
+        ContextMenuFirst,
+        ContextMenuPrev,
+        ContextMenuNext,
+        ContextMenuLast,
     ]
 );
 
@@ -468,6 +472,10 @@ pub fn init(cx: &mut AppContext) {
     cx.add_action(Editor::next_copilot_suggestion);
     cx.add_action(Editor::previous_copilot_suggestion);
     cx.add_action(Editor::copilot_suggest);
+    cx.add_action(Editor::context_menu_first);
+    cx.add_action(Editor::context_menu_prev);
+    cx.add_action(Editor::context_menu_next);
+    cx.add_action(Editor::context_menu_last);
 
     hover_popover::init(cx);
     scroll::actions::init(cx);
@@ -1654,7 +1662,7 @@ impl Editor {
             .excerpt_containing(self.selections.newest_anchor().head(), cx)
     }
 
-    fn style(&self, cx: &AppContext) -> EditorStyle {
+    pub fn style(&self, cx: &AppContext) -> EditorStyle {
         build_style(
             settings::get::<ThemeSettings>(cx),
             self.get_field_editor_theme.as_deref(),
@@ -5166,12 +5174,6 @@ impl Editor {
             return;
         }
 
-        if let Some(context_menu) = self.context_menu.as_mut() {
-            if context_menu.select_prev(cx) {
-                return;
-            }
-        }
-
         if matches!(self.mode, EditorMode::SingleLine) {
             cx.propagate_action();
             return;
@@ -5194,15 +5196,6 @@ impl Editor {
             return;
         }
 
-        if self
-            .context_menu
-            .as_mut()
-            .map(|menu| menu.select_first(cx))
-            .unwrap_or(false)
-        {
-            return;
-        }
-
         if matches!(self.mode, EditorMode::SingleLine) {
             cx.propagate_action();
             return;
@@ -5242,12 +5235,6 @@ impl Editor {
     pub fn move_down(&mut self, _: &MoveDown, cx: &mut ViewContext<Self>) {
         self.take_rename(true, cx);
 
-        if let Some(context_menu) = self.context_menu.as_mut() {
-            if context_menu.select_next(cx) {
-                return;
-            }
-        }
-
         if self.mode == EditorMode::SingleLine {
             cx.propagate_action();
             return;
@@ -5315,6 +5302,30 @@ impl Editor {
         });
     }
 
+    pub fn context_menu_first(&mut self, _: &ContextMenuFirst, cx: &mut ViewContext<Self>) {
+        if let Some(context_menu) = self.context_menu.as_mut() {
+            context_menu.select_first(cx);
+        }
+    }
+
+    pub fn context_menu_prev(&mut self, _: &ContextMenuPrev, cx: &mut ViewContext<Self>) {
+        if let Some(context_menu) = self.context_menu.as_mut() {
+            context_menu.select_prev(cx);
+        }
+    }
+
+    pub fn context_menu_next(&mut self, _: &ContextMenuNext, cx: &mut ViewContext<Self>) {
+        if let Some(context_menu) = self.context_menu.as_mut() {
+            context_menu.select_next(cx);
+        }
+    }
+
+    pub fn context_menu_last(&mut self, _: &ContextMenuLast, cx: &mut ViewContext<Self>) {
+        if let Some(context_menu) = self.context_menu.as_mut() {
+            context_menu.select_last(cx);
+        }
+    }
+
     pub fn move_to_previous_word_start(
         &mut self,
         _: &MoveToPreviousWordStart,
@@ -8666,17 +8677,20 @@ impl View for Editor {
         if self.pending_rename.is_some() {
             keymap.add_identifier("renaming");
         }
-        match self.context_menu.as_ref() {
-            Some(ContextMenu::Completions(_)) => {
-                keymap.add_identifier("menu");
-                keymap.add_identifier("showing_completions")
-            }
-            Some(ContextMenu::CodeActions(_)) => {
-                keymap.add_identifier("menu");
-                keymap.add_identifier("showing_code_actions")
+        if self.context_menu_visible() {
+            match self.context_menu.as_ref() {
+                Some(ContextMenu::Completions(_)) => {
+                    keymap.add_identifier("menu");
+                    keymap.add_identifier("showing_completions")
+                }
+                Some(ContextMenu::CodeActions(_)) => {
+                    keymap.add_identifier("menu");
+                    keymap.add_identifier("showing_code_actions")
+                }
+                None => {}
             }
-            None => {}
         }
+
         for layer in self.keymap_context_layers.values() {
             keymap.extend(layer);
         }

crates/editor/src/editor_tests.rs 🔗

@@ -5340,7 +5340,7 @@ async fn test_completion(cx: &mut gpui::TestAppContext) {
     cx.condition(|editor, _| editor.context_menu_visible())
         .await;
     let apply_additional_edits = cx.update_editor(|editor, cx| {
-        editor.move_down(&MoveDown, cx);
+        editor.context_menu_next(&Default::default(), cx);
         editor
             .confirm_completion(&ConfirmCompletion::default(), cx)
             .unwrap()

crates/editor/src/movement.rs 🔗

@@ -1,8 +1,14 @@
 use super::{Bias, DisplayPoint, DisplaySnapshot, SelectionGoal, ToDisplayPoint};
-use crate::{char_kind, CharKind, ToPoint};
+use crate::{char_kind, CharKind, ToOffset, ToPoint};
 use language::Point;
 use std::ops::Range;
 
+#[derive(Debug, PartialEq)]
+pub enum FindRange {
+    SingleLine,
+    MultiLine,
+}
+
 pub fn left(map: &DisplaySnapshot, mut point: DisplayPoint) -> DisplayPoint {
     if point.column() > 0 {
         *point.column_mut() -= 1;
@@ -179,7 +185,7 @@ pub fn previous_word_start(map: &DisplaySnapshot, point: DisplayPoint) -> Displa
     let raw_point = point.to_point(map);
     let scope = map.buffer_snapshot.language_scope_at(raw_point);
 
-    find_preceding_boundary(map, point, |left, right| {
+    find_preceding_boundary(map, point, FindRange::MultiLine, |left, right| {
         (char_kind(&scope, left) != char_kind(&scope, right) && !right.is_whitespace())
             || left == '\n'
     })
@@ -188,7 +194,8 @@ pub fn previous_word_start(map: &DisplaySnapshot, point: DisplayPoint) -> Displa
 pub fn previous_subword_start(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint {
     let raw_point = point.to_point(map);
     let scope = map.buffer_snapshot.language_scope_at(raw_point);
-    find_preceding_boundary(map, point, |left, right| {
+
+    find_preceding_boundary(map, point, FindRange::MultiLine, |left, right| {
         let is_word_start =
             char_kind(&scope, left) != char_kind(&scope, right) && !right.is_whitespace();
         let is_subword_start =
@@ -200,7 +207,8 @@ pub fn previous_subword_start(map: &DisplaySnapshot, point: DisplayPoint) -> Dis
 pub fn next_word_end(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint {
     let raw_point = point.to_point(map);
     let scope = map.buffer_snapshot.language_scope_at(raw_point);
-    find_boundary(map, point, |left, right| {
+
+    find_boundary(map, point, FindRange::MultiLine, |left, right| {
         (char_kind(&scope, left) != char_kind(&scope, right) && !left.is_whitespace())
             || right == '\n'
     })
@@ -209,7 +217,8 @@ pub fn next_word_end(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint
 pub fn next_subword_end(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint {
     let raw_point = point.to_point(map);
     let scope = map.buffer_snapshot.language_scope_at(raw_point);
-    find_boundary(map, point, |left, right| {
+
+    find_boundary(map, point, FindRange::MultiLine, |left, right| {
         let is_word_end =
             (char_kind(&scope, left) != char_kind(&scope, right)) && !left.is_whitespace();
         let is_subword_end =
@@ -272,79 +281,34 @@ pub fn end_of_paragraph(
     map.max_point()
 }
 
-/// Scans for a boundary preceding the given start point `from` until a boundary is found, indicated by the
-/// given predicate returning true. The predicate is called with the character to the left and right
-/// of the candidate boundary location, and will be called with `\n` characters indicating the start
-/// or end of a line.
+/// Scans for a boundary preceding the given start point `from` until a boundary is found,
+/// indicated by the given predicate returning true.
+/// The predicate is called with the character to the left and right of the candidate boundary location.
+/// If FindRange::SingleLine is specified and no boundary is found before the start of the current line, the start of the current line will be returned.
 pub fn find_preceding_boundary(
     map: &DisplaySnapshot,
     from: DisplayPoint,
+    find_range: FindRange,
     mut is_boundary: impl FnMut(char, char) -> bool,
 ) -> DisplayPoint {
-    let mut start_column = 0;
-    let mut soft_wrap_row = from.row() + 1;
-
-    let mut prev = None;
-    for (ch, point) in map.reverse_chars_at(from) {
-        // Recompute soft_wrap_indent if the row has changed
-        if point.row() != soft_wrap_row {
-            soft_wrap_row = point.row();
-
-            if point.row() == 0 {
-                start_column = 0;
-            } else if let Some(indent) = map.soft_wrap_indent(point.row() - 1) {
-                start_column = indent;
-            }
-        }
-
-        // If the current point is in the soft_wrap, skip comparing it
-        if point.column() < start_column {
-            continue;
-        }
-
-        if let Some((prev_ch, prev_point)) = prev {
-            if is_boundary(ch, prev_ch) {
-                return map.clip_point(prev_point, Bias::Left);
-            }
-        }
-
-        prev = Some((ch, point));
-    }
-    map.clip_point(DisplayPoint::zero(), Bias::Left)
-}
+    let mut prev_ch = None;
+    let mut offset = from.to_point(map).to_offset(&map.buffer_snapshot);
 
-/// Scans for a boundary preceding the given start point `from` until a boundary is found, indicated by the
-/// given predicate returning true. The predicate is called with the character to the left and right
-/// of the candidate boundary location, and will be called with `\n` characters indicating the start
-/// or end of a line. If no boundary is found, the start of the line is returned.
-pub fn find_preceding_boundary_in_line(
-    map: &DisplaySnapshot,
-    from: DisplayPoint,
-    mut is_boundary: impl FnMut(char, char) -> bool,
-) -> DisplayPoint {
-    let mut start_column = 0;
-    if from.row() > 0 {
-        if let Some(indent) = map.soft_wrap_indent(from.row() - 1) {
-            start_column = indent;
+    for ch in map.buffer_snapshot.reversed_chars_at(offset) {
+        if find_range == FindRange::SingleLine && ch == '\n' {
+            break;
         }
-    }
-
-    let mut prev = None;
-    for (ch, point) in map.reverse_chars_at(from) {
-        if let Some((prev_ch, prev_point)) = prev {
+        if let Some(prev_ch) = prev_ch {
             if is_boundary(ch, prev_ch) {
-                return map.clip_point(prev_point, Bias::Left);
+                break;
             }
         }
 
-        if ch == '\n' || point.column() < start_column {
-            break;
-        }
-
-        prev = Some((ch, point));
+        offset -= ch.len_utf8();
+        prev_ch = Some(ch);
     }
 
-    map.clip_point(prev.map(|(_, point)| point).unwrap_or(from), Bias::Left)
+    map.clip_point(offset.to_display_point(map), Bias::Left)
 }
 
 /// Scans for a boundary following the given start point until a boundary is found, indicated by the
@@ -354,47 +318,26 @@ pub fn find_preceding_boundary_in_line(
 pub fn find_boundary(
     map: &DisplaySnapshot,
     from: DisplayPoint,
+    find_range: FindRange,
     mut is_boundary: impl FnMut(char, char) -> bool,
 ) -> DisplayPoint {
+    let mut offset = from.to_offset(&map, Bias::Right);
     let mut prev_ch = None;
-    for (ch, point) in map.chars_at(from) {
+
+    for ch in map.buffer_snapshot.chars_at(offset) {
+        if find_range == FindRange::SingleLine && ch == '\n' {
+            break;
+        }
         if let Some(prev_ch) = prev_ch {
             if is_boundary(prev_ch, ch) {
-                return map.clip_point(point, Bias::Right);
+                break;
             }
         }
 
+        offset += ch.len_utf8();
         prev_ch = Some(ch);
     }
-    map.clip_point(map.max_point(), Bias::Right)
-}
-
-/// Scans for a boundary following the given start point until a boundary is found, indicated by the
-/// given predicate returning true. The predicate is called with the character to the left and right
-/// of the candidate boundary location, and will be called with `\n` characters indicating the start
-/// or end of a line. If no boundary is found, the end of the line is returned
-pub fn find_boundary_in_line(
-    map: &DisplaySnapshot,
-    from: DisplayPoint,
-    mut is_boundary: impl FnMut(char, char) -> bool,
-) -> DisplayPoint {
-    let mut prev = None;
-    for (ch, point) in map.chars_at(from) {
-        if let Some((prev_ch, _)) = prev {
-            if is_boundary(prev_ch, ch) {
-                return map.clip_point(point, Bias::Right);
-            }
-        }
-
-        prev = Some((ch, point));
-
-        if ch == '\n' {
-            break;
-        }
-    }
-
-    // Return the last position checked so that we give a point right before the newline or eof.
-    map.clip_point(prev.map(|(_, point)| point).unwrap_or(from), Bias::Right)
+    map.clip_point(offset.to_display_point(map), Bias::Right)
 }
 
 pub fn is_inside_word(map: &DisplaySnapshot, point: DisplayPoint) -> bool {
@@ -533,7 +476,12 @@ mod tests {
         ) {
             let (snapshot, display_points) = marked_display_snapshot(marked_text, cx);
             assert_eq!(
-                find_preceding_boundary(&snapshot, display_points[1], is_boundary),
+                find_preceding_boundary(
+                    &snapshot,
+                    display_points[1],
+                    FindRange::MultiLine,
+                    is_boundary
+                ),
                 display_points[0]
             );
         }
@@ -612,21 +560,15 @@ mod tests {
             find_preceding_boundary(
                 &snapshot,
                 buffer_snapshot.len().to_display_point(&snapshot),
-                |left, _| left == 'a',
+                FindRange::MultiLine,
+                |left, _| left == 'e',
             ),
-            0.to_display_point(&snapshot),
+            snapshot
+                .buffer_snapshot
+                .offset_to_point(5)
+                .to_display_point(&snapshot),
             "Should not stop at inlays when looking for boundaries"
         );
-
-        assert_eq!(
-            find_preceding_boundary_in_line(
-                &snapshot,
-                buffer_snapshot.len().to_display_point(&snapshot),
-                |left, _| left == 'a',
-            ),
-            0.to_display_point(&snapshot),
-            "Should not stop at inlays when looking for boundaries in line"
-        );
     }
 
     #[gpui::test]
@@ -699,7 +641,12 @@ mod tests {
         ) {
             let (snapshot, display_points) = marked_display_snapshot(marked_text, cx);
             assert_eq!(
-                find_boundary(&snapshot, display_points[0], is_boundary),
+                find_boundary(
+                    &snapshot,
+                    display_points[0],
+                    FindRange::MultiLine,
+                    is_boundary
+                ),
                 display_points[1]
             );
         }

crates/editor/src/scroll.rs 🔗

@@ -378,10 +378,6 @@ impl Editor {
             return;
         }
 
-        if amount.move_context_menu_selection(self, cx) {
-            return;
-        }
-
         let cur_position = self.scroll_position(cx);
         let new_pos = cur_position + vec2f(0., amount.lines(self));
         self.set_scroll_position(new_pos, cx);

crates/editor/src/scroll/scroll_amount.rs 🔗

@@ -1,8 +1,5 @@
-use gpui::ViewContext;
-use serde::Deserialize;
-use util::iife;
-
 use crate::Editor;
+use serde::Deserialize;
 
 #[derive(Clone, PartialEq, Deserialize)]
 pub enum ScrollAmount {
@@ -13,25 +10,6 @@ pub enum ScrollAmount {
 }
 
 impl ScrollAmount {
-    pub fn move_context_menu_selection(
-        &self,
-        editor: &mut Editor,
-        cx: &mut ViewContext<Editor>,
-    ) -> bool {
-        iife!({
-            let context_menu = editor.context_menu.as_mut()?;
-
-            match self {
-                Self::Line(c) if *c > 0. => context_menu.select_next(cx),
-                Self::Line(_) => context_menu.select_prev(cx),
-                Self::Page(c) if *c > 0. => context_menu.select_last(cx),
-                Self::Page(_) => context_menu.select_first(cx),
-            }
-            .then_some(())
-        })
-        .is_some()
-    }
-
     pub fn lines(&self, editor: &mut Editor) -> f32 {
         match self {
             Self::Line(count) => *count,
@@ -39,7 +17,7 @@ impl ScrollAmount {
                 .visible_line_count()
                 // subtract one to leave an anchor line
                 // round towards zero (so page-up and page-down are symmetric)
-                .map(|l| ((l - 1.) * count).trunc())
+                .map(|l| (l * count).trunc() - count.signum())
                 .unwrap_or(0.),
         }
     }

crates/gpui/src/executor.rs 🔗

@@ -106,6 +106,7 @@ pub struct Deterministic {
     parker: parking_lot::Mutex<parking::Parker>,
 }
 
+#[must_use]
 pub enum Timer {
     Production(smol::Timer),
     #[cfg(any(test, feature = "test-support"))]

crates/gpui_macros/src/gpui_macros.rs 🔗

@@ -37,8 +37,14 @@ pub fn test(args: TokenStream, function: TokenStream) -> TokenStream {
                         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));
+                                let mut path = syn::Path {
+                                    leading_colon: None,
+                                    segments: Default::default(),
+                                };
+                                for part in name.value().split("::") {
+                                    path.segments.push(Ident::new(part, name.span()).into());
+                                }
+                                on_failure_fn_name = quote!(Some(#path));
                             } else {
                                 return Err(TokenStream::from(
                                     syn::Error::new(

crates/language/src/proto.rs 🔗

@@ -127,6 +127,31 @@ pub fn serialize_undo_map_entry(
     }
 }
 
+pub fn split_operations(
+    mut operations: Vec<proto::Operation>,
+) -> impl Iterator<Item = Vec<proto::Operation>> {
+    #[cfg(any(test, feature = "test-support"))]
+    const CHUNK_SIZE: usize = 5;
+
+    #[cfg(not(any(test, feature = "test-support")))]
+    const CHUNK_SIZE: usize = 100;
+
+    let mut done = false;
+    std::iter::from_fn(move || {
+        if done {
+            return None;
+        }
+
+        let operations = operations
+            .drain(..std::cmp::min(CHUNK_SIZE, operations.len()))
+            .collect::<Vec<_>>();
+        if operations.is_empty() {
+            done = true;
+        }
+        Some(operations)
+    })
+}
+
 pub fn serialize_selections(selections: &Arc<[Selection<Anchor>]>) -> Vec<proto::Selection> {
     selections.iter().map(serialize_selection).collect()
 }

crates/language_tools/src/lsp_log.rs 🔗

@@ -570,10 +570,12 @@ impl View for LspLogToolbarItemView {
         let Some(log_view) = self.log_view.as_ref() else {
             return Empty::new().into_any();
         };
-        let log_view = log_view.read(cx);
-        let menu_rows = log_view.menu_items(cx).unwrap_or_default();
+        let (menu_rows, current_server_id) = log_view.update(cx, |log_view, cx| {
+            let menu_rows = log_view.menu_items(cx).unwrap_or_default();
+            let current_server_id = log_view.current_server_id;
+            (menu_rows, current_server_id)
+        });
 
-        let current_server_id = log_view.current_server_id;
         let current_server = current_server_id.and_then(|current_server_id| {
             if let Ok(ix) = menu_rows.binary_search_by_key(&current_server_id, |e| e.server_id) {
                 Some(menu_rows[ix].clone())
@@ -581,10 +583,10 @@ impl View for LspLogToolbarItemView {
                 None
             }
         });
+        let server_selected = current_server.is_some();
 
         enum Menu {}
-
-        Stack::new()
+        let lsp_menu = Stack::new()
             .with_child(Self::render_language_server_menu_header(
                 current_server,
                 &theme,
@@ -631,8 +633,47 @@ impl View for LspLogToolbarItemView {
             })
             .aligned()
             .left()
-            .clipped()
-            .into_any()
+            .clipped();
+
+        enum LspCleanupButton {}
+        let log_cleanup_button =
+            MouseEventHandler::new::<LspCleanupButton, _>(1, cx, |state, cx| {
+                let theme = theme::current(cx).clone();
+                let style = theme
+                    .workspace
+                    .toolbar
+                    .toggleable_text_tool
+                    .in_state(server_selected)
+                    .style_for(state);
+                Label::new("Clear", style.text.clone())
+                    .aligned()
+                    .contained()
+                    .with_style(style.container)
+                    .constrained()
+                    .with_height(theme.toolbar_dropdown_menu.row_height / 6.0 * 5.0)
+            })
+            .on_click(MouseButton::Left, move |_, this, cx| {
+                if let Some(log_view) = this.log_view.as_ref() {
+                    log_view.update(cx, |log_view, cx| {
+                        log_view.editor.update(cx, |editor, cx| {
+                            editor.set_read_only(false);
+                            editor.clear(cx);
+                            editor.set_read_only(true);
+                        });
+                    })
+                }
+            })
+            .with_cursor_style(CursorStyle::PointingHand)
+            .aligned()
+            .right();
+
+        Flex::row()
+            .with_child(lsp_menu)
+            .with_child(log_cleanup_button)
+            .contained()
+            .aligned()
+            .left()
+            .into_any_named("lsp log controls")
     }
 }
 

crates/live_kit_client/LiveKitBridge/Package.resolved 🔗

@@ -42,8 +42,8 @@
         "repositoryURL": "https://github.com/apple/swift-protobuf.git",
         "state": {
           "branch": null,
-          "revision": "ce20dc083ee485524b802669890291c0d8090170",
-          "version": "1.22.1"
+          "revision": "0af9125c4eae12a4973fb66574c53a54962a9e1e",
+          "version": "1.21.0"
         }
       }
     ]

crates/node_runtime/Cargo.toml 🔗

@@ -14,6 +14,7 @@ util = { path = "../util" }
 async-compression = { version = "0.3", features = ["gzip", "futures-bufread"] }
 async-tar = "0.4.2"
 futures.workspace = true
+async-trait.workspace = true
 anyhow.workspace = true
 parking_lot.workspace = true
 serde.workspace = true

crates/node_runtime/src/node_runtime.rs 🔗

@@ -7,14 +7,12 @@ use std::process::{Output, Stdio};
 use std::{
     env::consts,
     path::{Path, PathBuf},
-    sync::{Arc, OnceLock},
+    sync::Arc,
 };
 use util::http::HttpClient;
 
 const VERSION: &str = "v18.15.0";
 
-static RUNTIME_INSTANCE: OnceLock<Arc<NodeRuntime>> = OnceLock::new();
-
 #[derive(Debug, Deserialize)]
 #[serde(rename_all = "kebab-case")]
 pub struct NpmInfo {
@@ -28,23 +26,88 @@ pub struct NpmInfoDistTags {
     latest: Option<String>,
 }
 
-pub struct NodeRuntime {
+#[async_trait::async_trait]
+pub trait NodeRuntime: Send + Sync {
+    async fn binary_path(&self) -> Result<PathBuf>;
+
+    async fn run_npm_subcommand(
+        &self,
+        directory: Option<&Path>,
+        subcommand: &str,
+        args: &[&str],
+    ) -> Result<Output>;
+
+    async fn npm_package_latest_version(&self, name: &str) -> Result<String>;
+
+    async fn npm_install_packages(&self, directory: &Path, packages: &[(&str, &str)])
+        -> Result<()>;
+}
+
+pub struct RealNodeRuntime {
     http: Arc<dyn HttpClient>,
 }
 
-impl NodeRuntime {
-    pub fn instance(http: Arc<dyn HttpClient>) -> Arc<NodeRuntime> {
-        RUNTIME_INSTANCE
-            .get_or_init(|| Arc::new(NodeRuntime { http }))
-            .clone()
+impl RealNodeRuntime {
+    pub fn new(http: Arc<dyn HttpClient>) -> Arc<dyn NodeRuntime> {
+        Arc::new(RealNodeRuntime { http })
+    }
+
+    async fn install_if_needed(&self) -> Result<PathBuf> {
+        log::info!("Node runtime install_if_needed");
+
+        let arch = match consts::ARCH {
+            "x86_64" => "x64",
+            "aarch64" => "arm64",
+            other => bail!("Running on unsupported platform: {other}"),
+        };
+
+        let folder_name = format!("node-{VERSION}-darwin-{arch}");
+        let node_containing_dir = util::paths::SUPPORT_DIR.join("node");
+        let node_dir = node_containing_dir.join(folder_name);
+        let node_binary = node_dir.join("bin/node");
+        let npm_file = node_dir.join("bin/npm");
+
+        let result = Command::new(&node_binary)
+            .arg(npm_file)
+            .arg("--version")
+            .stdin(Stdio::null())
+            .stdout(Stdio::null())
+            .stderr(Stdio::null())
+            .status()
+            .await;
+        let valid = matches!(result, Ok(status) if status.success());
+
+        if !valid {
+            _ = fs::remove_dir_all(&node_containing_dir).await;
+            fs::create_dir(&node_containing_dir)
+                .await
+                .context("error creating node containing dir")?;
+
+            let file_name = format!("node-{VERSION}-darwin-{arch}.tar.gz");
+            let url = format!("https://nodejs.org/dist/{VERSION}/{file_name}");
+            let mut response = self
+                .http
+                .get(&url, Default::default(), true)
+                .await
+                .context("error downloading Node binary tarball")?;
+
+            let decompressed_bytes = GzipDecoder::new(BufReader::new(response.body_mut()));
+            let archive = Archive::new(decompressed_bytes);
+            archive.unpack(&node_containing_dir).await?;
+        }
+
+        anyhow::Ok(node_dir)
     }
+}
 
-    pub async fn binary_path(&self) -> Result<PathBuf> {
+#[async_trait::async_trait]
+impl NodeRuntime for RealNodeRuntime {
+    async fn binary_path(&self) -> Result<PathBuf> {
         let installation_path = self.install_if_needed().await?;
         Ok(installation_path.join("bin/node"))
     }
 
-    pub async fn run_npm_subcommand(
+    async fn run_npm_subcommand(
         &self,
         directory: Option<&Path>,
         subcommand: &str,
@@ -106,7 +169,7 @@ impl NodeRuntime {
         output.map_err(|e| anyhow!("{e}"))
     }
 
-    pub async fn npm_package_latest_version(&self, name: &str) -> Result<String> {
+    async fn npm_package_latest_version(&self, name: &str) -> Result<String> {
         let output = self
             .run_npm_subcommand(
                 None,
@@ -131,10 +194,10 @@ impl NodeRuntime {
             .ok_or_else(|| anyhow!("no version found for npm package {}", name))
     }
 
-    pub async fn npm_install_packages(
+    async fn npm_install_packages(
         &self,
         directory: &Path,
-        packages: impl IntoIterator<Item = (&str, &str)>,
+        packages: &[(&str, &str)],
     ) -> Result<()> {
         let packages: Vec<_> = packages
             .into_iter()
@@ -155,51 +218,31 @@ impl NodeRuntime {
             .await?;
         Ok(())
     }
+}
 
-    async fn install_if_needed(&self) -> Result<PathBuf> {
-        log::info!("Node runtime install_if_needed");
-
-        let arch = match consts::ARCH {
-            "x86_64" => "x64",
-            "aarch64" => "arm64",
-            other => bail!("Running on unsupported platform: {other}"),
-        };
-
-        let folder_name = format!("node-{VERSION}-darwin-{arch}");
-        let node_containing_dir = util::paths::SUPPORT_DIR.join("node");
-        let node_dir = node_containing_dir.join(folder_name);
-        let node_binary = node_dir.join("bin/node");
-        let npm_file = node_dir.join("bin/npm");
+pub struct FakeNodeRuntime;
 
-        let result = Command::new(&node_binary)
-            .arg(npm_file)
-            .arg("--version")
-            .stdin(Stdio::null())
-            .stdout(Stdio::null())
-            .stderr(Stdio::null())
-            .status()
-            .await;
-        let valid = matches!(result, Ok(status) if status.success());
+impl FakeNodeRuntime {
+    pub fn new() -> Arc<dyn NodeRuntime> {
+        Arc::new(FakeNodeRuntime)
+    }
+}
 
-        if !valid {
-            _ = fs::remove_dir_all(&node_containing_dir).await;
-            fs::create_dir(&node_containing_dir)
-                .await
-                .context("error creating node containing dir")?;
+#[async_trait::async_trait]
+impl NodeRuntime for FakeNodeRuntime {
+    async fn binary_path(&self) -> Result<PathBuf> {
+        unreachable!()
+    }
 
-            let file_name = format!("node-{VERSION}-darwin-{arch}.tar.gz");
-            let url = format!("https://nodejs.org/dist/{VERSION}/{file_name}");
-            let mut response = self
-                .http
-                .get(&url, Default::default(), true)
-                .await
-                .context("error downloading Node binary tarball")?;
+    async fn run_npm_subcommand(&self, _: Option<&Path>, _: &str, _: &[&str]) -> Result<Output> {
+        unreachable!()
+    }
 
-            let decompressed_bytes = GzipDecoder::new(BufReader::new(response.body_mut()));
-            let archive = Archive::new(decompressed_bytes);
-            archive.unpack(&node_containing_dir).await?;
-        }
+    async fn npm_package_latest_version(&self, _: &str) -> Result<String> {
+        unreachable!()
+    }
 
-        anyhow::Ok(node_dir)
+    async fn npm_install_packages(&self, _: &Path, _: &[(&str, &str)]) -> Result<()> {
+        unreachable!()
     }
 }

crates/project/src/project.rs 🔗

@@ -35,7 +35,7 @@ use language::{
     point_to_lsp,
     proto::{
         deserialize_anchor, deserialize_fingerprint, deserialize_line_ending, deserialize_version,
-        serialize_anchor, serialize_version,
+        serialize_anchor, serialize_version, split_operations,
     },
     range_from_lsp, range_to_lsp, Bias, Buffer, BufferSnapshot, CachedLspAdapter, CodeAction,
     CodeLabel, Completion, Diagnostic, DiagnosticEntry, DiagnosticSet, Diff, Event as BufferEvent,
@@ -8200,31 +8200,6 @@ impl LspAdapterDelegate for ProjectLspAdapterDelegate {
     }
 }
 
-fn split_operations(
-    mut operations: Vec<proto::Operation>,
-) -> impl Iterator<Item = Vec<proto::Operation>> {
-    #[cfg(any(test, feature = "test-support"))]
-    const CHUNK_SIZE: usize = 5;
-
-    #[cfg(not(any(test, feature = "test-support")))]
-    const CHUNK_SIZE: usize = 100;
-
-    let mut done = false;
-    std::iter::from_fn(move || {
-        if done {
-            return None;
-        }
-
-        let operations = operations
-            .drain(..cmp::min(CHUNK_SIZE, operations.len()))
-            .collect::<Vec<_>>();
-        if operations.is_empty() {
-            done = true;
-        }
-        Some(operations)
-    })
-}
-
 fn serialize_symbol(symbol: &Symbol) -> proto::Symbol {
     proto::Symbol {
         language_server_name: symbol.language_server_name.0.to_string(),

crates/rpc/proto/zed.proto 🔗

@@ -1,6 +1,8 @@
 syntax = "proto3";
 package zed.messages;
 
+// Looking for a number? Search "// Current max"
+
 message PeerId {
     uint32 owner_id = 1;
     uint32 id = 2;
@@ -151,6 +153,9 @@ message Envelope {
         LeaveChannelBuffer leave_channel_buffer = 134;
         AddChannelBufferCollaborator add_channel_buffer_collaborator = 135;
         RemoveChannelBufferCollaborator remove_channel_buffer_collaborator = 136;
+        UpdateChannelBufferCollaborator update_channel_buffer_collaborator = 139;
+        RejoinChannelBuffers rejoin_channel_buffers = 140;
+        RejoinChannelBuffersResponse rejoin_channel_buffers_response = 141; // Current max
     }
 }
 
@@ -430,6 +435,12 @@ message RemoveChannelBufferCollaborator {
     PeerId peer_id = 2;
 }
 
+message UpdateChannelBufferCollaborator {
+    uint64 channel_id = 1;
+    PeerId old_peer_id = 2;
+    PeerId new_peer_id = 3;
+}
+
 message GetDefinition {
      uint64 project_id = 1;
      uint64 buffer_id = 2;
@@ -616,6 +627,12 @@ message BufferVersion {
     repeated VectorClockEntry version = 2;
 }
 
+message ChannelBufferVersion {
+    uint64 channel_id = 1;
+    repeated VectorClockEntry version = 2;
+    uint64 epoch = 3;
+}
+
 enum FormatTrigger {
     Save = 0;
     Manual = 1;
@@ -1008,12 +1025,28 @@ message JoinChannelBuffer {
     uint64 channel_id = 1;
 }
 
+message RejoinChannelBuffers {
+    repeated ChannelBufferVersion buffers = 1;
+}
+
+message RejoinChannelBuffersResponse {
+    repeated RejoinedChannelBuffer buffers = 1;
+}
+
 message JoinChannelBufferResponse {
     uint64 buffer_id = 1;
     uint32 replica_id = 2;
     string base_text = 3;
     repeated Operation operations = 4;
     repeated Collaborator collaborators = 5;
+    uint64 epoch = 6;
+}
+
+message RejoinedChannelBuffer {
+    uint64 channel_id = 1;
+    repeated VectorClockEntry version = 2;
+    repeated Operation operations = 3;
+    repeated Collaborator collaborators = 4;
 }
 
 message LeaveChannelBuffer {

crates/rpc/src/proto.rs 🔗

@@ -229,6 +229,8 @@ messages!(
     (StartLanguageServer, Foreground),
     (SynchronizeBuffers, Foreground),
     (SynchronizeBuffersResponse, Foreground),
+    (RejoinChannelBuffers, Foreground),
+    (RejoinChannelBuffersResponse, Foreground),
     (Test, Foreground),
     (Unfollow, Foreground),
     (UnshareProject, Foreground),
@@ -257,6 +259,7 @@ messages!(
     (UpdateChannelBuffer, Foreground),
     (RemoveChannelBufferCollaborator, Foreground),
     (AddChannelBufferCollaborator, Foreground),
+    (UpdateChannelBufferCollaborator, Foreground),
 );
 
 request_messages!(
@@ -319,6 +322,7 @@ request_messages!(
     (SearchProject, SearchProjectResponse),
     (ShareProject, ShareProjectResponse),
     (SynchronizeBuffers, SynchronizeBuffersResponse),
+    (RejoinChannelBuffers, RejoinChannelBuffersResponse),
     (Test, Test),
     (UpdateBuffer, Ack),
     (UpdateParticipantLocation, Ack),
@@ -386,7 +390,8 @@ entity_messages!(
     channel_id,
     UpdateChannelBuffer,
     RemoveChannelBufferCollaborator,
-    AddChannelBufferCollaborator
+    AddChannelBufferCollaborator,
+    UpdateChannelBufferCollaborator
 );
 
 const KIB: usize = 1024;

crates/search/src/project_search.rs 🔗

@@ -12,15 +12,13 @@ use editor::{
     SelectAll, MAX_TAB_TITLE_LEN,
 };
 use futures::StreamExt;
-
-use gpui::platform::PromptLevel;
-
 use gpui::{
-    actions, elements::*, platform::MouseButton, Action, AnyElement, AnyViewHandle, AppContext,
-    Entity, ModelContext, ModelHandle, Subscription, Task, View, ViewContext, ViewHandle,
-    WeakModelHandle, WeakViewHandle,
+    actions,
+    elements::*,
+    platform::{MouseButton, PromptLevel},
+    Action, AnyElement, AnyViewHandle, AppContext, Entity, ModelContext, ModelHandle, Subscription,
+    Task, View, ViewContext, ViewHandle, WeakModelHandle, WeakViewHandle,
 };
-
 use menu::Confirm;
 use postage::stream::Stream;
 use project::{
@@ -132,8 +130,7 @@ pub struct ProjectSearchView {
 }
 
 struct SemanticSearchState {
-    file_count: usize,
-    outstanding_file_count: usize,
+    pending_file_count: usize,
     _progress_task: Task<()>,
 }
 
@@ -319,12 +316,8 @@ impl View for ProjectSearchView {
             };
 
             let semantic_status = if let Some(semantic) = &self.semantic_state {
-                if semantic.outstanding_file_count > 0 {
-                    format!(
-                        "Indexing: {} of {}...",
-                        semantic.file_count - semantic.outstanding_file_count,
-                        semantic.file_count
-                    )
+                if semantic.pending_file_count > 0 {
+                    format!("Remaining files to index: {}", semantic.pending_file_count)
                 } else {
                     "Indexing complete".to_string()
                 }
@@ -641,26 +634,27 @@ impl ProjectSearchView {
 
             let project = self.model.read(cx).project.clone();
 
-            let index_task = semantic_index.update(cx, |semantic_index, cx| {
-                semantic_index.index_project(project, cx)
+            let mut pending_file_count_rx = semantic_index.update(cx, |semantic_index, cx| {
+                semantic_index
+                    .index_project(project.clone(), cx)
+                    .detach_and_log_err(cx);
+                semantic_index.pending_file_count(&project).unwrap()
             });
 
             cx.spawn(|search_view, mut cx| async move {
-                let (files_to_index, mut files_remaining_rx) = index_task.await?;
-
                 search_view.update(&mut cx, |search_view, cx| {
                     cx.notify();
+                    let pending_file_count = *pending_file_count_rx.borrow();
                     search_view.semantic_state = Some(SemanticSearchState {
-                        file_count: files_to_index,
-                        outstanding_file_count: files_to_index,
+                        pending_file_count,
                         _progress_task: cx.spawn(|search_view, mut cx| async move {
-                            while let Some(count) = files_remaining_rx.recv().await {
+                            while let Some(count) = pending_file_count_rx.recv().await {
                                 search_view
                                     .update(&mut cx, |search_view, cx| {
                                         if let Some(semantic_search_state) =
                                             &mut search_view.semantic_state
                                         {
-                                            semantic_search_state.outstanding_file_count = count;
+                                            semantic_search_state.pending_file_count = count;
                                             cx.notify();
                                             if count == 0 {
                                                 return;
@@ -959,7 +953,7 @@ impl ProjectSearchView {
         match mode {
             SearchMode::Semantic => {
                 if let Some(semantic) = &mut self.semantic_state {
-                    if semantic.outstanding_file_count > 0 {
+                    if semantic.pending_file_count > 0 {
                         return;
                     }
 

crates/semantic_index/Cargo.toml 🔗

@@ -9,6 +9,7 @@ path = "src/semantic_index.rs"
 doctest = false
 
 [dependencies]
+collections = { path = "../collections" }
 gpui = { path = "../gpui" }
 language = { path = "../language" }
 project = { path = "../project" }
@@ -39,8 +40,10 @@ rand.workspace = true
 schemars.workspace = true
 globset.workspace = true
 sha1 = "0.10.5"
+parse_duration = "2.1.1"
 
 [dev-dependencies]
+collections = { path = "../collections", features = ["test-support"] }
 gpui = { path = "../gpui", features = ["test-support"] }
 language = { path = "../language", features = ["test-support"] }
 project = { path = "../project", features = ["test-support"] }

crates/semantic_index/src/db.rs 🔗

@@ -1,20 +1,26 @@
-use crate::{parsing::Document, SEMANTIC_INDEX_VERSION};
+use crate::{
+    embedding::Embedding,
+    parsing::{Span, SpanDigest},
+    SEMANTIC_INDEX_VERSION,
+};
 use anyhow::{anyhow, Context, Result};
+use collections::HashMap;
+use futures::channel::oneshot;
+use gpui::executor;
 use project::{search::PathMatcher, Fs};
 use rpc::proto::Timestamp;
-use rusqlite::{
-    params,
-    types::{FromSql, FromSqlResult, ValueRef},
-};
+use rusqlite::params;
+use rusqlite::types::Value;
 use std::{
     cmp::Ordering,
-    collections::HashMap,
+    future::Future,
     ops::Range,
     path::{Path, PathBuf},
     rc::Rc,
     sync::Arc,
-    time::SystemTime,
+    time::{Instant, SystemTime},
 };
+use util::TryFutureExt;
 
 #[derive(Debug)]
 pub struct FileRecord {
@@ -23,286 +29,371 @@ pub struct FileRecord {
     pub mtime: Timestamp,
 }
 
-#[derive(Debug)]
-struct Embedding(pub Vec<f32>);
-
-#[derive(Debug)]
-struct Sha1(pub Vec<u8>);
-
-impl FromSql for Embedding {
-    fn column_result(value: ValueRef) -> FromSqlResult<Self> {
-        let bytes = value.as_blob()?;
-        let embedding: Result<Vec<f32>, Box<bincode::ErrorKind>> = bincode::deserialize(bytes);
-        if embedding.is_err() {
-            return Err(rusqlite::types::FromSqlError::Other(embedding.unwrap_err()));
-        }
-        return Ok(Embedding(embedding.unwrap()));
-    }
-}
-
-impl FromSql for Sha1 {
-    fn column_result(value: ValueRef) -> FromSqlResult<Self> {
-        let bytes = value.as_blob()?;
-        let sha1: Result<Vec<u8>, Box<bincode::ErrorKind>> = bincode::deserialize(bytes);
-        if sha1.is_err() {
-            return Err(rusqlite::types::FromSqlError::Other(sha1.unwrap_err()));
-        }
-        return Ok(Sha1(sha1.unwrap()));
-    }
-}
-
+#[derive(Clone)]
 pub struct VectorDatabase {
-    db: rusqlite::Connection,
+    path: Arc<Path>,
+    transactions:
+        smol::channel::Sender<Box<dyn 'static + Send + FnOnce(&mut rusqlite::Connection)>>,
 }
 
 impl VectorDatabase {
-    pub async fn new(fs: Arc<dyn Fs>, path: Arc<PathBuf>) -> Result<Self> {
+    pub async fn new(
+        fs: Arc<dyn Fs>,
+        path: Arc<Path>,
+        executor: Arc<executor::Background>,
+    ) -> Result<Self> {
         if let Some(db_directory) = path.parent() {
             fs.create_dir(db_directory).await?;
         }
 
+        let (transactions_tx, transactions_rx) = smol::channel::unbounded::<
+            Box<dyn 'static + Send + FnOnce(&mut rusqlite::Connection)>,
+        >();
+        executor
+            .spawn({
+                let path = path.clone();
+                async move {
+                    let mut connection = rusqlite::Connection::open(&path)?;
+
+                    connection.pragma_update(None, "journal_mode", "wal")?;
+                    connection.pragma_update(None, "synchronous", "normal")?;
+                    connection.pragma_update(None, "cache_size", 1000000)?;
+                    connection.pragma_update(None, "temp_store", "MEMORY")?;
+
+                    while let Ok(transaction) = transactions_rx.recv().await {
+                        transaction(&mut connection);
+                    }
+
+                    anyhow::Ok(())
+                }
+                .log_err()
+            })
+            .detach();
         let this = Self {
-            db: rusqlite::Connection::open(path.as_path())?,
+            transactions: transactions_tx,
+            path,
         };
-        this.initialize_database()?;
+        this.initialize_database().await?;
         Ok(this)
     }
 
-    fn get_existing_version(&self) -> Result<i64> {
-        let mut version_query = self
-            .db
-            .prepare("SELECT version from semantic_index_config")?;
-        version_query
-            .query_row([], |row| Ok(row.get::<_, i64>(0)?))
-            .map_err(|err| anyhow!("version query failed: {err}"))
+    pub fn path(&self) -> &Arc<Path> {
+        &self.path
     }
 
-    fn initialize_database(&self) -> Result<()> {
-        rusqlite::vtab::array::load_module(&self.db)?;
-
-        // Delete existing tables, if SEMANTIC_INDEX_VERSION is bumped
-        if self
-            .get_existing_version()
-            .map_or(false, |version| version == SEMANTIC_INDEX_VERSION as i64)
-        {
-            log::trace!("vector database schema up to date");
-            return Ok(());
+    fn transact<F, T>(&self, f: F) -> impl Future<Output = Result<T>>
+    where
+        F: 'static + Send + FnOnce(&rusqlite::Transaction) -> Result<T>,
+        T: 'static + Send,
+    {
+        let (tx, rx) = oneshot::channel();
+        let transactions = self.transactions.clone();
+        async move {
+            if transactions
+                .send(Box::new(|connection| {
+                    let result = connection
+                        .transaction()
+                        .map_err(|err| anyhow!(err))
+                        .and_then(|transaction| {
+                            let result = f(&transaction)?;
+                            transaction.commit()?;
+                            Ok(result)
+                        });
+                    let _ = tx.send(result);
+                }))
+                .await
+                .is_err()
+            {
+                return Err(anyhow!("connection was dropped"))?;
+            }
+            rx.await?
         }
+    }
 
-        log::trace!("vector database schema out of date. updating...");
-        self.db
-            .execute("DROP TABLE IF EXISTS documents", [])
-            .context("failed to drop 'documents' table")?;
-        self.db
-            .execute("DROP TABLE IF EXISTS files", [])
-            .context("failed to drop 'files' table")?;
-        self.db
-            .execute("DROP TABLE IF EXISTS worktrees", [])
-            .context("failed to drop 'worktrees' table")?;
-        self.db
-            .execute("DROP TABLE IF EXISTS semantic_index_config", [])
-            .context("failed to drop 'semantic_index_config' table")?;
-
-        // Initialize Vector Databasing Tables
-        self.db.execute(
-            "CREATE TABLE semantic_index_config (
-                version INTEGER NOT NULL
-            )",
-            [],
-        )?;
+    fn initialize_database(&self) -> impl Future<Output = Result<()>> {
+        self.transact(|db| {
+            rusqlite::vtab::array::load_module(&db)?;
+
+            // Delete existing tables, if SEMANTIC_INDEX_VERSION is bumped
+            let version_query = db.prepare("SELECT version from semantic_index_config");
+            let version = version_query
+                .and_then(|mut query| query.query_row([], |row| Ok(row.get::<_, i64>(0)?)));
+            if version.map_or(false, |version| version == SEMANTIC_INDEX_VERSION as i64) {
+                log::trace!("vector database schema up to date");
+                return Ok(());
+            }
 
-        self.db.execute(
-            "INSERT INTO semantic_index_config (version) VALUES (?1)",
-            params![SEMANTIC_INDEX_VERSION],
-        )?;
+            log::trace!("vector database schema out of date. updating...");
+            // We renamed the `documents` table to `spans`, so we want to drop
+            // `documents` without recreating it if it exists.
+            db.execute("DROP TABLE IF EXISTS documents", [])
+                .context("failed to drop 'documents' table")?;
+            db.execute("DROP TABLE IF EXISTS spans", [])
+                .context("failed to drop 'spans' table")?;
+            db.execute("DROP TABLE IF EXISTS files", [])
+                .context("failed to drop 'files' table")?;
+            db.execute("DROP TABLE IF EXISTS worktrees", [])
+                .context("failed to drop 'worktrees' table")?;
+            db.execute("DROP TABLE IF EXISTS semantic_index_config", [])
+                .context("failed to drop 'semantic_index_config' table")?;
+
+            // Initialize Vector Databasing Tables
+            db.execute(
+                "CREATE TABLE semantic_index_config (
+                    version INTEGER NOT NULL
+                )",
+                [],
+            )?;
 
-        self.db.execute(
-            "CREATE TABLE worktrees (
-                id INTEGER PRIMARY KEY AUTOINCREMENT,
-                absolute_path VARCHAR NOT NULL
-            );
-            CREATE UNIQUE INDEX worktrees_absolute_path ON worktrees (absolute_path);
-            ",
-            [],
-        )?;
+            db.execute(
+                "INSERT INTO semantic_index_config (version) VALUES (?1)",
+                params![SEMANTIC_INDEX_VERSION],
+            )?;
 
-        self.db.execute(
-            "CREATE TABLE files (
-                id INTEGER PRIMARY KEY AUTOINCREMENT,
-                worktree_id INTEGER NOT NULL,
-                relative_path VARCHAR NOT NULL,
-                mtime_seconds INTEGER NOT NULL,
-                mtime_nanos INTEGER NOT NULL,
-                FOREIGN KEY(worktree_id) REFERENCES worktrees(id) ON DELETE CASCADE
-            )",
-            [],
-        )?;
+            db.execute(
+                "CREATE TABLE worktrees (
+                    id INTEGER PRIMARY KEY AUTOINCREMENT,
+                    absolute_path VARCHAR NOT NULL
+                );
+                CREATE UNIQUE INDEX worktrees_absolute_path ON worktrees (absolute_path);
+                ",
+                [],
+            )?;
 
-        self.db.execute(
-            "CREATE TABLE documents (
-                id INTEGER PRIMARY KEY AUTOINCREMENT,
-                file_id INTEGER NOT NULL,
-                start_byte INTEGER NOT NULL,
-                end_byte INTEGER NOT NULL,
-                name VARCHAR NOT NULL,
-                embedding BLOB NOT NULL,
-                sha1 BLOB NOT NULL,
-                FOREIGN KEY(file_id) REFERENCES files(id) ON DELETE CASCADE
-            )",
-            [],
-        )?;
+            db.execute(
+                "CREATE TABLE files (
+                    id INTEGER PRIMARY KEY AUTOINCREMENT,
+                    worktree_id INTEGER NOT NULL,
+                    relative_path VARCHAR NOT NULL,
+                    mtime_seconds INTEGER NOT NULL,
+                    mtime_nanos INTEGER NOT NULL,
+                    FOREIGN KEY(worktree_id) REFERENCES worktrees(id) ON DELETE CASCADE
+                )",
+                [],
+            )?;
 
-        log::trace!("vector database initialized with updated schema.");
-        Ok(())
+            db.execute(
+                "CREATE UNIQUE INDEX files_worktree_id_and_relative_path ON files (worktree_id, relative_path)",
+                [],
+            )?;
+
+            db.execute(
+                "CREATE TABLE spans (
+                    id INTEGER PRIMARY KEY AUTOINCREMENT,
+                    file_id INTEGER NOT NULL,
+                    start_byte INTEGER NOT NULL,
+                    end_byte INTEGER NOT NULL,
+                    name VARCHAR NOT NULL,
+                    embedding BLOB NOT NULL,
+                    digest BLOB NOT NULL,
+                    FOREIGN KEY(file_id) REFERENCES files(id) ON DELETE CASCADE
+                )",
+                [],
+            )?;
+
+            log::trace!("vector database initialized with updated schema.");
+            Ok(())
+        })
     }
 
-    pub fn delete_file(&self, worktree_id: i64, delete_path: PathBuf) -> Result<()> {
-        self.db.execute(
-            "DELETE FROM files WHERE worktree_id = ?1 AND relative_path = ?2",
-            params![worktree_id, delete_path.to_str()],
-        )?;
-        Ok(())
+    pub fn delete_file(
+        &self,
+        worktree_id: i64,
+        delete_path: Arc<Path>,
+    ) -> impl Future<Output = Result<()>> {
+        self.transact(move |db| {
+            db.execute(
+                "DELETE FROM files WHERE worktree_id = ?1 AND relative_path = ?2",
+                params![worktree_id, delete_path.to_str()],
+            )?;
+            Ok(())
+        })
     }
 
     pub fn insert_file(
         &self,
         worktree_id: i64,
-        path: PathBuf,
+        path: Arc<Path>,
         mtime: SystemTime,
-        documents: Vec<Document>,
-    ) -> Result<()> {
-        // Return the existing ID, if both the file and mtime match
-        let mtime = Timestamp::from(mtime);
-        let mut existing_id_query = self.db.prepare("SELECT id FROM files WHERE worktree_id = ?1 AND relative_path = ?2 AND mtime_seconds = ?3 AND mtime_nanos = ?4")?;
-        let existing_id = existing_id_query
-            .query_row(
+        spans: Vec<Span>,
+    ) -> impl Future<Output = Result<()>> {
+        self.transact(move |db| {
+            // Return the existing ID, if both the file and mtime match
+            let mtime = Timestamp::from(mtime);
+
+            db.execute(
+                "
+                REPLACE INTO files
+                (worktree_id, relative_path, mtime_seconds, mtime_nanos)
+                VALUES (?1, ?2, ?3, ?4)
+                ",
                 params![worktree_id, path.to_str(), mtime.seconds, mtime.nanos],
-                |row| Ok(row.get::<_, i64>(0)?),
-            )
-            .map_err(|err| anyhow!(err));
-        let file_id = if existing_id.is_ok() {
-            // If already exists, just return the existing id
-            existing_id.unwrap()
-        } else {
-            // Delete Existing Row
-            self.db.execute(
-                "DELETE FROM files WHERE worktree_id = ?1 AND relative_path = ?2;",
-                params![worktree_id, path.to_str()],
             )?;
-            self.db.execute("INSERT INTO files (worktree_id, relative_path, mtime_seconds, mtime_nanos) VALUES (?1, ?2, ?3, ?4);", params![worktree_id, path.to_str(), mtime.seconds, mtime.nanos])?;
-            self.db.last_insert_rowid()
-        };
 
-        // Currently inserting at approximately 3400 documents a second
-        // I imagine we can speed this up with a bulk insert of some kind.
-        for document in documents {
-            let embedding_blob = bincode::serialize(&document.embedding)?;
-            let sha_blob = bincode::serialize(&document.sha1)?;
+            let file_id = db.last_insert_rowid();
 
-            self.db.execute(
-                "INSERT INTO documents (file_id, start_byte, end_byte, name, embedding, sha1) VALUES (?1, ?2, ?3, ?4, ?5, ?6)",
-                params![
-                    file_id,
-                    document.range.start.to_string(),
-                    document.range.end.to_string(),
-                    document.name,
-                    embedding_blob,
-                    sha_blob
-                ],
+            let t0 = Instant::now();
+            let mut query = db.prepare(
+                "
+                INSERT INTO spans
+                (file_id, start_byte, end_byte, name, embedding, digest)
+                VALUES (?1, ?2, ?3, ?4, ?5, ?6)
+                ",
             )?;
-        }
+            log::trace!(
+                "Preparing Query Took: {:?} milliseconds",
+                t0.elapsed().as_millis()
+            );
 
-        Ok(())
-    }
+            for span in spans {
+                query.execute(params![
+                    file_id,
+                    span.range.start.to_string(),
+                    span.range.end.to_string(),
+                    span.name,
+                    span.embedding,
+                    span.digest
+                ])?;
+            }
 
-    pub fn worktree_previously_indexed(&self, worktree_root_path: &Path) -> Result<bool> {
-        let mut worktree_query = self
-            .db
-            .prepare("SELECT id FROM worktrees WHERE absolute_path = ?1")?;
-        let worktree_id = worktree_query
-            .query_row(params![worktree_root_path.to_string_lossy()], |row| {
-                Ok(row.get::<_, i64>(0)?)
-            })
-            .map_err(|err| anyhow!(err));
+            Ok(())
+        })
+    }
 
-        if worktree_id.is_ok() {
-            return Ok(true);
-        } else {
-            return Ok(false);
-        }
+    pub fn worktree_previously_indexed(
+        &self,
+        worktree_root_path: &Path,
+    ) -> impl Future<Output = Result<bool>> {
+        let worktree_root_path = worktree_root_path.to_string_lossy().into_owned();
+        self.transact(move |db| {
+            let mut worktree_query =
+                db.prepare("SELECT id FROM worktrees WHERE absolute_path = ?1")?;
+            let worktree_id = worktree_query
+                .query_row(params![worktree_root_path], |row| Ok(row.get::<_, i64>(0)?));
+
+            if worktree_id.is_ok() {
+                return Ok(true);
+            } else {
+                return Ok(false);
+            }
+        })
     }
 
-    pub fn find_or_create_worktree(&self, worktree_root_path: &Path) -> Result<i64> {
-        // Check that the absolute path doesnt exist
-        let mut worktree_query = self
-            .db
-            .prepare("SELECT id FROM worktrees WHERE absolute_path = ?1")?;
+    pub fn embeddings_for_files(
+        &self,
+        worktree_id_file_paths: HashMap<i64, Vec<Arc<Path>>>,
+    ) -> impl Future<Output = Result<HashMap<SpanDigest, Embedding>>> {
+        self.transact(move |db| {
+            let mut query = db.prepare(
+                "
+                SELECT digest, embedding
+                FROM spans
+                LEFT JOIN files ON files.id = spans.file_id
+                WHERE files.worktree_id = ? AND files.relative_path IN rarray(?)
+            ",
+            )?;
+            let mut embeddings_by_digest = HashMap::default();
+            for (worktree_id, file_paths) in worktree_id_file_paths {
+                let file_paths = Rc::new(
+                    file_paths
+                        .into_iter()
+                        .map(|p| Value::Text(p.to_string_lossy().into_owned()))
+                        .collect::<Vec<_>>(),
+                );
+                let rows = query.query_map(params![worktree_id, file_paths], |row| {
+                    Ok((row.get::<_, SpanDigest>(0)?, row.get::<_, Embedding>(1)?))
+                })?;
+
+                for row in rows {
+                    if let Ok(row) = row {
+                        embeddings_by_digest.insert(row.0, row.1);
+                    }
+                }
+            }
 
-        let worktree_id = worktree_query
-            .query_row(params![worktree_root_path.to_string_lossy()], |row| {
-                Ok(row.get::<_, i64>(0)?)
-            })
-            .map_err(|err| anyhow!(err));
+            Ok(embeddings_by_digest)
+        })
+    }
 
-        if worktree_id.is_ok() {
-            return worktree_id;
-        }
+    pub fn find_or_create_worktree(
+        &self,
+        worktree_root_path: Arc<Path>,
+    ) -> impl Future<Output = Result<i64>> {
+        self.transact(move |db| {
+            let mut worktree_query =
+                db.prepare("SELECT id FROM worktrees WHERE absolute_path = ?1")?;
+            let worktree_id = worktree_query
+                .query_row(params![worktree_root_path.to_string_lossy()], |row| {
+                    Ok(row.get::<_, i64>(0)?)
+                });
+
+            if worktree_id.is_ok() {
+                return Ok(worktree_id?);
+            }
 
-        // If worktree_id is Err, insert new worktree
-        self.db.execute(
-            "
-            INSERT into worktrees (absolute_path) VALUES (?1)
-            ",
-            params![worktree_root_path.to_string_lossy()],
-        )?;
-        Ok(self.db.last_insert_rowid())
+            // If worktree_id is Err, insert new worktree
+            db.execute(
+                "INSERT into worktrees (absolute_path) VALUES (?1)",
+                params![worktree_root_path.to_string_lossy()],
+            )?;
+            Ok(db.last_insert_rowid())
+        })
     }
 
-    pub fn get_file_mtimes(&self, worktree_id: i64) -> Result<HashMap<PathBuf, SystemTime>> {
-        let mut statement = self.db.prepare(
-            "
-            SELECT relative_path, mtime_seconds, mtime_nanos
-            FROM files
-            WHERE worktree_id = ?1
-            ORDER BY relative_path",
-        )?;
-        let mut result: HashMap<PathBuf, SystemTime> = HashMap::new();
-        for row in statement.query_map(params![worktree_id], |row| {
-            Ok((
-                row.get::<_, String>(0)?.into(),
-                Timestamp {
-                    seconds: row.get(1)?,
-                    nanos: row.get(2)?,
-                }
-                .into(),
-            ))
-        })? {
-            let row = row?;
-            result.insert(row.0, row.1);
-        }
-        Ok(result)
+    pub fn get_file_mtimes(
+        &self,
+        worktree_id: i64,
+    ) -> impl Future<Output = Result<HashMap<PathBuf, SystemTime>>> {
+        self.transact(move |db| {
+            let mut statement = db.prepare(
+                "
+                SELECT relative_path, mtime_seconds, mtime_nanos
+                FROM files
+                WHERE worktree_id = ?1
+                ORDER BY relative_path",
+            )?;
+            let mut result: HashMap<PathBuf, SystemTime> = HashMap::default();
+            for row in statement.query_map(params![worktree_id], |row| {
+                Ok((
+                    row.get::<_, String>(0)?.into(),
+                    Timestamp {
+                        seconds: row.get(1)?,
+                        nanos: row.get(2)?,
+                    }
+                    .into(),
+                ))
+            })? {
+                let row = row?;
+                result.insert(row.0, row.1);
+            }
+            Ok(result)
+        })
     }
 
     pub fn top_k_search(
         &self,
-        query_embedding: &Vec<f32>,
+        query_embedding: &Embedding,
         limit: usize,
         file_ids: &[i64],
-    ) -> Result<Vec<(i64, f32)>> {
-        let mut results = Vec::<(i64, f32)>::with_capacity(limit + 1);
-        self.for_each_document(file_ids, |id, embedding| {
-            let similarity = dot(&embedding, &query_embedding);
-            let ix = match results
-                .binary_search_by(|(_, s)| similarity.partial_cmp(&s).unwrap_or(Ordering::Equal))
-            {
-                Ok(ix) => ix,
-                Err(ix) => ix,
-            };
-            results.insert(ix, (id, similarity));
-            results.truncate(limit);
-        })?;
-
-        Ok(results)
+    ) -> impl Future<Output = Result<Vec<(i64, f32)>>> {
+        let query_embedding = query_embedding.clone();
+        let file_ids = file_ids.to_vec();
+        self.transact(move |db| {
+            let mut results = Vec::<(i64, f32)>::with_capacity(limit + 1);
+            Self::for_each_span(db, &file_ids, |id, embedding| {
+                let similarity = embedding.similarity(&query_embedding);
+                let ix = match results.binary_search_by(|(_, s)| {
+                    similarity.partial_cmp(&s).unwrap_or(Ordering::Equal)
+                }) {
+                    Ok(ix) => ix,
+                    Err(ix) => ix,
+                };
+                results.insert(ix, (id, similarity));
+                results.truncate(limit);
+            })?;
+
+            anyhow::Ok(results)
+        })
     }
 
     pub fn retrieve_included_file_ids(
@@ -310,42 +401,51 @@ impl VectorDatabase {
         worktree_ids: &[i64],
         includes: &[PathMatcher],
         excludes: &[PathMatcher],
-    ) -> Result<Vec<i64>> {
-        let mut file_query = self.db.prepare(
-            "
-            SELECT
-                id, relative_path
-            FROM
-                files
-            WHERE
-                worktree_id IN rarray(?)
-            ",
-        )?;
+    ) -> impl Future<Output = Result<Vec<i64>>> {
+        let worktree_ids = worktree_ids.to_vec();
+        let includes = includes.to_vec();
+        let excludes = excludes.to_vec();
+        self.transact(move |db| {
+            let mut file_query = db.prepare(
+                "
+                SELECT
+                    id, relative_path
+                FROM
+                    files
+                WHERE
+                    worktree_id IN rarray(?)
+                ",
+            )?;
 
-        let mut file_ids = Vec::<i64>::new();
-        let mut rows = file_query.query([ids_to_sql(worktree_ids)])?;
-
-        while let Some(row) = rows.next()? {
-            let file_id = row.get(0)?;
-            let relative_path = row.get_ref(1)?.as_str()?;
-            let included =
-                includes.is_empty() || includes.iter().any(|glob| glob.is_match(relative_path));
-            let excluded = excludes.iter().any(|glob| glob.is_match(relative_path));
-            if included && !excluded {
-                file_ids.push(file_id);
+            let mut file_ids = Vec::<i64>::new();
+            let mut rows = file_query.query([ids_to_sql(&worktree_ids)])?;
+
+            while let Some(row) = rows.next()? {
+                let file_id = row.get(0)?;
+                let relative_path = row.get_ref(1)?.as_str()?;
+                let included =
+                    includes.is_empty() || includes.iter().any(|glob| glob.is_match(relative_path));
+                let excluded = excludes.iter().any(|glob| glob.is_match(relative_path));
+                if included && !excluded {
+                    file_ids.push(file_id);
+                }
             }
-        }
 
-        Ok(file_ids)
+            anyhow::Ok(file_ids)
+        })
     }
 
-    fn for_each_document(&self, file_ids: &[i64], mut f: impl FnMut(i64, Vec<f32>)) -> Result<()> {
-        let mut query_statement = self.db.prepare(
+    fn for_each_span(
+        db: &rusqlite::Connection,
+        file_ids: &[i64],
+        mut f: impl FnMut(i64, Embedding),
+    ) -> Result<()> {
+        let mut query_statement = db.prepare(
             "
             SELECT
                 id, embedding
             FROM
-                documents
+                spans
             WHERE
                 file_id IN rarray(?)
             ",
@@ -356,51 +456,57 @@ impl VectorDatabase {
                 Ok((row.get(0)?, row.get::<_, Embedding>(1)?))
             })?
             .filter_map(|row| row.ok())
-            .for_each(|(id, embedding)| f(id, embedding.0));
+            .for_each(|(id, embedding)| f(id, embedding));
         Ok(())
     }
 
-    pub fn get_documents_by_ids(&self, ids: &[i64]) -> Result<Vec<(i64, PathBuf, Range<usize>)>> {
-        let mut statement = self.db.prepare(
-            "
-                SELECT
-                    documents.id,
-                    files.worktree_id,
-                    files.relative_path,
-                    documents.start_byte,
-                    documents.end_byte
-                FROM
-                    documents, files
-                WHERE
-                    documents.file_id = files.id AND
-                    documents.id in rarray(?)
-            ",
-        )?;
+    pub fn spans_for_ids(
+        &self,
+        ids: &[i64],
+    ) -> impl Future<Output = Result<Vec<(i64, PathBuf, Range<usize>)>>> {
+        let ids = ids.to_vec();
+        self.transact(move |db| {
+            let mut statement = db.prepare(
+                "
+                    SELECT
+                        spans.id,
+                        files.worktree_id,
+                        files.relative_path,
+                        spans.start_byte,
+                        spans.end_byte
+                    FROM
+                        spans, files
+                    WHERE
+                        spans.file_id = files.id AND
+                        spans.id in rarray(?)
+                ",
+            )?;
 
-        let result_iter = statement.query_map(params![ids_to_sql(ids)], |row| {
-            Ok((
-                row.get::<_, i64>(0)?,
-                row.get::<_, i64>(1)?,
-                row.get::<_, String>(2)?.into(),
-                row.get(3)?..row.get(4)?,
-            ))
-        })?;
-
-        let mut values_by_id = HashMap::<i64, (i64, PathBuf, Range<usize>)>::default();
-        for row in result_iter {
-            let (id, worktree_id, path, range) = row?;
-            values_by_id.insert(id, (worktree_id, path, range));
-        }
+            let result_iter = statement.query_map(params![ids_to_sql(&ids)], |row| {
+                Ok((
+                    row.get::<_, i64>(0)?,
+                    row.get::<_, i64>(1)?,
+                    row.get::<_, String>(2)?.into(),
+                    row.get(3)?..row.get(4)?,
+                ))
+            })?;
+
+            let mut values_by_id = HashMap::<i64, (i64, PathBuf, Range<usize>)>::default();
+            for row in result_iter {
+                let (id, worktree_id, path, range) = row?;
+                values_by_id.insert(id, (worktree_id, path, range));
+            }
 
-        let mut results = Vec::with_capacity(ids.len());
-        for id in ids {
-            let value = values_by_id
-                .remove(id)
-                .ok_or(anyhow!("missing document id {}", id))?;
-            results.push(value);
-        }
+            let mut results = Vec::with_capacity(ids.len());
+            for id in &ids {
+                let value = values_by_id
+                    .remove(id)
+                    .ok_or(anyhow!("missing span id {}", id))?;
+                results.push(value);
+            }
 
-        Ok(results)
+            Ok(results)
+        })
     }
 }
 
@@ -412,29 +518,3 @@ fn ids_to_sql(ids: &[i64]) -> Rc<Vec<rusqlite::types::Value>> {
             .collect::<Vec<_>>(),
     )
 }
-
-pub(crate) fn dot(vec_a: &[f32], vec_b: &[f32]) -> f32 {
-    let len = vec_a.len();
-    assert_eq!(len, vec_b.len());
-
-    let mut result = 0.0;
-    unsafe {
-        matrixmultiply::sgemm(
-            1,
-            len,
-            1,
-            1.0,
-            vec_a.as_ptr(),
-            len as isize,
-            1,
-            vec_b.as_ptr(),
-            1,
-            len as isize,
-            0.0,
-            &mut result as *mut f32,
-            1,
-            1,
-        );
-    }
-    result
-}

crates/semantic_index/src/embedding.rs 🔗

@@ -7,6 +7,9 @@ use isahc::http::StatusCode;
 use isahc::prelude::Configurable;
 use isahc::{AsyncBody, Response};
 use lazy_static::lazy_static;
+use parse_duration::parse;
+use rusqlite::types::{FromSql, FromSqlResult, ToSqlOutput, ValueRef};
+use rusqlite::ToSql;
 use serde::{Deserialize, Serialize};
 use std::env;
 use std::sync::Arc;
@@ -19,6 +22,62 @@ lazy_static! {
     static ref OPENAI_BPE_TOKENIZER: CoreBPE = cl100k_base().unwrap();
 }
 
+#[derive(Debug, PartialEq, Clone)]
+pub struct Embedding(Vec<f32>);
+
+impl From<Vec<f32>> for Embedding {
+    fn from(value: Vec<f32>) -> Self {
+        Embedding(value)
+    }
+}
+
+impl Embedding {
+    pub fn similarity(&self, other: &Self) -> f32 {
+        let len = self.0.len();
+        assert_eq!(len, other.0.len());
+
+        let mut result = 0.0;
+        unsafe {
+            matrixmultiply::sgemm(
+                1,
+                len,
+                1,
+                1.0,
+                self.0.as_ptr(),
+                len as isize,
+                1,
+                other.0.as_ptr(),
+                1,
+                len as isize,
+                0.0,
+                &mut result as *mut f32,
+                1,
+                1,
+            );
+        }
+        result
+    }
+}
+
+impl FromSql for Embedding {
+    fn column_result(value: ValueRef) -> FromSqlResult<Self> {
+        let bytes = value.as_blob()?;
+        let embedding: Result<Vec<f32>, Box<bincode::ErrorKind>> = bincode::deserialize(bytes);
+        if embedding.is_err() {
+            return Err(rusqlite::types::FromSqlError::Other(embedding.unwrap_err()));
+        }
+        Ok(Embedding(embedding.unwrap()))
+    }
+}
+
+impl ToSql for Embedding {
+    fn to_sql(&self) -> rusqlite::Result<ToSqlOutput> {
+        let bytes = bincode::serialize(&self.0)
+            .map_err(|err| rusqlite::Error::ToSqlConversionFailure(Box::new(err)))?;
+        Ok(ToSqlOutput::Owned(rusqlite::types::Value::Blob(bytes)))
+    }
+}
+
 #[derive(Clone)]
 pub struct OpenAIEmbeddings {
     pub client: Arc<dyn HttpClient>,
@@ -52,42 +111,53 @@ struct OpenAIEmbeddingUsage {
 
 #[async_trait]
 pub trait EmbeddingProvider: Sync + Send {
-    async fn embed_batch(&self, spans: Vec<&str>) -> Result<Vec<Vec<f32>>>;
+    async fn embed_batch(&self, spans: Vec<String>) -> Result<Vec<Embedding>>;
+    fn max_tokens_per_batch(&self) -> usize;
+    fn truncate(&self, span: &str) -> (String, usize);
 }
 
 pub struct DummyEmbeddings {}
 
 #[async_trait]
 impl EmbeddingProvider for DummyEmbeddings {
-    async fn embed_batch(&self, spans: Vec<&str>) -> Result<Vec<Vec<f32>>> {
+    async fn embed_batch(&self, spans: Vec<String>) -> Result<Vec<Embedding>> {
         // 1024 is the OpenAI Embeddings size for ada models.
         // the model we will likely be starting with.
-        let dummy_vec = vec![0.32 as f32; 1536];
+        let dummy_vec = Embedding::from(vec![0.32 as f32; 1536]);
         return Ok(vec![dummy_vec; spans.len()]);
     }
-}
 
-const OPENAI_INPUT_LIMIT: usize = 8190;
+    fn max_tokens_per_batch(&self) -> usize {
+        OPENAI_INPUT_LIMIT
+    }
 
-impl OpenAIEmbeddings {
-    fn truncate(span: String) -> String {
-        let mut tokens = OPENAI_BPE_TOKENIZER.encode_with_special_tokens(span.as_ref());
-        if tokens.len() > OPENAI_INPUT_LIMIT {
+    fn truncate(&self, span: &str) -> (String, usize) {
+        let mut tokens = OPENAI_BPE_TOKENIZER.encode_with_special_tokens(span);
+        let token_count = tokens.len();
+        let output = if token_count > OPENAI_INPUT_LIMIT {
             tokens.truncate(OPENAI_INPUT_LIMIT);
-            let result = OPENAI_BPE_TOKENIZER.decode(tokens.clone());
-            if result.is_ok() {
-                let transformed = result.unwrap();
-                return transformed;
-            }
-        }
+            let new_input = OPENAI_BPE_TOKENIZER.decode(tokens.clone());
+            new_input.ok().unwrap_or_else(|| span.to_string())
+        } else {
+            span.to_string()
+        };
 
-        span
+        (output, tokens.len())
     }
+}
+
+const OPENAI_INPUT_LIMIT: usize = 8190;
 
-    async fn send_request(&self, api_key: &str, spans: Vec<&str>) -> Result<Response<AsyncBody>> {
+impl OpenAIEmbeddings {
+    async fn send_request(
+        &self,
+        api_key: &str,
+        spans: Vec<&str>,
+        request_timeout: u64,
+    ) -> Result<Response<AsyncBody>> {
         let request = Request::post("https://api.openai.com/v1/embeddings")
             .redirect_policy(isahc::config::RedirectPolicy::Follow)
-            .timeout(Duration::from_secs(4))
+            .timeout(Duration::from_secs(request_timeout))
             .header("Content-Type", "application/json")
             .header("Authorization", format!("Bearer {}", api_key))
             .body(
@@ -105,7 +175,26 @@ impl OpenAIEmbeddings {
 
 #[async_trait]
 impl EmbeddingProvider for OpenAIEmbeddings {
-    async fn embed_batch(&self, spans: Vec<&str>) -> Result<Vec<Vec<f32>>> {
+    fn max_tokens_per_batch(&self) -> usize {
+        50000
+    }
+
+    fn truncate(&self, span: &str) -> (String, usize) {
+        let mut tokens = OPENAI_BPE_TOKENIZER.encode_with_special_tokens(span);
+        let output = if tokens.len() > OPENAI_INPUT_LIMIT {
+            tokens.truncate(OPENAI_INPUT_LIMIT);
+            OPENAI_BPE_TOKENIZER
+                .decode(tokens.clone())
+                .ok()
+                .unwrap_or_else(|| span.to_string())
+        } else {
+            span.to_string()
+        };
+
+        (output, tokens.len())
+    }
+
+    async fn embed_batch(&self, spans: Vec<String>) -> Result<Vec<Embedding>> {
         const BACKOFF_SECONDS: [usize; 4] = [3, 5, 15, 45];
         const MAX_RETRIES: usize = 4;
 
@@ -114,45 +203,21 @@ impl EmbeddingProvider for OpenAIEmbeddings {
             .ok_or_else(|| anyhow!("no api key"))?;
 
         let mut request_number = 0;
-        let mut truncated = false;
+        let mut request_timeout: u64 = 15;
         let mut response: Response<AsyncBody>;
-        let mut spans: Vec<String> = spans.iter().map(|x| x.to_string()).collect();
         while request_number < MAX_RETRIES {
             response = self
-                .send_request(api_key, spans.iter().map(|x| &**x).collect())
+                .send_request(
+                    api_key,
+                    spans.iter().map(|x| &**x).collect(),
+                    request_timeout,
+                )
                 .await?;
             request_number += 1;
 
-            if request_number + 1 == MAX_RETRIES && response.status() != StatusCode::OK {
-                return Err(anyhow!(
-                    "openai max retries, error: {:?}",
-                    &response.status()
-                ));
-            }
-
             match response.status() {
-                StatusCode::TOO_MANY_REQUESTS => {
-                    let delay = Duration::from_secs(BACKOFF_SECONDS[request_number - 1] as u64);
-                    log::trace!(
-                        "open ai rate limiting, delaying request by {:?} seconds",
-                        delay.as_secs()
-                    );
-                    self.executor.timer(delay).await;
-                }
-                StatusCode::BAD_REQUEST => {
-                    // Only truncate if it hasnt been truncated before
-                    if !truncated {
-                        for span in spans.iter_mut() {
-                            *span = Self::truncate(span.clone());
-                        }
-                        truncated = true;
-                    } else {
-                        // If failing once already truncated, log the error and break the loop
-                        let mut body = String::new();
-                        response.body_mut().read_to_string(&mut body).await?;
-                        log::trace!("open ai bad request: {:?} {:?}", &response.status(), body);
-                        break;
-                    }
+                StatusCode::REQUEST_TIMEOUT => {
+                    request_timeout += 5;
                 }
                 StatusCode::OK => {
                     let mut body = String::new();
@@ -163,18 +228,96 @@ impl EmbeddingProvider for OpenAIEmbeddings {
                         "openai embedding completed. tokens: {:?}",
                         response.usage.total_tokens
                     );
+
                     return Ok(response
                         .data
                         .into_iter()
-                        .map(|embedding| embedding.embedding)
+                        .map(|embedding| Embedding::from(embedding.embedding))
                         .collect());
                 }
+                StatusCode::TOO_MANY_REQUESTS => {
+                    let mut body = String::new();
+                    response.body_mut().read_to_string(&mut body).await?;
+
+                    let delay_duration = {
+                        let delay = Duration::from_secs(BACKOFF_SECONDS[request_number - 1] as u64);
+                        if let Some(time_to_reset) =
+                            response.headers().get("x-ratelimit-reset-tokens")
+                        {
+                            if let Ok(time_str) = time_to_reset.to_str() {
+                                parse(time_str).unwrap_or(delay)
+                            } else {
+                                delay
+                            }
+                        } else {
+                            delay
+                        }
+                    };
+
+                    log::trace!(
+                        "openai rate limiting: waiting {:?} until lifted",
+                        &delay_duration
+                    );
+
+                    self.executor.timer(delay_duration).await;
+                }
                 _ => {
-                    return Err(anyhow!("openai embedding failed {}", response.status()));
+                    let mut body = String::new();
+                    response.body_mut().read_to_string(&mut body).await?;
+                    return Err(anyhow!(
+                        "open ai bad request: {:?} {:?}",
+                        &response.status(),
+                        body
+                    ));
                 }
             }
         }
+        Err(anyhow!("openai max retries"))
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use rand::prelude::*;
+
+    #[gpui::test]
+    fn test_similarity(mut rng: StdRng) {
+        assert_eq!(
+            Embedding::from(vec![1., 0., 0., 0., 0.])
+                .similarity(&Embedding::from(vec![0., 1., 0., 0., 0.])),
+            0.
+        );
+        assert_eq!(
+            Embedding::from(vec![2., 0., 0., 0., 0.])
+                .similarity(&Embedding::from(vec![3., 1., 0., 0., 0.])),
+            6.
+        );
 
-        Err(anyhow!("openai embedding failed"))
+        for _ in 0..100 {
+            let size = 1536;
+            let mut a = vec![0.; size];
+            let mut b = vec![0.; size];
+            for (a, b) in a.iter_mut().zip(b.iter_mut()) {
+                *a = rng.gen();
+                *b = rng.gen();
+            }
+            let a = Embedding::from(a);
+            let b = Embedding::from(b);
+
+            assert_eq!(
+                round_to_decimals(a.similarity(&b), 1),
+                round_to_decimals(reference_dot(&a.0, &b.0), 1)
+            );
+        }
+
+        fn round_to_decimals(n: f32, decimal_places: i32) -> f32 {
+            let factor = (10.0 as f32).powi(decimal_places);
+            (n * factor).round() / factor
+        }
+
+        fn reference_dot(a: &[f32], b: &[f32]) -> f32 {
+            a.iter().zip(b.iter()).map(|(a, b)| a * b).sum()
+        }
     }
 }

crates/semantic_index/src/embedding_queue.rs 🔗

@@ -0,0 +1,165 @@
+use crate::{embedding::EmbeddingProvider, parsing::Span, JobHandle};
+use gpui::executor::Background;
+use parking_lot::Mutex;
+use smol::channel;
+use std::{mem, ops::Range, path::Path, sync::Arc, time::SystemTime};
+
+#[derive(Clone)]
+pub struct FileToEmbed {
+    pub worktree_id: i64,
+    pub path: Arc<Path>,
+    pub mtime: SystemTime,
+    pub spans: Vec<Span>,
+    pub job_handle: JobHandle,
+}
+
+impl std::fmt::Debug for FileToEmbed {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        f.debug_struct("FileToEmbed")
+            .field("worktree_id", &self.worktree_id)
+            .field("path", &self.path)
+            .field("mtime", &self.mtime)
+            .field("spans", &self.spans)
+            .finish_non_exhaustive()
+    }
+}
+
+impl PartialEq for FileToEmbed {
+    fn eq(&self, other: &Self) -> bool {
+        self.worktree_id == other.worktree_id
+            && self.path == other.path
+            && self.mtime == other.mtime
+            && self.spans == other.spans
+    }
+}
+
+pub struct EmbeddingQueue {
+    embedding_provider: Arc<dyn EmbeddingProvider>,
+    pending_batch: Vec<FileFragmentToEmbed>,
+    executor: Arc<Background>,
+    pending_batch_token_count: usize,
+    finished_files_tx: channel::Sender<FileToEmbed>,
+    finished_files_rx: channel::Receiver<FileToEmbed>,
+}
+
+#[derive(Clone)]
+pub struct FileFragmentToEmbed {
+    file: Arc<Mutex<FileToEmbed>>,
+    span_range: Range<usize>,
+}
+
+impl EmbeddingQueue {
+    pub fn new(embedding_provider: Arc<dyn EmbeddingProvider>, executor: Arc<Background>) -> Self {
+        let (finished_files_tx, finished_files_rx) = channel::unbounded();
+        Self {
+            embedding_provider,
+            executor,
+            pending_batch: Vec::new(),
+            pending_batch_token_count: 0,
+            finished_files_tx,
+            finished_files_rx,
+        }
+    }
+
+    pub fn push(&mut self, file: FileToEmbed) {
+        if file.spans.is_empty() {
+            self.finished_files_tx.try_send(file).unwrap();
+            return;
+        }
+
+        let file = Arc::new(Mutex::new(file));
+
+        self.pending_batch.push(FileFragmentToEmbed {
+            file: file.clone(),
+            span_range: 0..0,
+        });
+
+        let mut fragment_range = &mut self.pending_batch.last_mut().unwrap().span_range;
+        for (ix, span) in file.lock().spans.iter().enumerate() {
+            let span_token_count = if span.embedding.is_none() {
+                span.token_count
+            } else {
+                0
+            };
+
+            let next_token_count = self.pending_batch_token_count + span_token_count;
+            if next_token_count > self.embedding_provider.max_tokens_per_batch() {
+                let range_end = fragment_range.end;
+                self.flush();
+                self.pending_batch.push(FileFragmentToEmbed {
+                    file: file.clone(),
+                    span_range: range_end..range_end,
+                });
+                fragment_range = &mut self.pending_batch.last_mut().unwrap().span_range;
+            }
+
+            fragment_range.end = ix + 1;
+            self.pending_batch_token_count += span_token_count;
+        }
+    }
+
+    pub fn flush(&mut self) {
+        let batch = mem::take(&mut self.pending_batch);
+        self.pending_batch_token_count = 0;
+        if batch.is_empty() {
+            return;
+        }
+
+        let finished_files_tx = self.finished_files_tx.clone();
+        let embedding_provider = self.embedding_provider.clone();
+
+        self.executor
+            .spawn(async move {
+                let mut spans = Vec::new();
+                for fragment in &batch {
+                    let file = fragment.file.lock();
+                    spans.extend(
+                        file.spans[fragment.span_range.clone()]
+                            .iter()
+                            .filter(|d| d.embedding.is_none())
+                            .map(|d| d.content.clone()),
+                    );
+                }
+
+                // If spans is 0, just send the fragment to the finished files if its the last one.
+                if spans.is_empty() {
+                    for fragment in batch.clone() {
+                        if let Some(file) = Arc::into_inner(fragment.file) {
+                            finished_files_tx.try_send(file.into_inner()).unwrap();
+                        }
+                    }
+                    return;
+                };
+
+                match embedding_provider.embed_batch(spans).await {
+                    Ok(embeddings) => {
+                        let mut embeddings = embeddings.into_iter();
+                        for fragment in batch {
+                            for span in &mut fragment.file.lock().spans[fragment.span_range.clone()]
+                                .iter_mut()
+                                .filter(|d| d.embedding.is_none())
+                            {
+                                if let Some(embedding) = embeddings.next() {
+                                    span.embedding = Some(embedding);
+                                } else {
+                                    log::error!("number of embeddings != number of documents");
+                                }
+                            }
+
+                            if let Some(file) = Arc::into_inner(fragment.file) {
+                                finished_files_tx.try_send(file.into_inner()).unwrap();
+                            }
+                        }
+                    }
+                    Err(error) => {
+                        log::error!("{:?}", error);
+                    }
+                }
+            })
+            .detach();
+    }
+
+    pub fn finished_files(&self) -> channel::Receiver<FileToEmbed> {
+        self.finished_files_rx.clone()
+    }
+}

crates/semantic_index/src/parsing.rs 🔗

@@ -1,5 +1,10 @@
-use anyhow::{anyhow, Ok, Result};
+use crate::embedding::{Embedding, EmbeddingProvider};
+use anyhow::{anyhow, Result};
 use language::{Grammar, Language};
+use rusqlite::{
+    types::{FromSql, FromSqlResult, ToSqlOutput, ValueRef},
+    ToSql,
+};
 use sha1::{Digest, Sha1};
 use std::{
     cmp::{self, Reverse},
@@ -10,13 +15,44 @@ use std::{
 };
 use tree_sitter::{Parser, QueryCursor};
 
+#[derive(Debug, PartialEq, Eq, Clone, Hash)]
+pub struct SpanDigest([u8; 20]);
+
+impl FromSql for SpanDigest {
+    fn column_result(value: ValueRef) -> FromSqlResult<Self> {
+        let blob = value.as_blob()?;
+        let bytes =
+            blob.try_into()
+                .map_err(|_| rusqlite::types::FromSqlError::InvalidBlobSize {
+                    expected_size: 20,
+                    blob_size: blob.len(),
+                })?;
+        return Ok(SpanDigest(bytes));
+    }
+}
+
+impl ToSql for SpanDigest {
+    fn to_sql(&self) -> rusqlite::Result<ToSqlOutput> {
+        self.0.to_sql()
+    }
+}
+
+impl From<&'_ str> for SpanDigest {
+    fn from(value: &'_ str) -> Self {
+        let mut sha1 = Sha1::new();
+        sha1.update(value);
+        Self(sha1.finalize().into())
+    }
+}
+
 #[derive(Debug, PartialEq, Clone)]
-pub struct Document {
+pub struct Span {
     pub name: String,
     pub range: Range<usize>,
     pub content: String,
-    pub embedding: Vec<f32>,
-    pub sha1: [u8; 20],
+    pub embedding: Option<Embedding>,
+    pub digest: SpanDigest,
+    pub token_count: usize,
 }
 
 const CODE_CONTEXT_TEMPLATE: &str =
@@ -30,6 +66,7 @@ pub const PARSEABLE_ENTIRE_FILE_TYPES: &[&str] =
 pub struct CodeContextRetriever {
     pub parser: Parser,
     pub cursor: QueryCursor,
+    pub embedding_provider: Arc<dyn EmbeddingProvider>,
 }
 
 // Every match has an item, this represents the fundamental treesitter symbol and anchors the search
@@ -47,10 +84,11 @@ pub struct CodeContextMatch {
 }
 
 impl CodeContextRetriever {
-    pub fn new() -> Self {
+    pub fn new(embedding_provider: Arc<dyn EmbeddingProvider>) -> Self {
         Self {
             parser: Parser::new(),
             cursor: QueryCursor::new(),
+            embedding_provider,
         }
     }
 
@@ -59,38 +97,36 @@ impl CodeContextRetriever {
         relative_path: &Path,
         language_name: Arc<str>,
         content: &str,
-    ) -> Result<Vec<Document>> {
+    ) -> Result<Vec<Span>> {
         let document_span = ENTIRE_FILE_TEMPLATE
             .replace("<path>", relative_path.to_string_lossy().as_ref())
             .replace("<language>", language_name.as_ref())
             .replace("<item>", &content);
-
-        let mut sha1 = Sha1::new();
-        sha1.update(&document_span);
-
-        Ok(vec![Document {
+        let digest = SpanDigest::from(document_span.as_str());
+        let (document_span, token_count) = self.embedding_provider.truncate(&document_span);
+        Ok(vec![Span {
             range: 0..content.len(),
             content: document_span,
-            embedding: Vec::new(),
+            embedding: Default::default(),
             name: language_name.to_string(),
-            sha1: sha1.finalize().into(),
+            digest,
+            token_count,
         }])
     }
 
-    fn parse_markdown_file(&self, relative_path: &Path, content: &str) -> Result<Vec<Document>> {
+    fn parse_markdown_file(&self, relative_path: &Path, content: &str) -> Result<Vec<Span>> {
         let document_span = MARKDOWN_CONTEXT_TEMPLATE
             .replace("<path>", relative_path.to_string_lossy().as_ref())
             .replace("<item>", &content);
-
-        let mut sha1 = Sha1::new();
-        sha1.update(&document_span);
-
-        Ok(vec![Document {
+        let digest = SpanDigest::from(document_span.as_str());
+        let (document_span, token_count) = self.embedding_provider.truncate(&document_span);
+        Ok(vec![Span {
             range: 0..content.len(),
             content: document_span,
-            embedding: Vec::new(),
+            embedding: None,
             name: "Markdown".to_string(),
-            sha1: sha1.finalize().into(),
+            digest,
+            token_count,
         }])
     }
 
@@ -155,26 +191,32 @@ impl CodeContextRetriever {
         relative_path: &Path,
         content: &str,
         language: Arc<Language>,
-    ) -> Result<Vec<Document>> {
+    ) -> Result<Vec<Span>> {
         let language_name = language.name();
 
         if PARSEABLE_ENTIRE_FILE_TYPES.contains(&language_name.as_ref()) {
             return self.parse_entire_file(relative_path, language_name, &content);
-        } else if &language_name.to_string() == &"Markdown".to_string() {
+        } else if language_name.as_ref() == "Markdown" {
             return self.parse_markdown_file(relative_path, &content);
         }
 
-        let mut documents = self.parse_file(content, language)?;
-        for document in &mut documents {
-            document.content = CODE_CONTEXT_TEMPLATE
+        let mut spans = self.parse_file(content, language)?;
+        for span in &mut spans {
+            let document_content = CODE_CONTEXT_TEMPLATE
                 .replace("<path>", relative_path.to_string_lossy().as_ref())
                 .replace("<language>", language_name.as_ref())
-                .replace("item", &document.content);
+                .replace("item", &span.content);
+
+            let (document_content, token_count) =
+                self.embedding_provider.truncate(&document_content);
+
+            span.content = document_content;
+            span.token_count = token_count;
         }
-        Ok(documents)
+        Ok(spans)
     }
 
-    pub fn parse_file(&mut self, content: &str, language: Arc<Language>) -> Result<Vec<Document>> {
+    pub fn parse_file(&mut self, content: &str, language: Arc<Language>) -> Result<Vec<Span>> {
         let grammar = language
             .grammar()
             .ok_or_else(|| anyhow!("no grammar for language"))?;
@@ -185,7 +227,7 @@ impl CodeContextRetriever {
         let language_scope = language.default_scope();
         let placeholder = language_scope.collapsed_placeholder();
 
-        let mut documents = Vec::new();
+        let mut spans = Vec::new();
         let mut collapsed_ranges_within = Vec::new();
         let mut parsed_name_ranges = HashSet::new();
         for (i, context_match) in matches.iter().enumerate() {
@@ -225,22 +267,22 @@ impl CodeContextRetriever {
 
             collapsed_ranges_within.sort_by_key(|r| (r.start, Reverse(r.end)));
 
-            let mut document_content = String::new();
+            let mut span_content = String::new();
             for context_range in &context_match.context_ranges {
                 add_content_from_range(
-                    &mut document_content,
+                    &mut span_content,
                     content,
                     context_range.clone(),
                     context_match.start_col,
                 );
-                document_content.push_str("\n");
+                span_content.push_str("\n");
             }
 
             let mut offset = item_range.start;
             for collapsed_range in &collapsed_ranges_within {
                 if collapsed_range.start > offset {
                     add_content_from_range(
-                        &mut document_content,
+                        &mut span_content,
                         content,
                         offset..collapsed_range.start,
                         context_match.start_col,
@@ -249,33 +291,32 @@ impl CodeContextRetriever {
                 }
 
                 if collapsed_range.end > offset {
-                    document_content.push_str(placeholder);
+                    span_content.push_str(placeholder);
                     offset = collapsed_range.end;
                 }
             }
 
             if offset < item_range.end {
                 add_content_from_range(
-                    &mut document_content,
+                    &mut span_content,
                     content,
                     offset..item_range.end,
                     context_match.start_col,
                 );
             }
 
-            let mut sha1 = Sha1::new();
-            sha1.update(&document_content);
-
-            documents.push(Document {
+            let sha1 = SpanDigest::from(span_content.as_str());
+            spans.push(Span {
                 name,
-                content: document_content,
+                content: span_content,
                 range: item_range.clone(),
-                embedding: vec![],
-                sha1: sha1.finalize().into(),
+                embedding: None,
+                digest: sha1,
+                token_count: 0,
             })
         }
 
-        return Ok(documents);
+        return Ok(spans);
     }
 }
 

crates/semantic_index/src/semantic_index.rs 🔗

@@ -1,5 +1,6 @@
 mod db;
 mod embedding;
+mod embedding_queue;
 mod parsing;
 pub mod semantic_index_settings;
 
@@ -8,24 +9,25 @@ mod semantic_index_tests;
 
 use crate::semantic_index_settings::SemanticIndexSettings;
 use anyhow::{anyhow, Result};
+use collections::{BTreeMap, HashMap, HashSet};
 use db::VectorDatabase;
-use embedding::{EmbeddingProvider, OpenAIEmbeddings};
-use futures::{channel::oneshot, Future};
+use embedding::{Embedding, EmbeddingProvider, OpenAIEmbeddings};
+use embedding_queue::{EmbeddingQueue, FileToEmbed};
+use futures::{future, FutureExt, StreamExt};
 use gpui::{AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle, Task, WeakModelHandle};
-use language::{Anchor, Buffer, Language, LanguageRegistry};
+use language::{Anchor, Bias, Buffer, Language, LanguageRegistry};
 use parking_lot::Mutex;
-use parsing::{CodeContextRetriever, Document, PARSEABLE_ENTIRE_FILE_TYPES};
+use parsing::{CodeContextRetriever, SpanDigest, PARSEABLE_ENTIRE_FILE_TYPES};
 use postage::watch;
-use project::{search::PathMatcher, Fs, PathChange, Project, ProjectEntryId, WorktreeId};
+use project::{search::PathMatcher, Fs, PathChange, Project, ProjectEntryId, Worktree, WorktreeId};
 use smol::channel;
 use std::{
     cmp::Ordering,
-    collections::HashMap,
-    mem,
+    future::Future,
     ops::Range,
     path::{Path, PathBuf},
     sync::{Arc, Weak},
-    time::{Instant, SystemTime},
+    time::{Duration, Instant, SystemTime},
 };
 use util::{
     channel::{ReleaseChannel, RELEASE_CHANNEL, RELEASE_CHANNEL_NAME},
@@ -33,10 +35,10 @@ use util::{
     paths::EMBEDDINGS_DIR,
     ResultExt,
 };
-use workspace::WorkspaceCreated;
 
-const SEMANTIC_INDEX_VERSION: usize = 7;
-const EMBEDDINGS_BATCH_SIZE: usize = 80;
+const SEMANTIC_INDEX_VERSION: usize = 10;
+const BACKGROUND_INDEXING_DELAY: Duration = Duration::from_secs(5 * 60);
+const EMBEDDING_QUEUE_FLUSH_TIMEOUT: Duration = Duration::from_millis(250);
 
 pub fn init(
     fs: Arc<dyn Fs>,
@@ -55,24 +57,6 @@ pub fn init(
         return;
     }
 
-    cx.subscribe_global::<WorkspaceCreated, _>({
-        move |event, cx| {
-            let Some(semantic_index) = SemanticIndex::global(cx) else {
-                return;
-            };
-            let workspace = &event.0;
-            if let Some(workspace) = workspace.upgrade(cx) {
-                let project = workspace.read(cx).project().clone();
-                if project.read(cx).is_local() {
-                    semantic_index.update(cx, |index, cx| {
-                        index.initialize_project(project, cx).detach_and_log_err(cx)
-                    });
-                }
-            }
-        }
-    })
-    .detach();
-
     cx.spawn(move |mut cx| async move {
         let semantic_index = SemanticIndex::new(
             fs,
@@ -97,29 +81,87 @@ pub fn init(
 
 pub struct SemanticIndex {
     fs: Arc<dyn Fs>,
-    database_url: Arc<PathBuf>,
+    db: VectorDatabase,
     embedding_provider: Arc<dyn EmbeddingProvider>,
     language_registry: Arc<LanguageRegistry>,
-    db_update_tx: channel::Sender<DbOperation>,
-    parsing_files_tx: channel::Sender<PendingFile>,
-    _db_update_task: Task<()>,
-    _embed_batch_tasks: Vec<Task<()>>,
-    _batch_files_task: Task<()>,
+    parsing_files_tx: channel::Sender<(Arc<HashMap<SpanDigest, Embedding>>, PendingFile)>,
+    _embedding_task: Task<()>,
     _parsing_files_tasks: Vec<Task<()>>,
     projects: HashMap<WeakModelHandle<Project>, ProjectState>,
 }
 
 struct ProjectState {
-    worktree_db_ids: Vec<(WorktreeId, i64)>,
+    worktrees: HashMap<WorktreeId, WorktreeState>,
+    pending_file_count_rx: watch::Receiver<usize>,
+    pending_file_count_tx: Arc<Mutex<watch::Sender<usize>>>,
     _subscription: gpui::Subscription,
-    outstanding_job_count_rx: watch::Receiver<usize>,
-    _outstanding_job_count_tx: Arc<Mutex<watch::Sender<usize>>>,
-    job_queue_tx: channel::Sender<IndexOperation>,
-    _queue_update_task: Task<()>,
+}
+
+enum WorktreeState {
+    Registering(RegisteringWorktreeState),
+    Registered(RegisteredWorktreeState),
+}
+
+impl WorktreeState {
+    fn paths_changed(
+        &mut self,
+        changes: Arc<[(Arc<Path>, ProjectEntryId, PathChange)]>,
+        worktree: &Worktree,
+    ) {
+        let changed_paths = match self {
+            Self::Registering(state) => &mut state.changed_paths,
+            Self::Registered(state) => &mut state.changed_paths,
+        };
+
+        for (path, entry_id, change) in changes.iter() {
+            let Some(entry) = worktree.entry_for_id(*entry_id) else {
+                continue;
+            };
+            if entry.is_ignored || entry.is_symlink || entry.is_external || entry.is_dir() {
+                continue;
+            }
+            changed_paths.insert(
+                path.clone(),
+                ChangedPathInfo {
+                    mtime: entry.mtime,
+                    is_deleted: *change == PathChange::Removed,
+                },
+            );
+        }
+    }
+}
+
+struct RegisteringWorktreeState {
+    changed_paths: BTreeMap<Arc<Path>, ChangedPathInfo>,
+    done_rx: watch::Receiver<Option<()>>,
+    _registration: Task<()>,
+}
+
+impl RegisteringWorktreeState {
+    fn done(&self) -> impl Future<Output = ()> {
+        let mut done_rx = self.done_rx.clone();
+        async move {
+            while let Some(result) = done_rx.next().await {
+                if result.is_some() {
+                    break;
+                }
+            }
+        }
+    }
+}
+
+struct RegisteredWorktreeState {
+    db_id: i64,
+    changed_paths: BTreeMap<Arc<Path>, ChangedPathInfo>,
+}
+
+struct ChangedPathInfo {
+    mtime: SystemTime,
+    is_deleted: bool,
 }
 
 #[derive(Clone)]
-struct JobHandle {
+pub struct JobHandle {
     /// The outer Arc is here to count the clones of a JobHandle instance;
     /// when the last handle to a given job is dropped, we decrement a counter (just once).
     tx: Arc<Weak<Mutex<watch::Sender<usize>>>>,
@@ -133,94 +175,25 @@ impl JobHandle {
         }
     }
 }
-impl ProjectState {
-    fn new(
-        cx: &mut AppContext,
-        subscription: gpui::Subscription,
-        worktree_db_ids: Vec<(WorktreeId, i64)>,
-        outstanding_job_count_rx: watch::Receiver<usize>,
-        _outstanding_job_count_tx: Arc<Mutex<watch::Sender<usize>>>,
-    ) -> Self {
-        let (job_queue_tx, job_queue_rx) = channel::unbounded();
-        let _queue_update_task = cx.background().spawn({
-            let mut worktree_queue = HashMap::new();
-            async move {
-                while let Ok(operation) = job_queue_rx.recv().await {
-                    Self::update_queue(&mut worktree_queue, operation);
-                }
-            }
-        });
 
+impl ProjectState {
+    fn new(subscription: gpui::Subscription) -> Self {
+        let (pending_file_count_tx, pending_file_count_rx) = watch::channel_with(0);
+        let pending_file_count_tx = Arc::new(Mutex::new(pending_file_count_tx));
         Self {
-            worktree_db_ids,
-            outstanding_job_count_rx,
-            _outstanding_job_count_tx,
+            worktrees: Default::default(),
+            pending_file_count_rx,
+            pending_file_count_tx,
             _subscription: subscription,
-            _queue_update_task,
-            job_queue_tx,
-        }
-    }
-
-    pub fn get_outstanding_count(&self) -> usize {
-        self.outstanding_job_count_rx.borrow().clone()
-    }
-
-    fn update_queue(queue: &mut HashMap<PathBuf, IndexOperation>, operation: IndexOperation) {
-        match operation {
-            IndexOperation::FlushQueue => {
-                let queue = std::mem::take(queue);
-                for (_, op) in queue {
-                    match op {
-                        IndexOperation::IndexFile {
-                            absolute_path: _,
-                            payload,
-                            tx,
-                        } => {
-                            let _ = tx.try_send(payload);
-                        }
-                        IndexOperation::DeleteFile {
-                            absolute_path: _,
-                            payload,
-                            tx,
-                        } => {
-                            let _ = tx.try_send(payload);
-                        }
-                        _ => {}
-                    }
-                }
-            }
-            IndexOperation::IndexFile {
-                ref absolute_path, ..
-            }
-            | IndexOperation::DeleteFile {
-                ref absolute_path, ..
-            } => {
-                queue.insert(absolute_path.clone(), operation);
-            }
         }
     }
 
-    fn db_id_for_worktree_id(&self, id: WorktreeId) -> Option<i64> {
-        self.worktree_db_ids
-            .iter()
-            .find_map(|(worktree_id, db_id)| {
-                if *worktree_id == id {
-                    Some(*db_id)
-                } else {
-                    None
-                }
-            })
-    }
-
     fn worktree_id_for_db_id(&self, id: i64) -> Option<WorktreeId> {
-        self.worktree_db_ids
+        self.worktrees
             .iter()
-            .find_map(|(worktree_id, db_id)| {
-                if *db_id == id {
-                    Some(*worktree_id)
-                } else {
-                    None
-                }
+            .find_map(|(worktree_id, worktree_state)| match worktree_state {
+                WorktreeState::Registered(state) if state.db_id == id => Some(*worktree_id),
+                _ => None,
             })
     }
 }
@@ -228,68 +201,18 @@ impl ProjectState {
 #[derive(Clone)]
 pub struct PendingFile {
     worktree_db_id: i64,
-    relative_path: PathBuf,
+    relative_path: Arc<Path>,
     absolute_path: PathBuf,
-    language: Arc<Language>,
+    language: Option<Arc<Language>>,
     modified_time: SystemTime,
     job_handle: JobHandle,
 }
-enum IndexOperation {
-    IndexFile {
-        absolute_path: PathBuf,
-        payload: PendingFile,
-        tx: channel::Sender<PendingFile>,
-    },
-    DeleteFile {
-        absolute_path: PathBuf,
-        payload: DbOperation,
-        tx: channel::Sender<DbOperation>,
-    },
-    FlushQueue,
-}
 
 pub struct SearchResult {
     pub buffer: ModelHandle<Buffer>,
     pub range: Range<Anchor>,
 }
 
-enum DbOperation {
-    InsertFile {
-        worktree_id: i64,
-        documents: Vec<Document>,
-        path: PathBuf,
-        mtime: SystemTime,
-        job_handle: JobHandle,
-    },
-    Delete {
-        worktree_id: i64,
-        path: PathBuf,
-    },
-    FindOrCreateWorktree {
-        path: PathBuf,
-        sender: oneshot::Sender<Result<i64>>,
-    },
-    FileMTimes {
-        worktree_id: i64,
-        sender: oneshot::Sender<Result<HashMap<PathBuf, SystemTime>>>,
-    },
-    WorktreePreviouslyIndexed {
-        path: Arc<Path>,
-        sender: oneshot::Sender<Result<bool>>,
-    },
-}
-
-enum EmbeddingJob {
-    Enqueue {
-        worktree_id: i64,
-        path: PathBuf,
-        mtime: SystemTime,
-        documents: Vec<Document>,
-        job_handle: JobHandle,
-    },
-    Flush,
-}
-
 impl SemanticIndex {
     pub fn global(cx: &AppContext) -> Option<ModelHandle<SemanticIndex>> {
         if cx.has_global::<ModelHandle<Self>>() {
@@ -306,18 +229,14 @@ impl SemanticIndex {
 
     async fn new(
         fs: Arc<dyn Fs>,
-        database_url: PathBuf,
+        database_path: PathBuf,
         embedding_provider: Arc<dyn EmbeddingProvider>,
         language_registry: Arc<LanguageRegistry>,
         mut cx: AsyncAppContext,
     ) -> Result<ModelHandle<Self>> {
         let t0 = Instant::now();
-        let database_url = Arc::new(database_url);
-
-        let db = cx
-            .background()
-            .spawn(VectorDatabase::new(fs.clone(), database_url.clone()))
-            .await?;
+        let database_path = Arc::from(database_path);
+        let db = VectorDatabase::new(fs.clone(), database_path, cx.background()).await?;
 
         log::trace!(
             "db initialization took {:?} milliseconds",
@@ -326,73 +245,55 @@ impl SemanticIndex {
 
         Ok(cx.add_model(|cx| {
             let t0 = Instant::now();
-            // Perform database operations
-            let (db_update_tx, db_update_rx) = channel::unbounded();
-            let _db_update_task = cx.background().spawn({
+            let embedding_queue =
+                EmbeddingQueue::new(embedding_provider.clone(), cx.background().clone());
+            let _embedding_task = cx.background().spawn({
+                let embedded_files = embedding_queue.finished_files();
+                let db = db.clone();
                 async move {
-                    while let Ok(job) = db_update_rx.recv().await {
-                        Self::run_db_operation(&db, job)
-                    }
-                }
-            });
-
-            // Group documents into batches and send them to the embedding provider.
-            let (embed_batch_tx, embed_batch_rx) =
-                channel::unbounded::<Vec<(i64, Vec<Document>, PathBuf, SystemTime, JobHandle)>>();
-            let mut _embed_batch_tasks = Vec::new();
-            for _ in 0..cx.background().num_cpus() {
-                let embed_batch_rx = embed_batch_rx.clone();
-                _embed_batch_tasks.push(cx.background().spawn({
-                    let db_update_tx = db_update_tx.clone();
-                    let embedding_provider = embedding_provider.clone();
-                    async move {
-                        while let Ok(embeddings_queue) = embed_batch_rx.recv().await {
-                            Self::compute_embeddings_for_batch(
-                                embeddings_queue,
-                                &embedding_provider,
-                                &db_update_tx,
-                            )
-                            .await;
-                        }
+                    while let Ok(file) = embedded_files.recv().await {
+                        db.insert_file(file.worktree_id, file.path, file.mtime, file.spans)
+                            .await
+                            .log_err();
                     }
-                }));
-            }
-
-            // Group documents into batches and send them to the embedding provider.
-            let (batch_files_tx, batch_files_rx) = channel::unbounded::<EmbeddingJob>();
-            let _batch_files_task = cx.background().spawn(async move {
-                let mut queue_len = 0;
-                let mut embeddings_queue = vec![];
-                while let Ok(job) = batch_files_rx.recv().await {
-                    Self::enqueue_documents_to_embed(
-                        job,
-                        &mut queue_len,
-                        &mut embeddings_queue,
-                        &embed_batch_tx,
-                    );
                 }
             });
 
-            // Parse files into embeddable documents.
-            let (parsing_files_tx, parsing_files_rx) = channel::unbounded::<PendingFile>();
+            // Parse files into embeddable spans.
+            let (parsing_files_tx, parsing_files_rx) =
+                channel::unbounded::<(Arc<HashMap<SpanDigest, Embedding>>, PendingFile)>();
+            let embedding_queue = Arc::new(Mutex::new(embedding_queue));
             let mut _parsing_files_tasks = Vec::new();
             for _ in 0..cx.background().num_cpus() {
                 let fs = fs.clone();
-                let parsing_files_rx = parsing_files_rx.clone();
-                let batch_files_tx = batch_files_tx.clone();
-                let db_update_tx = db_update_tx.clone();
+                let mut parsing_files_rx = parsing_files_rx.clone();
+                let embedding_provider = embedding_provider.clone();
+                let embedding_queue = embedding_queue.clone();
+                let background = cx.background().clone();
                 _parsing_files_tasks.push(cx.background().spawn(async move {
-                    let mut retriever = CodeContextRetriever::new();
-                    while let Ok(pending_file) = parsing_files_rx.recv().await {
-                        Self::parse_file(
-                            &fs,
-                            pending_file,
-                            &mut retriever,
-                            &batch_files_tx,
-                            &parsing_files_rx,
-                            &db_update_tx,
-                        )
-                        .await;
+                    let mut retriever = CodeContextRetriever::new(embedding_provider.clone());
+                    loop {
+                        let mut timer = background.timer(EMBEDDING_QUEUE_FLUSH_TIMEOUT).fuse();
+                        let mut next_file_to_parse = parsing_files_rx.next().fuse();
+                        futures::select_biased! {
+                            next_file_to_parse = next_file_to_parse => {
+                                if let Some((embeddings_for_digest, pending_file)) = next_file_to_parse {
+                                    Self::parse_file(
+                                        &fs,
+                                        pending_file,
+                                        &mut retriever,
+                                        &embedding_queue,
+                                        &embeddings_for_digest,
+                                    )
+                                    .await
+                                } else {
+                                    break;
+                                }
+                            },
+                            _ = timer => {
+                                embedding_queue.lock().flush();
+                            }
+                        }
                     }
                 }));
             }
@@ -403,260 +304,56 @@ impl SemanticIndex {
             );
             Self {
                 fs,
-                database_url,
+                db,
                 embedding_provider,
                 language_registry,
-                db_update_tx,
                 parsing_files_tx,
-                _db_update_task,
-                _embed_batch_tasks,
-                _batch_files_task,
+                _embedding_task,
                 _parsing_files_tasks,
-                projects: HashMap::new(),
+                projects: Default::default(),
             }
         }))
     }
 
-    fn run_db_operation(db: &VectorDatabase, job: DbOperation) {
-        match job {
-            DbOperation::InsertFile {
-                worktree_id,
-                documents,
-                path,
-                mtime,
-                job_handle,
-            } => {
-                db.insert_file(worktree_id, path, mtime, documents)
-                    .log_err();
-                drop(job_handle)
-            }
-            DbOperation::Delete { worktree_id, path } => {
-                db.delete_file(worktree_id, path).log_err();
-            }
-            DbOperation::FindOrCreateWorktree { path, sender } => {
-                let id = db.find_or_create_worktree(&path);
-                sender.send(id).ok();
-            }
-            DbOperation::FileMTimes {
-                worktree_id: worktree_db_id,
-                sender,
-            } => {
-                let file_mtimes = db.get_file_mtimes(worktree_db_id);
-                sender.send(file_mtimes).ok();
-            }
-            DbOperation::WorktreePreviouslyIndexed { path, sender } => {
-                let worktree_indexed = db.worktree_previously_indexed(path.as_ref());
-                sender.send(worktree_indexed).ok();
-            }
-        }
-    }
-
-    async fn compute_embeddings_for_batch(
-        mut embeddings_queue: Vec<(i64, Vec<Document>, PathBuf, SystemTime, JobHandle)>,
-        embedding_provider: &Arc<dyn EmbeddingProvider>,
-        db_update_tx: &channel::Sender<DbOperation>,
-    ) {
-        let mut batch_documents = vec![];
-        for (_, documents, _, _, _) in embeddings_queue.iter() {
-            batch_documents.extend(documents.iter().map(|document| document.content.as_str()));
-        }
-
-        if let Ok(embeddings) = embedding_provider.embed_batch(batch_documents).await {
-            log::trace!(
-                "created {} embeddings for {} files",
-                embeddings.len(),
-                embeddings_queue.len(),
-            );
-
-            let mut i = 0;
-            let mut j = 0;
-
-            for embedding in embeddings.iter() {
-                while embeddings_queue[i].1.len() == j {
-                    i += 1;
-                    j = 0;
-                }
-
-                embeddings_queue[i].1[j].embedding = embedding.to_owned();
-                j += 1;
-            }
-
-            for (worktree_id, documents, path, mtime, job_handle) in embeddings_queue.into_iter() {
-                db_update_tx
-                    .send(DbOperation::InsertFile {
-                        worktree_id,
-                        documents,
-                        path,
-                        mtime,
-                        job_handle,
-                    })
-                    .await
-                    .unwrap();
-            }
-        } else {
-            // Insert the file in spite of failure so that future attempts to index it do not take place (unless the file is changed).
-            for (worktree_id, _, path, mtime, job_handle) in embeddings_queue.into_iter() {
-                db_update_tx
-                    .send(DbOperation::InsertFile {
-                        worktree_id,
-                        documents: vec![],
-                        path,
-                        mtime,
-                        job_handle,
-                    })
-                    .await
-                    .unwrap();
-            }
-        }
-    }
-
-    fn enqueue_documents_to_embed(
-        job: EmbeddingJob,
-        queue_len: &mut usize,
-        embeddings_queue: &mut Vec<(i64, Vec<Document>, PathBuf, SystemTime, JobHandle)>,
-        embed_batch_tx: &channel::Sender<Vec<(i64, Vec<Document>, PathBuf, SystemTime, JobHandle)>>,
-    ) {
-        // Handle edge case where individual file has more documents than max batch size
-        let should_flush = match job {
-            EmbeddingJob::Enqueue {
-                documents,
-                worktree_id,
-                path,
-                mtime,
-                job_handle,
-            } => {
-                // If documents is greater than embeddings batch size, recursively batch existing rows.
-                if &documents.len() > &EMBEDDINGS_BATCH_SIZE {
-                    let first_job = EmbeddingJob::Enqueue {
-                        documents: documents[..EMBEDDINGS_BATCH_SIZE].to_vec(),
-                        worktree_id,
-                        path: path.clone(),
-                        mtime,
-                        job_handle: job_handle.clone(),
-                    };
-
-                    Self::enqueue_documents_to_embed(
-                        first_job,
-                        queue_len,
-                        embeddings_queue,
-                        embed_batch_tx,
-                    );
-
-                    let second_job = EmbeddingJob::Enqueue {
-                        documents: documents[EMBEDDINGS_BATCH_SIZE..].to_vec(),
-                        worktree_id,
-                        path: path.clone(),
-                        mtime,
-                        job_handle: job_handle.clone(),
-                    };
-
-                    Self::enqueue_documents_to_embed(
-                        second_job,
-                        queue_len,
-                        embeddings_queue,
-                        embed_batch_tx,
-                    );
-                    return;
-                } else {
-                    *queue_len += &documents.len();
-                    embeddings_queue.push((worktree_id, documents, path, mtime, job_handle));
-                    *queue_len >= EMBEDDINGS_BATCH_SIZE
-                }
-            }
-            EmbeddingJob::Flush => true,
-        };
-
-        if should_flush {
-            embed_batch_tx
-                .try_send(mem::take(embeddings_queue))
-                .unwrap();
-            *queue_len = 0;
-        }
-    }
-
     async fn parse_file(
         fs: &Arc<dyn Fs>,
         pending_file: PendingFile,
         retriever: &mut CodeContextRetriever,
-        batch_files_tx: &channel::Sender<EmbeddingJob>,
-        parsing_files_rx: &channel::Receiver<PendingFile>,
-        db_update_tx: &channel::Sender<DbOperation>,
+        embedding_queue: &Arc<Mutex<EmbeddingQueue>>,
+        embeddings_for_digest: &HashMap<SpanDigest, Embedding>,
     ) {
+        let Some(language) = pending_file.language else {
+            return;
+        };
+
         if let Some(content) = fs.load(&pending_file.absolute_path).await.log_err() {
-            if let Some(documents) = retriever
-                .parse_file_with_template(
-                    &pending_file.relative_path,
-                    &content,
-                    pending_file.language,
-                )
+            if let Some(mut spans) = retriever
+                .parse_file_with_template(&pending_file.relative_path, &content, language)
                 .log_err()
             {
                 log::trace!(
-                    "parsed path {:?}: {} documents",
+                    "parsed path {:?}: {} spans",
                     pending_file.relative_path,
-                    documents.len()
+                    spans.len()
                 );
 
-                if documents.len() == 0 {
-                    db_update_tx
-                        .send(DbOperation::InsertFile {
-                            worktree_id: pending_file.worktree_db_id,
-                            documents,
-                            path: pending_file.relative_path,
-                            mtime: pending_file.modified_time,
-                            job_handle: pending_file.job_handle,
-                        })
-                        .await
-                        .unwrap();
-                } else {
-                    batch_files_tx
-                        .try_send(EmbeddingJob::Enqueue {
-                            worktree_id: pending_file.worktree_db_id,
-                            path: pending_file.relative_path,
-                            mtime: pending_file.modified_time,
-                            job_handle: pending_file.job_handle,
-                            documents,
-                        })
-                        .unwrap();
+                for span in &mut spans {
+                    if let Some(embedding) = embeddings_for_digest.get(&span.digest) {
+                        span.embedding = Some(embedding.to_owned());
+                    }
                 }
-            }
-        }
 
-        if parsing_files_rx.len() == 0 {
-            batch_files_tx.try_send(EmbeddingJob::Flush).unwrap();
+                embedding_queue.lock().push(FileToEmbed {
+                    worktree_id: pending_file.worktree_db_id,
+                    path: pending_file.relative_path,
+                    mtime: pending_file.modified_time,
+                    job_handle: pending_file.job_handle,
+                    spans: spans,
+                });
+            }
         }
     }
 
-    fn find_or_create_worktree(&self, path: PathBuf) -> impl Future<Output = Result<i64>> {
-        let (tx, rx) = oneshot::channel();
-        self.db_update_tx
-            .try_send(DbOperation::FindOrCreateWorktree { path, sender: tx })
-            .unwrap();
-        async move { rx.await? }
-    }
-
-    fn get_file_mtimes(
-        &self,
-        worktree_id: i64,
-    ) -> impl Future<Output = Result<HashMap<PathBuf, SystemTime>>> {
-        let (tx, rx) = oneshot::channel();
-        self.db_update_tx
-            .try_send(DbOperation::FileMTimes {
-                worktree_id,
-                sender: tx,
-            })
-            .unwrap();
-        async move { rx.await? }
-    }
-
-    fn worktree_previously_indexed(&self, path: Arc<Path>) -> impl Future<Output = Result<bool>> {
-        let (tx, rx) = oneshot::channel();
-        self.db_update_tx
-            .try_send(DbOperation::WorktreePreviouslyIndexed { path, sender: tx })
-            .unwrap();
-        async move { rx.await? }
-    }
-
     pub fn project_previously_indexed(
         &mut self,
         project: ModelHandle<Project>,
@@ -665,7 +362,10 @@ impl SemanticIndex {
         let worktrees_indexed_previously = project
             .read(cx)
             .worktrees(cx)
-            .map(|worktree| self.worktree_previously_indexed(worktree.read(cx).abs_path()))
+            .map(|worktree| {
+                self.db
+                    .worktree_previously_indexed(&worktree.read(cx).abs_path())
+            })
             .collect::<Vec<_>>();
         cx.spawn(|_, _cx| async move {
             let worktree_indexed_previously =
@@ -679,298 +379,233 @@ impl SemanticIndex {
     }
 
     fn project_entries_changed(
-        &self,
+        &mut self,
         project: ModelHandle<Project>,
+        worktree_id: WorktreeId,
         changes: Arc<[(Arc<Path>, ProjectEntryId, PathChange)]>,
-        cx: &mut ModelContext<'_, SemanticIndex>,
-        worktree_id: &WorktreeId,
-    ) -> Result<()> {
-        let parsing_files_tx = self.parsing_files_tx.clone();
-        let db_update_tx = self.db_update_tx.clone();
-        let (job_queue_tx, outstanding_job_tx, worktree_db_id) = {
-            let state = self
-                .projects
-                .get(&project.downgrade())
-                .ok_or(anyhow!("Project not yet initialized"))?;
-            let worktree_db_id = state
-                .db_id_for_worktree_id(*worktree_id)
-                .ok_or(anyhow!("Worktree ID in Database Not Available"))?;
-            (
-                state.job_queue_tx.clone(),
-                state._outstanding_job_count_tx.clone(),
-                worktree_db_id,
-            )
+        cx: &mut ModelContext<Self>,
+    ) {
+        let Some(worktree) = project.read(cx).worktree_for_id(worktree_id.clone(), cx) else {
+            return;
+        };
+        let project = project.downgrade();
+        let Some(project_state) = self.projects.get_mut(&project) else {
+            return;
         };
 
-        let language_registry = self.language_registry.clone();
-        let parsing_files_tx = parsing_files_tx.clone();
-        let db_update_tx = db_update_tx.clone();
-
-        let worktree = project
-            .read(cx)
-            .worktree_for_id(worktree_id.clone(), cx)
-            .ok_or(anyhow!("Worktree not available"))?
-            .read(cx)
-            .snapshot();
-        cx.spawn(|_, _| async move {
-            let worktree = worktree.clone();
-            for (path, entry_id, path_change) in changes.iter() {
-                let relative_path = path.to_path_buf();
-                let absolute_path = worktree.absolutize(path);
-
-                let Some(entry) = worktree.entry_for_id(*entry_id) else {
-                    continue;
-                };
-                if entry.is_ignored || entry.is_symlink || entry.is_external {
-                    continue;
-                }
-
-                log::trace!("File Event: {:?}, Path: {:?}", &path_change, &path);
-                match path_change {
-                    PathChange::AddedOrUpdated | PathChange::Updated | PathChange::Added => {
-                        if let Ok(language) = language_registry
-                            .language_for_file(&relative_path, None)
-                            .await
-                        {
-                            if !PARSEABLE_ENTIRE_FILE_TYPES.contains(&language.name().as_ref())
-                                && &language.name().as_ref() != &"Markdown"
-                                && language
-                                    .grammar()
-                                    .and_then(|grammar| grammar.embedding_config.as_ref())
-                                    .is_none()
-                            {
-                                continue;
-                            }
-
-                            let job_handle = JobHandle::new(&outstanding_job_tx);
-                            let new_operation = IndexOperation::IndexFile {
-                                absolute_path: absolute_path.clone(),
-                                payload: PendingFile {
-                                    worktree_db_id,
-                                    relative_path,
-                                    absolute_path,
-                                    language,
-                                    modified_time: entry.mtime,
-                                    job_handle,
-                                },
-                                tx: parsing_files_tx.clone(),
-                            };
-                            let _ = job_queue_tx.try_send(new_operation);
-                        }
-                    }
-                    PathChange::Removed => {
-                        let new_operation = IndexOperation::DeleteFile {
-                            absolute_path,
-                            payload: DbOperation::Delete {
-                                worktree_id: worktree_db_id,
-                                path: relative_path,
-                            },
-                            tx: db_update_tx.clone(),
-                        };
-                        let _ = job_queue_tx.try_send(new_operation);
-                    }
-                    _ => {}
+        let worktree = worktree.read(cx);
+        let worktree_state =
+            if let Some(worktree_state) = project_state.worktrees.get_mut(&worktree_id) {
+                worktree_state
+            } else {
+                return;
+            };
+        worktree_state.paths_changed(changes, worktree);
+        if let WorktreeState::Registered(_) = worktree_state {
+            cx.spawn_weak(|this, mut cx| async move {
+                cx.background().timer(BACKGROUND_INDEXING_DELAY).await;
+                if let Some((this, project)) = this.upgrade(&cx).zip(project.upgrade(&cx)) {
+                    this.update(&mut cx, |this, cx| {
+                        this.index_project(project, cx).detach_and_log_err(cx)
+                    });
                 }
-            }
-        })
-        .detach();
-
-        Ok(())
+            })
+            .detach();
+        }
     }
 
-    pub fn initialize_project(
+    fn register_worktree(
         &mut self,
         project: ModelHandle<Project>,
+        worktree: ModelHandle<Worktree>,
         cx: &mut ModelContext<Self>,
-    ) -> Task<Result<()>> {
-        log::trace!("Initializing Project for Semantic Index");
-        let worktree_scans_complete = project
-            .read(cx)
-            .worktrees(cx)
-            .map(|worktree| {
-                let scan_complete = worktree.read(cx).as_local().unwrap().scan_complete();
-                async move {
-                    scan_complete.await;
-                }
-            })
-            .collect::<Vec<_>>();
-
-        let worktree_db_ids = project
-            .read(cx)
-            .worktrees(cx)
-            .map(|worktree| {
-                self.find_or_create_worktree(worktree.read(cx).abs_path().to_path_buf())
-            })
-            .collect::<Vec<_>>();
-
-        let _subscription = cx.subscribe(&project, |this, project, event, cx| {
-            if let project::Event::WorktreeUpdatedEntries(worktree_id, changes) = event {
-                let _ =
-                    this.project_entries_changed(project.clone(), changes.clone(), cx, worktree_id);
-            };
-        });
-
+    ) {
+        let project = project.downgrade();
+        let project_state = if let Some(project_state) = self.projects.get_mut(&project) {
+            project_state
+        } else {
+            return;
+        };
+        let worktree = if let Some(worktree) = worktree.read(cx).as_local() {
+            worktree
+        } else {
+            return;
+        };
+        let worktree_abs_path = worktree.abs_path().clone();
+        let scan_complete = worktree.scan_complete();
+        let worktree_id = worktree.id();
+        let db = self.db.clone();
         let language_registry = self.language_registry.clone();
-        let parsing_files_tx = self.parsing_files_tx.clone();
-        let db_update_tx = self.db_update_tx.clone();
-
-        cx.spawn(|this, mut cx| async move {
-            futures::future::join_all(worktree_scans_complete).await;
-
-            let worktree_db_ids = futures::future::join_all(worktree_db_ids).await;
-            let worktrees = project.read_with(&cx, |project, cx| {
-                project
-                    .worktrees(cx)
-                    .map(|worktree| worktree.read(cx).snapshot())
-                    .collect::<Vec<_>>()
-            });
-
-            let mut worktree_file_mtimes = HashMap::new();
-            let mut db_ids_by_worktree_id = HashMap::new();
-
-            for (worktree, db_id) in worktrees.iter().zip(worktree_db_ids) {
-                let db_id = db_id?;
-                db_ids_by_worktree_id.insert(worktree.id(), db_id);
-                worktree_file_mtimes.insert(
-                    worktree.id(),
-                    this.read_with(&cx, |this, _| this.get_file_mtimes(db_id))
-                        .await?,
-                );
-            }
-
-            let worktree_db_ids = db_ids_by_worktree_id
-                .iter()
-                .map(|(a, b)| (*a, *b))
-                .collect();
-
-            let (job_count_tx, job_count_rx) = watch::channel_with(0);
-            let job_count_tx = Arc::new(Mutex::new(job_count_tx));
-            let job_count_tx_longlived = job_count_tx.clone();
-
-            let worktree_files = cx
-                .background()
-                .spawn(async move {
-                    let mut worktree_files = Vec::new();
-                    for worktree in worktrees.into_iter() {
-                        let mut file_mtimes = worktree_file_mtimes.remove(&worktree.id()).unwrap();
-                        let worktree_db_id = db_ids_by_worktree_id[&worktree.id()];
-                        for file in worktree.files(false, 0) {
-                            let absolute_path = worktree.absolutize(&file.path);
-
-                            if file.is_external || file.is_ignored || file.is_symlink {
-                                continue;
-                            }
-
-                            if let Ok(language) = language_registry
-                                .language_for_file(&absolute_path, None)
-                                .await
-                            {
-                                // Test if file is valid parseable file
-                                if !PARSEABLE_ENTIRE_FILE_TYPES.contains(&language.name().as_ref())
-                                    && &language.name().as_ref() != &"Markdown"
-                                    && language
-                                        .grammar()
-                                        .and_then(|grammar| grammar.embedding_config.as_ref())
-                                        .is_none()
-                                {
+        let (mut done_tx, done_rx) = watch::channel();
+        let registration = cx.spawn(|this, mut cx| {
+            async move {
+                let register = async {
+                    scan_complete.await;
+                    let db_id = db.find_or_create_worktree(worktree_abs_path).await?;
+                    let mut file_mtimes = db.get_file_mtimes(db_id).await?;
+                    let worktree = if let Some(project) = project.upgrade(&cx) {
+                        project
+                            .read_with(&cx, |project, cx| project.worktree_for_id(worktree_id, cx))
+                            .ok_or_else(|| anyhow!("worktree not found"))?
+                    } else {
+                        return anyhow::Ok(());
+                    };
+                    let worktree = worktree.read_with(&cx, |worktree, _| worktree.snapshot());
+                    let mut changed_paths = cx
+                        .background()
+                        .spawn(async move {
+                            let mut changed_paths = BTreeMap::new();
+                            for file in worktree.files(false, 0) {
+                                let absolute_path = worktree.absolutize(&file.path);
+
+                                if file.is_external || file.is_ignored || file.is_symlink {
                                     continue;
                                 }
 
-                                let path_buf = file.path.to_path_buf();
-                                let stored_mtime = file_mtimes.remove(&file.path.to_path_buf());
-                                let already_stored = stored_mtime
-                                    .map_or(false, |existing_mtime| existing_mtime == file.mtime);
-
-                                if !already_stored {
-                                    let job_handle = JobHandle::new(&job_count_tx);
-                                    worktree_files.push(IndexOperation::IndexFile {
-                                        absolute_path: absolute_path.clone(),
-                                        payload: PendingFile {
-                                            worktree_db_id,
-                                            relative_path: path_buf,
-                                            absolute_path,
-                                            language,
-                                            job_handle,
-                                            modified_time: file.mtime,
-                                        },
-                                        tx: parsing_files_tx.clone(),
-                                    });
+                                if let Ok(language) = language_registry
+                                    .language_for_file(&absolute_path, None)
+                                    .await
+                                {
+                                    // Test if file is valid parseable file
+                                    if !PARSEABLE_ENTIRE_FILE_TYPES
+                                        .contains(&language.name().as_ref())
+                                        && &language.name().as_ref() != &"Markdown"
+                                        && language
+                                            .grammar()
+                                            .and_then(|grammar| grammar.embedding_config.as_ref())
+                                            .is_none()
+                                    {
+                                        continue;
+                                    }
+
+                                    let stored_mtime = file_mtimes.remove(&file.path.to_path_buf());
+                                    let already_stored = stored_mtime
+                                        .map_or(false, |existing_mtime| {
+                                            existing_mtime == file.mtime
+                                        });
+
+                                    if !already_stored {
+                                        changed_paths.insert(
+                                            file.path.clone(),
+                                            ChangedPathInfo {
+                                                mtime: file.mtime,
+                                                is_deleted: false,
+                                            },
+                                        );
+                                    }
                                 }
                             }
-                        }
-                        // Clean up entries from database that are no longer in the worktree.
-                        for (path, _) in file_mtimes {
-                            worktree_files.push(IndexOperation::DeleteFile {
-                                absolute_path: worktree.absolutize(path.as_path()),
-                                payload: DbOperation::Delete {
-                                    worktree_id: worktree_db_id,
-                                    path,
-                                },
-                                tx: db_update_tx.clone(),
-                            });
-                        }
-                    }
 
-                    anyhow::Ok(worktree_files)
-                })
-                .await?;
+                            // Clean up entries from database that are no longer in the worktree.
+                            for (path, mtime) in file_mtimes {
+                                changed_paths.insert(
+                                    path.into(),
+                                    ChangedPathInfo {
+                                        mtime,
+                                        is_deleted: true,
+                                    },
+                                );
+                            }
 
-            this.update(&mut cx, |this, cx| {
-                let project_state = ProjectState::new(
-                    cx,
-                    _subscription,
-                    worktree_db_ids,
-                    job_count_rx,
-                    job_count_tx_longlived,
-                );
+                            anyhow::Ok(changed_paths)
+                        })
+                        .await?;
+                    this.update(&mut cx, |this, cx| {
+                        let project_state = this
+                            .projects
+                            .get_mut(&project)
+                            .ok_or_else(|| anyhow!("project not registered"))?;
+                        let project = project
+                            .upgrade(cx)
+                            .ok_or_else(|| anyhow!("project was dropped"))?;
+
+                        if let Some(WorktreeState::Registering(state)) =
+                            project_state.worktrees.remove(&worktree_id)
+                        {
+                            changed_paths.extend(state.changed_paths);
+                        }
+                        project_state.worktrees.insert(
+                            worktree_id,
+                            WorktreeState::Registered(RegisteredWorktreeState {
+                                db_id,
+                                changed_paths,
+                            }),
+                        );
+                        this.index_project(project, cx).detach_and_log_err(cx);
+
+                        anyhow::Ok(())
+                    })?;
+
+                    anyhow::Ok(())
+                };
 
-                for op in worktree_files {
-                    let _ = project_state.job_queue_tx.try_send(op);
+                if register.await.log_err().is_none() {
+                    // Stop tracking this worktree if the registration failed.
+                    this.update(&mut cx, |this, _| {
+                        this.projects.get_mut(&project).map(|project_state| {
+                            project_state.worktrees.remove(&worktree_id);
+                        });
+                    })
                 }
 
-                this.projects.insert(project.downgrade(), project_state);
-            });
-            Result::<(), _>::Ok(())
-        })
+                *done_tx.borrow_mut() = Some(());
+            }
+        });
+        project_state.worktrees.insert(
+            worktree_id,
+            WorktreeState::Registering(RegisteringWorktreeState {
+                changed_paths: Default::default(),
+                done_rx,
+                _registration: registration,
+            }),
+        );
     }
 
-    pub fn index_project(
+    fn project_worktrees_changed(
         &mut self,
         project: ModelHandle<Project>,
         cx: &mut ModelContext<Self>,
-    ) -> Task<Result<(usize, watch::Receiver<usize>)>> {
-        let state = self.projects.get_mut(&project.downgrade());
-        let state = if state.is_none() {
-            return Task::Ready(Some(Err(anyhow!("Project not yet initialized"))));
+    ) {
+        let project_state = if let Some(project_state) = self.projects.get_mut(&project.downgrade())
+        {
+            project_state
         } else {
-            state.unwrap()
+            return;
         };
 
-        // let parsing_files_tx = self.parsing_files_tx.clone();
-        // let db_update_tx = self.db_update_tx.clone();
-        let job_count_rx = state.outstanding_job_count_rx.clone();
-        let count = state.get_outstanding_count();
-
-        cx.spawn(|this, mut cx| async move {
-            this.update(&mut cx, |this, _| {
-                let Some(state) = this.projects.get_mut(&project.downgrade()) else {
-                    return;
-                };
-                let _ = state.job_queue_tx.try_send(IndexOperation::FlushQueue);
-            });
-
-            Ok((count, job_count_rx))
-        })
+        let mut worktrees = project
+            .read(cx)
+            .worktrees(cx)
+            .filter(|worktree| worktree.read(cx).is_local())
+            .collect::<Vec<_>>();
+        let worktree_ids = worktrees
+            .iter()
+            .map(|worktree| worktree.read(cx).id())
+            .collect::<HashSet<_>>();
+
+        // Remove worktrees that are no longer present
+        project_state
+            .worktrees
+            .retain(|worktree_id, _| worktree_ids.contains(worktree_id));
+
+        // Register new worktrees
+        worktrees.retain(|worktree| {
+            let worktree_id = worktree.read(cx).id();
+            !project_state.worktrees.contains_key(&worktree_id)
+        });
+        for worktree in worktrees {
+            self.register_worktree(project.clone(), worktree, cx);
+        }
     }
 
-    pub fn outstanding_job_count_rx(
+    pub fn pending_file_count(
         &self,
         project: &ModelHandle<Project>,
     ) -> Option<watch::Receiver<usize>> {
         Some(
             self.projects
                 .get(&project.downgrade())?
-                .outstanding_job_count_rx
+                .pending_file_count_rx
                 .clone(),
         )
     }

crates/semantic_index/src/semantic_index_tests.rs 🔗

@@ -1,14 +1,15 @@
 use crate::{
-    db::dot,
-    embedding::EmbeddingProvider,
-    parsing::{subtract_ranges, CodeContextRetriever, Document},
+    embedding::{DummyEmbeddings, Embedding, EmbeddingProvider},
+    embedding_queue::EmbeddingQueue,
+    parsing::{subtract_ranges, CodeContextRetriever, Span, SpanDigest},
     semantic_index_settings::SemanticIndexSettings,
-    SearchResult, SemanticIndex,
+    FileToEmbed, JobHandle, SearchResult, SemanticIndex, EMBEDDING_QUEUE_FLUSH_TIMEOUT,
 };
 use anyhow::Result;
 use async_trait::async_trait;
-use gpui::{Task, TestAppContext};
+use gpui::{executor::Deterministic, Task, TestAppContext};
 use language::{Language, LanguageConfig, LanguageRegistry, ToOffset};
+use parking_lot::Mutex;
 use pretty_assertions::assert_eq;
 use project::{project_settings::ProjectSettings, search::PathMatcher, FakeFs, Fs, Project};
 use rand::{rngs::StdRng, Rng};
@@ -20,8 +21,10 @@ use std::{
         atomic::{self, AtomicUsize},
         Arc,
     },
+    time::SystemTime,
 };
 use unindent::Unindent;
+use util::RandomCharIter;
 
 #[ctor::ctor]
 fn init_logger() {
@@ -31,12 +34,8 @@ fn init_logger() {
 }
 
 #[gpui::test]
-async fn test_semantic_index(cx: &mut TestAppContext) {
-    cx.update(|cx| {
-        cx.set_global(SettingsStore::test(cx));
-        settings::register::<SemanticIndexSettings>(cx);
-        settings::register::<ProjectSettings>(cx);
-    });
+async fn test_semantic_index(deterministic: Arc<Deterministic>, cx: &mut TestAppContext) {
+    init_test(cx);
 
     let fs = FakeFs::new(cx.background());
     fs.insert_tree(
@@ -56,6 +55,7 @@ async fn test_semantic_index(cx: &mut TestAppContext) {
                     fn bbb() {
                         println!(\"bbbbbbbbbbbbb!\");
                     }
+                    struct pqpqpqp {}
                 ".unindent(),
                 "file3.toml": "
                     ZZZZZZZZZZZZZZZZZZ = 5
@@ -75,7 +75,7 @@ async fn test_semantic_index(cx: &mut TestAppContext) {
     let db_path = db_dir.path().join("db.sqlite");
 
     let embedding_provider = Arc::new(FakeEmbeddingProvider::default());
-    let store = SemanticIndex::new(
+    let semantic_index = SemanticIndex::new(
         fs.clone(),
         db_path,
         embedding_provider.clone(),
@@ -87,34 +87,24 @@ async fn test_semantic_index(cx: &mut TestAppContext) {
 
     let project = Project::test(fs.clone(), ["/the-root".as_ref()], cx).await;
 
-    let _ = store
-        .update(cx, |store, cx| {
-            store.initialize_project(project.clone(), cx)
-        })
-        .await;
-
-    let (file_count, outstanding_file_count) = store
-        .update(cx, |store, cx| store.index_project(project.clone(), cx))
-        .await
-        .unwrap();
-    assert_eq!(file_count, 3);
-    cx.foreground().run_until_parked();
-    assert_eq!(*outstanding_file_count.borrow(), 0);
-
-    let search_results = store
-        .update(cx, |store, cx| {
-            store.search_project(
-                project.clone(),
-                "aaaaaabbbbzz".to_string(),
-                5,
-                vec![],
-                vec![],
-                cx,
-            )
-        })
-        .await
-        .unwrap();
-
+    let search_results = semantic_index.update(cx, |store, cx| {
+        store.search_project(
+            project.clone(),
+            "aaaaaabbbbzz".to_string(),
+            5,
+            vec![],
+            vec![],
+            cx,
+        )
+    });
+    let pending_file_count =
+        semantic_index.read_with(cx, |index, _| index.pending_file_count(&project).unwrap());
+    deterministic.run_until_parked();
+    assert_eq!(*pending_file_count.borrow(), 3);
+    deterministic.advance_clock(EMBEDDING_QUEUE_FLUSH_TIMEOUT);
+    assert_eq!(*pending_file_count.borrow(), 0);
+
+    let search_results = search_results.await.unwrap();
     assert_search_results(
         &search_results,
         &[
@@ -122,6 +112,7 @@ async fn test_semantic_index(cx: &mut TestAppContext) {
             (Path::new("src/file2.rs").into(), 0),
             (Path::new("src/file3.toml").into(), 0),
             (Path::new("src/file1.rs").into(), 45),
+            (Path::new("src/file2.rs").into(), 45),
         ],
         cx,
     );
@@ -129,7 +120,7 @@ async fn test_semantic_index(cx: &mut TestAppContext) {
     // Test Include Files Functonality
     let include_files = vec![PathMatcher::new("*.rs").unwrap()];
     let exclude_files = vec![PathMatcher::new("*.rs").unwrap()];
-    let rust_only_search_results = store
+    let rust_only_search_results = semantic_index
         .update(cx, |store, cx| {
             store.search_project(
                 project.clone(),
@@ -149,11 +140,12 @@ async fn test_semantic_index(cx: &mut TestAppContext) {
             (Path::new("src/file1.rs").into(), 0),
             (Path::new("src/file2.rs").into(), 0),
             (Path::new("src/file1.rs").into(), 45),
+            (Path::new("src/file2.rs").into(), 45),
         ],
         cx,
     );
 
-    let no_rust_search_results = store
+    let no_rust_search_results = semantic_index
         .update(cx, |store, cx| {
             store.search_project(
                 project.clone(),
@@ -186,24 +178,85 @@ async fn test_semantic_index(cx: &mut TestAppContext) {
     .await
     .unwrap();
 
-    cx.foreground().run_until_parked();
+    deterministic.advance_clock(EMBEDDING_QUEUE_FLUSH_TIMEOUT);
 
     let prev_embedding_count = embedding_provider.embedding_count();
-    let (file_count, outstanding_file_count) = store
-        .update(cx, |store, cx| store.index_project(project.clone(), cx))
-        .await
-        .unwrap();
-    assert_eq!(file_count, 1);
-
-    cx.foreground().run_until_parked();
-    assert_eq!(*outstanding_file_count.borrow(), 0);
+    let index = semantic_index.update(cx, |store, cx| store.index_project(project.clone(), cx));
+    deterministic.run_until_parked();
+    assert_eq!(*pending_file_count.borrow(), 1);
+    deterministic.advance_clock(EMBEDDING_QUEUE_FLUSH_TIMEOUT);
+    assert_eq!(*pending_file_count.borrow(), 0);
+    index.await.unwrap();
 
     assert_eq!(
         embedding_provider.embedding_count() - prev_embedding_count,
-        2
+        1
     );
 }
 
+#[gpui::test(iterations = 10)]
+async fn test_embedding_batching(cx: &mut TestAppContext, mut rng: StdRng) {
+    let (outstanding_job_count, _) = postage::watch::channel_with(0);
+    let outstanding_job_count = Arc::new(Mutex::new(outstanding_job_count));
+
+    let files = (1..=3)
+        .map(|file_ix| FileToEmbed {
+            worktree_id: 5,
+            path: Path::new(&format!("path-{file_ix}")).into(),
+            mtime: SystemTime::now(),
+            spans: (0..rng.gen_range(4..22))
+                .map(|document_ix| {
+                    let content_len = rng.gen_range(10..100);
+                    let content = RandomCharIter::new(&mut rng)
+                        .with_simple_text()
+                        .take(content_len)
+                        .collect::<String>();
+                    let digest = SpanDigest::from(content.as_str());
+                    Span {
+                        range: 0..10,
+                        embedding: None,
+                        name: format!("document {document_ix}"),
+                        content,
+                        digest,
+                        token_count: rng.gen_range(10..30),
+                    }
+                })
+                .collect(),
+            job_handle: JobHandle::new(&outstanding_job_count),
+        })
+        .collect::<Vec<_>>();
+
+    let embedding_provider = Arc::new(FakeEmbeddingProvider::default());
+
+    let mut queue = EmbeddingQueue::new(embedding_provider.clone(), cx.background());
+    for file in &files {
+        queue.push(file.clone());
+    }
+    queue.flush();
+
+    cx.foreground().run_until_parked();
+    let finished_files = queue.finished_files();
+    let mut embedded_files: Vec<_> = files
+        .iter()
+        .map(|_| finished_files.try_recv().expect("no finished file"))
+        .collect();
+
+    let expected_files: Vec<_> = files
+        .iter()
+        .map(|file| {
+            let mut file = file.clone();
+            for doc in &mut file.spans {
+                doc.embedding = Some(embedding_provider.embed_sync(doc.content.as_ref()));
+            }
+            file
+        })
+        .collect();
+
+    embedded_files.sort_by_key(|f| f.path.clone());
+
+    assert_eq!(embedded_files, expected_files);
+}
+
 #[track_caller]
 fn assert_search_results(
     actual: &[SearchResult],
@@ -227,7 +280,8 @@ fn assert_search_results(
 #[gpui::test]
 async fn test_code_context_retrieval_rust() {
     let language = rust_lang();
-    let mut retriever = CodeContextRetriever::new();
+    let embedding_provider = Arc::new(DummyEmbeddings {});
+    let mut retriever = CodeContextRetriever::new(embedding_provider);
 
     let text = "
         /// A doc comment
@@ -314,7 +368,8 @@ async fn test_code_context_retrieval_rust() {
 #[gpui::test]
 async fn test_code_context_retrieval_json() {
     let language = json_lang();
-    let mut retriever = CodeContextRetriever::new();
+    let embedding_provider = Arc::new(DummyEmbeddings {});
+    let mut retriever = CodeContextRetriever::new(embedding_provider);
 
     let text = r#"
         {
@@ -382,7 +437,7 @@ async fn test_code_context_retrieval_json() {
 }
 
 fn assert_documents_eq(
-    documents: &[Document],
+    documents: &[Span],
     expected_contents_and_start_offsets: &[(String, usize)],
 ) {
     assert_eq!(
@@ -397,7 +452,8 @@ fn assert_documents_eq(
 #[gpui::test]
 async fn test_code_context_retrieval_javascript() {
     let language = js_lang();
-    let mut retriever = CodeContextRetriever::new();
+    let embedding_provider = Arc::new(DummyEmbeddings {});
+    let mut retriever = CodeContextRetriever::new(embedding_provider);
 
     let text = "
         /* globals importScripts, backend */
@@ -495,7 +551,8 @@ async fn test_code_context_retrieval_javascript() {
 #[gpui::test]
 async fn test_code_context_retrieval_lua() {
     let language = lua_lang();
-    let mut retriever = CodeContextRetriever::new();
+    let embedding_provider = Arc::new(DummyEmbeddings {});
+    let mut retriever = CodeContextRetriever::new(embedding_provider);
 
     let text = r#"
         -- Creates a new class
@@ -568,7 +625,8 @@ async fn test_code_context_retrieval_lua() {
 #[gpui::test]
 async fn test_code_context_retrieval_elixir() {
     let language = elixir_lang();
-    let mut retriever = CodeContextRetriever::new();
+    let embedding_provider = Arc::new(DummyEmbeddings {});
+    let mut retriever = CodeContextRetriever::new(embedding_provider);
 
     let text = r#"
         defmodule File.Stream do
@@ -684,7 +742,8 @@ async fn test_code_context_retrieval_elixir() {
 #[gpui::test]
 async fn test_code_context_retrieval_cpp() {
     let language = cpp_lang();
-    let mut retriever = CodeContextRetriever::new();
+    let embedding_provider = Arc::new(DummyEmbeddings {});
+    let mut retriever = CodeContextRetriever::new(embedding_provider);
 
     let text = "
     /**
@@ -836,7 +895,8 @@ async fn test_code_context_retrieval_cpp() {
 #[gpui::test]
 async fn test_code_context_retrieval_ruby() {
     let language = ruby_lang();
-    let mut retriever = CodeContextRetriever::new();
+    let embedding_provider = Arc::new(DummyEmbeddings {});
+    let mut retriever = CodeContextRetriever::new(embedding_provider);
 
     let text = r#"
         # This concern is inspired by "sudo mode" on GitHub. It
@@ -1026,7 +1086,8 @@ async fn test_code_context_retrieval_ruby() {
 #[gpui::test]
 async fn test_code_context_retrieval_php() {
     let language = php_lang();
-    let mut retriever = CodeContextRetriever::new();
+    let embedding_provider = Arc::new(DummyEmbeddings {});
+    let mut retriever = CodeContextRetriever::new(embedding_provider);
 
     let text = r#"
         <?php
@@ -1173,36 +1234,6 @@ async fn test_code_context_retrieval_php() {
     );
 }
 
-#[gpui::test]
-fn test_dot_product(mut rng: StdRng) {
-    assert_eq!(dot(&[1., 0., 0., 0., 0.], &[0., 1., 0., 0., 0.]), 0.);
-    assert_eq!(dot(&[2., 0., 0., 0., 0.], &[3., 1., 0., 0., 0.]), 6.);
-
-    for _ in 0..100 {
-        let size = 1536;
-        let mut a = vec![0.; size];
-        let mut b = vec![0.; size];
-        for (a, b) in a.iter_mut().zip(b.iter_mut()) {
-            *a = rng.gen();
-            *b = rng.gen();
-        }
-
-        assert_eq!(
-            round_to_decimals(dot(&a, &b), 1),
-            round_to_decimals(reference_dot(&a, &b), 1)
-        );
-    }
-
-    fn round_to_decimals(n: f32, decimal_places: i32) -> f32 {
-        let factor = (10.0 as f32).powi(decimal_places);
-        (n * factor).round() / factor
-    }
-
-    fn reference_dot(a: &[f32], b: &[f32]) -> f32 {
-        a.iter().zip(b.iter()).map(|(a, b)| a * b).sum()
-    }
-}
-
 #[derive(Default)]
 struct FakeEmbeddingProvider {
     embedding_count: AtomicUsize,
@@ -1212,35 +1243,42 @@ impl FakeEmbeddingProvider {
     fn embedding_count(&self) -> usize {
         self.embedding_count.load(atomic::Ordering::SeqCst)
     }
+
+    fn embed_sync(&self, span: &str) -> Embedding {
+        let mut result = vec![1.0; 26];
+        for letter in span.chars() {
+            let letter = letter.to_ascii_lowercase();
+            if letter as u32 >= 'a' as u32 {
+                let ix = (letter as u32) - ('a' as u32);
+                if ix < 26 {
+                    result[ix as usize] += 1.0;
+                }
+            }
+        }
+
+        let norm = result.iter().map(|x| x * x).sum::<f32>().sqrt();
+        for x in &mut result {
+            *x /= norm;
+        }
+
+        result.into()
+    }
 }
 
 #[async_trait]
 impl EmbeddingProvider for FakeEmbeddingProvider {
-    async fn embed_batch(&self, spans: Vec<&str>) -> Result<Vec<Vec<f32>>> {
-        self.embedding_count
-            .fetch_add(spans.len(), atomic::Ordering::SeqCst);
-        Ok(spans
-            .iter()
-            .map(|span| {
-                let mut result = vec![1.0; 26];
-                for letter in span.chars() {
-                    let letter = letter.to_ascii_lowercase();
-                    if letter as u32 >= 'a' as u32 {
-                        let ix = (letter as u32) - ('a' as u32);
-                        if ix < 26 {
-                            result[ix as usize] += 1.0;
-                        }
-                    }
-                }
+    fn truncate(&self, span: &str) -> (String, usize) {
+        (span.to_string(), 1)
+    }
 
-                let norm = result.iter().map(|x| x * x).sum::<f32>().sqrt();
-                for x in &mut result {
-                    *x /= norm;
-                }
+    fn max_tokens_per_batch(&self) -> usize {
+        200
+    }
 
-                result
-            })
-            .collect())
+    async fn embed_batch(&self, spans: Vec<String>) -> Result<Vec<Embedding>> {
+        self.embedding_count
+            .fetch_add(spans.len(), atomic::Ordering::SeqCst);
+        Ok(spans.iter().map(|span| self.embed_sync(span)).collect())
     }
 }
 
@@ -1684,3 +1722,11 @@ fn test_subtract_ranges() {
 
     assert_eq!(subtract_ranges(&[0..5], &[1..2]), &[0..1, 2..5]);
 }
+
+fn init_test(cx: &mut TestAppContext) {
+    cx.update(|cx| {
+        cx.set_global(SettingsStore::test(cx));
+        settings::register::<SemanticIndexSettings>(cx);
+        settings::register::<ProjectSettings>(cx);
+    });
+}

crates/theme/src/theme.rs 🔗

@@ -417,6 +417,7 @@ pub struct Toolbar {
     pub height: f32,
     pub item_spacing: f32,
     pub toggleable_tool: Toggleable<Interactive<IconButton>>,
+    pub toggleable_text_tool: Toggleable<Interactive<ContainedText>>,
     pub breadcrumb_height: f32,
     pub breadcrumbs: Interactive<ContainedText>,
 }

crates/util/src/util.rs 🔗

@@ -269,11 +269,22 @@ pub fn defer<F: FnOnce()>(f: F) -> Deferred<F> {
     Deferred(Some(f))
 }
 
-pub struct RandomCharIter<T: Rng>(T);
+pub struct RandomCharIter<T: Rng> {
+    rng: T,
+    simple_text: bool,
+}
 
 impl<T: Rng> RandomCharIter<T> {
     pub fn new(rng: T) -> Self {
-        Self(rng)
+        Self {
+            rng,
+            simple_text: std::env::var("SIMPLE_TEXT").map_or(false, |v| !v.is_empty()),
+        }
+    }
+
+    pub fn with_simple_text(mut self) -> Self {
+        self.simple_text = true;
+        self
     }
 }
 
@@ -281,25 +292,27 @@ impl<T: Rng> Iterator for RandomCharIter<T> {
     type Item = char;
 
     fn next(&mut self) -> Option<Self::Item> {
-        if std::env::var("SIMPLE_TEXT").map_or(false, |v| !v.is_empty()) {
-            return if self.0.gen_range(0..100) < 5 {
+        if self.simple_text {
+            return if self.rng.gen_range(0..100) < 5 {
                 Some('\n')
             } else {
-                Some(self.0.gen_range(b'a'..b'z' + 1).into())
+                Some(self.rng.gen_range(b'a'..b'z' + 1).into())
             };
         }
 
-        match self.0.gen_range(0..100) {
+        match self.rng.gen_range(0..100) {
             // whitespace
-            0..=19 => [' ', '\n', '\r', '\t'].choose(&mut self.0).copied(),
+            0..=19 => [' ', '\n', '\r', '\t'].choose(&mut self.rng).copied(),
             // two-byte greek letters
-            20..=32 => char::from_u32(self.0.gen_range(('α' as u32)..('ω' as u32 + 1))),
+            20..=32 => char::from_u32(self.rng.gen_range(('α' as u32)..('ω' as u32 + 1))),
             // // three-byte characters
-            33..=45 => ['✋', '✅', '❌', '❎', '⭐'].choose(&mut self.0).copied(),
+            33..=45 => ['✋', '✅', '❌', '❎', '⭐']
+                .choose(&mut self.rng)
+                .copied(),
             // // four-byte characters
-            46..=58 => ['🍐', '🏀', '🍗', '🎉'].choose(&mut self.0).copied(),
+            46..=58 => ['🍐', '🏀', '🍗', '🎉'].choose(&mut self.rng).copied(),
             // ascii letters
-            _ => Some(self.0.gen_range(b'a'..b'z' + 1).into()),
+            _ => Some(self.rng.gen_range(b'a'..b'z' + 1).into()),
         }
     }
 }

crates/vim/src/motion.rs 🔗

@@ -3,7 +3,8 @@ use std::{cmp, sync::Arc};
 use editor::{
     char_kind,
     display_map::{DisplaySnapshot, FoldPoint, ToDisplayPoint},
-    movement, Bias, CharKind, DisplayPoint, ToOffset,
+    movement::{self, FindRange},
+    Bias, CharKind, DisplayPoint, ToOffset,
 };
 use gpui::{actions, impl_actions, AppContext, WindowContext};
 use language::{Point, Selection, SelectionGoal};
@@ -592,7 +593,7 @@ pub(crate) fn next_word_start(
     let scope = map.buffer_snapshot.language_scope_at(point.to_point(map));
     for _ in 0..times {
         let mut crossed_newline = false;
-        point = movement::find_boundary(map, point, |left, right| {
+        point = movement::find_boundary(map, point, FindRange::MultiLine, |left, right| {
             let left_kind = char_kind(&scope, left).coerce_punctuation(ignore_punctuation);
             let right_kind = char_kind(&scope, right).coerce_punctuation(ignore_punctuation);
             let at_newline = right == '\n';
@@ -616,8 +617,13 @@ fn next_word_end(
 ) -> DisplayPoint {
     let scope = map.buffer_snapshot.language_scope_at(point.to_point(map));
     for _ in 0..times {
-        *point.column_mut() += 1;
-        point = movement::find_boundary(map, point, |left, right| {
+        if point.column() < map.line_len(point.row()) {
+            *point.column_mut() += 1;
+        } else if point.row() < map.max_buffer_row() {
+            *point.row_mut() += 1;
+            *point.column_mut() = 0;
+        }
+        point = movement::find_boundary(map, point, FindRange::MultiLine, |left, right| {
             let left_kind = char_kind(&scope, left).coerce_punctuation(ignore_punctuation);
             let right_kind = char_kind(&scope, right).coerce_punctuation(ignore_punctuation);
 
@@ -649,12 +655,13 @@ fn previous_word_start(
     for _ in 0..times {
         // This works even though find_preceding_boundary is called for every character in the line containing
         // cursor because the newline is checked only once.
-        point = movement::find_preceding_boundary(map, point, |left, right| {
-            let left_kind = char_kind(&scope, left).coerce_punctuation(ignore_punctuation);
-            let right_kind = char_kind(&scope, right).coerce_punctuation(ignore_punctuation);
+        point =
+            movement::find_preceding_boundary(map, point, FindRange::MultiLine, |left, right| {
+                let left_kind = char_kind(&scope, left).coerce_punctuation(ignore_punctuation);
+                let right_kind = char_kind(&scope, right).coerce_punctuation(ignore_punctuation);
 
-            (left_kind != right_kind && !right.is_whitespace()) || left == '\n'
-        });
+                (left_kind != right_kind && !right.is_whitespace()) || left == '\n'
+            });
     }
     point
 }

crates/vim/src/normal.rs 🔗

@@ -27,7 +27,6 @@ use self::{
     case::change_case,
     change::{change_motion, change_object},
     delete::{delete_motion, delete_object},
-    substitute::substitute,
     yank::{yank_motion, yank_object},
 };
 
@@ -44,7 +43,6 @@ actions!(
         ChangeToEndOfLine,
         DeleteToEndOfLine,
         Yank,
-        Substitute,
         ChangeCase,
     ]
 );
@@ -56,13 +54,8 @@ pub fn init(cx: &mut AppContext) {
     cx.add_action(insert_line_above);
     cx.add_action(insert_line_below);
     cx.add_action(change_case);
+    substitute::init(cx);
     search::init(cx);
-    cx.add_action(|_: &mut Workspace, _: &Substitute, cx| {
-        Vim::update(cx, |vim, cx| {
-            let times = vim.pop_number_operator(cx);
-            substitute(vim, times, cx);
-        })
-    });
     cx.add_action(|_: &mut Workspace, _: &DeleteLeft, cx| {
         Vim::update(cx, |vim, cx| {
             let times = vim.pop_number_operator(cx);
@@ -445,7 +438,7 @@ mod test {
     }
 
     #[gpui::test]
-    async fn test_e(cx: &mut gpui::TestAppContext) {
+    async fn test_end_of_word(cx: &mut gpui::TestAppContext) {
         let mut cx = NeovimBackedTestContext::new(cx).await.binding(["e"]);
         cx.assert_all(indoc! {"
             Thˇe quicˇkˇ-browˇn

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

@@ -1,7 +1,10 @@
 use crate::{motion::Motion, object::Object, state::Mode, utils::copy_selections_content, Vim};
 use editor::{
-    char_kind, display_map::DisplaySnapshot, movement, scroll::autoscroll::Autoscroll, CharKind,
-    DisplayPoint,
+    char_kind,
+    display_map::DisplaySnapshot,
+    movement::{self, FindRange},
+    scroll::autoscroll::Autoscroll,
+    CharKind, DisplayPoint,
 };
 use gpui::WindowContext;
 use language::Selection;
@@ -96,12 +99,14 @@ fn expand_changed_word_selection(
             .unwrap_or_default();
 
         if in_word {
-            selection.end = movement::find_boundary(map, selection.end, |left, right| {
-                let left_kind = char_kind(&scope, left).coerce_punctuation(ignore_punctuation);
-                let right_kind = char_kind(&scope, right).coerce_punctuation(ignore_punctuation);
+            selection.end =
+                movement::find_boundary(map, selection.end, FindRange::MultiLine, |left, right| {
+                    let left_kind = char_kind(&scope, left).coerce_punctuation(ignore_punctuation);
+                    let right_kind =
+                        char_kind(&scope, right).coerce_punctuation(ignore_punctuation);
 
-                left_kind != right_kind && left_kind != CharKind::Whitespace
-            });
+                    left_kind != right_kind && left_kind != CharKind::Whitespace
+                });
             true
         } else {
             Motion::NextWordStart { ignore_punctuation }

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

@@ -67,7 +67,8 @@ fn scroll_editor(editor: &mut Editor, amount: &ScrollAmount, cx: &mut ViewContex
         let top_anchor = editor.scroll_manager.anchor().anchor;
 
         editor.change_selections(None, cx, |s| {
-            s.move_heads_with(|map, head, goal| {
+            s.move_with(|map, selection| {
+                let head = selection.head();
                 let top = top_anchor.to_display_point(map);
                 let min_row = top.row() + VERTICAL_SCROLL_MARGIN as u32;
                 let max_row = top.row() + visible_rows - VERTICAL_SCROLL_MARGIN as u32 - 1;
@@ -79,7 +80,11 @@ fn scroll_editor(editor: &mut Editor, amount: &ScrollAmount, cx: &mut ViewContex
                 } else {
                     head
                 };
-                (new_head, goal)
+                if selection.is_empty() {
+                    selection.collapse_to(new_head, selection.goal)
+                } else {
+                    selection.set_head(new_head, selection.goal)
+                };
             })
         });
     }
@@ -90,12 +95,35 @@ mod test {
     use crate::{state::Mode, test::VimTestContext};
     use gpui::geometry::vector::vec2f;
     use indoc::indoc;
+    use language::Point;
 
     #[gpui::test]
     async fn test_scroll(cx: &mut gpui::TestAppContext) {
         let mut cx = VimTestContext::new(cx, true).await;
 
-        cx.set_state(indoc! {"ˇa\nb\nc\nd\ne\n"}, Mode::Normal);
+        let window = cx.window;
+        let line_height =
+            cx.editor(|editor, cx| editor.style(cx).text.line_height(cx.font_cache()));
+        window.simulate_resize(vec2f(1000., 8.0 * line_height - 1.0), &mut cx);
+
+        cx.set_state(
+            indoc!(
+                "ˇone
+                two
+                three
+                four
+                five
+                six
+                seven
+                eight
+                nine
+                ten
+                eleven
+                twelve
+            "
+            ),
+            Mode::Normal,
+        );
 
         cx.update_editor(|editor, cx| {
             assert_eq!(editor.snapshot(cx).scroll_position(), vec2f(0., 0.))
@@ -112,5 +140,33 @@ mod test {
         cx.update_editor(|editor, cx| {
             assert_eq!(editor.snapshot(cx).scroll_position(), vec2f(0., 2.))
         });
+
+        // does not select in normal mode
+        cx.simulate_keystrokes(["g", "g"]);
+        cx.update_editor(|editor, cx| {
+            assert_eq!(editor.snapshot(cx).scroll_position(), vec2f(0., 0.))
+        });
+        cx.simulate_keystrokes(["ctrl-d"]);
+        cx.update_editor(|editor, cx| {
+            assert_eq!(editor.snapshot(cx).scroll_position(), vec2f(0., 2.0));
+            assert_eq!(
+                editor.selections.newest(cx).range(),
+                Point::new(5, 0)..Point::new(5, 0)
+            )
+        });
+
+        // does select in visual mode
+        cx.simulate_keystrokes(["g", "g"]);
+        cx.update_editor(|editor, cx| {
+            assert_eq!(editor.snapshot(cx).scroll_position(), vec2f(0., 0.))
+        });
+        cx.simulate_keystrokes(["v", "ctrl-d"]);
+        cx.update_editor(|editor, cx| {
+            assert_eq!(editor.snapshot(cx).scroll_position(), vec2f(0., 2.0));
+            assert_eq!(
+                editor.selections.newest(cx).range(),
+                Point::new(0, 0)..Point::new(5, 1)
+            )
+        });
     }
 }

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

@@ -1,10 +1,32 @@
-use gpui::WindowContext;
+use editor::movement;
+use gpui::{actions, AppContext, WindowContext};
 use language::Point;
+use workspace::Workspace;
 
 use crate::{motion::Motion, utils::copy_selections_content, Mode, Vim};
 
-pub fn substitute(vim: &mut Vim, count: Option<usize>, cx: &mut WindowContext) {
-    let line_mode = vim.state().mode == Mode::VisualLine;
+actions!(vim, [Substitute, SubstituteLine]);
+
+pub(crate) fn init(cx: &mut AppContext) {
+    cx.add_action(|_: &mut Workspace, _: &Substitute, cx| {
+        Vim::update(cx, |vim, cx| {
+            let count = vim.pop_number_operator(cx);
+            substitute(vim, count, vim.state().mode == Mode::VisualLine, cx);
+        })
+    });
+
+    cx.add_action(|_: &mut Workspace, _: &SubstituteLine, cx| {
+        Vim::update(cx, |vim, cx| {
+            if matches!(vim.state().mode, Mode::VisualBlock | Mode::Visual) {
+                vim.switch_mode(Mode::VisualLine, false, cx)
+            }
+            let count = vim.pop_number_operator(cx);
+            substitute(vim, count, true, cx)
+        })
+    });
+}
+
+pub fn substitute(vim: &mut Vim, count: Option<usize>, line_mode: bool, cx: &mut WindowContext) {
     vim.update_active_editor(cx, |editor, cx| {
         editor.set_clip_at_line_ends(false, cx);
         editor.transact(cx, |editor, cx| {
@@ -14,6 +36,11 @@ pub fn substitute(vim: &mut Vim, count: Option<usize>, cx: &mut WindowContext) {
                         Motion::Right.expand_selection(map, selection, count, true);
                     }
                     if line_mode {
+                        // in Visual mode when the selection contains the newline at the end
+                        // of the line, we should exclude it.
+                        if !selection.is_empty() && selection.end.column() == 0 {
+                            selection.end = movement::left(map, selection.end);
+                        }
                         Motion::CurrentLine.expand_selection(map, selection, None, false);
                         if let Some((point, _)) = (Motion::FirstNonWhitespace {
                             display_lines: false,
@@ -166,4 +193,68 @@ mod test {
             the laˇzy dog"})
             .await;
     }
+
+    #[gpui::test]
+    async fn test_substitute_line(cx: &mut gpui::TestAppContext) {
+        let mut cx = NeovimBackedTestContext::new(cx).await;
+
+        let initial_state = indoc! {"
+                    The quick brown
+                    fox juˇmps over
+                    the lazy dog
+                    "};
+
+        // normal mode
+        cx.set_shared_state(initial_state).await;
+        cx.simulate_shared_keystrokes(["shift-s", "o"]).await;
+        cx.assert_shared_state(indoc! {"
+            The quick brown
+            oˇ
+            the lazy dog
+            "})
+            .await;
+
+        // visual mode
+        cx.set_shared_state(initial_state).await;
+        cx.simulate_shared_keystrokes(["v", "k", "shift-s", "o"])
+            .await;
+        cx.assert_shared_state(indoc! {"
+            oˇ
+            the lazy dog
+            "})
+            .await;
+
+        // visual block mode
+        cx.set_shared_state(initial_state).await;
+        cx.simulate_shared_keystrokes(["ctrl-v", "j", "shift-s", "o"])
+            .await;
+        cx.assert_shared_state(indoc! {"
+            The quick brown
+            oˇ
+            "})
+            .await;
+
+        // visual mode including newline
+        cx.set_shared_state(initial_state).await;
+        cx.simulate_shared_keystrokes(["v", "$", "shift-s", "o"])
+            .await;
+        cx.assert_shared_state(indoc! {"
+            The quick brown
+            oˇ
+            the lazy dog
+            "})
+            .await;
+
+        // indentation
+        cx.set_neovim_option("shiftwidth=4").await;
+        cx.set_shared_state(initial_state).await;
+        cx.simulate_shared_keystrokes([">", ">", "shift-s", "o"])
+            .await;
+        cx.assert_shared_state(indoc! {"
+            The quick brown
+                oˇ
+            the lazy dog
+            "})
+            .await;
+    }
 }

crates/vim/src/object.rs 🔗

@@ -1,6 +1,11 @@
 use std::ops::Range;
 
-use editor::{char_kind, display_map::DisplaySnapshot, movement, Bias, CharKind, DisplayPoint};
+use editor::{
+    char_kind,
+    display_map::DisplaySnapshot,
+    movement::{self, FindRange},
+    Bias, CharKind, DisplayPoint,
+};
 use gpui::{actions, impl_actions, AppContext, WindowContext};
 use language::Selection;
 use serde::Deserialize;
@@ -180,15 +185,17 @@ fn in_word(
     let scope = map
         .buffer_snapshot
         .language_scope_at(relative_to.to_point(map));
-    let start = movement::find_preceding_boundary_in_line(
+    let start = movement::find_preceding_boundary(
         map,
         right(map, relative_to, 1),
+        movement::FindRange::SingleLine,
         |left, right| {
             char_kind(&scope, left).coerce_punctuation(ignore_punctuation)
                 != char_kind(&scope, right).coerce_punctuation(ignore_punctuation)
         },
     );
-    let end = movement::find_boundary_in_line(map, relative_to, |left, right| {
+
+    let end = movement::find_boundary(map, relative_to, FindRange::SingleLine, |left, right| {
         char_kind(&scope, left).coerce_punctuation(ignore_punctuation)
             != char_kind(&scope, right).coerce_punctuation(ignore_punctuation)
     });
@@ -247,9 +254,10 @@ fn around_next_word(
         .buffer_snapshot
         .language_scope_at(relative_to.to_point(map));
     // Get the start of the word
-    let start = movement::find_preceding_boundary_in_line(
+    let start = movement::find_preceding_boundary(
         map,
         right(map, relative_to, 1),
+        FindRange::SingleLine,
         |left, right| {
             char_kind(&scope, left).coerce_punctuation(ignore_punctuation)
                 != char_kind(&scope, right).coerce_punctuation(ignore_punctuation)
@@ -257,7 +265,7 @@ fn around_next_word(
     );
 
     let mut word_found = false;
-    let end = movement::find_boundary(map, relative_to, |left, right| {
+    let end = movement::find_boundary(map, relative_to, FindRange::MultiLine, |left, right| {
         let left_kind = char_kind(&scope, left).coerce_punctuation(ignore_punctuation);
         let right_kind = char_kind(&scope, right).coerce_punctuation(ignore_punctuation);
 
@@ -572,11 +580,18 @@ mod test {
     async fn test_visual_word_object(cx: &mut gpui::TestAppContext) {
         let mut cx = NeovimBackedTestContext::new(cx).await;
 
-        cx.set_shared_state("The quick ˇbrown\nfox").await;
+        /*
+                cx.set_shared_state("The quick ˇbrown\nfox").await;
+                cx.simulate_shared_keystrokes(["v"]).await;
+                cx.assert_shared_state("The quick «bˇ»rown\nfox").await;
+                cx.simulate_shared_keystrokes(["i", "w"]).await;
+                cx.assert_shared_state("The quick «brownˇ»\nfox").await;
+        */
+        cx.set_shared_state("The quick brown\nˇ\nfox").await;
         cx.simulate_shared_keystrokes(["v"]).await;
-        cx.assert_shared_state("The quick «bˇ»rown\nfox").await;
+        cx.assert_shared_state("The quick brown\n«\nˇ»fox").await;
         cx.simulate_shared_keystrokes(["i", "w"]).await;
-        cx.assert_shared_state("The quick «brownˇ»\nfox").await;
+        cx.assert_shared_state("The quick brown\n«\nˇ»fox").await;
 
         cx.assert_binding_matches_all(["v", "i", "w"], WORD_LOCATIONS)
             .await;

crates/vim/src/test.rs 🔗

@@ -431,6 +431,24 @@ async fn test_wrapped_lines(cx: &mut gpui::TestAppContext) {
         twelve char
     "})
         .await;
+
+    // line wraps as:
+    // fourteen ch
+    // ar
+    // fourteen ch
+    // ar
+    cx.set_shared_state(indoc! { "
+        fourteen chaˇr
+        fourteen char
+    "})
+        .await;
+
+    cx.simulate_shared_keystrokes(["d", "i", "w"]).await;
+    cx.assert_shared_state(indoc! {"
+        fourteenˇ•
+        fourteen char
+    "})
+        .await;
 }
 
 #[gpui::test]

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

@@ -153,6 +153,7 @@ impl<'a> NeovimBackedTestContext<'a> {
     }
 
     pub async fn assert_shared_state(&mut self, marked_text: &str) {
+        let marked_text = marked_text.replace("•", " ");
         let neovim = self.neovim_state().await;
         let editor = self.editor_state();
         if neovim == marked_text && neovim == editor {
@@ -184,9 +185,9 @@ impl<'a> NeovimBackedTestContext<'a> {
             message,
             initial_state,
             self.recent_keystrokes.join(" "),
-            marked_text,
-            neovim,
-            editor
+            marked_text.replace(" \n", "•\n"),
+            neovim.replace(" \n", "•\n"),
+            editor.replace(" \n", "•\n")
         )
     }
 

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

@@ -237,6 +237,9 @@ impl NeovimConnection {
 
     #[cfg(not(feature = "neovim"))]
     pub async fn set_option(&mut self, value: &str) {
+        if let Some(NeovimData::Get { .. }) = self.data.front() {
+            self.data.pop_front();
+        };
         assert_eq!(
             self.data.pop_front(),
             Some(NeovimData::SetOption {

crates/vim/test_data/test_end_of_word.json 🔗

@@ -0,0 +1,32 @@
+{"Put":{"state":"Thˇe quick-brown\n\n\nfox_jumps over\nthe"}}
+{"Key":"e"}
+{"Get":{"state":"The quicˇk-brown\n\n\nfox_jumps over\nthe","mode":"Normal"}}
+{"Key":"e"}
+{"Get":{"state":"The quickˇ-brown\n\n\nfox_jumps over\nthe","mode":"Normal"}}
+{"Key":"e"}
+{"Get":{"state":"The quick-browˇn\n\n\nfox_jumps over\nthe","mode":"Normal"}}
+{"Key":"e"}
+{"Get":{"state":"The quick-brown\n\n\nfox_jumpˇs over\nthe","mode":"Normal"}}
+{"Key":"e"}
+{"Get":{"state":"The quick-brown\n\n\nfox_jumps oveˇr\nthe","mode":"Normal"}}
+{"Key":"e"}
+{"Get":{"state":"The quick-brown\n\n\nfox_jumps over\nthˇe","mode":"Normal"}}
+{"Key":"e"}
+{"Get":{"state":"The quick-brown\n\n\nfox_jumps over\nthˇe","mode":"Normal"}}
+{"Put":{"state":"Thˇe quick-brown\n\n\nfox_jumps over\nthe"}}
+{"Key":"shift-e"}
+{"Get":{"state":"The quick-browˇn\n\n\nfox_jumps over\nthe","mode":"Normal"}}
+{"Put":{"state":"The quicˇk-brown\n\n\nfox_jumps over\nthe"}}
+{"Key":"shift-e"}
+{"Get":{"state":"The quick-browˇn\n\n\nfox_jumps over\nthe","mode":"Normal"}}
+{"Put":{"state":"The quickˇ-brown\n\n\nfox_jumps over\nthe"}}
+{"Key":"shift-e"}
+{"Get":{"state":"The quick-browˇn\n\n\nfox_jumps over\nthe","mode":"Normal"}}
+{"Key":"shift-e"}
+{"Get":{"state":"The quick-brown\n\n\nfox_jumpˇs over\nthe","mode":"Normal"}}
+{"Key":"shift-e"}
+{"Get":{"state":"The quick-brown\n\n\nfox_jumps oveˇr\nthe","mode":"Normal"}}
+{"Key":"shift-e"}
+{"Get":{"state":"The quick-brown\n\n\nfox_jumps over\nthˇe","mode":"Normal"}}
+{"Key":"shift-e"}
+{"Get":{"state":"The quick-brown\n\n\nfox_jumps over\nthˇe","mode":"Normal"}}

crates/vim/test_data/test_substitute_line.json 🔗

@@ -0,0 +1,29 @@
+{"Put":{"state":"The quick brown\nfox juˇmps over\nthe lazy dog\n"}}
+{"Key":"shift-s"}
+{"Key":"o"}
+{"Get":{"state":"The quick brown\noˇ\nthe lazy dog\n","mode":"Insert"}}
+{"Put":{"state":"The quick brown\nfox juˇmps over\nthe lazy dog\n"}}
+{"Key":"v"}
+{"Key":"k"}
+{"Key":"shift-s"}
+{"Key":"o"}
+{"Get":{"state":"oˇ\nthe lazy dog\n","mode":"Insert"}}
+{"Put":{"state":"The quick brown\nfox juˇmps over\nthe lazy dog\n"}}
+{"Key":"ctrl-v"}
+{"Key":"j"}
+{"Key":"shift-s"}
+{"Key":"o"}
+{"Get":{"state":"The quick brown\noˇ\n","mode":"Insert"}}
+{"Put":{"state":"The quick brown\nfox juˇmps over\nthe lazy dog\n"}}
+{"Key":"v"}
+{"Key":"$"}
+{"Key":"shift-s"}
+{"Key":"o"}
+{"Get":{"state":"The quick brown\noˇ\nthe lazy dog\n","mode":"Insert"}}
+{"SetOption":{"value":"shiftwidth=4"}}
+{"Put":{"state":"The quick brown\nfox juˇmps over\nthe lazy dog\n"}}
+{"Key":">"}
+{"Key":">"}
+{"Key":"shift-s"}
+{"Key":"o"}
+{"Get":{"state":"The quick brown\n    oˇ\nthe lazy dog\n","mode":"Insert"}}

crates/vim/test_data/test_visual_word_object.json 🔗

@@ -1,9 +1,9 @@
-{"Put":{"state":"The quick ˇbrown\nfox"}}
+{"Put":{"state":"The quick brown\nˇ\nfox"}}
 {"Key":"v"}
-{"Get":{"state":"The quick «bˇ»rown\nfox","mode":"Visual"}}
+{"Get":{"state":"The quick brown\n«\nˇ»fox","mode":"Visual"}}
 {"Key":"i"}
 {"Key":"w"}
-{"Get":{"state":"The quick «brownˇ»\nfox","mode":"Visual"}}
+{"Get":{"state":"The quick brown\n«\nˇ»fox","mode":"Visual"}}
 {"Put":{"state":"The quick ˇbrown   \nfox jumps over\nthe lazy dog  \n\n\n\nThe-quick brown \n  \n  \n  fox-jumps over\nthe lazy dog \n\n"}}
 {"Key":"v"}
 {"Key":"i"}

crates/vim/test_data/test_wrapped_lines.json 🔗

@@ -48,3 +48,8 @@
 {"Key":"o"}
 {"Key":"escape"}
 {"Get":{"state":"twelve char\nˇo\ntwelve char twelve char\ntwelve char\n","mode":"Normal"}}
+{"Put":{"state":"fourteen chaˇr\nfourteen char\n"}}
+{"Key":"d"}
+{"Key":"i"}
+{"Key":"w"}
+{"Get":{"state":"fourteenˇ \nfourteen char\n","mode":"Normal"}}

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.103.0"
+version = "0.104.0"
 publish = false
 
 [lib]

crates/zed/src/languages.rs 🔗

@@ -37,7 +37,7 @@ mod yaml;
 #[exclude = "*.rs"]
 struct LanguageDir;
 
-pub fn init(languages: Arc<LanguageRegistry>, node_runtime: Arc<NodeRuntime>) {
+pub fn init(languages: Arc<LanguageRegistry>, node_runtime: Arc<dyn NodeRuntime>) {
     let language = |name, grammar, adapters| {
         languages.register(name, load_config(name), grammar, adapters, load_queries)
     };

crates/zed/src/languages/css.rs 🔗

@@ -22,11 +22,11 @@ fn server_binary_arguments(server_path: &Path) -> Vec<OsString> {
 }
 
 pub struct CssLspAdapter {
-    node: Arc<NodeRuntime>,
+    node: Arc<dyn NodeRuntime>,
 }
 
 impl CssLspAdapter {
-    pub fn new(node: Arc<NodeRuntime>) -> Self {
+    pub fn new(node: Arc<dyn NodeRuntime>) -> Self {
         CssLspAdapter { node }
     }
 }
@@ -65,7 +65,7 @@ impl LspAdapter for CssLspAdapter {
             self.node
                 .npm_install_packages(
                     &container_dir,
-                    [("vscode-langservers-extracted", version.as_str())],
+                    &[("vscode-langservers-extracted", version.as_str())],
                 )
                 .await?;
         }
@@ -81,14 +81,14 @@ impl LspAdapter for CssLspAdapter {
         container_dir: PathBuf,
         _: &dyn LspAdapterDelegate,
     ) -> Option<LanguageServerBinary> {
-        get_cached_server_binary(container_dir, &self.node).await
+        get_cached_server_binary(container_dir, &*self.node).await
     }
 
     async fn installation_test_binary(
         &self,
         container_dir: PathBuf,
     ) -> Option<LanguageServerBinary> {
-        get_cached_server_binary(container_dir, &self.node).await
+        get_cached_server_binary(container_dir, &*self.node).await
     }
 
     async fn initialization_options(&self) -> Option<serde_json::Value> {
@@ -100,7 +100,7 @@ impl LspAdapter for CssLspAdapter {
 
 async fn get_cached_server_binary(
     container_dir: PathBuf,
-    node: &NodeRuntime,
+    node: &dyn NodeRuntime,
 ) -> Option<LanguageServerBinary> {
     (|| async move {
         let mut last_version_dir = None;

crates/zed/src/languages/html.rs 🔗

@@ -22,11 +22,11 @@ fn server_binary_arguments(server_path: &Path) -> Vec<OsString> {
 }
 
 pub struct HtmlLspAdapter {
-    node: Arc<NodeRuntime>,
+    node: Arc<dyn NodeRuntime>,
 }
 
 impl HtmlLspAdapter {
-    pub fn new(node: Arc<NodeRuntime>) -> Self {
+    pub fn new(node: Arc<dyn NodeRuntime>) -> Self {
         HtmlLspAdapter { node }
     }
 }
@@ -65,7 +65,7 @@ impl LspAdapter for HtmlLspAdapter {
             self.node
                 .npm_install_packages(
                     &container_dir,
-                    [("vscode-langservers-extracted", version.as_str())],
+                    &[("vscode-langservers-extracted", version.as_str())],
                 )
                 .await?;
         }
@@ -81,14 +81,14 @@ impl LspAdapter for HtmlLspAdapter {
         container_dir: PathBuf,
         _: &dyn LspAdapterDelegate,
     ) -> Option<LanguageServerBinary> {
-        get_cached_server_binary(container_dir, &self.node).await
+        get_cached_server_binary(container_dir, &*self.node).await
     }
 
     async fn installation_test_binary(
         &self,
         container_dir: PathBuf,
     ) -> Option<LanguageServerBinary> {
-        get_cached_server_binary(container_dir, &self.node).await
+        get_cached_server_binary(container_dir, &*self.node).await
     }
 
     async fn initialization_options(&self) -> Option<serde_json::Value> {
@@ -100,7 +100,7 @@ impl LspAdapter for HtmlLspAdapter {
 
 async fn get_cached_server_binary(
     container_dir: PathBuf,
-    node: &NodeRuntime,
+    node: &dyn NodeRuntime,
 ) -> Option<LanguageServerBinary> {
     (|| async move {
         let mut last_version_dir = None;

crates/zed/src/languages/json.rs 🔗

@@ -27,12 +27,12 @@ fn server_binary_arguments(server_path: &Path) -> Vec<OsString> {
 }
 
 pub struct JsonLspAdapter {
-    node: Arc<NodeRuntime>,
+    node: Arc<dyn NodeRuntime>,
     languages: Arc<LanguageRegistry>,
 }
 
 impl JsonLspAdapter {
-    pub fn new(node: Arc<NodeRuntime>, languages: Arc<LanguageRegistry>) -> Self {
+    pub fn new(node: Arc<dyn NodeRuntime>, languages: Arc<LanguageRegistry>) -> Self {
         JsonLspAdapter { node, languages }
     }
 }
@@ -71,7 +71,7 @@ impl LspAdapter for JsonLspAdapter {
             self.node
                 .npm_install_packages(
                     &container_dir,
-                    [("vscode-json-languageserver", version.as_str())],
+                    &[("vscode-json-languageserver", version.as_str())],
                 )
                 .await?;
         }
@@ -87,14 +87,14 @@ impl LspAdapter for JsonLspAdapter {
         container_dir: PathBuf,
         _: &dyn LspAdapterDelegate,
     ) -> Option<LanguageServerBinary> {
-        get_cached_server_binary(container_dir, &self.node).await
+        get_cached_server_binary(container_dir, &*self.node).await
     }
 
     async fn installation_test_binary(
         &self,
         container_dir: PathBuf,
     ) -> Option<LanguageServerBinary> {
-        get_cached_server_binary(container_dir, &self.node).await
+        get_cached_server_binary(container_dir, &*self.node).await
     }
 
     async fn initialization_options(&self) -> Option<serde_json::Value> {
@@ -148,7 +148,7 @@ impl LspAdapter for JsonLspAdapter {
 
 async fn get_cached_server_binary(
     container_dir: PathBuf,
-    node: &NodeRuntime,
+    node: &dyn NodeRuntime,
 ) -> Option<LanguageServerBinary> {
     (|| async move {
         let mut last_version_dir = None;

crates/zed/src/languages/php.rs 🔗

@@ -23,14 +23,14 @@ fn intelephense_server_binary_arguments(server_path: &Path) -> Vec<OsString> {
 pub struct IntelephenseVersion(String);
 
 pub struct IntelephenseLspAdapter {
-    node: Arc<NodeRuntime>,
+    node: Arc<dyn NodeRuntime>,
 }
 
 impl IntelephenseLspAdapter {
     const SERVER_PATH: &'static str = "node_modules/intelephense/lib/intelephense.js";
 
     #[allow(unused)]
-    pub fn new(node: Arc<NodeRuntime>) -> Self {
+    pub fn new(node: Arc<dyn NodeRuntime>) -> Self {
         Self { node }
     }
 }
@@ -65,7 +65,7 @@ impl LspAdapter for IntelephenseLspAdapter {
 
         if fs::metadata(&server_path).await.is_err() {
             self.node
-                .npm_install_packages(&container_dir, [("intelephense", version.0.as_str())])
+                .npm_install_packages(&container_dir, &[("intelephense", version.0.as_str())])
                 .await?;
         }
         Ok(LanguageServerBinary {
@@ -79,14 +79,14 @@ impl LspAdapter for IntelephenseLspAdapter {
         container_dir: PathBuf,
         _: &dyn LspAdapterDelegate,
     ) -> Option<LanguageServerBinary> {
-        get_cached_server_binary(container_dir, &self.node).await
+        get_cached_server_binary(container_dir, &*self.node).await
     }
 
     async fn installation_test_binary(
         &self,
         container_dir: PathBuf,
     ) -> Option<LanguageServerBinary> {
-        get_cached_server_binary(container_dir, &self.node).await
+        get_cached_server_binary(container_dir, &*self.node).await
     }
 
     async fn label_for_completion(
@@ -107,7 +107,7 @@ impl LspAdapter for IntelephenseLspAdapter {
 
 async fn get_cached_server_binary(
     container_dir: PathBuf,
-    node: &NodeRuntime,
+    node: &dyn NodeRuntime,
 ) -> Option<LanguageServerBinary> {
     (|| async move {
         let mut last_version_dir = None;

crates/zed/src/languages/python.rs 🔗

@@ -20,11 +20,11 @@ fn server_binary_arguments(server_path: &Path) -> Vec<OsString> {
 }
 
 pub struct PythonLspAdapter {
-    node: Arc<NodeRuntime>,
+    node: Arc<dyn NodeRuntime>,
 }
 
 impl PythonLspAdapter {
-    pub fn new(node: Arc<NodeRuntime>) -> Self {
+    pub fn new(node: Arc<dyn NodeRuntime>) -> Self {
         PythonLspAdapter { node }
     }
 }
@@ -57,7 +57,7 @@ impl LspAdapter for PythonLspAdapter {
 
         if fs::metadata(&server_path).await.is_err() {
             self.node
-                .npm_install_packages(&container_dir, [("pyright", version.as_str())])
+                .npm_install_packages(&container_dir, &[("pyright", version.as_str())])
                 .await?;
         }
 
@@ -72,14 +72,14 @@ impl LspAdapter for PythonLspAdapter {
         container_dir: PathBuf,
         _: &dyn LspAdapterDelegate,
     ) -> Option<LanguageServerBinary> {
-        get_cached_server_binary(container_dir, &self.node).await
+        get_cached_server_binary(container_dir, &*self.node).await
     }
 
     async fn installation_test_binary(
         &self,
         container_dir: PathBuf,
     ) -> Option<LanguageServerBinary> {
-        get_cached_server_binary(container_dir, &self.node).await
+        get_cached_server_binary(container_dir, &*self.node).await
     }
 
     async fn process_completion(&self, item: &mut lsp::CompletionItem) {
@@ -162,7 +162,7 @@ impl LspAdapter for PythonLspAdapter {
 
 async fn get_cached_server_binary(
     container_dir: PathBuf,
-    node: &NodeRuntime,
+    node: &dyn NodeRuntime,
 ) -> Option<LanguageServerBinary> {
     (|| async move {
         let mut last_version_dir = None;

crates/zed/src/languages/svelte.rs 🔗

@@ -21,11 +21,11 @@ fn server_binary_arguments(server_path: &Path) -> Vec<OsString> {
 }
 
 pub struct SvelteLspAdapter {
-    node: Arc<NodeRuntime>,
+    node: Arc<dyn NodeRuntime>,
 }
 
 impl SvelteLspAdapter {
-    pub fn new(node: Arc<NodeRuntime>) -> Self {
+    pub fn new(node: Arc<dyn NodeRuntime>) -> Self {
         SvelteLspAdapter { node }
     }
 }
@@ -64,7 +64,7 @@ impl LspAdapter for SvelteLspAdapter {
             self.node
                 .npm_install_packages(
                     &container_dir,
-                    [("svelte-language-server", version.as_str())],
+                    &[("svelte-language-server", version.as_str())],
                 )
                 .await?;
         }
@@ -80,14 +80,14 @@ impl LspAdapter for SvelteLspAdapter {
         container_dir: PathBuf,
         _: &dyn LspAdapterDelegate,
     ) -> Option<LanguageServerBinary> {
-        get_cached_server_binary(container_dir, &self.node).await
+        get_cached_server_binary(container_dir, &*self.node).await
     }
 
     async fn installation_test_binary(
         &self,
         container_dir: PathBuf,
     ) -> Option<LanguageServerBinary> {
-        get_cached_server_binary(container_dir, &self.node).await
+        get_cached_server_binary(container_dir, &*self.node).await
     }
 
     async fn initialization_options(&self) -> Option<serde_json::Value> {
@@ -99,7 +99,7 @@ impl LspAdapter for SvelteLspAdapter {
 
 async fn get_cached_server_binary(
     container_dir: PathBuf,
-    node: &NodeRuntime,
+    node: &dyn NodeRuntime,
 ) -> Option<LanguageServerBinary> {
     (|| async move {
         let mut last_version_dir = None;

crates/zed/src/languages/tailwind.rs 🔗

@@ -26,11 +26,11 @@ fn server_binary_arguments(server_path: &Path) -> Vec<OsString> {
 }
 
 pub struct TailwindLspAdapter {
-    node: Arc<NodeRuntime>,
+    node: Arc<dyn NodeRuntime>,
 }
 
 impl TailwindLspAdapter {
-    pub fn new(node: Arc<NodeRuntime>) -> Self {
+    pub fn new(node: Arc<dyn NodeRuntime>) -> Self {
         TailwindLspAdapter { node }
     }
 }
@@ -69,7 +69,7 @@ impl LspAdapter for TailwindLspAdapter {
             self.node
                 .npm_install_packages(
                     &container_dir,
-                    [("@tailwindcss/language-server", version.as_str())],
+                    &[("@tailwindcss/language-server", version.as_str())],
                 )
                 .await?;
         }
@@ -85,14 +85,14 @@ impl LspAdapter for TailwindLspAdapter {
         container_dir: PathBuf,
         _: &dyn LspAdapterDelegate,
     ) -> Option<LanguageServerBinary> {
-        get_cached_server_binary(container_dir, &self.node).await
+        get_cached_server_binary(container_dir, &*self.node).await
     }
 
     async fn installation_test_binary(
         &self,
         container_dir: PathBuf,
     ) -> Option<LanguageServerBinary> {
-        get_cached_server_binary(container_dir, &self.node).await
+        get_cached_server_binary(container_dir, &*self.node).await
     }
 
     async fn initialization_options(&self) -> Option<serde_json::Value> {
@@ -131,7 +131,7 @@ impl LspAdapter for TailwindLspAdapter {
 
 async fn get_cached_server_binary(
     container_dir: PathBuf,
-    node: &NodeRuntime,
+    node: &dyn NodeRuntime,
 ) -> Option<LanguageServerBinary> {
     (|| async move {
         let mut last_version_dir = None;

crates/zed/src/languages/typescript.rs 🔗

@@ -33,14 +33,14 @@ fn eslint_server_binary_arguments(server_path: &Path) -> Vec<OsString> {
 }
 
 pub struct TypeScriptLspAdapter {
-    node: Arc<NodeRuntime>,
+    node: Arc<dyn NodeRuntime>,
 }
 
 impl TypeScriptLspAdapter {
     const OLD_SERVER_PATH: &'static str = "node_modules/typescript-language-server/lib/cli.js";
     const NEW_SERVER_PATH: &'static str = "node_modules/typescript-language-server/lib/cli.mjs";
 
-    pub fn new(node: Arc<NodeRuntime>) -> Self {
+    pub fn new(node: Arc<dyn NodeRuntime>) -> Self {
         TypeScriptLspAdapter { node }
     }
 }
@@ -86,7 +86,7 @@ impl LspAdapter for TypeScriptLspAdapter {
             self.node
                 .npm_install_packages(
                     &container_dir,
-                    [
+                    &[
                         ("typescript", version.typescript_version.as_str()),
                         (
                             "typescript-language-server",
@@ -108,14 +108,14 @@ impl LspAdapter for TypeScriptLspAdapter {
         container_dir: PathBuf,
         _: &dyn LspAdapterDelegate,
     ) -> Option<LanguageServerBinary> {
-        get_cached_ts_server_binary(container_dir, &self.node).await
+        get_cached_ts_server_binary(container_dir, &*self.node).await
     }
 
     async fn installation_test_binary(
         &self,
         container_dir: PathBuf,
     ) -> Option<LanguageServerBinary> {
-        get_cached_ts_server_binary(container_dir, &self.node).await
+        get_cached_ts_server_binary(container_dir, &*self.node).await
     }
 
     fn code_action_kinds(&self) -> Option<Vec<CodeActionKind>> {
@@ -165,7 +165,7 @@ impl LspAdapter for TypeScriptLspAdapter {
 
 async fn get_cached_ts_server_binary(
     container_dir: PathBuf,
-    node: &NodeRuntime,
+    node: &dyn NodeRuntime,
 ) -> Option<LanguageServerBinary> {
     (|| async move {
         let old_server_path = container_dir.join(TypeScriptLspAdapter::OLD_SERVER_PATH);
@@ -192,14 +192,14 @@ async fn get_cached_ts_server_binary(
 }
 
 pub struct EsLintLspAdapter {
-    node: Arc<NodeRuntime>,
+    node: Arc<dyn NodeRuntime>,
 }
 
 impl EsLintLspAdapter {
     const SERVER_PATH: &'static str = "vscode-eslint/server/out/eslintServer.js";
 
     #[allow(unused)]
-    pub fn new(node: Arc<NodeRuntime>) -> Self {
+    pub fn new(node: Arc<dyn NodeRuntime>) -> Self {
         EsLintLspAdapter { node }
     }
 }
@@ -288,14 +288,14 @@ impl LspAdapter for EsLintLspAdapter {
         container_dir: PathBuf,
         _: &dyn LspAdapterDelegate,
     ) -> Option<LanguageServerBinary> {
-        get_cached_eslint_server_binary(container_dir, &self.node).await
+        get_cached_eslint_server_binary(container_dir, &*self.node).await
     }
 
     async fn installation_test_binary(
         &self,
         container_dir: PathBuf,
     ) -> Option<LanguageServerBinary> {
-        get_cached_eslint_server_binary(container_dir, &self.node).await
+        get_cached_eslint_server_binary(container_dir, &*self.node).await
     }
 
     async fn label_for_completion(
@@ -313,7 +313,7 @@ impl LspAdapter for EsLintLspAdapter {
 
 async fn get_cached_eslint_server_binary(
     container_dir: PathBuf,
-    node: &NodeRuntime,
+    node: &dyn NodeRuntime,
 ) -> Option<LanguageServerBinary> {
     (|| async move {
         // This is unfortunate but we don't know what the version is to build a path directly

crates/zed/src/languages/yaml.rs 🔗

@@ -25,11 +25,11 @@ fn server_binary_arguments(server_path: &Path) -> Vec<OsString> {
 }
 
 pub struct YamlLspAdapter {
-    node: Arc<NodeRuntime>,
+    node: Arc<dyn NodeRuntime>,
 }
 
 impl YamlLspAdapter {
-    pub fn new(node: Arc<NodeRuntime>) -> Self {
+    pub fn new(node: Arc<dyn NodeRuntime>) -> Self {
         YamlLspAdapter { node }
     }
 }
@@ -66,7 +66,10 @@ impl LspAdapter for YamlLspAdapter {
 
         if fs::metadata(&server_path).await.is_err() {
             self.node
-                .npm_install_packages(&container_dir, [("yaml-language-server", version.as_str())])
+                .npm_install_packages(
+                    &container_dir,
+                    &[("yaml-language-server", version.as_str())],
+                )
                 .await?;
         }
 
@@ -81,14 +84,14 @@ impl LspAdapter for YamlLspAdapter {
         container_dir: PathBuf,
         _: &dyn LspAdapterDelegate,
     ) -> Option<LanguageServerBinary> {
-        get_cached_server_binary(container_dir, &self.node).await
+        get_cached_server_binary(container_dir, &*self.node).await
     }
 
     async fn installation_test_binary(
         &self,
         container_dir: PathBuf,
     ) -> Option<LanguageServerBinary> {
-        get_cached_server_binary(container_dir, &self.node).await
+        get_cached_server_binary(container_dir, &*self.node).await
     }
     fn workspace_configuration(&self, cx: &mut AppContext) -> BoxFuture<'static, Value> {
         let tab_size = all_language_settings(None, cx)
@@ -109,7 +112,7 @@ impl LspAdapter for YamlLspAdapter {
 
 async fn get_cached_server_binary(
     container_dir: PathBuf,
-    node: &NodeRuntime,
+    node: &dyn NodeRuntime,
 ) -> Option<LanguageServerBinary> {
     (|| async move {
         let mut last_version_dir = None;

crates/zed/src/main.rs 🔗

@@ -19,7 +19,7 @@ use gpui::{Action, App, AppContext, AssetSource, AsyncAppContext, Task};
 use isahc::{config::Configurable, Request};
 use language::{LanguageRegistry, Point};
 use log::LevelFilter;
-use node_runtime::NodeRuntime;
+use node_runtime::RealNodeRuntime;
 use parking_lot::Mutex;
 use project::Fs;
 use serde::{Deserialize, Serialize};
@@ -138,7 +138,7 @@ fn main() {
         languages.set_executor(cx.background().clone());
         languages.set_language_server_download_dir(paths::LANGUAGES_DIR.clone());
         let languages = Arc::new(languages);
-        let node_runtime = NodeRuntime::instance(http.clone());
+        let node_runtime = RealNodeRuntime::new(http.clone());
 
         languages::init(languages.clone(), node_runtime.clone());
         let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http.clone(), cx));

crates/zed/src/zed.rs 🔗

@@ -723,7 +723,6 @@ mod tests {
         AppContext, AssetSource, Element, Entity, TestAppContext, View, ViewHandle,
     };
     use language::LanguageRegistry;
-    use node_runtime::NodeRuntime;
     use project::{Project, ProjectPath};
     use serde_json::json;
     use settings::{handle_settings_file_changes, watch_config_file, SettingsStore};
@@ -732,7 +731,6 @@ mod tests {
         path::{Path, PathBuf},
     };
     use theme::{ThemeRegistry, ThemeSettings};
-    use util::http::FakeHttpClient;
     use workspace::{
         item::{Item, ItemHandle},
         open_new, open_paths, pane, NewFile, SplitDirection, WorkspaceHandle,
@@ -2364,8 +2362,7 @@ mod tests {
         let mut languages = LanguageRegistry::test();
         languages.set_executor(cx.background().clone());
         let languages = Arc::new(languages);
-        let http = FakeHttpClient::with_404_response();
-        let node_runtime = NodeRuntime::instance(http);
+        let node_runtime = node_runtime::FakeNodeRuntime::new();
         languages::init(languages.clone(), node_runtime);
         for name in languages.language_names() {
             languages.language_for_name(&name);

script/randomized-test-minimize 🔗

@@ -9,7 +9,6 @@ const CARGO_TEST_ARGS = [
   '--release',
   '--lib',
   '--package', 'collab',
-  'random_collaboration',
 ]
 
 if (require.main === module) {
@@ -99,7 +98,7 @@ function buildTests() {
 }
 
 function runTests(env) {
-  const {status, stdout} = spawnSync('cargo', ['test', ...CARGO_TEST_ARGS], {
+  const {status, stdout} = spawnSync('cargo', ['test', ...CARGO_TEST_ARGS, 'random_project_collaboration'], {
     stdio: 'pipe',
     encoding: 'utf8',
     env: {

styles/src/component/text_button.ts 🔗

@@ -1,5 +1,6 @@
 import { interactive, toggleable } from "../element"
 import {
+    Border,
     TextProperties,
     background,
     foreground,
@@ -16,6 +17,7 @@ interface TextButtonOptions {
     margin?: Partial<Margin>
     disabled?: boolean
     text_properties?: TextProperties
+    border?: Border
 }
 
 type ToggleableTextButtonOptions = TextButtonOptions & {
@@ -29,6 +31,7 @@ export function text_button({
     margin,
     disabled,
     text_properties,
+    border,
 }: TextButtonOptions = {}) {
     const theme = useTheme()
     if (!color) color = "base"
@@ -66,6 +69,7 @@ export function text_button({
         },
         state: {
             default: {
+                border,
                 background: background_color,
                 color: disabled
                     ? foreground(layer ?? theme.lowest, "disabled")
@@ -74,6 +78,7 @@ export function text_button({
             hovered: disabled
                 ? {}
                 : {
+                    border,
                     background: background(
                         layer ?? theme.lowest,
                         color,
@@ -88,6 +93,7 @@ export function text_button({
             clicked: disabled
                 ? {}
                 : {
+                    border,
                     background: background(
                         layer ?? theme.lowest,
                         color,

styles/src/style_tree/search.ts 🔗

@@ -48,7 +48,7 @@ export default function search(): any {
     }
 
     return {
-        padding: { top: 0, bottom: 0 },
+        padding: { top: 4, bottom: 4 },
 
         option_button: toggleable({
             base: interactive({
@@ -394,7 +394,7 @@ export default function search(): any {
                 }),
             },
         }),
-        search_bar_row_height: 32,
+        search_bar_row_height: 34,
         search_row_spacing: 8,
         option_button_height: 22,
         modes_container: {},

styles/src/style_tree/toolbar.ts 🔗

@@ -1,14 +1,15 @@
 import { useTheme } from "../common"
 import { toggleable_icon_button } from "../component/icon_button"
-import { interactive } from "../element"
+import { interactive, toggleable } from "../element"
 import { background, border, foreground, text } from "./components"
+import { text_button } from "../component";
 
 export const toolbar = () => {
     const theme = useTheme()
 
     return {
-        height: 32,
-        padding: { left: 4, right: 4, top: 4, bottom: 4 },
+        height: 42,
+        padding: { left: 4, right: 4 },
         background: background(theme.highest),
         border: border(theme.highest, { bottom: true }),
         item_spacing: 4,
@@ -34,5 +35,24 @@ export const toolbar = () => {
                 },
             },
         }),
+        toggleable_text_tool: toggleable({
+            state: {
+                inactive: text_button({
+                    disabled: true,
+                    variant: "ghost",
+                    layer: theme.highest,
+                    margin: { left: 4 },
+                    text_properties: { size: "sm" },
+                    border: border(theme.middle),
+                }),
+                active: text_button({
+                    variant: "ghost",
+                    layer: theme.highest,
+                    margin: { left: 4 },
+                    text_properties: { size: "sm" },
+                    border: border(theme.middle),
+                }),
+            }
+        }),
     }
 }