Extract worktree, rpc_client, and util crates

Nathan Sobo and Max Brunsfeld created

Co-Authored-By: Max Brunsfeld <maxbrunsfeld@gmail.com>

Change summary

Cargo.lock                             |  66 +++++++++
Cargo.toml                             |   3 
buffer/src/lib.rs                      |   2 
rpc_client/Cargo.toml                  |  55 +++++++
rpc_client/src/lib.rs                  |   5 
rpc_client/src/test.rs                 | 156 +++++++++++++++++++++
util/Cargo.toml                        |  15 ++
util/src/lib.rs                        |  34 ----
util/src/test.rs                       |  37 +++++
worktree/Cargo.toml                    |  61 ++++++++
worktree/src/fs.rs                     |   0 
worktree/src/ignore.rs                 |   0 
worktree/src/lib.rs                    |  29 ++-
zed/Cargo.toml                         |  10 +
zed/src/channel.rs                     |  19 +-
zed/src/chat_panel.rs                  |  11 
zed/src/editor.rs                      |  12 -
zed/src/editor/display_map.rs          |   4 
zed/src/editor/display_map/fold_map.rs |   3 
zed/src/editor/display_map/tab_map.rs  |   2 
zed/src/editor/display_map/wrap_map.rs |   6 
zed/src/file_finder.rs                 |   6 
zed/src/fuzzy.rs                       |   6 
zed/src/lib.rs                         |  11 
zed/src/project.rs                     |  16 -
zed/src/project_panel.rs               |   2 
zed/src/test.rs                        | 200 ---------------------------
zed/src/user.rs                        |  20 +-
zed/src/workspace.rs                   |   5 
29 files changed, 489 insertions(+), 307 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -4087,6 +4087,27 @@ dependencies = [
  "xmlparser",
 ]
 
+[[package]]
+name = "rpc_client"
+version = "0.1.0"
+dependencies = [
+ "anyhow",
+ "async-recursion",
+ "async-tungstenite",
+ "gpui",
+ "lazy_static",
+ "log",
+ "parking_lot",
+ "postage",
+ "rand 0.8.3",
+ "smol",
+ "surf",
+ "thiserror",
+ "tiny_http",
+ "util",
+ "zrpc",
+]
+
 [[package]]
 name = "rsa"
 version = "0.4.0"
@@ -5625,6 +5646,18 @@ version = "0.7.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9"
 
+[[package]]
+name = "util"
+version = "0.1.0"
+dependencies = [
+ "anyhow",
+ "futures",
+ "log",
+ "serde_json 1.0.64",
+ "surf",
+ "tempdir",
+]
+
 [[package]]
 name = "uuid"
 version = "0.5.1"
@@ -5897,6 +5930,36 @@ dependencies = [
  "winapi 0.3.9",
 ]
 
+[[package]]
+name = "worktree"
+version = "0.1.0"
+dependencies = [
+ "anyhow",
+ "async-trait",
+ "buffer",
+ "clock",
+ "fsevent",
+ "futures",
+ "fuzzy",
+ "gpui",
+ "ignore",
+ "lazy_static",
+ "libc",
+ "log",
+ "parking_lot",
+ "postage",
+ "rand 0.8.3",
+ "rpc_client",
+ "serde 1.0.125",
+ "serde_json 1.0.64",
+ "smol",
+ "sum_tree",
+ "tempdir",
+ "toml 0.5.8",
+ "util",
+ "zrpc",
+]
+
 [[package]]
 name = "wyz"
 version = "0.2.0"
@@ -5962,6 +6025,7 @@ dependencies = [
  "parking_lot",
  "postage",
  "rand 0.8.3",
+ "rpc_client",
  "rsa",
  "rust-embed",
  "serde 1.0.125",
@@ -5981,6 +6045,8 @@ dependencies = [
  "tree-sitter-rust",
  "unindent",
  "url",
+ "util",
+ "worktree",
  "zrpc",
 ]
 

Cargo.toml 🔗

@@ -6,8 +6,11 @@ members = [
     "fuzzy",
     "gpui",
     "gpui_macros",
+    "rpc_client",
     "server",
     "sum_tree",
+    "util",
+    "worktree",
     "zed",
     "zrpc"
 ]

buffer/src/lib.rs 🔗

@@ -20,6 +20,8 @@ use lazy_static::lazy_static;
 use operation_queue::OperationQueue;
 use parking_lot::Mutex;
 pub use point::*;
+#[cfg(any(test, feature = "test-support"))]
+pub use random_char_iter::*;
 pub use rope::{Chunks, Rope, TextSummary};
 use seahash::SeaHasher;
 pub use selection::*;

rpc_client/Cargo.toml 🔗

@@ -0,0 +1,55 @@
+[package]
+name = "rpc_client"
+version = "0.1.0"
+edition = "2018"
+
+[features]
+test-support = []
+
+[dependencies]
+anyhow = "1.0.38"
+async-recursion = "0.3"
+# async-trait = "0.1"
+async-tungstenite = { version = "0.14", features = ["async-tls"] }
+# buffer = { path = "../buffer" }
+# clock = { path = "../clock" }
+# crossbeam-channel = "0.5.0"
+# ctor = "0.1.20"
+# dirs = "3.0"
+# easy-parallel = "3.1.0"
+# fsevent = { path = "../fsevent" }
+# futures = "0.3"
+# fuzzy = { path = "../fuzzy" }
+gpui = { path = "../gpui" }
+# http-auth-basic = "0.1.3"
+# ignore = "0.4"
+# image = "0.23"
+# indexmap = "1.6.2"
+lazy_static = "1.4.0"
+# libc = "0.2"
+log = "0.4"
+# log-panics = { version = "2.0", features = ["with-backtrace"] }
+# num_cpus = "1.13.0"
+parking_lot = "0.11.1"
+postage = { version = "0.4.1", features = ["futures-traits"] }
+rand = "0.8.3"
+# rsa = "0.4"
+# rust-embed = { version = "6.2", features = ["include-exclude"] }
+# serde = { version = "1", features = ["derive"] }
+# serde_json = { version = "1.0.64", features = ["preserve_order"] }
+# serde_path_to_error = "0.1.4"
+# simplelog = "0.9"
+# smallvec = { version = "1.6", features = ["union"] }
+smol = "1.2.5"
+# sum_tree = { path = "../sum_tree" }
+surf = "2.2"
+# tempdir = { version = "0.3.7", optional = true }
+thiserror = "1.0.29"
+# time = { version = "0.3" }
+tiny_http = "0.8"
+# toml = "0.5"
+# tree-sitter = "0.19.5"
+# tree-sitter-rust = "0.19.0"
+util = { path = "../util" }
+# url = "2.2"
+zrpc = { path = "../zrpc" }

zed/src/rpc.rs → rpc_client/src/lib.rs 🔗

@@ -1,4 +1,6 @@
-use crate::util::ResultExt;
+#[cfg(any(test, feature = "test-support"))]
+pub mod test;
+
 use anyhow::{anyhow, Context, Result};
 use async_recursion::async_recursion;
 use async_tungstenite::tungstenite::{
@@ -21,6 +23,7 @@ use std::{
 };
 use surf::Url;
 use thiserror::Error;
+use util::ResultExt;
 pub use zrpc::{proto, ConnectionId, PeerId, TypedEnvelope};
 use zrpc::{
     proto::{AnyTypedEnvelope, EntityMessage, EnvelopedMessage, RequestMessage},

rpc_client/src/test.rs 🔗

@@ -0,0 +1,156 @@
+use super::*;
+use std::sync::atomic::Ordering::SeqCst;
+
+use super::Client;
+use gpui::TestAppContext;
+use parking_lot::Mutex;
+use postage::{mpsc, prelude::Stream};
+use std::sync::{
+    atomic::{AtomicBool, AtomicUsize},
+    Arc,
+};
+use zrpc::{proto, ConnectionId, Peer, Receipt, TypedEnvelope};
+
+pub struct FakeServer {
+    peer: Arc<Peer>,
+    incoming: Mutex<Option<mpsc::Receiver<Box<dyn proto::AnyTypedEnvelope>>>>,
+    connection_id: Mutex<Option<ConnectionId>>,
+    forbid_connections: AtomicBool,
+    auth_count: AtomicUsize,
+    access_token: AtomicUsize,
+    user_id: u64,
+}
+
+impl FakeServer {
+    pub async fn for_client(
+        client_user_id: u64,
+        client: &mut Arc<Client>,
+        cx: &TestAppContext,
+    ) -> Arc<Self> {
+        let server = Arc::new(Self {
+            peer: Peer::new(),
+            incoming: Default::default(),
+            connection_id: Default::default(),
+            forbid_connections: Default::default(),
+            auth_count: Default::default(),
+            access_token: Default::default(),
+            user_id: client_user_id,
+        });
+
+        Arc::get_mut(client)
+            .unwrap()
+            .override_authenticate({
+                let server = server.clone();
+                move |cx| {
+                    server.auth_count.fetch_add(1, SeqCst);
+                    let access_token = server.access_token.load(SeqCst).to_string();
+                    cx.spawn(move |_| async move {
+                        Ok(Credentials {
+                            user_id: client_user_id,
+                            access_token,
+                        })
+                    })
+                }
+            })
+            .override_establish_connection({
+                let server = server.clone();
+                move |credentials, cx| {
+                    let credentials = credentials.clone();
+                    cx.spawn({
+                        let server = server.clone();
+                        move |cx| async move { server.establish_connection(&credentials, &cx).await }
+                    })
+                }
+            });
+
+        client
+            .authenticate_and_connect(&cx.to_async())
+            .await
+            .unwrap();
+        server
+    }
+
+    pub async fn disconnect(&self) {
+        self.peer.disconnect(self.connection_id()).await;
+        self.connection_id.lock().take();
+        self.incoming.lock().take();
+    }
+
+    async fn establish_connection(
+        &self,
+        credentials: &Credentials,
+        cx: &AsyncAppContext,
+    ) -> Result<Connection, EstablishConnectionError> {
+        assert_eq!(credentials.user_id, self.user_id);
+
+        if self.forbid_connections.load(SeqCst) {
+            Err(EstablishConnectionError::Other(anyhow!(
+                "server is forbidding connections"
+            )))?
+        }
+
+        if credentials.access_token != self.access_token.load(SeqCst).to_string() {
+            Err(EstablishConnectionError::Unauthorized)?
+        }
+
+        let (client_conn, server_conn, _) = Connection::in_memory();
+        let (connection_id, io, incoming) = self.peer.add_connection(server_conn).await;
+        cx.background().spawn(io).detach();
+        *self.incoming.lock() = Some(incoming);
+        *self.connection_id.lock() = Some(connection_id);
+        Ok(client_conn)
+    }
+
+    pub fn auth_count(&self) -> usize {
+        self.auth_count.load(SeqCst)
+    }
+
+    pub fn roll_access_token(&self) {
+        self.access_token.fetch_add(1, 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 send<T: proto::EnvelopedMessage>(&self, message: T) {
+        self.peer.send(self.connection_id(), message).await.unwrap();
+    }
+
+    pub async fn receive<M: proto::EnvelopedMessage>(&self) -> Result<TypedEnvelope<M>> {
+        let message = self
+            .incoming
+            .lock()
+            .as_mut()
+            .expect("not connected")
+            .recv()
+            .await
+            .ok_or_else(|| anyhow!("other half hung up"))?;
+        let type_name = message.payload_type_name();
+        Ok(*message
+            .into_any()
+            .downcast::<TypedEnvelope<M>>()
+            .unwrap_or_else(|_| {
+                panic!(
+                    "fake server received unexpected message type: {:?}",
+                    type_name
+                );
+            }))
+    }
+
+    pub async fn respond<T: proto::RequestMessage>(
+        &self,
+        receipt: Receipt<T>,
+        response: T::Response,
+    ) {
+        self.peer.respond(receipt, response).await.unwrap()
+    }
+
+    fn connection_id(&self) -> ConnectionId {
+        self.connection_id.lock().expect("not connected")
+    }
+}

util/Cargo.toml 🔗

@@ -0,0 +1,15 @@
+[package]
+name = "util"
+version = "0.1.0"
+edition = "2018"
+
+[features]
+test-support = ["serde_json", "tempdir"]
+
+[dependencies]
+anyhow = "1.0.38"
+futures = "0.3"
+log = "0.4"
+surf = "2.2"
+tempdir = { version = "0.3.7", optional = true }
+serde_json = { version = "1.0.64", features = ["preserve_order"], optional = true }

zed/src/util.rs → util/src/lib.rs 🔗

@@ -1,11 +1,12 @@
+#[cfg(feature = "test-support")]
+pub mod test;
+
 use futures::Future;
-use rand::prelude::*;
 use std::{
     cmp::Ordering,
     pin::Pin,
     task::{Context, Poll},
 };
-pub use sum_tree::Bias;
 
 pub fn post_inc(value: &mut usize) -> usize {
     let prev = *value;
@@ -35,35 +36,6 @@ where
         }
     }
 }
-
-pub struct RandomCharIter<T: Rng>(T);
-
-impl<T: Rng> RandomCharIter<T> {
-    #[cfg(test)]
-    pub fn new(rng: T) -> Self {
-        Self(rng)
-    }
-}
-
-impl<T: Rng> Iterator for RandomCharIter<T> {
-    type Item = char;
-
-    fn next(&mut self) -> Option<Self::Item> {
-        match self.0.gen_range(0..100) {
-            // whitespace
-            0..=19 => [' ', '\n', '\t'].choose(&mut self.0).copied(),
-            // two-byte greek letters
-            20..=32 => char::from_u32(self.0.gen_range(('α' as u32)..('ω' as u32 + 1))),
-            // three-byte characters
-            33..=45 => ['✋', '✅', '❌', '❎', '⭐'].choose(&mut self.0).copied(),
-            // four-byte characters
-            46..=58 => ['🍐', '🏀', '🍗', '🎉'].choose(&mut self.0).copied(),
-            // ascii letters
-            _ => Some(self.0.gen_range(b'a'..b'z' + 1).into()),
-        }
-    }
-}
-
 pub trait ResultExt {
     type Ok;
 

util/src/test.rs 🔗

@@ -0,0 +1,37 @@
+use std::path::{Path, PathBuf};
+use tempdir::TempDir;
+
+pub fn temp_tree(tree: serde_json::Value) -> TempDir {
+    let dir = TempDir::new("").unwrap();
+    write_tree(dir.path(), tree);
+    dir
+}
+
+fn write_tree(path: &Path, tree: serde_json::Value) {
+    use serde_json::Value;
+    use std::fs;
+
+    if let Value::Object(map) = tree {
+        for (name, contents) in map {
+            let mut path = PathBuf::from(path);
+            path.push(name);
+            match contents {
+                Value::Object(_) => {
+                    fs::create_dir(&path).unwrap();
+                    write_tree(&path, contents);
+                }
+                Value::Null => {
+                    fs::create_dir(&path).unwrap();
+                }
+                Value::String(contents) => {
+                    fs::write(&path, contents).unwrap();
+                }
+                _ => {
+                    panic!("JSON object must contain only objects, strings, or null");
+                }
+            }
+        }
+    } else {
+        panic!("You must pass a JSON object to this helper")
+    }
+}

worktree/Cargo.toml 🔗

@@ -0,0 +1,61 @@
+[package]
+name = "worktree"
+version = "0.1.0"
+edition = "2018"
+
+[features]
+test-support = []
+
+[dependencies]
+anyhow = "1.0.38"
+# async-recursion = "0.3"
+async-trait = "0.1"
+# async-tungstenite = { version = "0.14", features = ["async-tls"] }
+buffer = { path = "../buffer" }
+clock = { path = "../clock" }
+# crossbeam-channel = "0.5.0"
+# ctor = "0.1.20"
+# dirs = "3.0"
+# easy-parallel = "3.1.0"
+fsevent = { path = "../fsevent" }
+futures = "0.3"
+fuzzy = { path = "../fuzzy" }
+gpui = { path = "../gpui" }
+# http-auth-basic = "0.1.3"
+ignore = "0.4"
+# image = "0.23"
+# indexmap = "1.6.2"
+lazy_static = "1.4.0"
+libc = "0.2"
+log = "0.4"
+# log-panics = { version = "2.0", features = ["with-backtrace"] }
+# num_cpus = "1.13.0"
+parking_lot = "0.11.1"
+postage = { version = "0.4.1", features = ["futures-traits"] }
+rpc_client = { path = "../rpc_client" }
+# rand = "0.8.3"
+# rsa = "0.4"
+# rust-embed = { version = "6.2", features = ["include-exclude"] }
+serde = { version = "1", features = ["derive"] }
+serde_json = { version = "1.0.64", features = ["preserve_order"] }
+# serde_path_to_error = "0.1.4"
+# simplelog = "0.9"
+# smallvec = { version = "1.6", features = ["union"] }
+smol = "1.2.5"
+sum_tree = { path = "../sum_tree" }
+util = { path = "../util" }
+# surf = "2.2"
+# tempdir = { version = "0.3.7", optional = true }
+# thiserror = "1.0.29"
+# time = { version = "0.3" }
+# tiny_http = "0.8"
+toml = "0.5"
+# tree-sitter = "0.19.5"
+# tree-sitter-rust = "0.19.0"
+# url = "2.2"
+zrpc = { path = "../zrpc" }
+
+[dev-dependencies]
+rand = "0.8.3"
+tempdir = { version = "0.3.7" }
+util = { path = "../util", features = ["test-support"] }

zed/src/worktree.rs → worktree/src/lib.rs 🔗

@@ -1,17 +1,14 @@
+pub mod fs;
 mod ignore;
 
 use self::ignore::IgnoreStack;
-use crate::{
-    fs::{self, Fs},
-    fuzzy::CharBag,
-    rpc::{self, proto, Status},
-    util::{Bias, TryFutureExt},
-};
 use ::ignore::gitignore::{Gitignore, GitignoreBuilder};
 use anyhow::{anyhow, Result};
 use buffer::{self, Buffer, History, LanguageRegistry, Operation, Rope};
 use clock::ReplicaId;
+pub use fs::*;
 use futures::{Stream, StreamExt};
+use fuzzy::CharBag;
 use gpui::{
     executor, AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle, MutableAppContext,
     Task, UpgradeModelHandle, WeakModelHandle,
@@ -22,6 +19,7 @@ use postage::{
     prelude::{Sink as _, Stream as _},
     watch,
 };
+use rpc_client as rpc;
 use serde::Deserialize;
 use smol::channel::{self, Sender};
 use std::{
@@ -40,8 +38,10 @@ use std::{
     },
     time::{Duration, SystemTime},
 };
+use sum_tree::Bias;
 use sum_tree::{self, Edit, SeekTarget, SumTree};
-use zrpc::{PeerId, TypedEnvelope};
+use util::TryFutureExt;
+use zrpc::{proto, PeerId, TypedEnvelope};
 
 lazy_static! {
     static ref GITIGNORE: &'static OsStr = OsStr::new(".gitignore");
@@ -755,7 +755,7 @@ impl LocalWorktree {
                         let mut status = rpc.status();
                         while let Some(status) = status.recv().await {
                             if let Some(this) = this.upgrade(&cx) {
-                                let remote_id = if let Status::Connected { .. } = status {
+                                let remote_id = if let rpc::Status::Connected { .. } = status {
                                     let collaborator_logins = this.read_with(&cx, |this, _| {
                                         this.as_local().unwrap().config.collaborators.clone()
                                     });
@@ -2832,16 +2832,19 @@ impl<'a> TryFrom<(&'a CharBag, proto::Entry)> for Entry {
 #[cfg(test)]
 mod tests {
     use super::*;
+    use rpc_client::test::FakeServer;
     use crate::fs::FakeFs;
-    use crate::test::*;
     use anyhow::Result;
     use fs::RealFs;
     use rand::prelude::*;
     use serde_json::json;
-    use std::cell::RefCell;
-    use std::rc::Rc;
-    use std::time::UNIX_EPOCH;
-    use std::{env, fmt::Write, time::SystemTime};
+    use std::{cell::RefCell, rc::Rc};
+    use std::{
+        env,
+        fmt::Write,
+        time::{SystemTime, UNIX_EPOCH},
+    };
+    use util::test::temp_tree;
 
     #[gpui::test]
     async fn test_traversal(cx: gpui::TestAppContext) {

zed/Cargo.toml 🔗

@@ -17,7 +17,9 @@ path = "src/main.rs"
 test-support = [
     "buffer/test-support",
     "gpui/test-support",
+    "rpc_client/test-support",
     "tempdir",
+    "worktree/test-support",
     "zrpc/test-support",
 ]
 
@@ -48,6 +50,7 @@ num_cpus = "1.13.0"
 parking_lot = "0.11.1"
 postage = { version = "0.4.1", features = ["futures-traits"] }
 rand = "0.8.3"
+rpc_client = { path = "../rpc_client" }
 rsa = "0.4"
 rust-embed = { version = "6.2", features = ["include-exclude"] }
 serde = { version = "1", features = ["derive"] }
@@ -66,6 +69,8 @@ toml = "0.5"
 tree-sitter = "0.19.5"
 tree-sitter-rust = "0.19.0"
 url = "2.2"
+util = { path = "../util" }
+worktree =  { path = "../worktree" }
 zrpc = { path = "../zrpc" }
 
 [dev-dependencies]
@@ -75,8 +80,11 @@ serde_json = { version = "1.0.64", features = ["preserve_order"] }
 tempdir = { version = "0.3.7" }
 unindent = "0.1.7"
 buffer = { path = "../buffer", features = ["test-support"] }
-zrpc = { path = "../zrpc", features = ["test-support"] }
 gpui = { path = "../gpui", features = ["test-support"] }
+rpc_client = { path = "../rpc_client", features = ["test-support"] }
+util = { path = "../util", features = ["test-support"] }
+worktree = { path = "../worktree", features = ["test-support"] }
+zrpc = { path = "../zrpc", features = ["test-support"] }
 
 [package.metadata.bundle]
 icon = ["app-icon@2x.png", "app-icon.png"]

zed/src/channel.rs 🔗

@@ -1,14 +1,11 @@
-use crate::{
-    rpc::{self, Client},
-    user::{User, UserStore},
-    util::{post_inc, TryFutureExt},
-};
+use crate::user::{User, UserStore};
 use anyhow::{anyhow, Context, Result};
 use gpui::{
     AsyncAppContext, Entity, ModelContext, ModelHandle, MutableAppContext, Task, WeakModelHandle,
 };
 use postage::prelude::Stream;
 use rand::prelude::*;
+use rpc_client as rpc;
 use std::{
     collections::{HashMap, HashSet},
     mem,
@@ -17,6 +14,7 @@ use std::{
 };
 use sum_tree::{self, Bias, SumTree};
 use time::OffsetDateTime;
+use util::{post_inc, TryFutureExt};
 use zrpc::{
     proto::{self, ChannelMessageSent},
     TypedEnvelope,
@@ -25,7 +23,7 @@ use zrpc::{
 pub struct ChannelList {
     available_channels: Option<Vec<ChannelDetails>>,
     channels: HashMap<u64, WeakModelHandle<Channel>>,
-    rpc: Arc<Client>,
+    rpc: Arc<rpc::Client>,
     user_store: ModelHandle<UserStore>,
     _task: Task<Option<()>>,
 }
@@ -42,7 +40,7 @@ pub struct Channel {
     loaded_all_messages: bool,
     next_pending_message_id: usize,
     user_store: ModelHandle<UserStore>,
-    rpc: Arc<Client>,
+    rpc: Arc<rpc::Client>,
     rng: StdRng,
     _subscription: rpc::Subscription,
 }
@@ -187,7 +185,7 @@ impl Channel {
     pub fn new(
         details: ChannelDetails,
         user_store: ModelHandle<UserStore>,
-        rpc: Arc<Client>,
+        rpc: Arc<rpc::Client>,
         cx: &mut ModelContext<Self>,
     ) -> Self {
         let _subscription = rpc.subscribe_to_entity(details.id, cx, Self::handle_message_sent);
@@ -594,14 +592,15 @@ impl<'a> sum_tree::Dimension<'a, ChannelMessageSummary> for Count {
 #[cfg(test)]
 mod tests {
     use super::*;
-    use crate::test::{FakeHttpClient, FakeServer};
+    use crate::test::FakeHttpClient;
     use gpui::TestAppContext;
+    use rpc_client::test::FakeServer;
     use surf::http::Response;
 
     #[gpui::test]
     async fn test_channel_messages(mut cx: TestAppContext) {
         let user_id = 5;
-        let mut client = Client::new();
+        let mut client = rpc::Client::new();
         let http_client = FakeHttpClient::new(|_| async move { Ok(Response::new(404)) });
         let server = FakeServer::for_client(user_id, &mut client, &cx).await;
         let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http_client, cx));

zed/src/chat_panel.rs 🔗

@@ -3,10 +3,7 @@ use std::sync::Arc;
 use crate::{
     channel::{Channel, ChannelEvent, ChannelList, ChannelMessage},
     editor::Editor,
-    rpc::{self, Client},
-    theme,
-    util::{ResultExt, TryFutureExt},
-    Settings,
+    theme, Settings,
 };
 use gpui::{
     action,
@@ -18,12 +15,14 @@ use gpui::{
     ViewContext, ViewHandle,
 };
 use postage::{prelude::Stream, watch};
+use rpc_client as rpc;
 use time::{OffsetDateTime, UtcOffset};
+use util::{ResultExt, TryFutureExt};
 
 const MESSAGE_LOADING_THRESHOLD: usize = 50;
 
 pub struct ChatPanel {
-    rpc: Arc<Client>,
+    rpc: Arc<rpc::Client>,
     channel_list: ModelHandle<ChannelList>,
     active_channel: Option<(ModelHandle<Channel>, Subscription)>,
     message_list: ListState,
@@ -48,7 +47,7 @@ pub fn init(cx: &mut MutableAppContext) {
 
 impl ChatPanel {
     pub fn new(
-        rpc: Arc<Client>,
+        rpc: Arc<rpc::Client>,
         channel_list: ModelHandle<ChannelList>,
         settings: watch::Receiver<Settings>,
         cx: &mut ViewContext<Self>,

zed/src/editor.rs 🔗

@@ -2,14 +2,7 @@ pub mod display_map;
 mod element;
 pub mod movement;
 
-use crate::{
-    project::ProjectPath,
-    settings::Settings,
-    theme::Theme,
-    util::{post_inc, Bias},
-    workspace,
-    worktree::Worktree,
-};
+use crate::{project::ProjectPath, settings::Settings, theme::Theme, workspace};
 use anyhow::Result;
 use buffer::*;
 use clock::ReplicaId;
@@ -35,6 +28,9 @@ use std::{
     sync::Arc,
     time::Duration,
 };
+use sum_tree::Bias;
+use util::post_inc;
+use worktree::Worktree;
 
 const CURSOR_BLINK_INTERVAL: Duration = Duration::from_millis(500);
 const MAX_LINE_LEN: usize = 1024;

zed/src/editor/display_map.rs 🔗

@@ -357,8 +357,8 @@ impl ToDisplayPoint for Anchor {
 #[cfg(test)]
 mod tests {
     use super::*;
-    use crate::{editor::movement, test::*, util::RandomCharIter};
-    use buffer::{History, Language, LanguageConfig, SelectionGoal, SyntaxTheme};
+    use crate::{editor::movement, test::*};
+    use buffer::{History, Language, LanguageConfig, RandomCharIter, SelectionGoal, SyntaxTheme};
     use gpui::{color::Color, MutableAppContext};
     use rand::{prelude::StdRng, Rng};
     use std::{env, sync::Arc};

zed/src/editor/display_map/fold_map.rs 🔗

@@ -1128,7 +1128,8 @@ impl FoldEdit {
 #[cfg(test)]
 mod tests {
     use super::*;
-    use crate::{editor::ToPoint, test::sample_text, util::RandomCharIter};
+    use crate::{editor::ToPoint, test::sample_text};
+    use buffer::RandomCharIter;
     use rand::prelude::*;
     use std::{env, mem};
     use Bias::{Left, Right};

zed/src/editor/display_map/tab_map.rs 🔗

@@ -1,8 +1,8 @@
 use super::fold_map::{self, FoldEdit, FoldPoint, Snapshot as FoldSnapshot};
-use crate::util::Bias;
 use buffer::{rope, HighlightId};
 use parking_lot::Mutex;
 use std::{mem, ops::Range};
+use sum_tree::Bias;
 
 pub struct TabMap(Mutex<Snapshot>);
 

zed/src/editor/display_map/wrap_map.rs 🔗

@@ -2,13 +2,13 @@ use super::{
     fold_map,
     tab_map::{self, Edit as TabEdit, Snapshot as TabSnapshot, TabPoint, TextSummary},
 };
-use crate::{editor::Point, util::Bias};
+use crate::editor::Point;
 use buffer::HighlightId;
 use gpui::{fonts::FontId, text_layout::LineWrapper, Entity, ModelContext, Task};
 use lazy_static::lazy_static;
 use smol::future::yield_now;
 use std::{collections::VecDeque, ops::Range, time::Duration};
-use sum_tree::{self, Cursor, SumTree};
+use sum_tree::{self, Bias, Cursor, SumTree};
 
 pub struct WrapMap {
     snapshot: Snapshot,
@@ -902,8 +902,8 @@ mod tests {
             Buffer,
         },
         test::Observer,
-        util::RandomCharIter,
     };
+    use buffer::RandomCharIter;
     use rand::prelude::*;
     use std::env;
 

zed/src/file_finder.rs 🔗

@@ -3,7 +3,6 @@ use crate::{
     fuzzy::PathMatch,
     project::{Project, ProjectPath},
     settings::Settings,
-    util,
     workspace::Workspace,
 };
 use gpui::{
@@ -26,6 +25,7 @@ use std::{
         Arc,
     },
 };
+use util::post_inc;
 
 pub struct FileFinder {
     handle: WeakViewHandle<Self>,
@@ -315,7 +315,7 @@ impl FileFinder {
             editor::Event::Edited => {
                 let query = self.query_editor.update(cx, |buffer, cx| buffer.text(cx));
                 if query.is_empty() {
-                    self.latest_search_id = util::post_inc(&mut self.search_count);
+                    self.latest_search_id = post_inc(&mut self.search_count);
                     self.matches.clear();
                     cx.notify();
                 } else {
@@ -422,12 +422,12 @@ mod tests {
     use super::*;
     use crate::{
         editor::{self, Insert},
-        fs::FakeFs,
         test::test_app_state,
         workspace::Workspace,
     };
     use serde_json::json;
     use std::path::PathBuf;
+    use worktree::fs::FakeFs;
 
     #[gpui::test]
     async fn test_matching_paths(mut cx: gpui::TestAppContext) {

zed/src/fuzzy.rs 🔗

@@ -1,12 +1,10 @@
-use crate::{
-    util,
-    worktree::{EntryKind, Snapshot},
-};
 use gpui::executor;
 use std::{
     cmp,
     sync::{atomic::AtomicBool, Arc},
 };
+use util;
+use worktree::{EntryKind, Snapshot};
 
 pub use fuzzy::*;
 

zed/src/lib.rs 🔗

@@ -3,7 +3,6 @@ pub mod channel;
 pub mod chat_panel;
 pub mod editor;
 pub mod file_finder;
-pub mod fs;
 mod fuzzy;
 pub mod http;
 pub mod language;
@@ -11,27 +10,25 @@ pub mod menus;
 pub mod people_panel;
 pub mod project;
 pub mod project_panel;
-pub mod rpc;
 pub mod settings;
 #[cfg(any(test, feature = "test-support"))]
 pub mod test;
 pub mod theme;
 pub mod theme_selector;
 pub mod user;
-mod util;
 pub mod workspace;
-pub mod worktree;
 
-use crate::util::TryFutureExt;
 pub use buffer;
 use buffer::LanguageRegistry;
 use channel::ChannelList;
 use gpui::{action, keymap::Binding, ModelHandle};
 use parking_lot::Mutex;
 use postage::watch;
-use std::sync::Arc;
-
+pub use rpc_client as rpc;
 pub use settings::Settings;
+use std::sync::Arc;
+use util::TryFutureExt;
+pub use worktree::{self, fs};
 
 action!(About);
 action!(Quit);

zed/src/project.rs 🔗

@@ -1,25 +1,24 @@
 use crate::{
-    fs::Fs,
     fuzzy::{self, PathMatch},
-    rpc::Client,
-    util::TryFutureExt as _,
-    worktree::{self, Worktree},
     AppState,
 };
 use anyhow::Result;
 use buffer::LanguageRegistry;
 use futures::Future;
 use gpui::{AppContext, Entity, ModelContext, ModelHandle, Task};
+use rpc_client as rpc;
 use std::{
     path::Path,
     sync::{atomic::AtomicBool, Arc},
 };
+use util::TryFutureExt as _;
+use worktree::{fs::Fs, Worktree};
 
 pub struct Project {
     worktrees: Vec<ModelHandle<Worktree>>,
     active_entry: Option<ProjectEntry>,
     languages: Arc<LanguageRegistry>,
-    rpc: Arc<Client>,
+    rpc: Arc<rpc::Client>,
     fs: Arc<dyn Fs>,
 }
 
@@ -237,12 +236,11 @@ impl Entity for Project {
 #[cfg(test)]
 mod tests {
     use super::*;
-    use crate::{
-        fs::RealFs,
-        test::{temp_tree, test_app_state},
-    };
+    use crate::test::test_app_state;
     use serde_json::json;
     use std::{os::unix, path::PathBuf};
+    use util::test::temp_tree;
+    use worktree::fs::RealFs;
 
     #[gpui::test]
     async fn test_populate_and_search(mut cx: gpui::TestAppContext) {

zed/src/project_panel.rs 🔗

@@ -2,7 +2,6 @@ use crate::{
     project::{self, Project, ProjectEntry, ProjectPath},
     theme,
     workspace::Workspace,
-    worktree::{self, Worktree},
     Settings,
 };
 use gpui::{
@@ -26,6 +25,7 @@ use std::{
     ffi::OsStr,
     ops::Range,
 };
+use worktree::Worktree;
 
 pub struct ProjectPanel {
     project: ModelHandle<Project>,

zed/src/test.rs 🔗

@@ -1,32 +1,21 @@
 use crate::{
     assets::Assets,
     channel::ChannelList,
-    fs::FakeFs,
     http::{HttpClient, Request, Response, ServerResponse},
     language,
-    rpc::{self, Client, Credentials, EstablishConnectionError},
     settings::{self, ThemeRegistry},
     user::UserStore,
     AppState,
 };
-use anyhow::{anyhow, Result};
+use anyhow::Result;
 use buffer::LanguageRegistry;
 use futures::{future::BoxFuture, Future};
-use gpui::{AsyncAppContext, Entity, ModelHandle, MutableAppContext, TestAppContext};
+use gpui::{Entity, ModelHandle, MutableAppContext};
 use parking_lot::Mutex;
-use postage::{mpsc, prelude::Stream as _};
+use rpc_client as rpc;
 use smol::channel;
-use std::{
-    fmt,
-    marker::PhantomData,
-    path::{Path, PathBuf},
-    sync::{
-        atomic::{AtomicBool, AtomicUsize, Ordering::SeqCst},
-        Arc,
-    },
-};
-use tempdir::TempDir;
-use zrpc::{proto, Connection, ConnectionId, Peer, Receipt, TypedEnvelope};
+use std::{fmt, marker::PhantomData, sync::Arc};
+use worktree::fs::FakeFs;
 
 #[cfg(test)]
 #[ctor::ctor]
@@ -47,41 +36,6 @@ pub fn sample_text(rows: usize, cols: usize) -> String {
     text
 }
 
-pub fn temp_tree(tree: serde_json::Value) -> TempDir {
-    let dir = TempDir::new("").unwrap();
-    write_tree(dir.path(), tree);
-    dir
-}
-
-fn write_tree(path: &Path, tree: serde_json::Value) {
-    use serde_json::Value;
-    use std::fs;
-
-    if let Value::Object(map) = tree {
-        for (name, contents) in map {
-            let mut path = PathBuf::from(path);
-            path.push(name);
-            match contents {
-                Value::Object(_) => {
-                    fs::create_dir(&path).unwrap();
-                    write_tree(&path, contents);
-                }
-                Value::Null => {
-                    fs::create_dir(&path).unwrap();
-                }
-                Value::String(contents) => {
-                    fs::write(&path, contents).unwrap();
-                }
-                _ => {
-                    panic!("JSON object must contain only objects, strings, or null");
-                }
-            }
-        }
-    } else {
-        panic!("You must pass a JSON object to this helper")
-    }
-}
-
 pub fn test_app_state(cx: &mut MutableAppContext) -> Arc<AppState> {
     let (settings_tx, settings) = settings::test(cx);
     let mut languages = LanguageRegistry::new();
@@ -125,150 +79,6 @@ impl<T: Entity> Observer<T> {
     }
 }
 
-pub struct FakeServer {
-    peer: Arc<Peer>,
-    incoming: Mutex<Option<mpsc::Receiver<Box<dyn proto::AnyTypedEnvelope>>>>,
-    connection_id: Mutex<Option<ConnectionId>>,
-    forbid_connections: AtomicBool,
-    auth_count: AtomicUsize,
-    access_token: AtomicUsize,
-    user_id: u64,
-}
-
-impl FakeServer {
-    pub async fn for_client(
-        client_user_id: u64,
-        client: &mut Arc<Client>,
-        cx: &TestAppContext,
-    ) -> Arc<Self> {
-        let server = Arc::new(Self {
-            peer: Peer::new(),
-            incoming: Default::default(),
-            connection_id: Default::default(),
-            forbid_connections: Default::default(),
-            auth_count: Default::default(),
-            access_token: Default::default(),
-            user_id: client_user_id,
-        });
-
-        Arc::get_mut(client)
-            .unwrap()
-            .override_authenticate({
-                let server = server.clone();
-                move |cx| {
-                    server.auth_count.fetch_add(1, SeqCst);
-                    let access_token = server.access_token.load(SeqCst).to_string();
-                    cx.spawn(move |_| async move {
-                        Ok(Credentials {
-                            user_id: client_user_id,
-                            access_token,
-                        })
-                    })
-                }
-            })
-            .override_establish_connection({
-                let server = server.clone();
-                move |credentials, cx| {
-                    let credentials = credentials.clone();
-                    cx.spawn({
-                        let server = server.clone();
-                        move |cx| async move { server.establish_connection(&credentials, &cx).await }
-                    })
-                }
-            });
-
-        client
-            .authenticate_and_connect(&cx.to_async())
-            .await
-            .unwrap();
-        server
-    }
-
-    pub async fn disconnect(&self) {
-        self.peer.disconnect(self.connection_id()).await;
-        self.connection_id.lock().take();
-        self.incoming.lock().take();
-    }
-
-    async fn establish_connection(
-        &self,
-        credentials: &Credentials,
-        cx: &AsyncAppContext,
-    ) -> Result<Connection, EstablishConnectionError> {
-        assert_eq!(credentials.user_id, self.user_id);
-
-        if self.forbid_connections.load(SeqCst) {
-            Err(EstablishConnectionError::Other(anyhow!(
-                "server is forbidding connections"
-            )))?
-        }
-
-        if credentials.access_token != self.access_token.load(SeqCst).to_string() {
-            Err(EstablishConnectionError::Unauthorized)?
-        }
-
-        let (client_conn, server_conn, _) = Connection::in_memory();
-        let (connection_id, io, incoming) = self.peer.add_connection(server_conn).await;
-        cx.background().spawn(io).detach();
-        *self.incoming.lock() = Some(incoming);
-        *self.connection_id.lock() = Some(connection_id);
-        Ok(client_conn)
-    }
-
-    pub fn auth_count(&self) -> usize {
-        self.auth_count.load(SeqCst)
-    }
-
-    pub fn roll_access_token(&self) {
-        self.access_token.fetch_add(1, 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 send<T: proto::EnvelopedMessage>(&self, message: T) {
-        self.peer.send(self.connection_id(), message).await.unwrap();
-    }
-
-    pub async fn receive<M: proto::EnvelopedMessage>(&self) -> Result<TypedEnvelope<M>> {
-        let message = self
-            .incoming
-            .lock()
-            .as_mut()
-            .expect("not connected")
-            .recv()
-            .await
-            .ok_or_else(|| anyhow!("other half hung up"))?;
-        let type_name = message.payload_type_name();
-        Ok(*message
-            .into_any()
-            .downcast::<TypedEnvelope<M>>()
-            .unwrap_or_else(|_| {
-                panic!(
-                    "fake server received unexpected message type: {:?}",
-                    type_name
-                );
-            }))
-    }
-
-    pub async fn respond<T: proto::RequestMessage>(
-        &self,
-        receipt: Receipt<T>,
-        response: T::Response,
-    ) {
-        self.peer.respond(receipt, response).await.unwrap()
-    }
-
-    fn connection_id(&self) -> ConnectionId {
-        self.connection_id.lock().expect("not connected")
-    }
-}
-
 pub struct FakeHttpClient {
     handler:
         Box<dyn 'static + Send + Sync + Fn(Request) -> BoxFuture<'static, Result<ServerResponse>>>,

zed/src/user.rs 🔗

@@ -1,16 +1,14 @@
-use crate::{
-    http::{HttpClient, Method, Request, Url},
-    rpc::{Client, Status},
-    util::TryFutureExt,
-};
+use crate::http::{HttpClient, Method, Request, Url};
 use anyhow::{anyhow, Context, Result};
 use futures::future;
 use gpui::{AsyncAppContext, Entity, ImageData, ModelContext, ModelHandle, Task};
 use postage::{prelude::Stream, sink::Sink, watch};
+use rpc_client as rpc;
 use std::{
     collections::{HashMap, HashSet},
     sync::Arc,
 };
+use util::TryFutureExt as _;
 use zrpc::{proto, TypedEnvelope};
 
 #[derive(Debug)]
@@ -38,7 +36,7 @@ pub struct UserStore {
     users: HashMap<u64, Arc<User>>,
     current_user: watch::Receiver<Option<Arc<User>>>,
     collaborators: Arc<[Collaborator]>,
-    rpc: Arc<Client>,
+    rpc: Arc<rpc::Client>,
     http: Arc<dyn HttpClient>,
     _maintain_collaborators: Task<()>,
     _maintain_current_user: Task<()>,
@@ -51,7 +49,11 @@ impl Entity for UserStore {
 }
 
 impl UserStore {
-    pub fn new(rpc: Arc<Client>, http: Arc<dyn HttpClient>, cx: &mut ModelContext<Self>) -> Self {
+    pub fn new(
+        rpc: Arc<rpc::Client>,
+        http: Arc<dyn HttpClient>,
+        cx: &mut ModelContext<Self>,
+    ) -> Self {
         let (mut current_user_tx, current_user_rx) = watch::channel();
         let (mut update_collaborators_tx, mut update_collaborators_rx) =
             watch::channel::<Option<proto::UpdateCollaborators>>();
@@ -82,7 +84,7 @@ impl UserStore {
                 let mut status = rpc.status();
                 while let Some(status) = status.recv().await {
                     match status {
-                        Status::Connected { .. } => {
+                        rpc::Status::Connected { .. } => {
                             if let Some((this, user_id)) = this.upgrade(&cx).zip(rpc.user_id()) {
                                 let user = this
                                     .update(&mut cx, |this, cx| this.fetch_user(user_id, cx))
@@ -91,7 +93,7 @@ impl UserStore {
                                 current_user_tx.send(user).await.ok();
                             }
                         }
-                        Status::SignedOut => {
+                        rpc::Status::SignedOut => {
                             current_user_tx.send(None).await.ok();
                         }
                         _ => {}

zed/src/workspace.rs 🔗

@@ -12,7 +12,6 @@ use crate::{
     settings::Settings,
     user,
     workspace::sidebar::{Side, Sidebar, SidebarItemId, ToggleSidebarItem, ToggleSidebarItemFocus},
-    worktree::Worktree,
     AppState, Authenticate,
 };
 use anyhow::Result;
@@ -38,6 +37,7 @@ use std::{
     path::{Path, PathBuf},
     sync::Arc,
 };
+use worktree::Worktree;
 
 action!(Open, Arc<AppState>);
 action!(OpenPaths, OpenParams);
@@ -1159,10 +1159,11 @@ mod tests {
     use crate::{
         editor::{Editor, Insert},
         fs::FakeFs,
-        test::{temp_tree, test_app_state},
+        test::test_app_state,
     };
     use serde_json::json;
     use std::collections::HashSet;
+    use util::test::temp_tree;
 
     #[gpui::test]
     async fn test_open_paths_action(mut cx: gpui::TestAppContext) {