Merge pull request #1068 from zed-industries/collab-snapshots

Antonio Scandurra created

Implement /rpc_server_snapshot endpoint in collab server API

Change summary

Cargo.lock                        | 30 +++++++++++++++++++++++++-----
crates/auto_update/Cargo.toml     |  2 +-
crates/collab/Cargo.toml          |  3 ++-
crates/collab/src/api.rs          | 14 +++++++++++---
crates/collab/src/main.rs         |  6 ++++++
crates/collab/src/rpc.rs          | 26 ++++++++++++++++++++++++++
crates/collab/src/rpc/store.rs    | 11 +++++++++--
crates/command_palette/Cargo.toml |  2 +-
crates/file_finder/Cargo.toml     |  2 +-
crates/gpui/Cargo.toml            |  2 +-
crates/picker/Cargo.toml          |  2 +-
crates/project/Cargo.toml         |  2 +-
crates/project_panel/Cargo.toml   |  2 +-
crates/rpc/src/peer.rs            | 18 ++++++++++++++++--
crates/search/Cargo.toml          |  2 +-
crates/settings/Cargo.toml        |  2 +-
crates/theme/Cargo.toml           |  2 +-
crates/util/Cargo.toml            |  2 +-
crates/workspace/Cargo.toml       |  2 +-
crates/zed/Cargo.toml             |  4 ++--
styles/src/styleTree/workspace.ts |  1 -
21 files changed, 109 insertions(+), 28 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -416,6 +416,25 @@ dependencies = [
  "mime",
 ]
 
+[[package]]
+name = "axum-extra"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "75330529f6b27544cedc6089108602a056d016df6aa4f2cb24408d840392ef2d"
+dependencies = [
+ "axum",
+ "bytes",
+ "http",
+ "mime",
+ "pin-project-lite 0.2.9",
+ "serde",
+ "serde_json",
+ "tower",
+ "tower-http",
+ "tower-layer",
+ "tower-service",
+]
+
 [[package]]
 name = "backtrace"
 version = "0.3.64"
@@ -593,9 +612,9 @@ checksum = "ae44d1a3d5a19df61dd0c8beb138458ac2a53a7ac09eba97d55592540004306b"
 
 [[package]]
 name = "bytes"
-version = "1.0.1"
+version = "1.1.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b700ce4376041dcd0a327fd0097c41095743c4c8af8887265942faf1100bd040"
+checksum = "c4872d67bab6358e59559027aa3b9157c53d9358c51423c17554809a8858e0f8"
 
 [[package]]
 name = "cache-padded"
@@ -841,6 +860,7 @@ dependencies = [
  "async-trait",
  "async-tungstenite",
  "axum",
+ "axum-extra",
  "base64 0.13.0",
  "clap 3.1.12",
  "client",
@@ -4237,12 +4257,12 @@ dependencies = [
 
 [[package]]
 name = "serde_json"
-version = "1.0.64"
+version = "1.0.81"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "799e97dc9fdae36a5c8b8f2cae9ce2ee9fdce2058c57a93e6099d919fd982f79"
+checksum = "9b7ce2b32a1aed03c558dc61a5cd328f15aff2dbc17daad8fb8af04d2100e15c"
 dependencies = [
  "indexmap",
- "itoa 0.4.7",
+ "itoa 1.0.1",
  "ryu",
  "serde",
 ]

crates/auto_update/Cargo.toml 🔗

@@ -18,6 +18,6 @@ isahc = "1.7"
 lazy_static = "1.4"
 log = "0.4"
 serde = { version = "1", features = ["derive"] }
-serde_json = { version = "1.0.64", features = ["preserve_order"] }
+serde_json = { version = "1.0", features = ["preserve_order"] }
 smol = "1.2.5"
 tempdir = "0.3.7"

crates/collab/Cargo.toml 🔗

@@ -21,6 +21,7 @@ anyhow = "1.0.40"
 async-trait = "0.1.50"
 async-tungstenite = "0.16"
 axum = { version = "0.5", features = ["json", "headers", "ws"] }
+axum-extra = { version = "0.3", features = ["erased-json"] }
 base64 = "0.13"
 clap = { version = "3.1", features = ["derive"], optional = true }
 envy = "0.4.2"
@@ -70,7 +71,7 @@ ctor = "0.1"
 env_logger = "0.8"
 util = { path = "../util" }
 lazy_static = "1.4"
-serde_json = { version = "1.0.64", features = ["preserve_order"] }
+serde_json = { version = "1.0", features = ["preserve_order"] }
 
 [features]
 seed-support = ["clap", "lipsum", "reqwest"]

crates/collab/src/api.rs 🔗

@@ -1,7 +1,7 @@
 use crate::{
     auth,
     db::{User, UserId},
-    rpc::ResultExt,
+    rpc::{self, ResultExt},
     AppState, Error, Result,
 };
 use anyhow::anyhow;
@@ -14,12 +14,13 @@ use axum::{
     routing::{get, post, put},
     Extension, Json, Router,
 };
+use axum_extra::response::ErasedJson;
 use serde::{Deserialize, Serialize};
 use std::sync::Arc;
 use tower::ServiceBuilder;
 use tracing::instrument;
 
-pub fn routes(rpc_server: &Arc<crate::rpc::Server>, state: Arc<AppState>) -> Router<Body> {
+pub fn routes(rpc_server: &Arc<rpc::Server>, state: Arc<AppState>) -> Router<Body> {
     Router::new()
         .route("/users", get(get_users).post(create_user))
         .route(
@@ -29,6 +30,7 @@ pub fn routes(rpc_server: &Arc<crate::rpc::Server>, state: Arc<AppState>) -> Rou
         .route("/users/:id/access_tokens", post(create_access_token))
         .route("/invite_codes/:code", get(get_user_for_invite_code))
         .route("/panic", post(trace_panic))
+        .route("/rpc_server_snapshot", get(get_rpc_server_snapshot))
         .layer(
             ServiceBuilder::new()
                 .layer(Extension(state))
@@ -84,7 +86,7 @@ struct CreateUserParams {
 async fn create_user(
     Json(params): Json<CreateUserParams>,
     Extension(app): Extension<Arc<AppState>>,
-    Extension(rpc_server): Extension<Arc<crate::rpc::Server>>,
+    Extension(rpc_server): Extension<Arc<rpc::Server>>,
 ) -> Result<Json<User>> {
     println!("{:?}", params);
 
@@ -177,6 +179,12 @@ async fn trace_panic(panic: Json<Panic>) -> Result<()> {
     Ok(())
 }
 
+async fn get_rpc_server_snapshot(
+    Extension(rpc_server): Extension<Arc<rpc::Server>>,
+) -> Result<ErasedJson> {
+    Ok(ErasedJson::pretty(rpc_server.snapshot().await))
+}
+
 #[derive(Deserialize)]
 struct CreateAccessTokenQueryParams {
     public_key: String,

crates/collab/src/main.rs 🔗

@@ -104,6 +104,12 @@ impl From<hyper::Error> for Error {
     }
 }
 
+impl From<serde_json::Error> for Error {
+    fn from(error: serde_json::Error) -> Self {
+        Self::Internal(error.into())
+    }
+}
+
 impl IntoResponse for Error {
     fn into_response(self) -> axum::response::Response {
         match self {

crates/collab/src/rpc.rs 🔗

@@ -33,6 +33,7 @@ use rpc::{
     proto::{self, AnyTypedEnvelope, EntityMessage, EnvelopedMessage, RequestMessage},
     Connection, ConnectionId, Peer, Receipt, TypedEnvelope,
 };
+use serde::{Serialize, Serializer};
 use std::{
     any::TypeId,
     future::Future,
@@ -85,6 +86,7 @@ pub struct Server {
     notifications: Option<mpsc::UnboundedSender<()>>,
 }
 
+
 pub trait Executor: Send + Clone {
     type Sleep: Send + Future;
     fn spawn_detached<F: 'static + Send + Future<Output = ()>>(&self, future: F);
@@ -107,6 +109,23 @@ struct StoreWriteGuard<'a> {
     _not_send: PhantomData<Rc<()>>,
 }
 
+#[derive(Serialize)]
+pub struct ServerSnapshot<'a> {
+    peer: &'a Peer,
+    #[serde(serialize_with = "serialize_deref")]
+    store: RwLockReadGuard<'a, Store>,
+}
+
+pub fn serialize_deref<S, T, U>(value: &T, serializer: S) -> Result<S::Ok, S::Error>
+where
+    S: Serializer,
+    T: Deref<Target = U>,
+    U: Serialize
+{
+    Serialize::serialize(value.deref(), serializer)
+}
+  
+
 impl Server {
     pub fn new(
         app_state: Arc<AppState>,
@@ -1469,6 +1488,13 @@ impl Server {
             _not_send: PhantomData,
         }
     }
+    
+    pub async fn snapshot<'a>(self: &'a Arc<Self>) -> ServerSnapshot<'a> {
+        ServerSnapshot {
+            store: self.store.read().await,
+            peer: &self.peer
+        }
+    }
 }
 
 impl<'a> Deref for StoreReadGuard<'a> {

crates/collab/src/rpc/store.rs 🔗

@@ -2,18 +2,21 @@ use crate::db::{self, ChannelId, UserId};
 use anyhow::{anyhow, Result};
 use collections::{hash_map::Entry, BTreeMap, HashMap, HashSet};
 use rpc::{proto, ConnectionId, Receipt};
+use serde::Serialize;
 use std::{collections::hash_map, mem, path::PathBuf};
 use tracing::instrument;
 
-#[derive(Default)]
+#[derive(Default, Serialize)]
 pub struct Store {
     connections: HashMap<ConnectionId, ConnectionState>,
     connections_by_user_id: HashMap<UserId, HashSet<ConnectionId>>,
     projects: HashMap<u64, Project>,
+    #[serde(skip)]
     channels: HashMap<ChannelId, Channel>,
     next_project_id: u64,
 }
 
+#[derive(Serialize)]
 struct ConnectionState {
     user_id: UserId,
     projects: HashSet<u64>,
@@ -21,21 +24,25 @@ struct ConnectionState {
     channels: HashSet<ChannelId>,
 }
 
+#[derive(Serialize)]
 pub struct Project {
     pub host_connection_id: ConnectionId,
     pub host_user_id: UserId,
     pub guests: HashMap<ConnectionId, (ReplicaId, UserId)>,
+    #[serde(skip)]
     pub join_requests: HashMap<UserId, Vec<Receipt<proto::JoinProject>>>,
     pub active_replica_ids: HashSet<ReplicaId>,
     pub worktrees: HashMap<u64, Worktree>,
     pub language_servers: Vec<proto::LanguageServer>,
 }
 
-#[derive(Default)]
+#[derive(Default, Serialize)]
 pub struct Worktree {
     pub root_name: String,
     pub visible: bool,
+    #[serde(skip)]
     pub entries: HashMap<u64, proto::Entry>,
+    #[serde(skip)]
     pub diagnostic_summaries: BTreeMap<PathBuf, proto::DiagnosticSummary>,
     pub scan_id: u64,
 }

crates/command_palette/Cargo.toml 🔗

@@ -22,7 +22,7 @@ workspace = { path = "../workspace" }
 gpui = { path = "../gpui", features = ["test-support"] }
 editor = { path = "../editor", features = ["test-support"] }
 project = { path = "../project", features = ["test-support"] }
-serde_json = { version = "1.0.64", features = ["preserve_order"] }
+serde_json = { version = "1.0", features = ["preserve_order"] }
 workspace = { path = "../workspace", features = ["test-support"] }
 ctor = "0.1"
 env_logger = "0.8"

crates/file_finder/Cargo.toml 🔗

@@ -21,7 +21,7 @@ postage = { version = "0.4.1", features = ["futures-traits"] }
 
 [dev-dependencies]
 gpui = { path = "../gpui", features = ["test-support"] }
-serde_json = { version = "1.0.64", features = ["preserve_order"] }
+serde_json = { version = "1.0", features = ["preserve_order"] }
 workspace = { path = "../workspace", features = ["test-support"] }
 ctor = "0.1"
 env_logger = "0.8"

crates/gpui/Cargo.toml 🔗

@@ -37,7 +37,7 @@ rand = "0.8.3"
 resvg = "0.14"
 seahash = "4.1"
 serde = { version = "1.0.125", features = ["derive"] }
-serde_json = "1.0.64"
+serde_json = "1.0"
 smallvec = { version = "1.6", features = ["union"] }
 smol = "1.2"
 time = { version = "0.3" }

crates/picker/Cargo.toml 🔗

@@ -17,7 +17,7 @@ workspace = { path = "../workspace" }
 
 [dev-dependencies]
 gpui = { path = "../gpui", features = ["test-support"] }
-serde_json = { version = "1.0.64", features = ["preserve_order"] }
+serde_json = { version = "1.0", features = ["preserve_order"] }
 workspace = { path = "../workspace", features = ["test-support"] }
 ctor = "0.1"
 env_logger = "0.8"

crates/project/Cargo.toml 🔗

@@ -41,7 +41,7 @@ postage = { version = "0.4.1", features = ["futures-traits"] }
 rand = "0.8.3"
 regex = "1.5"
 serde = { version = "1", features = ["derive"] }
-serde_json = { version = "1.0.64", features = ["preserve_order"] }
+serde_json = { version = "1.0", features = ["preserve_order"] }
 sha2 = "0.10"
 similar = "1.3"
 smol = "1.2.5"

crates/project_panel/Cargo.toml 🔗

@@ -23,4 +23,4 @@ unicase = "2.6"
 editor = { path = "../editor", features = ["test-support"] }
 gpui = { path = "../gpui", features = ["test-support"] }
 workspace = { path = "../workspace", features = ["test-support"] }
-serde_json = { version = "1.0.64", features = ["preserve_order"] }
+serde_json = { version = "1.0", features = ["preserve_order"] }

crates/rpc/src/peer.rs 🔗

@@ -10,6 +10,7 @@ use futures::{
     FutureExt, SinkExt, StreamExt,
 };
 use parking_lot::{Mutex, RwLock};
+use serde::{ser::SerializeStruct, Serialize};
 use smol_timeout::TimeoutExt;
 use std::sync::atomic::Ordering::SeqCst;
 use std::{
@@ -24,7 +25,7 @@ use std::{
 };
 use tracing::instrument;
 
-#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
+#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, Serialize)]
 pub struct ConnectionId(pub u32);
 
 impl fmt::Display for ConnectionId {
@@ -89,10 +90,12 @@ pub struct Peer {
     next_connection_id: AtomicU32,
 }
 
-#[derive(Clone)]
+#[derive(Clone, Serialize)]
 pub struct ConnectionState {
+    #[serde(skip)]
     outgoing_tx: mpsc::UnboundedSender<proto::Message>,
     next_message_id: Arc<AtomicU32>,
+    #[serde(skip)]
     response_channels:
         Arc<Mutex<Option<HashMap<u32, oneshot::Sender<(proto::Envelope, oneshot::Sender<()>)>>>>>,
 }
@@ -471,6 +474,17 @@ impl Peer {
     }
 }
 
+impl Serialize for Peer {
+    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+    where
+        S: serde::Serializer,
+    {
+        let mut state = serializer.serialize_struct("Peer", 2)?;
+        state.serialize_field("connections", &*self.connections.read())?;
+        state.end()
+    }
+}
+
 #[cfg(test)]
 mod tests {
     use super::*;

crates/search/Cargo.toml 🔗

@@ -26,6 +26,6 @@ smallvec = { version = "1.6", features = ["union"] }
 [dev-dependencies]
 editor = { path = "../editor", features = ["test-support"] }
 gpui = { path = "../gpui", features = ["test-support"] }
-serde_json = { version = "1.0.64", features = ["preserve_order"] }
+serde_json = { version = "1.0", features = ["preserve_order"] }
 workspace = { path = "../workspace", features = ["test-support"] }
 unindent = "0.1"

crates/settings/Cargo.toml 🔗

@@ -20,6 +20,6 @@ anyhow = "1.0.38"
 json_comments = "0.2"
 schemars = "0.8"
 serde = { version = "1", features = ["derive", "rc"] }
-serde_json = { version = "1.0.64", features = ["preserve_order"] }
+serde_json = { version = "1.0", features = ["preserve_order"] }
 serde_path_to_error = "0.1.4"
 toml = "0.5"

crates/theme/Cargo.toml 🔗

@@ -13,6 +13,6 @@ anyhow = "1.0.38"
 indexmap = "1.6.2"
 parking_lot = "0.11.1"
 serde = { version = "1", features = ["derive", "rc"] }
-serde_json = { version = "1.0.64", features = ["preserve_order"] }
+serde_json = { version = "1.0", features = ["preserve_order"] }
 serde_path_to_error = "0.1.4"
 toml = "0.5"

crates/util/Cargo.toml 🔗

@@ -15,6 +15,6 @@ futures = "0.3"
 log = { version = "0.4.16", features = ["kv_unstable_serde"] }
 rand = { version = "0.8", optional = true }
 tempdir = { version = "0.3.7", optional = true }
-serde_json = { version = "1.0.64", features = [
+serde_json = { version = "1.0", features = [
     "preserve_order",
 ], optional = true }

crates/workspace/Cargo.toml 🔗

@@ -26,7 +26,7 @@ log = { version = "0.4.16", features = ["kv_unstable_serde"] }
 parking_lot = "0.11.1"
 postage = { version = "0.4.1", features = ["futures-traits"] }
 serde = { version = "1", features = ["derive", "rc"] }
-serde_json = { version = "1", features = ["preserve_order"] }
+serde_json = { version = "1.0", features = ["preserve_order"] }
 smallvec = { version = "1.6", features = ["union"] }
 
 [dev-dependencies]

crates/zed/Cargo.toml 🔗

@@ -76,7 +76,7 @@ regex = "1.5"
 rsa = "0.4"
 rust-embed = { version = "6.3", features = ["include-exclude"] }
 serde = { version = "1", features = ["derive"] }
-serde_json = { version = "1.0.64", features = ["preserve_order"] }
+serde_json = { version = "1.0", features = ["preserve_order"] }
 serde_path_to_error = "0.1.4"
 simplelog = "0.9"
 smallvec = { version = "1.6", features = ["union"] }
@@ -107,7 +107,7 @@ settings = { path = "../settings", features = ["test-support"] }
 util = { path = "../util", features = ["test-support"] }
 workspace = { path = "../workspace", features = ["test-support"] }
 env_logger = "0.8"
-serde_json = { version = "1.0.64", features = ["preserve_order"] }
+serde_json = { version = "1.0", features = ["preserve_order"] }
 unindent = "0.1.7"
 
 [package.metadata.bundle]

styles/src/styleTree/workspace.ts 🔗

@@ -8,7 +8,6 @@ export function workspaceBackground(theme: Theme) {
 }
 
 export default function workspace(theme: Theme) {
-
   const tab = {
     height: 32,
     background: workspaceBackground(theme),