Add buffer integration test

Mikayla and Max created

Rearrange channel crate structure
Get channel buffer from database

co-authored-by: Max <max@zed.dev>

Change summary

Cargo.lock                                         | 41 +++++++
Cargo.toml                                         |  1 
crates/call/Cargo.toml                             |  1 
crates/call/src/call.rs                            |  5 
crates/channel/Cargo.toml                          | 51 +++++++++
crates/channel/src/channel.rs                      |  7 +
crates/channel/src/channel_buffer.rs               | 80 +++++++++++++++
crates/channel/src/channel_store.rs                |  6 
crates/channel/src/channel_store_tests.rs          |  3 
crates/client/Cargo.toml                           |  1 
crates/client/src/client.rs                        |  5 
crates/client/src/user.rs                          |  4 
crates/collab/Cargo.toml                           |  1 
crates/collab/src/db.rs                            |  5 
crates/collab/src/db/queries.rs                    |  3 
crates/collab/src/db/queries/channels.rs           | 28 +++++
crates/collab/src/db/tables/channel.rs             |  3 
crates/collab/src/db/tests.rs                      | 10 +
crates/collab/src/db/tests/buffer_tests.rs         |  0 
crates/collab/src/db/tests/db_tests.rs             | 31 +++++
crates/collab/src/rpc.rs                           | 25 ++++
crates/collab/src/tests.rs                         |  7 
crates/collab/src/tests/channel_buffer_tests.rs    | 84 ++++++++++++++++
crates/collab/src/tests/channel_tests.rs           |  3 
crates/collab_ui/Cargo.toml                        |  1 
crates/collab_ui/src/collab_panel.rs               |  6 
crates/collab_ui/src/collab_panel/channel_modal.rs |  3 
crates/rpc/proto/zed.proto                         | 12 ++
crates/rpc/src/proto.rs                            |  5 
crates/workspace/Cargo.toml                        |  1 
crates/workspace/src/workspace.rs                  |  3 
crates/zed/Cargo.toml                              |  1 
crates/zed/src/main.rs                             |  5 
33 files changed, 403 insertions(+), 39 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -1063,6 +1063,7 @@ dependencies = [
  "anyhow",
  "async-broadcast",
  "audio",
+ "channel",
  "client",
  "collections",
  "fs",
@@ -1190,6 +1191,41 @@ version = "1.0.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
 
+[[package]]
+name = "channel"
+version = "0.1.0"
+dependencies = [
+ "anyhow",
+ "client",
+ "collections",
+ "db",
+ "futures 0.3.28",
+ "gpui",
+ "image",
+ "language",
+ "lazy_static",
+ "log",
+ "parking_lot 0.11.2",
+ "postage",
+ "rand 0.8.5",
+ "rpc",
+ "schemars",
+ "serde",
+ "serde_derive",
+ "settings",
+ "smol",
+ "staff_mode",
+ "sum_tree",
+ "tempfile",
+ "text",
+ "thiserror",
+ "time 0.3.24",
+ "tiny_http",
+ "url",
+ "util",
+ "uuid 1.4.1",
+]
+
 [[package]]
 name = "chrono"
 version = "0.4.26"
@@ -1354,6 +1390,7 @@ dependencies = [
  "staff_mode",
  "sum_tree",
  "tempfile",
+ "text",
  "thiserror",
  "time 0.3.24",
  "tiny_http",
@@ -1418,6 +1455,7 @@ dependencies = [
  "axum-extra",
  "base64 0.13.1",
  "call",
+ "channel",
  "clap 3.2.25",
  "client",
  "collections",
@@ -1480,6 +1518,7 @@ dependencies = [
  "anyhow",
  "auto_update",
  "call",
+ "channel",
  "client",
  "clock",
  "collections",
@@ -9536,6 +9575,7 @@ dependencies = [
  "async-recursion 1.0.4",
  "bincode",
  "call",
+ "channel",
  "client",
  "collections",
  "context_menu",
@@ -9661,6 +9701,7 @@ dependencies = [
  "backtrace",
  "breadcrumbs",
  "call",
+ "channel",
  "chrono",
  "cli",
  "client",

Cargo.toml 🔗

@@ -6,6 +6,7 @@ members = [
     "crates/auto_update",
     "crates/breadcrumbs",
     "crates/call",
+    "crates/channel",
     "crates/cli",
     "crates/client",
     "crates/clock",

crates/call/Cargo.toml 🔗

@@ -20,6 +20,7 @@ test-support = [
 
 [dependencies]
 audio = { path = "../audio" }
+channel = { path = "../channel" }
 client = { path = "../client" }
 collections = { path = "../collections" }
 gpui = { path = "../gpui" }

crates/call/src/call.rs 🔗

@@ -7,9 +7,8 @@ use std::sync::Arc;
 use anyhow::{anyhow, Result};
 use audio::Audio;
 use call_settings::CallSettings;
-use client::{
-    proto, ChannelId, ClickhouseEvent, Client, TelemetrySettings, TypedEnvelope, User, UserStore,
-};
+use channel::ChannelId;
+use client::{proto, ClickhouseEvent, Client, TelemetrySettings, TypedEnvelope, User, UserStore};
 use collections::HashSet;
 use futures::{future::Shared, FutureExt};
 use postage::watch;

crates/channel/Cargo.toml 🔗

@@ -0,0 +1,51 @@
+[package]
+name = "channel"
+version = "0.1.0"
+edition = "2021"
+publish = false
+
+[lib]
+path = "src/channel.rs"
+doctest = false
+
+[features]
+test-support = ["collections/test-support", "gpui/test-support", "rpc/test-support"]
+
+[dependencies]
+client = { path = "../client" }
+collections = { path = "../collections" }
+db = { path = "../db" }
+gpui = { path = "../gpui" }
+util = { path = "../util" }
+rpc = { path = "../rpc" }
+text = { path = "../text" }
+language = { path = "../language" }
+settings = { path = "../settings" }
+staff_mode = { path = "../staff_mode" }
+sum_tree = { path = "../sum_tree" }
+
+anyhow.workspace = true
+futures.workspace = true
+image = "0.23"
+lazy_static.workspace = true
+log.workspace = true
+parking_lot.workspace = true
+postage.workspace = true
+rand.workspace = true
+schemars.workspace = true
+smol.workspace = true
+thiserror.workspace = true
+time.workspace = true
+tiny_http = "0.8"
+uuid = { version = "1.1.2", features = ["v4"] }
+url = "2.2"
+serde.workspace = true
+serde_derive.workspace = true
+tempfile = "3"
+
+[dev-dependencies]
+collections = { path = "../collections", features = ["test-support"] }
+gpui = { path = "../gpui", features = ["test-support"] }
+rpc = { path = "../rpc", features = ["test-support"] }
+settings = { path = "../settings", features = ["test-support"] }
+util = { path = "../util", features = ["test-support"] }

crates/channel/src/channel.rs 🔗

@@ -0,0 +1,7 @@
+mod channel_store;
+
+pub mod channel_buffer;
+pub use channel_store::*;
+
+#[cfg(test)]
+mod channel_store_tests;

crates/channel/src/channel_buffer.rs 🔗

@@ -0,0 +1,80 @@
+use crate::ChannelId;
+use anyhow::Result;
+use client::Client;
+use gpui::{Entity, ModelContext, ModelHandle, Task};
+use rpc::proto::GetChannelBuffer;
+use std::sync::Arc;
+
+// Open the channel document
+// ChannelDocumentView { ChannelDocument, Editor } -> On clone, clones internal ChannelDocument handle, instantiates new editor
+// Produces a view which is: (ChannelDocument, Editor), ChannelDocument manages subscriptions
+// ChannelDocuments -> Buffers -> Editor with that buffer
+
+// ChannelDocuments {
+//     ChannleBuffers: HashMap<bufferId, ModelHandle<language::Buffer>>
+// }
+
+pub struct ChannelBuffer {
+    channel_id: ChannelId,
+    buffer: Option<ModelHandle<language::Buffer>>,
+    client: Arc<Client>,
+}
+
+impl Entity for ChannelBuffer {
+    type Event = ();
+}
+
+impl ChannelBuffer {
+    pub fn for_channel(
+        channel_id: ChannelId,
+        client: Arc<Client>,
+        cx: &mut ModelContext<Self>,
+    ) -> Self {
+        Self {
+            channel_id,
+            client,
+            buffer: None,
+        }
+    }
+
+    fn on_buffer_update(
+        &mut self,
+        buffer: ModelHandle<language::Buffer>,
+        event: &language::Event,
+        cx: &mut ModelContext<Self>,
+    ) {
+        //
+    }
+
+    pub fn buffer(
+        &mut self,
+        cx: &mut ModelContext<Self>,
+    ) -> Task<Result<ModelHandle<language::Buffer>>> {
+        if let Some(buffer) = &self.buffer {
+            Task::ready(Ok(buffer.clone()))
+        } else {
+            let channel_id = self.channel_id;
+            let client = self.client.clone();
+            cx.spawn(|this, mut cx| async move {
+                let response = client.request(GetChannelBuffer { channel_id }).await?;
+
+                let base_text = response.base_text;
+                let operations = response
+                    .operations
+                    .into_iter()
+                    .map(language::proto::deserialize_operation)
+                    .collect::<Result<Vec<_>, _>>()?;
+
+                this.update(&mut cx, |this, cx| {
+                    let buffer = cx.add_model(|cx| language::Buffer::new(0, base_text, cx));
+                    buffer.update(cx, |buffer, cx| buffer.apply_ops(operations, cx))?;
+
+                    cx.subscribe(&buffer, Self::on_buffer_update).detach();
+
+                    this.buffer = Some(buffer.clone());
+                    anyhow::Ok(buffer)
+                })
+            })
+        }
+    }
+}

crates/client/src/channel_store.rs → crates/channel/src/channel_store.rs 🔗

@@ -1,7 +1,8 @@
-use crate::Status;
-use crate::{Client, Subscription, User, UserStore};
 use anyhow::anyhow;
 use anyhow::Result;
+use client::Status;
+use client::UserId;
+use client::{Client, Subscription, User, UserStore};
 use collections::HashMap;
 use collections::HashSet;
 use futures::channel::mpsc;
@@ -13,7 +14,6 @@ use std::sync::Arc;
 use util::ResultExt;
 
 pub type ChannelId = u64;
-pub type UserId = u64;
 
 pub struct ChannelStore {
     channels_by_id: HashMap<ChannelId, Arc<Channel>>,

crates/client/src/channel_store_tests.rs → crates/channel/src/channel_store_tests.rs 🔗

@@ -1,4 +1,7 @@
 use super::*;
+use client::{Client, UserStore};
+use gpui::{AppContext, ModelHandle};
+use rpc::proto;
 use util::http::FakeHttpClient;
 
 #[gpui::test]

crates/client/Cargo.toml 🔗

@@ -17,6 +17,7 @@ db = { path = "../db" }
 gpui = { path = "../gpui" }
 util = { path = "../util" }
 rpc = { path = "../rpc" }
+text = { path = "../text" }
 settings = { path = "../settings" }
 staff_mode = { path = "../staff_mode" }
 sum_tree = { path = "../sum_tree" }

crates/client/src/client.rs 🔗

@@ -1,10 +1,6 @@
 #[cfg(any(test, feature = "test-support"))]
 pub mod test;
 
-#[cfg(test)]
-mod channel_store_tests;
-
-pub mod channel_store;
 pub mod telemetry;
 pub mod user;
 
@@ -48,7 +44,6 @@ use util::channel::ReleaseChannel;
 use util::http::HttpClient;
 use util::{ResultExt, TryFutureExt};
 
-pub use channel_store::*;
 pub use rpc::*;
 pub use telemetry::ClickhouseEvent;
 pub use user::*;

crates/client/src/user.rs 🔗

@@ -10,9 +10,11 @@ use std::sync::{Arc, Weak};
 use util::http::HttpClient;
 use util::TryFutureExt as _;
 
+pub type UserId = u64;
+
 #[derive(Default, Debug)]
 pub struct User {
-    pub id: u64,
+    pub id: UserId,
     pub github_login: String,
     pub avatar: Option<Arc<ImageData>>,
 }

crates/collab/Cargo.toml 🔗

@@ -64,6 +64,7 @@ collections = { path = "../collections", features = ["test-support"] }
 gpui = { path = "../gpui", features = ["test-support"] }
 call = { path = "../call", features = ["test-support"] }
 client = { path = "../client", features = ["test-support"] }
+channel = { path = "../channel" }
 editor = { path = "../editor", features = ["test-support"] }
 language = { path = "../language", features = ["test-support"] }
 fs = { path = "../fs", features = ["test-support"] }

crates/collab/src/db.rs 🔗

@@ -1,7 +1,8 @@
 #[cfg(test)]
-mod db_tests;
+pub mod tests;
+
 #[cfg(test)]
-pub mod test_db;
+pub use tests::TestDb;
 
 mod ids;
 mod queries;

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

@@ -689,6 +689,34 @@ impl Database {
         })
         .await
     }
+
+    pub async fn get_or_create_buffer_for_channel(
+        &self,
+        channel_id: ChannelId,
+    ) -> Result<BufferId> {
+        self.transaction(|tx| async move {
+            let tx = tx;
+            let channel = channel::Entity::find_by_id(channel_id)
+                .one(&*tx)
+                .await?
+                .ok_or_else(|| anyhow!("invalid channel"))?;
+
+            if let Some(id) = channel.main_buffer_id {
+                return Ok(id);
+            } else {
+                let buffer = buffer::ActiveModel::new().insert(&*tx).await?;
+                channel::ActiveModel {
+                    id: ActiveValue::Unchanged(channel_id),
+                    main_buffer_id: ActiveValue::Set(Some(buffer.id)),
+                    ..Default::default()
+                }
+                .update(&*tx)
+                .await?;
+                Ok(buffer.id)
+            }
+        })
+        .await
+    }
 }
 
 #[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)]

crates/collab/src/db/tables/channel.rs 🔗

@@ -1,4 +1,4 @@
-use crate::db::ChannelId;
+use crate::db::{BufferId, ChannelId};
 use sea_orm::entity::prelude::*;
 
 #[derive(Clone, Debug, Default, PartialEq, Eq, DeriveEntityModel)]
@@ -7,6 +7,7 @@ pub struct Model {
     #[sea_orm(primary_key)]
     pub id: ChannelId,
     pub name: String,
+    pub main_buffer_id: Option<BufferId>,
 }
 
 impl ActiveModelBehavior for ActiveModel {}

crates/collab/src/db/test_db.rs → crates/collab/src/db/tests.rs 🔗

@@ -1,3 +1,6 @@
+mod buffer_tests;
+mod db_tests;
+
 use super::*;
 use gpui::executor::Background;
 use parking_lot::Mutex;
@@ -96,7 +99,7 @@ macro_rules! test_both_dbs {
     ($test_name:ident, $postgres_test_name:ident, $sqlite_test_name:ident) => {
         #[gpui::test]
         async fn $postgres_test_name() {
-            let test_db = crate::db::test_db::TestDb::postgres(
+            let test_db = crate::db::TestDb::postgres(
                 gpui::executor::Deterministic::new(0).build_background(),
             );
             $test_name(test_db.db()).await;
@@ -104,9 +107,8 @@ macro_rules! test_both_dbs {
 
         #[gpui::test]
         async fn $sqlite_test_name() {
-            let test_db = crate::db::test_db::TestDb::sqlite(
-                gpui::executor::Deterministic::new(0).build_background(),
-            );
+            let test_db =
+                crate::db::TestDb::sqlite(gpui::executor::Deterministic::new(0).build_background());
             $test_name(test_db.db()).await;
         }
     };

crates/collab/src/db/db_tests.rs → crates/collab/src/db/tests/db_tests.rs 🔗

@@ -3,7 +3,7 @@ use crate::test_both_dbs;
 use gpui::executor::{Background, Deterministic};
 use pretty_assertions::{assert_eq, assert_ne};
 use std::sync::Arc;
-use test_db::TestDb;
+use tests::TestDb;
 
 test_both_dbs!(
     test_get_users,
@@ -1329,6 +1329,35 @@ async fn test_channel_renames(db: &Arc<Database>) {
     assert!(bad_name_rename.is_err())
 }
 
+test_both_dbs!(
+    test_get_or_create_channel_buffer,
+    test_get_or_create_channel_buffer_postgres,
+    test_get_or_create_channel_buffer_sqlite
+);
+
+async fn test_get_or_create_channel_buffer(db: &Arc<Database>) {
+    let a_id = db
+        .create_user(
+            "user1@example.com",
+            false,
+            NewUserParams {
+                github_login: "user1".into(),
+                github_user_id: 5,
+                invite_count: 0,
+            },
+        )
+        .await
+        .unwrap()
+        .user_id;
+
+    let zed_id = db.create_root_channel("zed", "1", a_id).await.unwrap();
+
+    let first_buffer_id = db.get_or_create_buffer_for_channel(zed_id).await.unwrap();
+    let second_buffer_id = db.get_or_create_buffer_for_channel(zed_id).await.unwrap();
+
+    assert_eq!(first_buffer_id, second_buffer_id);
+}
+
 #[gpui::test]
 async fn test_multiple_signup_overwrite() {
     let test_db = TestDb::postgres(build_background_executor());

crates/collab/src/rpc.rs 🔗

@@ -35,8 +35,8 @@ use lazy_static::lazy_static;
 use prometheus::{register_int_gauge, IntGauge};
 use rpc::{
     proto::{
-        self, AnyTypedEnvelope, EntityMessage, EnvelopedMessage, LiveKitConnectionInfo,
-        RequestMessage,
+        self, AnyTypedEnvelope, EntityMessage, EnvelopedMessage, GetChannelBufferResponse,
+        LiveKitConnectionInfo, RequestMessage,
     },
     Connection, ConnectionId, Peer, Receipt, TypedEnvelope,
 };
@@ -248,6 +248,7 @@ impl Server {
             .add_request_handler(remove_channel_member)
             .add_request_handler(set_channel_member_admin)
             .add_request_handler(rename_channel)
+            .add_request_handler(get_channel_buffer)
             .add_request_handler(get_channel_members)
             .add_request_handler(respond_to_channel_invite)
             .add_request_handler(join_channel)
@@ -2478,6 +2479,26 @@ async fn join_channel(
     Ok(())
 }
 
+async fn get_channel_buffer(
+    request: proto::GetChannelBuffer,
+    response: Response<proto::GetChannelBuffer>,
+    session: Session,
+) -> Result<()> {
+    let db = session.db().await;
+    let channel_id = ChannelId::from_proto(request.channel_id);
+
+    let buffer_id = db.get_or_create_buffer_for_channel(channel_id).await?;
+
+    let buffer = db.get_buffer(buffer_id).await?;
+
+    response.send(GetChannelBufferResponse {
+        base_text: buffer.base_text,
+        operations: buffer.operations,
+    })?;
+
+    Ok(())
+}
+
 async fn update_diff_base(request: proto::UpdateDiffBase, session: Session) -> Result<()> {
     let project_id = ProjectId::from_proto(request.project_id);
     let project_connection_ids = session

crates/collab/src/tests.rs 🔗

@@ -1,14 +1,14 @@
 use crate::{
-    db::{test_db::TestDb, NewUserParams, UserId},
+    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, ChannelStore, Client, Connection, Credentials, EstablishConnectionError,
-    UserStore,
+    self, proto::PeerId, Client, Connection, Credentials, EstablishConnectionError, UserStore,
 };
 use collections::{HashMap, HashSet};
 use fs::FakeFs;
@@ -31,6 +31,7 @@ use std::{
 use util::http::FakeHttpClient;
 use workspace::Workspace;
 
+mod channel_buffer_tests;
 mod channel_tests;
 mod integration_tests;
 mod randomized_integration_tests;

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

@@ -0,0 +1,84 @@
+use crate::tests::TestServer;
+
+use channel::channel_buffer::ChannelBuffer;
+use gpui::{executor::Deterministic, ModelHandle, TestAppContext};
+use std::{ops::Range, sync::Arc};
+
+#[gpui::test]
+async fn test_channel_buffers(
+    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 zed_id = server
+        .make_channel("zed", (&client_a, cx_a), &mut [(&client_b, cx_b)])
+        .await;
+
+    let a_document =
+        cx_a.add_model(|cx| ChannelBuffer::for_channel(zed_id, client_a.client().to_owned(), cx));
+    let channel_buffer_a = a_document
+        .update(cx_a, |doc, cx| doc.buffer(cx))
+        .await
+        .unwrap();
+
+    edit_channel_buffer(&channel_buffer_a, cx_a, [(0..0, "hello world")]);
+    edit_channel_buffer(&channel_buffer_a, cx_a, [(5..5, ", cruel")]);
+    edit_channel_buffer(&channel_buffer_a, cx_a, [(0..5, "goodbye")]);
+    undo_channel_buffer(&channel_buffer_a, cx_a);
+
+    assert_eq!(
+        channel_buffer_text(&channel_buffer_a, cx_a),
+        "hello, cruel world"
+    );
+
+    let b_document =
+        cx_b.add_model(|cx| ChannelBuffer::for_channel(zed_id, client_b.client().to_owned(), cx));
+    let channel_buffer_b = b_document
+        .update(cx_b, |doc, cx| doc.buffer(cx))
+        .await
+        .unwrap();
+
+    assert_eq!(
+        channel_buffer_text(&channel_buffer_b, cx_b),
+        "hello, cruel world"
+    );
+
+    edit_channel_buffer(&channel_buffer_b, cx_b, [(7..12, "beautiful")]);
+
+    deterministic.run_until_parked();
+
+    assert_eq!(
+        channel_buffer_text(&channel_buffer_a, cx_a),
+        "hello, beautiful world"
+    );
+    assert_eq!(
+        channel_buffer_text(&channel_buffer_b, cx_b),
+        "hello, beautiful world"
+    );
+}
+
+fn edit_channel_buffer<I>(
+    channel_buffer: &ModelHandle<language::Buffer>,
+    cx: &mut TestAppContext,
+    edits: I,
+) where
+    I: IntoIterator<Item = (Range<usize>, &'static str)>,
+{
+    channel_buffer.update(cx, |buffer, cx| buffer.edit(edits, None, cx));
+}
+
+fn undo_channel_buffer(channel_buffer: &ModelHandle<language::Buffer>, cx: &mut TestAppContext) {
+    channel_buffer.update(cx, |buffer, cx| buffer.undo(cx));
+}
+
+fn channel_buffer_text(
+    channel_buffer: &ModelHandle<language::Buffer>,
+    cx: &mut TestAppContext,
+) -> String {
+    channel_buffer.read_with(cx, |buffer, _| buffer.text())
+}

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

@@ -3,7 +3,8 @@ use crate::{
     tests::{room_participants, RoomParticipants, TestServer},
 };
 use call::ActiveCall;
-use client::{ChannelId, ChannelMembership, ChannelStore, User};
+use channel::{ChannelId, ChannelMembership, ChannelStore};
+use client::User;
 use gpui::{executor::Deterministic, ModelHandle, TestAppContext};
 use rpc::{proto, RECEIVE_TIMEOUT};
 use std::sync::Arc;

crates/collab_ui/Cargo.toml 🔗

@@ -26,6 +26,7 @@ auto_update = { path = "../auto_update" }
 db = { path = "../db" }
 call = { path = "../call" }
 client = { path = "../client" }
+channel = { path = "../channel" }
 clock = { path = "../clock" }
 collections = { path = "../collections" }
 context_menu = { path = "../context_menu" }

crates/collab_ui/src/collab_panel.rs 🔗

@@ -4,10 +4,8 @@ mod panel_settings;
 
 use anyhow::Result;
 use call::ActiveCall;
-use client::{
-    proto::PeerId, Channel, ChannelEvent, ChannelId, ChannelStore, Client, Contact, User, UserStore,
-};
-
+use channel::{Channel, ChannelEvent, ChannelId, ChannelStore};
+use client::{proto::PeerId, Client, Contact, User, UserStore};
 use context_menu::{ContextMenu, ContextMenuItem};
 use db::kvp::KEY_VALUE_STORE;
 use editor::{Cancel, Editor};

crates/collab_ui/src/collab_panel/channel_modal.rs 🔗

@@ -1,4 +1,5 @@
-use client::{proto, ChannelId, ChannelMembership, ChannelStore, User, UserId, UserStore};
+use channel::{ChannelId, ChannelMembership, ChannelStore};
+use client::{proto, User, UserId, UserStore};
 use context_menu::{ContextMenu, ContextMenuItem};
 use fuzzy::{match_strings, StringMatchCandidate};
 use gpui::{

crates/rpc/proto/zed.proto 🔗

@@ -142,6 +142,9 @@ message Envelope {
         GetChannelMembersResponse get_channel_members_response = 128;
         SetChannelMemberAdmin set_channel_member_admin = 129;
         RenameChannel rename_channel = 130;
+
+        GetChannelBuffer get_channel_buffer = 131;
+        GetChannelBufferResponse get_channel_buffer_response = 132;
     }
 }
 
@@ -948,6 +951,15 @@ message RenameChannel {
     string name = 2;
 }
 
+message GetChannelBuffer {
+    uint64 channel_id = 1;
+}
+
+message GetChannelBufferResponse {
+    string base_text = 1;
+    repeated Operation operations = 2;
+}
+
 message RespondToChannelInvite {
     uint64 channel_id = 1;
     bool accept = 2;

crates/rpc/src/proto.rs 🔗

@@ -248,7 +248,9 @@ messages!(
     (GetPrivateUserInfo, Foreground),
     (GetPrivateUserInfoResponse, Foreground),
     (GetChannelMembers, Foreground),
-    (GetChannelMembersResponse, Foreground)
+    (GetChannelMembersResponse, Foreground),
+    (GetChannelBuffer, Foreground),
+    (GetChannelBufferResponse, Foreground)
 );
 
 request_messages!(
@@ -315,6 +317,7 @@ request_messages!(
     (UpdateParticipantLocation, Ack),
     (UpdateProject, Ack),
     (UpdateWorktree, Ack),
+    (GetChannelBuffer, GetChannelBufferResponse)
 );
 
 entity_messages!(

crates/workspace/Cargo.toml 🔗

@@ -22,6 +22,7 @@ test-support = [
 db = { path = "../db" }
 call = { path = "../call" }
 client = { path = "../client" }
+channel = { path = "../channel" }
 collections = { path = "../collections" }
 context_menu = { path = "../context_menu" }
 drag_and_drop = { path = "../drag_and_drop" }

crates/workspace/src/workspace.rs 🔗

@@ -12,9 +12,10 @@ mod workspace_settings;
 
 use anyhow::{anyhow, Context, Result};
 use call::ActiveCall;
+use channel::ChannelStore;
 use client::{
     proto::{self, PeerId},
-    ChannelStore, Client, TypedEnvelope, UserStore,
+    Client, TypedEnvelope, UserStore,
 };
 use collections::{hash_map, HashMap, HashSet};
 use drag_and_drop::DragAndDrop;

crates/zed/Cargo.toml 🔗

@@ -21,6 +21,7 @@ activity_indicator = { path = "../activity_indicator" }
 auto_update = { path = "../auto_update" }
 breadcrumbs = { path = "../breadcrumbs" }
 call = { path = "../call" }
+channel = { path = "../channel" }
 cli = { path = "../cli" }
 collab_ui = { path = "../collab_ui" }
 collections = { path = "../collections" }

crates/zed/src/main.rs 🔗

@@ -3,13 +3,12 @@
 
 use anyhow::{anyhow, Context, Result};
 use backtrace::Backtrace;
+use channel::ChannelStore;
 use cli::{
     ipc::{self, IpcSender},
     CliRequest, CliResponse, IpcHandshake, FORCE_CLI_MODE_ENV_VAR_NAME,
 };
-use client::{
-    self, ChannelStore, TelemetrySettings, UserStore, ZED_APP_VERSION, ZED_SECRET_CLIENT_TOKEN,
-};
+use client::{self, TelemetrySettings, UserStore, ZED_APP_VERSION, ZED_SECRET_CLIENT_TOKEN};
 use db::kvp::KEY_VALUE_STORE;
 use editor::{scroll::autoscroll::Autoscroll, Editor};
 use futures::{